Skip to content
On this page

Video.js

1. Introduction

This section will provide information on integrating SigmaMultiDRM system into Video.js player:

::: Information

In which, MERCHANT_ID and APP_ID will be obtained from Dashboard.

get_customer_info :::

2. Requirements

::: Prerequisite

  • Html 5 Browsers:
Html 5 browsersWidevinePlayReadyFairPlayLicense encryption support
Chrome (Window, MacOS, Android, ChromeOS, Linux)YesNoNoYes
Firefox (Window, MacOS, Linux)YesNoNoYes
Microsoft Edge (Window, MacOS, Android)YesYesNoNo
Safari (Safari 8+ on MacOS, Safari on iOS 11.2+)NoNoYesNo
iOS Browser (Chrome, Cốc Cốc, Microsoft Edge, Firefox, Opera)NoNoYesNo
Opera (Window, MacOS)YesNoNoYes
Internet Explorer (Window 8.1+)NoYesYesNo
  • Smart TVs:
Smart TVsWidevinePlayReadyFairPlayLicense encryption support
SamSung Tizen (2016-2017, 2018+ Models)YesYesNoNo
SamSung Tizen&Orsay (2010-2015 Models)NoYesNoNo
LG (WebOS 3.0+)YesYesNoNo
LG (WebOS 1.2 & Netcast)NoYesNoNo
Smart TV Alliance (LG, Philips, Toshiba, Panasonic)YesYesNoNo
Android TVYesYesNoNo

:::

3. Integrate license into Video.js Player

Installing the SDK

  • Source: sigma_packer.js

  • Add script:

    html
    <!-- FIXME: If you user license encrypt feature then please uncomment the lines below -->
    <!-- <script src="sigma_packer.js"></script> -->
    <script src="https://vjs.zencdn.net/8.16.1/video.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/videojs-contrib-eme@5.5.1/dist/videojs-contrib-eme.min.js"></script>

Note: The implementation will be presented in the following sections

3.2 Initialize Application

javascript
var player;
var keySystems = {};

document.addEventListener('DOMContentLoaded', function () {
  // FIXME: If you user license encrypt feature then please uncomment the lines below
  // window.sigmaPacker = new SigmaPacker();
  // window.sigmaPacker.onload = () => {
  //   console.log('SigmaPacker loaded');
  // };
  // window.sigmaPacker.init();

  var video = document.getElementById('VIDEO_ID'); // media element
  player = videojs(video, {
    controls: true,
    autoplay: true,
    preload: 'auto',
  });

  player.eme();
  player.src({
    src: MANIFEST_URI,
    type:
      MANIFEST_URI.includes('.mpd') &&
      (MANIFEST_URI.endsWith('.mpd') || MANIFEST_URI.includes('.mpd?'))
        ? 'application/dash+xml'
        : 'application/x-mpegURL',
    keySystems,
  });
});

3. Handling License Requests

3.1. FairPlay

javascript
player.eme.initLegacyFairplay();
keySystems['com.apple.fps.1_0'] = {
  licenseUri: FAIRPLAY_LICENSE_SERVER_URL, // Refer to Section 1
  certificateUri: FAIRPLAY_CERTIFICATE_URI, // Refer to Section 1
  getContentId: function (emeOptions, contentId) {
    return contentId.includes('skd://')
      ? 'https://' + contentId.split('skd://')[1]
      : contentId;
  },
  getLicense: function (emeOptions, contentId, keyMessage, callback) {
    var licenseUri = contentId;

    fetch(licenseUri, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'custom-data': btoa(
          JSON.stringify({
            merchantId: MERCHANT_ID,
            appId: APP_ID,
            userId: USER_ID,
            sessionId: SESSION_ID,
          })
        ),
      },
      body: JSON.stringify({
        spc: uint8ToBase64(keyMessage),
        assetId: new URL(licenseUri).searchParams.get('assetId'),
      }),
    })
      .then((response) => response.json())
      .then((response) => {
        try {
          var licenseBuffer = base64ToArrayBuffer(response.license);
          callback(null, licenseBuffer);
        } catch (error) {
          callback(error, null);
        }
      })
      .catch((error) => {
        console.error('❌ License request failed', error);
        callback(error, null);
      });
  },
};

3.2. Widevine

javascript
keySystems['com.widevine.alpha'] = {
  licenseUri: WIDEVINE_LICENSE_SERVER_URL, // Refer to Section 1
  getLicense: function (emeOptions, keyMessage, callback) {
    var customData = {
      merchantId: MERCHANT_ID,
      appId: APP_ID,
      userId: USER_ID,
      sessionId: SESSION_ID,
    };

    // FIXME: If you user license encrypt feature then please uncomment the lines below
    // var packInfo = window.sigmaPacker.getDataPacker(keyMessage) || {};
    // customData = {
    //   ...customData,
    //   reqId: packInfo.requestId,
    //   deviceInfo: packInfo.deviceInfo,
    // };

    fetch(WIDEVINE_LICENSE_SERVER_URL /* Refer to Section 1 */, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/octet-stream',
        'custom-data': btoa(JSON.stringify(customData)),
      },
      body: keyMessage,
    })
      .then((response) => response.json())
      .then((response) => {
        try {
          // FIXME: If you user license encrypt feature then please uncomment the lines below
          // if (needEncryptLicense && response['client-info']) {
          //   window.sigmaPacker.update(atob(response['client-info']));
          // } else if (response.clientInfo) {
          //   window.sigmaPacker.update(JSON.stringify(response.clientInfo));
          // }

          var licenseBuffer = base64ToArrayBuffer(response.license);
          callback(null, licenseBuffer);
        } catch (error) {
          callback(error, null);
        }
      })
      .catch((error) => {
        console.error('❌ License request failed', error);
        callback(error, null);
      });
  },
};

