Skip to main content

Sample Fairplay Player

The below is a sample code for playing Fairplay encrypted video using Encrypted-Media-Extensions.

  • Most of this sample is a simple implementation of HTML5 EME
  • The mechanism to exchange SPC and CKC as implemented in licenseRequestReady() is specific to the VdoCipher Key-Security-Module (KSM)

Adapting this example​

The following three variables need to be provided when using this sample code:

VariableDescription
serverCertificatePathURL to the .cer file provided after setting up Fairplay with VdoCipher. You can host this file yourself if needed.
serverProcessSPCPathLicense URL generated using Licensing API
video.srcURL to the m3u8 file

How to handle playback errors​

Error TypeCauseResolution
onCertificateErrorError loading the certificateThis is a simple GET request of a static file. The most likely reason is an issue with the Internet connection
onkeyerrorError loading the license keysThe details of the error event will be useful for this case. TheWebkitMediaKeyError has a systemCode which can be useful in debugging the cause. E.g systemCode of 1212433232 means that the display is not secure enough to play the video.
onerrorGeneral <video /> errorThis error will most likely happen just after one of the other errors. The error even will have a code property to get some details of the error, although the preceding error will be more useful for a resolution.
licenseRequestFailedServer Error from KSMThe response object for this error will contain a JSON body describing the cause of error. The JSON will have structure of {code: string, message, string} and the message can be displayed to the end-user. The code can be looked up from the license error table This error can precede The WebkitMediaKeyError and the <video /> error.

One error can result in another error. In such cases, the very first error event will be the most useful in understanding the cause of failure.

Code​

<!doctype html>
<html>
<!-- Portions Copyright (C) 2015 Apple Inc. All Rights Reserved.
See the Apple Developer Program License Agreement for this file's licensing information.
All use of these materials is subject to the terms of the Apple Developer Program License Agreement.
-->

<!-- Usage Notes:
At least 4 adjustments need to be made for functionality and to meet the needs of the integrator:
a) Path to your FPS certificate
b) Path to your manifest file for Fairplay encrypted HLS
c) Generated license URL

The sections requiring tweaking are marked with the "ADAPT" comment in the code below.
-->

<head>
<title>FPS Template</title>
<script>
/*
The EME specification (https://dvcs.w3.org/hg/html-media/raw-file/tip/encrypted-media/encrypted-media.html)
is supported starting OSX 10.10 and greater.
*/
var keySystem;
var certificate;
// ADAPT: This is the path to the fps certificate on your server.
var serverCertificatePath = '';
// ADAPT: This is the path/URL to the keyserver module that processes the SPC and returns a CKC
var serverProcessSPCPath = '';

function stringToArray(string) {
var buffer = new ArrayBuffer(string.length * 2); // 2 bytes for each char
var array = new Uint16Array(buffer);
for (var i = 0, strLen = string.length; i < strLen; i++) {
array[i] = string.charCodeAt(i);
}
return array;
}

function arrayToString(array) {
var uint16array = new Uint16Array(array.buffer);
return String.fromCharCode.apply(null, uint16array);
}

function base64DecodeUint8Array(input) {
var raw = window.atob(input);
var rawLength = raw.length;
var array = new Uint8Array(new ArrayBuffer(rawLength));

for (i = 0; i < rawLength; i++) array[i] = raw.charCodeAt(i);

return array;
}

