Video.js
1. Introduction
This section will provide information on integrating SigmaMultiDRM system into Video.js player:
::: Information
- Widevine:
- Playready:
- Fairplay:
- Staging:
- Production:
- Certificate URI: https://cert.sigmadrm.com/app/fairplay/{MERCHANT_ID}/{APP_ID}
- License URL: https://license.sigmadrm.com/license/verify/fairplay
In which, MERCHANT_ID and APP_ID will be obtained from Dashboard.
:::
2. Requirements
::: Prerequisite
- Html 5 Browsers:
Html 5 browsers | Widevine | PlayReady | FairPlay | License encryption support |
---|---|---|---|---|
Chrome (Window, MacOS, Android, ChromeOS, Linux) | Yes | No | No | Yes |
Firefox (Window, MacOS, Linux) | Yes | No | No | Yes |
Microsoft Edge (Window, MacOS, Android) | Yes | Yes | No | No |
Safari (Safari 8+ on MacOS, Safari on iOS 11.2+) | No | No | Yes | No |
iOS Browser (Chrome, Cốc Cốc, Microsoft Edge, Firefox, Opera) | No | No | Yes | No |
Opera (Window, MacOS) | Yes | No | No | Yes |
Internet Explorer (Window 8.1+) | No | Yes | Yes | No |
- Smart TVs:
Smart TVs | Widevine | PlayReady | FairPlay | License encryption support |
---|---|---|---|---|
SamSung Tizen (2016-2017, 2018+ Models) | Yes | Yes | No | No |
SamSung Tizen&Orsay (2010-2015 Models) | No | Yes | No | No |
LG (WebOS 3.0+) | Yes | Yes | No | No |
LG (WebOS 1.2 & Netcast) | No | Yes | No | No |
Smart TV Alliance (LG, Philips, Toshiba, Panasonic) | Yes | Yes | No | No |
Android TV | Yes | Yes | No | No |
:::
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
Props | Type | Description |
---|---|---|
MERCHANT_ID | String | Id of merchant's user |
APP_ID | String | Id of application |
USER_ID | String | UserId of merchant's client |
SESSION_ID | String | SessionId 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;
}