Shaka Player for media playback - implementation, use cases, pros and cons
Tags: webdev, javascript, opensource, video
Published: 2023-02-11 21:35:18

If you are looking for a library to play DRM-protected content, stream a live broadcast or monetize your content, **Shaka Player** can do all of that, and more! An open-source library, Shaka Player is the second-most-popular library for media playback. The library uses native browser APIs like [MediaSource](https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API) and [Encrypted Media Extensions](https://developer.mozilla.org/en-US/docs/Web/API/Encrypted_Media_Extensions_API). This article will assist you with the basic implementation of Shaka Player, describe its capabilities, and draw out its strengths and weaknesses.

## A little introduction

Shaka Player's core uses a few native browser APIs under the hood:

- **Encrypted Media Extensions** (**EME**) API that is used, when the browser receives the video and fires an 'encrypted' listener. [This Google article](https://web.dev/media-eme/#how-does-eme-work) does an amazing job of explaining the 'under-the-hood' workings of it, and how and when to use it.
- **Media Source Extension** API that helps create and put video segments together, insert ads and change video resolution based on device capabilities and performance. [Another Google article](https://web.dev/media-mse-basics/) explains these concepts in more detail in an accessible way.

- [**MediaCapabilities** API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capabilities_API) to determine whether your device supports the media you are trying to play and can tell you if your media configuration's capabilities allow for smooth and power-efficient playback.

- [**IndexedDB** API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) to cache downloaded videos and subsequently allow for offline playback.

Shaka Player is often used in conjunction with [Shaka Packager](https://shaka-project.github.io/shaka-packager/) on the back end to add content protection, text streaming, break the media file into segments for streaming and create a manifest file with metadata for [DASH](https://developer.mozilla.org/en-US/docs/Web/Media/DASH_Adaptive_Streaming_for_HTML_5_Video) or create respective packaging for [HLS](https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_delivery/Live_streaming_web_audio_and_video#hls).

## Basic usage
For the purpose of this article, we will use the freely available DASH manifest [from here](https://ottverse.com/free-mpeg-dash-mpd-manifest-example-test-urls/)

To download and use Shaka Player in your project, you can do it in two ways:

1. [Download the Shaka repository to your computer and follow this instruction](https://shaka-player-demo.appspot.com/docs/api/tutorial-welcome.html)
2. via npm/yarn:
```
npm install --save shaka-player
```
or
```
yarn add shaka-player
```

**Note**: if you are using Typescript in your project, you will need to declare types in the `d.ts` file in your project, like so:
```typescript
declare module 'shaka-player' {
  export = shaka;
}

declare module 'shaka-player/dist/shaka-player.compiled' {
  export = shaka;
}
```
> Shaka will start supporting types in v5, as mentioned [here](https://github.com/shaka-project/shaka-player/projects/7#card-85768645)

In order to initiate the player instance, you need a `<video>` element in your HTML you can attach to:

```html
<!DOCTYPE html>
<html>
  <head>
    <title>Basic Shaka implementation</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <video id="video" width="640" height="480" controls></video>

    <script src="index.js"></script>
  </body>
</html>
```
Now, in `index.js`:  
```javascript
import shaka from "shaka-player";

const exampleManifestUri = 
"https://bitmovin-a.akamaihd.net
/content/MI201109210084_1
/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd";

async function initPlayer() {
  // Install polyfills to patch out browser incompatibilities
  shaka.polyfill.installAll();
  // Check if your browser supports Shaka at all
  if (!shaka.Player.isBrowserSupported()) {
    console.error("Browser not supported!");
    return;
  }

  //Search for available video element and attach the player to it
  const video = document.getElementById("video");
  const player = new shaka.Player(video);
  // listen for errors for further error handling
  player.addEventListener("error", console.error);
  try {
    await player.load(exampleManifestUri);
    console.log("player has loaded the video, you can play it now");
  } catch (error) {
    console.log(error);
  }
}

document.addEventListener("DOMContentLoaded", initPlayer);
```

[**Working example**](https://codesandbox.io/s/elated-snow-zdlty0?file=/src/index.js)

### Usage with controls

Shaka Player has its own controls UI you can add before player initiation. For this, we need to update both our HTML and Javascript code.

In our HTML, we have to add styles and a container element:

```html
<!DOCTYPE html>
<html>
  <head>
    <title>Shaka player built-in UI example</title>
    <meta charset="UTF-8" />
    <!-- for the sake of this article we
 use a sandbox path to shaka styles -->
    <link
      rel="stylesheet"
      type="text/css"
      href="node_modules/shaka-player/dist/controls.css"
    />
    <link rel="stylesheet" type="text/css" href="src/styles.css" />
  </head>

  <body>
    <!-- We need a container to build the UI on top of it -->
    <div id="container" style="width: 100%; height: 100%;">
      <video autoplay id="video"></video>
    </div>

    <script src="src/index.js"></script>
  </body>
</html>
```

In our Javascript code, we change the import source and add some more logic:

```javascript
/* we need to import the shaka ui module instead of 
the regular module to gain access to the Overlay constructor */
import shaka from "shaka-player/dist/shaka-player.ui.js";

const manifestUri =
  "https://bitmovin-a.akamaihd.net/content
/MI201109210084_1
/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd";

async function init() {
  const video = document.getElementById("video");
  const container = document.getElementById("container");
  const player = new shaka.Player(video);
  // initialize the UI instance onto the HTML element we created
  const ui = new shaka.ui.Overlay(player, container, video);

  const controls = ui.getControls();
  // Listen for error events for possible error handling.
  player.addEventListener("error", console.error);
  controls.addEventListener("error", console.error);
  try {
    await player.load(manifestUri);
  } catch (e) {
    console.error(e);
  }
}

/* Listen to the custom shaka-ui-loaded event, 
to wait until the UI is loaded. */
document.addEventListener("shaka-ui-loaded", init);
```

[**Working example**](https://codesandbox.io/s/shaka-player-ui-example-04g3i0?file=/src/index.js)


Now player controls should be visible in your player overlay.
> Note: you can use [custom shaka styles ](https://lucksy.github.io/shaka-player-themes/) if you want to stand out!

### Adding configuration

Shaka Player lets you configure your player instance, as well as its controls. Imagine you added DRM protection to your content, or you want to cache your video, or you want to add more features to your controls. All of this is possible vide the `configure` method.
Let's start our video from the second 10 and add tooltips to our controls:

```javascript
async function init() {
  const video = document.getElementById("video");
  const container = document.getElementById("container");
  const player = new shaka.Player(video);

  // initialize the UI instance onto the HTML element we created
  const ui = new shaka.ui.Overlay(player, container, video);

  player.configure({
    playRangeStart: 10
  });

  ui.configure({
    enableTooltips: true
  });

  const controls = ui.getControls();
  // Listen for error events for possible error handling.
  player.addEventListener("error", console.error);
  controls.addEventListener("error", console.error);

  try {
    await player.load(manifestUri);
  } catch (e) {
    console.error(e);
  }
}
```

You can do more than that with [player config](https://shaka-player-demo.appspot.com/docs/api/shaka.extern.html#.PlayerConfiguration) and [UI config](https://shaka-player-demo.appspot.com/docs/api/shaka.extern.html#.UIConfiguration) - add custom DRM sources, toggle media auto adaptation, add more UI features, etc.

### Offline playback

Shaka allows us to download assets and store them in the browser database (IndexedDB API from above). The code in the snippet below can be a bit more complex, but some of it will already look familiar.


```javascript
import shaka from "shaka-player/dist/shaka-player.ui.js";

const exampleManifestUri =
  "https://bitmovin-a.akamaihd.net
  /content/MI201109210084_1/mpds
  /f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd";

function initApp() {
  // Install built-in polyfills to patch browser incompatibilities.
  shaka.polyfill.installAll();

  /* Check to see if the browser supports 
the basic APIs Shaka needs.*/
  if (!shaka.Player.isBrowserSupported()) {
    // This browser does not have the minimum set of APIs we need.
    console.error("Browser not supported!");
  }

  addNetworkListeners();
}

// this function initializes the player and adds the event listeners
async function initPlayer() {
  const video = document.getElementById("video");
  const container = document.getElementById("container");
  const player = new shaka.Player(video);

  /* we will need this in other functions 
so we assign it to the window object */
  window.player = player;

  const ui = new shaka.ui.Overlay(player, container, video);
  const controls = ui.getControls();
  controls.addEventListener("error", console.error);

  player.addEventListener("error", console.error);

  initOfflineStorage(player);

  const downloadButton = document.getElementById("download-button");
  downloadButton.addEventListener("click", downloadContent);

  try {
    await player.load(exampleManifestUri);
  } catch (error) {
    console.error(error);
  }
}

/* Update the online status and add listeners so that we can display
  the network state to the user. */
function addNetworkListeners() {
  updateOnlineStatus();
  window.addEventListener("online", updateOnlineStatus);
  window.addEventListener("offline", updateOnlineStatus);
}

/* grabs an HTML element and modifies it 
depending on your network status */
function updateOnlineStatus() {
  const signal = document.getElementById("online-indicator");
  if (navigator.onLine) {
    signal.innerHTML = "You are ONLINE";
    signal.style.background = "green";
  } else {
    signal.innerHTML = "You are OFFLINE";
    signal.style.background = "black";
  }
}

// downloads the content and stores it in the browser
async function downloadContent(event) {
  event.target.disabled = true;

  try {
    const metadata = {
      title: "Test content",
      downloaded: new Date(),
    };

    // use shaka.offline.Storage to download the content
    console.log("Downloading content...");
    await window.storage.store(exampleManifestUri, metadata);
    console.log("Content downloaded!");
  } catch (error) {
    console.error(error);
  }
}

function initOfflineStorage(player) {
  /* assign the storage object to the window so that we can
 access it later */
  window.storage = new shaka.offline.Storage(player);
  window.storage.configure({
    offline: {
      progressCallback: setDownloadProgress,
    },
  });
  refreshDownloads();
}

// updates the progress number in the HTML
function setDownloadProgress(_, progress) {
  const progressElement = document.getElementById("download-progress");
  const progressInPercent = progress.toFixed(2) * 100;
  progressElement.setAttribute("value", progressInPercent);
  progressElement.innerText = `${Math.floor(progressInPercent)}%`;

  if (progress === 1) {
    // refresh the download list when the download is finished
    refreshDownloads();
  }
}

// fetches list of downloaded files in indexedDB
async function refreshDownloads() {
  const downloadList = document.getElementById("downloaded-content");
  const content = await window.storage.list();
  downloadList.innerHTML = "";
  if (content.length === 0) {
    const listItem = document.createElement("li");
    listItem.innerText = "No downloads yet";
    downloadList.appendChild(listItem);
    return;
  }

  /* list content from indexedDB and 
add buttons to play and remove content */

  content.forEach((item) => {
    const listItem = document.createElement("li");

    const playButton = document.createElement("button");
    playButton.className = "play-button";
    playButton.innerText = "Play";
    playButton.addEventListener("click", () => {
      window.player.load(item.offlineUri);
    });

    const removeButton = document.createElement("button");
    removeButton.className = "remove-button";
    removeButton.innerText = "Remove";
    removeButton.addEventListener("click", async () => {
      await window.storage.remove(item.offlineUri);
      refreshDownloads();
    });

    listItem.innerText = item.appMetadata.title;
    listItem.appendChild(playButton);
    listItem.appendChild(removeButton);
    downloadList.appendChild(listItem);

    const downloadButton = document.getElementById("download-button");
    downloadButton.disabled = false;
  });
}

document.addEventListener("DOMContentLoaded", initApp);
document.addEventListener("shaka-ui-loaded", initPlayer);
```

> IndexedDB does not work in [CodeSandbox](https://codesandbox.io/), so I have created a small [git repository with the working feature](https://github.com/vanyaxk/shaka-player-offline-playback-test) for you to test it locally.

> You can turn off your connection in DevTools to fully test this feature, like so: 
![DevTools screenshot from the Network tab](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vz73qtmi6qikzuoi7fgw.png)




### Integrating ads

If you are reading this, you got through the hardest part - integrating ads into Shaka is a breeze! Let's start from scratch:

`index.html`:

```html
<!DOCTYPE html>
<html>
  <head>
    <title>Shaka player built-in UI example</title>
    <meta charset="UTF-8" />
    <!-- for the sake of this article 
we use a sandbox path to shaka styles -->
    <link
      rel="stylesheet"
      type="text/css"
      href="node_modules/shaka-player/dist/controls.css"
    />
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>

  <body>
    <!-- We need a container to build the UI on top of it -->
    <div id="container" style="width: 100%; height: 100%">
      <video autoplay id="video"></video>
    </div>

    <!-- add client-side IMA SDK -->
    <script
      type="text/javascript"
      src="https://imasdk.googleapis.com/js/sdkloader/ima3.js"
    ></script>
    <script type="module" src="src/index.js"></script>
  </body>
</html>
```

In our JS file, we will initialize the ad manager after the player initialization: 


```javascript
import shaka from "shaka-player/dist/shaka-player.ui.js";

const exampleManifestUri =
  "https://bitmovin-a.akamaihd.net/content/
MI201109210084_1/mpds
/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd";

function initApp() {
  // Install built-in polyfills to patch browser incompatibilities.
  shaka.polyfill.installAll();

  // Check to see if the browser supports the basic APIs Shaka needs.
  if (!shaka.Player.isBrowserSupported()) {
    // This browser does not have the minimum set of APIs we need.
    console.error("Browser not supported!");
    return;
  }
}

/* This function initializes the player 
and adds the event listeners */
async function initPlayer() {
  const video = document.getElementById("video");
  const container = document.getElementById("container");
  const player = new shaka.Player(video);

  /* we will need this in other functions 
  so we assign it to the window object */
  window.player = player;

  const ui = new shaka.ui.Overlay(player, container, video);
  const controls = ui.getControls();
  controls.addEventListener("error", console.error);

  player.addEventListener("error", console.error);

  /* Initiates the client-side ad manager
   and attaches it to the player */
  const adManager = player.getAdManager();
  const adContainer = video.ui
.getControls()
.getClientSideAdContainer();
  adManager.initClientSide(adContainer, video);
  runSampleAd();
  try {
    await player.load(exampleManifestUri);
  } catch (error) {
    console.error(error);
  }
}

function runSampleAd() {
  /* the script we added to index.html 
adds the google object to the window */
  const google = window.google;
  const adUrl =
    "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923
/external/single_ad_samples&sz=640x480&cust_params=sample_ct%3D
linear&ciu_szs=300x250%2C728x90&gdfp_req=1
&output=vast&unviewed_position_start=1
&env=vp&impl=s&correlator=";
  const adsRequest = new google.ima.AdsRequest();
  adsRequest.adTagUrl = adUrl;

  const adManager = player.getAdManager();
  adManager.requestClientSideAds(adsRequest);
}

document.addEventListener("DOMContentLoaded", initApp);
document.addEventListener("shaka-ui-loaded", initPlayer);
```

> [This GitHub repo](https://github.com/vanyaxk/shaka-player-playback-with-ads-test/) is a working example of this feature, as [Codesandbox](https://codesandbox.io/) does not allow third-party scripts to make requests.

## Pros and cons

It's hard not to be biased in favor of Shaka Player since it is such a versatile library, but I will do my best to be objective.

**Why is Shaka good**:

- Amazing community support, especially from the maintainers.
- You've got most of the video-streaming-related functionality out of the box
- Highly configurable
- Pretty simple basic implementation

**Where can Shaka improve**:

- Typescript support
- Library is not tree-shakable - you get the whole package in your projects regardless of how many parts of it you are using
- Lots of devices/engines with questionable support
- Chromecast issues 

## Conclusion

All in all, Shaka Player is a great library that can instantly bring an ecosystem of video playback-related functionalities to your project. With great community support and relatively simple implementation, the Shaka ecosystem can make playing content on the web smoother and more accessible. This article scratched the surface of what Shaka can do and offered a few working code examples. If you have got anything to ad to this content, feel free!