function base64EncodeUint8Array(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 waitForEvent(name, action, target) {
target.addEventListener(
name,
function () {
action(arguments[0]);
},
false,
);
}

function loadCertificate() {
var request = new XMLHttpRequest();
request.responseType = 'arraybuffer';
request.addEventListener('load', onCertificateLoaded, false);
request.addEventListener('error', onCertificateError, false);
request.open('GET', serverCertificatePath, true);
request.setRequestHeader('Pragma', 'Cache-Control: no-cache');
request.setRequestHeader('Cache-Control', 'max-age=0');
request.send();
}

function onCertificateLoaded(event) {
var request = event.target;
certificate = new Uint8Array(request.response);
startVideo();
}

function onCertificateError(event) {
window.console.error('Failed to retrieve the server certificate.');
}

function extractContentId(initData) {
contentId = arrayToString(initData.slice(4));
// contentId is passed up as a URI, from which the host must be extracted:
var link = document.createElement('a');
link.href = contentId;
return link.hostname;
}

function concatInitDataIdAndCertificate(initData, id, cert) {
if (typeof id == 'string') id = stringToArray(id);
// layout is [initData][4 byte: idLength][idLength byte: id][4 byte:certLength][certLength byte: cert]
var offset = 0;
var buffer = new ArrayBuffer(
initData.byteLength + 4 + id.byteLength + 4 + cert.byteLength,
);
var dataView = new DataView(buffer);

var initDataArray = new Uint8Array(buffer, offset, initData.byteLength);
initDataArray.set(initData);
offset += initData.byteLength;

dataView.setUint32(offset, id.byteLength, true);
offset += 4;

var idArray = new Uint16Array(buffer, offset, id.length);
idArray.set(id);
offset += idArray.byteLength;

dataView.setUint32(offset, cert.byteLength, true);
offset += 4;

var certArray = new Uint8Array(buffer, offset, cert.byteLength);
certArray.set(cert);

return new Uint8Array(buffer, 0, buffer.byteLength);
}

function selectKeySystem() {
if (WebKitMediaKeys.isTypeSupported('com.apple.fps.1_0', 'video/mp4')) {
keySystem = 'com.apple.fps.1_0';
} else {
throw 'Key System not supported';
}
}

function startVideo() {
var video = document.getElementsByTagName('video')[0];
video.addEventListener('webkitneedkey', onneedkey, false);
video.addEventListener('error', onerror, false);
// ADAPT: there must be logic here to fetch/build the appropriate m3u8 URL
video.src = '';
}

function onerror(event) {
window.console.error('A video playback error occurred');
}

function onneedkey(event) {
var video = event.target;
var initData = event.initData;
var contentId = extractContentId(initData);
initData = concatInitDataIdAndCertificate(
initData,
contentId,
certificate,
);

if (!video.webkitKeys) {
selectKeySystem();
video.webkitSetMediaKeys(new WebKitMediaKeys(keySystem));
}

if (!video.webkitKeys) throw 'Could not create MediaKeys';

var keySession = video.webkitKeys.createSession('video/mp4', initData);
if (!keySession) throw 'Could not create key session';

keySession.contentId = contentId;
waitForEvent('webkitkeymessage', licenseRequestReady, keySession);
waitForEvent('webkitkeyadded', onkeyadded, keySession);
waitForEvent('webkitkeyerror', onkeyerror, keySession);
}

function licenseRequestReady(event) {
var session = event.target;
var message = event.message;
var request = new XMLHttpRequest();
var sessionId = session.sessionId;
request.responseType = 'arraybuffer';
request.session = session;
request.addEventListener('load', licenseRequestLoaded, false);
request.addEventListener('error', licenseRequestFailed, false);
var params = JSON.stringify({
spc: base64EncodeUint8Array(message),
assetId: session.contentId,
sessionId: sessionId,
});
request.open('POST', serverProcessSPCPath, true);
request.setRequestHeader('Content-type', 'application/json');
request.send(params);
}

function licenseRequestLoaded(event) {
var request = event.target;
var session = request.session;
session.update(new Uint8Array(request.response));
}

function licenseRequestFailed(event) {
window.console.error('The license request failed.');
}

function onkeyerror(event) {
window.console.error('A decryption key error was encountered');
}

function onkeyadded(event) {
window.console.log('Decryption key was added to session.');
}
</script>
</head>
<body onload="loadCertificate()">
<video controls autoplay preload="auto" width="960"></video>
</body>
</html>