DRM with ReactJS

Guide to set up Widevine & Fairplay DRM with Shaka Player on ReactJS

Shaka Player is very popular player to play videos on react web applications. It can play both HLS and DASH streams and supports DRM.

To keep the scope of this document limited we expect you have already setup React JS development environment and have an application running.

Step 1: Install the shaka-player package

npm i shaka-player

Step 2: Add the video component to your code and load it with shaka player

For the simplicity of this document we are making the changes in App.js itself and our web application would only have a video player in it.

...

function App() {
  const controllerRef = useRef(null);
  const [assetLoaded, setAssetLoaded] = useState(false);
  // ADAPT: `videoURI` to your video URL
  const videoURI = "https://video.gumlet.com/example-hls-manifest";
  
  const onError = (event) => {
    console.error('Error code', event.detail.code, 'object', event.detail) // eslint-disable-line no-console
  }

  const onPlaybackError = (error) => {
    console.error('Error while loading playback: code', error.code, 'object', error);
  }

  useEffect(() => {
    if(!assetLoaded){
      let video = controllerRef.current;
  
      let player = new shaka.Player(video);

      player.addEventListener('error', onError);

      player.load(videoURI).then(function() {
        setAssetLoaded(true);
        console.log('The video has now been loaded!');
      }).catch(onPlaybackError);
    }
  });

  return (
    <div className="App">
      <video ref={controllerRef} preload="none" autoPlay={false} width="640" height="264" controls muted></video>
    </div>
  );
}

...

If you run the application and navigate to http://localhost:3000/ you would see a video playing. This video was just an example and it is not an encrypted video. We will move to a DRM encrypted video next.

Step 3: Enabling Widevine DRM playback

Widevine DRM playback also needs Licence Server URL along with playback URL to play the content. The licence server URL will inform player the place from where the licence can be acquired.

For security reasons, Licence Server URL needs authentication token to be passed so it knows the request for licence is legitimate, due to this reason the code for generating a Licence Server URL should be on the server and not reside in the web application code.

Refer to this guide to generate the licence server URL on a backend server with your choice of programming language.

🚧

Remember!

Never generate the one time token for widevine licence server on client side. You must generate token from your server and send it to client.

Let us assume that your server root URL is https://example.com and sending a GET request on https://example.com/licence-url returns a licence server URL. We will call the fetch method in our method and use the licence URL sent in the response to initialise the video player with the Licence Server URL.

...

function App() {
  const controllerRef = useRef(null);
  const [assetLoaded, setAssetLoaded] = useState(false);
  
  // ADAPT: `widevineLicenseURI` to your server URI which will be used by the client
  // to fetch the licence.
  const widevineLicenseURI = "https://example.com/widevine-licence-url";
  
  // ADAPT: `videoURI` to your video URL
  const videoURI = "https://video.gumlet.com/example-hls-manifest";
    
  const onError = (event) => {
    console.error('Error code', event.detail.code, 'object', event.detail) // eslint-disable-line no-console
  }
  
  const onPlaybackError = (error) => {
    console.error('Error while loading playback: code', error.code, 'object', error);
  }

  async function loadAssetWithWidevine() {
    const response = await fetch(widevineLicenseURI);
    const json = await response.json();

    let video = controllerRef.current;
  
    let player = new shaka.Player(video);

    player.addEventListener('error', onError);

    player.configure({
      drm: {
        servers: {
          'com.widevine.alpha': json.license
        },
        advanced: {
          'com.widevine.alpha': {
            'videoRobustness': 'SW_SECURE_CRYPTO',
            'audioRobustness': 'SW_SECURE_CRYPTO'
          }
        }
      }
    });

    player.load(videoURI).then(function() {
      setAssetLoaded(true);
      console.log('The video has now been loaded!');
    }).catch(onPlaybackError); 
  }

  useEffect(() => {
    if(!controllerRef.current.canPlayType('application/vnd.apple.mpegurl') && !assetLoaded){
      loadAssetWithWidevine();
    }
  });

  return (
    <div className="App">
        <video ref={controllerRef} preload="none" autoPlay={false} width="640" height="264" controls muted></video>
    </div>
  );
}

