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 and Encrypted Media Extensions. 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 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 explains these concepts in more detail in an accessible way.
MediaCapabilities 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 to cache downloaded videos and subsequently allow for offline playback.
Shaka Player is often used in conjunction with 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 or create respective packaging for HLS.
Basic usage
For the purpose of this article, we will use the freely available DASH manifest from here
To download and use Shaka Player in your project, you can do it in two ways:
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
In order to initiate the player instance, you need a <video>
element in your HTML you can attach to:
<!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
:
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);
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:
<!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:
/* 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);
Now player controls should be visible in your player overlay.
Note: you can use custom shaka styles 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:
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 and UI config - 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.
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, so I have created a small git repository with the working feature for you to test it locally.
You can turn off your connection in DevTools to fully test this feature, like so:
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
:
<!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:
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 is a working example of this feature, as Codesandbox 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!
Top comments (1)
This was helpful. Thanks @vanyaxk