3.3. PlayReady

javascript
keySystems['com.microsoft.playready'] = {
  licenseUri: PLAYREADY_LICENSE_SERVER_URL, // Refer to Section 1
  getLicense: function (emeOptions, keyMessage, callback) {
    var customData = {
      merchantId: MERCHANT_ID,
      appId: APP_ID,
      userId: USER_ID,
      sessionId: SESSION_ID,
    };

    try {
      var request = unpackPlayReadyRequest(keyMessage);
      // FIXME: If you user license encrypt feature then please uncomment the lines below
      // var packInfo = window.sigmaPacker.getDataPacker(request.body) || {};
      // customData = {
      //   ...customData,
      //   reqId: packInfo.requestId,
      //   deviceInfo: packInfo.deviceInfo,
      // };

      fetch(`https://${licenseDomain}${PLAYREADY_PATH}`, {
        method: 'POST',
        headers: {
          ...request.headers,
          'Content-Type': 'application/octet-stream',
          'custom-data': btoa(JSON.stringify(customData)),
        },
        body: request.body,
      })
        .then((response) => response.json())
        .then((response) => {
          try {
            // FIXME: If you user license encrypt feature then please uncomment the lines below
            // if (needEncryptLicense && response['client-info']) {
            //   window.sigmaPacker.update(atob(response['client-info']));
            // } else if (response.clientInfo) {
            //   window.sigmaPacker.update(JSON.stringify(response.clientInfo));
            // }

            var licenseBuffer = base64ToUint8(response.license);
            callback(null, licenseBuffer);
          } catch (error) {
            callback(error, null);
          }
        })
        .catch((error) => {
          console.error('❌ License request failed', error);
          callback(error, null);
        });
    } catch (error) {
      callback(error, null);
    }
  },
};

Information

Information to be provided

PropsTypeDescription
MERCHANT_IDStringId of merchant's user
APP_IDStringId of application
USER_IDStringUserId of merchant's client
SESSION_IDStringSessionId of merchant's client

Utility Functions

javascript
function base64ToUint8(str = '') {
  const bytes = window.atob(str.replace(/-/g, '+').replace(/_/g, '/'));
  const result = new Uint8Array(bytes.length);
  for (var i = 0; i < bytes.length; ++i) {
    result[i] = bytes.charCodeAt(i);
  }
  return result;
}

function uint8ToBase64(input) {
  input = new Uint8Array(input);
  var keyStr =
    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
  var output = '';
  var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
  var i = 0;

  while (i < input.length) {
    chr1 = input[i++];
    chr2 = i < input.length ? input[i++] : Number.NaN; // Not sure if the index
    chr3 = i < input.length ? input[i++] : Number.NaN; // checks are needed here

    enc1 = chr1 >> 2;
    enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
    enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
    enc4 = chr3 & 63;

    if (isNaN(chr2)) {
      enc3 = enc4 = 64;
    } else if (isNaN(chr3)) {
      enc4 = 64;
    }
    output +=
      keyStr.charAt(enc1) +
      keyStr.charAt(enc2) +
      keyStr.charAt(enc3) +
      keyStr.charAt(enc4);
  }
  return output;
}

function unpackPlayReadyRequest(keyMessage) {
  const request = {
    headers: {},
    body: {},
  };
  const decoder = new TextDecoder('utf-16le');
  const xmlString = decoder.decode(keyMessage);

  if (!xmlString.includes('PlayReadyKeyMessage')) {
    console.debug('PlayReady request is already unwrapped.');
    request.headers['Content-Type'] = 'text/xml; charset=utf-8';
    return request;
  }

  const parser = new DOMParser();
  const xmlDoc = parser.parseFromString(xmlString, 'application/xml');

  if (xmlDoc.querySelector('parsererror')) {
    throw new Error('Failed to parse PlayReady XML!');
  }

  const headers = xmlDoc.querySelectorAll('HttpHeader');
  headers.forEach((header) => {
    const nameElement = header.querySelector('name');
    const valueElement = header.querySelector('value');

    if (!nameElement || !valueElement) {
      throw new Error('Malformed PlayReady headers!');
    }

    request.headers[nameElement.textContent] = valueElement.textContent;
  });

  const challengeElement = xmlDoc.querySelector('Challenge');
  if (
    !challengeElement ||
    challengeElement.getAttribute('encoding') !== 'base64encoded'
  ) {
    throw new Error('Malformed or unexpected PlayReady challenge encoding!');
  }

  request.body = Uint8Array.from(atob(challengeElement.textContent), (c) =>
    c.charCodeAt(0)
  );
  return request;
}

4. Demo

Sample source code