...

Open the chrome or a chrome driver based browser and navigate to http://localhost:3000/ your DRM enabled video playback would be playing.

📘

Conditional rendering

Since Widevine is not supported on apple devices we have to conditionally call the player to load with Widevine license URL

Step 4: Enabling Fairplay DRM playback

Fairplay DRM playback needs Licence Server URL and a Certificate along with playback URL to play the content. The licence server URL will inform player the place from where the licence can be acquired.

For security reasons, Licence Server URL needs authentication token to be passed so it knows the request for licence is legitimate, due to this reason the code for generating a Licence Server URL and the Certificate should be on the server and not reside in the web application code.

Refer to this guide to generate the licence server URL on a backend server with your choice of programming language.

...

function App() {
  const controllerRef = useRef(null);
  const [assetLoaded, setAssetLoaded] = useState(false);
  
  // ADAPT: `fairplayLicenseAndCertificateURI` to your server URI to fetch the fairplay
  // licence and certificate from
  const fairplayLicenseAndCertificateURI = "https://example.com/fairplay-licence-and-certificate-url";

  // ADAPT: `widevineLicenseURI` to your server URI to fetch the widevine licence
  const widevineLicenseURI = "https://example.com/widevine-licence-url";
  
  // ADAPT: `videoURI` to your video URL
  const videoURI = "https://video.gumlet.com/example-hls-manifest";
    
  const onError = (event) => {
    console.error('Error code', event.detail.code, 'object', event.detail) // eslint-disable-line no-console
  }
  
  const onPlaybackError = (error) => {
    console.error('Error while loading playback: code', error.code, 'object', error);
  }

  async function loadAssetWithWidevine() {
    ...
  }
  
  async function loadAssetWithFairplay() {
    const response = await fetch(fairplayLicenseAndCertificateURI);
    const json = await response.json();
    
    const req = await fetch(json.certificate);
    const cert = await req.arrayBuffer();

    let video = controllerRef.current;
  
    let player = new shaka.Player(video);

    player.addEventListener('error', onError)

    player.configure({
      drm: {
          servers: {
            'com.apple.fps.1_0': json.license,
          },
          advanced: {
              'com.apple.fps.1_0':{
                serverCertificate: new Uint8Array(cert)
              }
          }
      }
    });

    player.getNetworkingEngine().registerRequestFilter((type, request) => {
      if (type != shaka.net.NetworkingEngine.RequestType.LICENSE) {
          return;
      }
      const originalPayload = new Uint8Array(request.body);
      let spc_string = btoa(String.fromCharCode.apply(null, new Uint8Array(request.body)));
      
      request.headers['Content-Type'] = 'application/json';
      request.body = JSON.stringify({
          "spc" : spc_string
      });
    });

    player.getNetworkingEngine().registerResponseFilter((type, response) => {
        if (type != shaka.net.NetworkingEngine.RequestType.LICENSE) {
            return;
        }

        let responseText = shaka.util.StringUtils.fromUTF8(response.data);
        const parsedResponse = JSON.parse(responseText);
        response.data = shaka.util.Uint8ArrayUtils.fromBase64(parsedResponse.ckc).buffer;
    });

    player.load(videoURI).then(function() {
      setAssetLoaded(true);
      console.log('The video has now been loaded!');
    }).catch(onPlaybackError);
  }

  useEffect(() => {
    if(!controllerRef.current.canPlayType('application/vnd.apple.mpegurl') && !assetLoaded){
      loadAssetWithWidevine();
    }else if (controllerRef.current.canPlayType('application/vnd.apple.mpegurl') && !assetLoaded) {
      loadAssetWithFairplay();
    }
  });

  return (
    <div className="App">
        <video ref={controllerRef} preload="none" autoPlay={false} width="640" height="264" controls muted></video>
    </div>
  );
}

...

Open the safari browser and navigate to http://localhost:3000/ your DRM enabled video playback would be playing.

👍

Done

You can now play your DRM protected videos in a React Web Applications

Full Code Snippet

import React, { useRef, useEffect, useState } from 'react';
import './App.css';
import shaka from 'shaka-player';

function App() {
  const controllerRef = useRef(null);
  const [assetLoaded, setAssetLoaded] = useState(false);

  // ADAPT: `fairplayLicenseAndCertificateURI` to your server URI to fetch the fairplay
  // licence and certificate from
  const fairplayLicenseAndCertificateURI = "https://example.com/fairplay-licence-and-certificate-url";

  // ADAPT: `widevineLicenseURI` to your server URI to fetch the widevine licence
  const widevineLicenseURI = "https://example.com/widevine-licence-url";
  
  // ADAPT: `videoURI` to your video URL
  const videoURI = "https://video.gumlet.com/example-hls-manifest";
  
  const onError = (event) => {
    console.error('Error code', event.detail.code, 'object', event.detail) // eslint-disable-line no-console
  }

  const onPlaybackError = (error) => {
        console.error('Error while loading playback: code', error.code, 'object', error);
  }

  async function loadAssetWithWidevine() {
    const response = await fetch(widevineLicenseURI);
    const json = await response.json();

    let video = controllerRef.current;
  
    let player = new shaka.Player(video);

    player.addEventListener('error', onError);

    player.configure({
      drm: {
        servers: {
          'com.widevine.alpha': json.license
        },
        advanced: {
          'com.widevine.alpha': {
            'videoRobustness': 'SW_SECURE_CRYPTO',
            'audioRobustness': 'SW_SECURE_CRYPTO'
          }
        }
      }
    });

    player.load(videoURI).then(function() {
      setAssetLoaded(true);
      console.log('The video has now been loaded!');
    }).catch(onPlaybackError); 
  }

  async function loadAssetWithFairplay() {
    const response = await fetch(fairplayLicenseAndCertificateURI);
        const json = await response.json();
    
    const req = await fetch(json.certificate);
    const cert = await req.arrayBuffer();

    let video = controllerRef.current;
  
    let player = new shaka.Player(video);

    player.addEventListener('error', onError)

    player.configure({
      drm: {
          servers: {
            'com.apple.fps.1_0': json.license,
          },
          advanced: {
              'com.apple.fps.1_0':{
                serverCertificate: new Uint8Array(cert)
              }
          }
      }
    });

    player.getNetworkingEngine().registerRequestFilter((type, request) => {
      if (type != shaka.net.NetworkingEngine.RequestType.LICENSE) {
          return;
      }
      const originalPayload = new Uint8Array(request.body);
      let spc_string = btoa(String.fromCharCode.apply(null, new Uint8Array(request.body)));
      
      request.headers['Content-Type'] = 'application/json';
      request.body = JSON.stringify({
          "spc" : spc_string
      });
    });

    player.getNetworkingEngine().registerResponseFilter((type, response) => {
        if (type != shaka.net.NetworkingEngine.RequestType.LICENSE) {
            return;
        }

        let responseText = shaka.util.StringUtils.fromUTF8(response.data);
        const parsedResponse = JSON.parse(responseText);
        response.data = shaka.util.Uint8ArrayUtils.fromBase64(parsedResponse.ckc).buffer;
    });

    player.load(videoURI).then(function() {
      setAssetLoaded(true);
      console.log('The video has now been loaded!');
    }).catch(onPlaybackError);
  }

  useEffect(() => {
    if(!controllerRef.current.canPlayType('application/vnd.apple.mpegurl') && !assetLoaded){
      loadAssetWithWidevine();
    }else if (controllerRef.current.canPlayType('application/vnd.apple.mpegurl') && !assetLoaded) {
      loadAssetWithFairplay();
    }
  });

  return (
    <div className="App">
      <video ref={controllerRef} preload="none" autoPlay={false} width="640" height="264" controls muted></video>
    </div>
  );
}

export default App;

You can find working repository of the above code at the following github link


Did this page help you?