videojs-contrib-eme
Version:
Supports Encrypted Media Extensions for playback of encrypted content in Video.js
1,657 lines (1,492 loc) • 47.9 kB
JavaScript
import document from 'global/document';
import QUnit from 'qunit';
import videojs from 'video.js';
import window from 'global/window';
import {
defaultGetLicense,
standard5July2016,
makeNewRequest,
getSupportedKeySystem,
addSession,
addPendingSessions,
getSupportedConfigurations
} from '../src/eme';
import { getMockEventBus } from './utils';
import sinon from 'sinon';
// mock session to make testing easier (so we can trigger events)
const getMockSession = () => {
const mockSession = {
addEventListener: (type, listener) => mockSession.listeners.push({ type, listener }),
generateRequest(initDataType, initData) {
// noop
return new Promise((resolve, reject) => resolve());
},
keyStatuses: new Map(),
close: () => {
mockSession.numCloses++;
// fake a promise for easy testing
return {
then: (nextCall) => {
nextCall();
return Promise.resolve();
}
};
},
numCloses: 0,
listeners: []
};
return mockSession;
};
const resolveReject = (rejectVariable, rejectMessage) => {
return new Promise((resolve, reject) => {
if (rejectVariable) {
reject(rejectMessage);
return;
}
resolve();
});
};
QUnit.module('videojs-contrib-eme eme', {
beforeEach() {
this.fixture = document.getElementById('qunit-fixture');
this.video = document.createElement('video');
this.fixture.appendChild(this.video);
this.player = videojs(this.video);
this.origXhr = videojs.xhr;
},
afterEach() {
videojs.xhr = this.origXhr;
}
});
QUnit.test('keystatuseschange triggers keystatuschange on eventBus for each key', function(assert) {
const callCount = {total: 0, 1: {}, 2: {}, 3: {}, 4: {}, 5: {}};
const initData = new Uint8Array([1, 2, 3]);
const mockSession = getMockSession();
const eventBus = {
trigger: (event) => {
if (typeof event === 'string' || event.type !== 'keystatuschange') {
return;
}
if (!callCount[event.keyId][event.status]) {
callCount[event.keyId][event.status] = 0;
}
callCount[event.keyId][event.status]++;
callCount.total++;
},
isDisposed: () => {
return false;
}
};
makeNewRequest(this.player, {
mediaKeys: {
createSession: () => mockSession
},
initDataType: '',
initData,
options: {},
getLicense() {},
removeSession() {},
eventBus,
emeError() {}
});
assert.equal(mockSession.listeners.length, 2, 'added listeners');
assert.equal(
mockSession.listeners[1].type,
'keystatuseschange',
'added keystatuseschange listener'
);
// no key statuses
mockSession.listeners[1].listener();
assert.equal(callCount.total, 0, 'no events dispatched yet');
mockSession.keyStatuses.set(1, 'unrecognized');
mockSession.keyStatuses.set(2, 'expired');
mockSession.keyStatuses.set(3, 'internal-error');
mockSession.keyStatuses.set(4, 'output-restricted');
mockSession.keyStatuses.set(5, 'output-restricted');
mockSession.listeners[1].listener();
assert.equal(
callCount[1].unrecognized, 1,
'dispatched `unrecognized` key status for key 1'
);
assert.equal(
callCount[2].expired, 1,
'dispatched `expired` key status for key 2'
);
assert.equal(
callCount[3]['internal-error'], 1,
'dispatched `internal-error` key status for key 3'
);
assert.equal(
callCount[4]['output-restricted'], 1,
'dispatched `output-restricted` key status for key 4'
);
assert.equal(
callCount[5]['output-restricted'], 1,
'dispatched `output-restricted` key status for key 5'
);
assert.equal(callCount.total, 5, '5 keystatuschange events received so far');
// Change a single key and check that it's triggered for all keys
mockSession.keyStatuses.set(1, 'usable');
mockSession.listeners[1].listener();
assert.equal(
callCount[1].usable, 1,
'dispatched `usable` key status for key 1'
);
assert.equal(
callCount[2].expired, 2,
'dispatched `expired` key status for key 2'
);
assert.equal(
callCount[3]['internal-error'], 2,
'dispatched `internal-error` key status for key 3'
);
assert.equal(
callCount[4]['output-restricted'], 2,
'dispatched `output-restricted` key status for key 4'
);
assert.equal(
callCount[5]['output-restricted'], 2,
'dispatched `output-restricted` key status for key 5'
);
assert.equal(callCount.total, 10, '10 keystatuschange events received so far');
// Change the key statuses and recheck
mockSession.keyStatuses.set(1, 'output-downscaled');
mockSession.keyStatuses.set(2, 'released');
mockSession.keyStatuses.set(3, 'usable');
mockSession.keyStatuses.set(4, 'status-pending');
mockSession.keyStatuses.set(5, 'usable');
mockSession.listeners[1].listener();
assert.equal(
callCount[1]['output-downscaled'], 1,
'dispatched `output-downscaled` key status for key 1'
);
assert.equal(
callCount[2].released, 1,
'dispatched `released` key status for key 2'
);
assert.equal(
callCount[3].usable, 1,
'dispatched `usable` key status for key 3'
);
assert.equal(
callCount[4]['status-pending'], 1,
'dispatched `status-pending` key status for key 4'
);
assert.equal(
callCount[5].usable, 1,
'dispatched `usable` key status for key 5'
);
assert.equal(callCount.total, 15, '15 keystatuschange events received so far');
});
QUnit.test('keystatuseschange with expired key closes and recreates session', function(assert) {
const removeSessionCalls = [];
// once the eme module gets the removeSession function, the session argument is already
// bound to the function (note that it's a custom session maintained by the plugin, not
// the native session), so only initData is passed
const removeSession = (initData) => removeSessionCalls.push(initData);
const initData = new Uint8Array([1, 2, 3]);
const mockSession = getMockSession();
const eventBus = {
trigger: (name) => {},
isDisposed: () => {
return false;
}
};
let creates = 0;
makeNewRequest(this.player, {
mediaKeys: {
createSession: () => {
creates++;
return mockSession;
}
},
initDataType: '',
initData,
options: {},
getLicense() {},
removeSession,
eventBus
});
assert.equal(creates, 1, 'created session');
assert.equal(mockSession.listeners.length, 2, 'added listeners');
assert.equal(
mockSession.listeners[1].type,
'keystatuseschange',
'added keystatuseschange listener'
);
assert.equal(mockSession.numCloses, 0, 'no session close calls');
assert.equal(removeSessionCalls.length, 0, 'no removeSession calls');
// no key statuses
mockSession.listeners[1].listener();
assert.equal(mockSession.numCloses, 0, 'no session close calls');
assert.equal(removeSessionCalls.length, 0, 'no removeSession calls');
mockSession.keyStatuses.set(1, 'unrecognized');
mockSession.listeners[1].listener();
assert.equal(mockSession.numCloses, 0, 'no session close calls');
assert.equal(removeSessionCalls.length, 0, 'no removeSession calls');
mockSession.keyStatuses.set(2, 'expired');
mockSession.listeners[1].listener();
assert.equal(mockSession.numCloses, 1, 'closed session');
// close promise is fake and resolves synchronously, so we can assert removes
// synchronously
assert.equal(removeSessionCalls.length, 1, 'called remove session');
assert.equal(removeSessionCalls[0], initData, 'called to remove session with initData');
assert.equal(creates, 2, 'created another session');
});
QUnit.test('keystatuseschange with internal-error logs a warning', function(assert) {
const origWarn = videojs.log.warn;
const initData = new Uint8Array([1, 2, 3]);
const mockSession = getMockSession();
const warnCalls = [];
const eventBus = {
trigger: (name) => {},
isDisposed: () => {
return false;
}
};
videojs.log.warn = (...args) => warnCalls.push(args);
makeNewRequest(this.player, {
mediaKeys: {
createSession: () => mockSession
},
initDataType: '',
initData,
options: {},
getLicense() {},
removeSession() {},
eventBus
});
assert.equal(mockSession.listeners.length, 2, 'added listeners');
assert.equal(
mockSession.listeners[1].type,
'keystatuseschange',
'added keystatuseschange listener'
);
// no key statuses
mockSession.listeners[1].listener();
assert.equal(warnCalls.length, 0, 'no warn logs');
mockSession.keyStatuses.set(1, 'internal-error');
const keyStatusChangeEvent = {};
mockSession.listeners[1].listener(keyStatusChangeEvent);
assert.equal(warnCalls.length, 1, 'one warn log');
assert.equal(
warnCalls[0][0],
'Key status reported as "internal-error." Leaving the session open ' +
'since we don\'t have enough details to know if this error is fatal.',
'logged correct warning'
);
assert.equal(warnCalls[0][1], keyStatusChangeEvent, 'logged event object');
videojs.log.warn = origWarn;
});
QUnit.test('accepts a license URL as an option', function(assert) {
const done = assert.async();
const origXhr = videojs.xhr;
const xhrCalls = [];
const mockSession = getMockSession();
const mockEventBus = getMockEventBus();
const mockMessageEvent = {
type: 'message',
message: 'the-message',
messageType: 'license-request'
};
videojs.xhr = (options) => {
xhrCalls.push(options);
};
const createSession = () => mockSession;
const keySystemAccess = {
keySystem: 'com.widevine.alpha',
createMediaKeys: () => {
return {
createSession
};
}
};
standard5July2016({
player: this.player,
keySystemAccess,
video: {
setMediaKeys: (createdMediaKeys) => Promise.resolve(createdMediaKeys)
},
initDataType: '',
initData: '',
options: {
keySystems: {
'com.widevine.alpha': 'some-url'
}
},
eventBus: mockEventBus
}).catch((e) => {});
setTimeout(() => {
assert.equal(mockSession.listeners.length, 2, 'added listeners');
assert.equal(
mockSession.listeners[0].type,
'message',
'added message listener'
);
// Simulate 'message' event
mockSession.listeners[0].listener(mockMessageEvent);
assert.equal(mockEventBus.calls[0].type, 'keysystemaccesscomplete', 'keysystemaccesscomplete fired');
assert.deepEqual(mockEventBus.calls[0].mediaKeys, { createSession }, 'keysystemaccesscomplete payload fired');
assert.equal(mockEventBus.calls[1].type, 'keysessioncreated', 'keysessioncreated fired');
assert.equal(mockEventBus.calls[1].keySession, mockSession, 'keysessioncreated payload fired');
assert.equal(mockEventBus.calls[2].type, 'keymessage', 'keymessage event type is expected type');
assert.equal(mockEventBus.calls[2].messageEvent, mockMessageEvent, 'keymessage event is expected message event');
assert.equal(xhrCalls.length, 1, 'made one XHR');
assert.deepEqual(xhrCalls[0], {
uri: 'some-url',
method: 'POST',
responseType: 'arraybuffer',
requestType: 'license',
metadata: { keySystem: 'com.widevine.alpha' },
body: 'the-message',
headers: {
'Content-type': 'application/octet-stream'
}
}, 'made request with proper options');
videojs.xhr = origXhr;
done();
});
});
QUnit.test('accepts a license URL as property', function(assert) {
const done = assert.async();
const origXhr = videojs.xhr;
const xhrCalls = [];
const mockSession = getMockSession();
const mockEventBus = getMockEventBus();
const createSession = () => mockSession;
const mockMessageEvent = {
type: 'message',
message: 'the-message',
messageType: 'license-request'
};
const keySystemAccess = {
keySystem: 'com.widevine.alpha',
createMediaKeys: () => {
return {
createSession
};
}
};
videojs.xhr = (options) => {
xhrCalls.push(options);
};
standard5July2016({
player: this.player,
keySystemAccess,
video: {
setMediaKeys: (createdMediaKeys) => Promise.resolve(createdMediaKeys)
},
initDataType: '',
initData: '',
options: {
keySystems: {
'com.widevine.alpha': {
url: 'some-url'
}
}
},
eventBus: mockEventBus
}).catch((e) => {});
setTimeout(() => {
assert.equal(mockSession.listeners.length, 2, 'added listeners');
assert.equal(
mockSession.listeners[0].type,
'message',
'added message listener'
);
// Simulate 'message' event
mockSession.listeners[0].listener(mockMessageEvent);
assert.equal(mockEventBus.calls[0].type, 'keysystemaccesscomplete', 'keysystemaccesscomplete fired');
assert.deepEqual(mockEventBus.calls[0].mediaKeys, { createSession }, 'keysystemaccesscomplete payload');
assert.equal(mockEventBus.calls[1].type, 'keysessioncreated', 'keymessage fired');
assert.equal(mockEventBus.calls[2].messageEvent, mockMessageEvent, 'keymessage event is expected message event');
assert.equal(xhrCalls.length, 1, 'made one XHR');
assert.deepEqual(xhrCalls[0], {
uri: 'some-url',
method: 'POST',
responseType: 'arraybuffer',
requestType: 'license',
metadata: { keySystem: 'com.widevine.alpha' },
body: 'the-message',
headers: {
'Content-type': 'application/octet-stream'
}
}, 'made request with proper options');
videojs.xhr = origXhr;
done();
});
});
QUnit.test('5 July 2016 lifecycle', function(assert) {
assert.expect(34);
let errors = 0;
const done = assert.async();
const callbacks = {};
const callCounts = {
getCertificate: 0,
getLicense: 0,
createSession: 0,
keySessionGenerateRequest: 0,
keySessionUpdate: 0,
createMediaKeys: 0,
licenseRequestAttempts: 0,
keysessionUpdatedEvent: 0
};
const getCertificate = (emeOptions, callback) => {
callCounts.getCertificate++;
callbacks.getCertificate = callback;
};
const getLicense = (emeOptions, keyMessage, callback) => {
callCounts.getLicense++;
callbacks.getLicense = callback;
};
let setMediaKeys;
const video = {
setMediaKeys: (mediaKeys) => {
setMediaKeys = mediaKeys;
return Promise.resolve(mediaKeys);
}
};
const options = {
keySystems: {
'org.w3.clearkey': {
getCertificate,
getLicense
}
}
};
const eventBus = {
trigger: (event) => {
const name = typeof event === 'string' ? event : event.type;
if (name === 'licenserequestattempted') {
callCounts.licenseRequestAttempts++;
}
if (name === 'keysessionupdated') {
callCounts.keysessionUpdatedEvent++;
}
},
isDisposed: () => {
return false;
}
};
const keySessionEventListeners = {};
const mediaKeys = {
createSession: () => {
callCounts.createSession++;
return {
addEventListener: (name, callback) => {
keySessionEventListeners[name] = callback;
},
generateRequest: () => {
callCounts.keySessionGenerateRequest++;
return new Promise(() => {});
},
update: () => {
callCounts.keySessionUpdate++;
return Promise.resolve();
},
close: () => {}
};
}
};
const keySystemAccess = {
keySystem: 'org.w3.clearkey',
createMediaKeys: () => {
callCounts.createMediaKeys++;
return mediaKeys;
}
};
standard5July2016({
player: this.player,
video,
initDataType: '',
initData: '',
keySystemAccess,
options,
eventBus
}).then(() => done()).catch(() => errors++);
// Step 1: get certificate
assert.equal(callCounts.getCertificate, 1, 'certificate requested');
assert.equal(callCounts.createMediaKeys, 0, 'media keys not created');
assert.notEqual(mediaKeys, setMediaKeys, 'media keys not yet set');
assert.equal(callCounts.createSession, 0, 'key session not created');
assert.equal(callCounts.keySessionGenerateRequest, 0, 'key session request not made');
assert.equal(callCounts.getLicense, 0, 'license not requested');
assert.equal(callCounts.keySessionUpdate, 0, 'key session not updated');
assert.equal(
callCounts.licenseRequestAttempts, 0,
'license request event not triggered (since no callback yet)'
);
callbacks.getCertificate(null, '');
// getCertificate promise resolution
setTimeout(() => {
// Step 2: create media keys, set them, and generate key session request
assert.equal(callCounts.getCertificate, 1, 'certificate requested');
assert.equal(callCounts.createMediaKeys, 1, 'media keys created');
assert.equal(mediaKeys, setMediaKeys, 'media keys set');
assert.equal(callCounts.createSession, 1, 'key session created');
assert.equal(callCounts.keySessionGenerateRequest, 1, 'key session request made');
assert.equal(callCounts.getLicense, 0, 'license not requested');
assert.equal(callCounts.keySessionUpdate, 0, 'key session not updated');
assert.equal(
callCounts.licenseRequestAttempts, 0,
'license request event not triggered (since no callback yet)'
);
keySessionEventListeners.message({messageType: 'license-request'});
// Step 3: get license
assert.equal(callCounts.getCertificate, 1, 'certificate requested');
assert.equal(callCounts.createMediaKeys, 1, 'media keys created');
assert.equal(mediaKeys, setMediaKeys, 'media keys set');
assert.equal(callCounts.createSession, 1, 'key session created');
assert.equal(callCounts.keySessionGenerateRequest, 1, 'key session request made');
assert.equal(callCounts.getLicense, 1, 'license requested');
assert.equal(callCounts.keySessionUpdate, 0, 'key session not updated');
assert.equal(
callCounts.licenseRequestAttempts, 0,
'license request event not triggered (since no callback yet)'
);
callbacks.getLicense();
// getLicense promise resolution
setTimeout(() => {
// Step 4: update key session
assert.equal(callCounts.getCertificate, 1, 'certificate requested');
assert.equal(callCounts.createMediaKeys, 1, 'media keys created');
assert.equal(mediaKeys, setMediaKeys, 'media keys set');
assert.equal(callCounts.createSession, 1, 'key session created');
assert.equal(callCounts.keySessionGenerateRequest, 1, 'key session request made');
assert.equal(callCounts.getLicense, 1, 'license requested');
assert.equal(callCounts.keySessionUpdate, 1, 'key session updated');
assert.equal(
callCounts.licenseRequestAttempts, 1,
'license request event triggered'
);
assert.equal(callCounts.keysessionUpdatedEvent, 1, 'keysessionupdated event fired once');
assert.equal(errors, 0, 'no errors occurred');
});
});
});
// Skip this test in Safari, getSupportedKeySystem is never used in Safari.
if (!videojs.browser.IS_ANY_SAFARI) {
QUnit.test('getSupportedKeySystem error', function(assert) {
const done = assert.async(1);
getSupportedKeySystem({'un.supported.keysystem': {}}).catch((err) => {
assert.equal(err.name, 'NotSupportedError', 'keysystem access request fails');
done();
});
});
}
QUnit.test('errors when missing url/licenseUri or getLicense', function(assert) {
const options = {
keySystems: {
'com.widevine.alpha': {}
}
};
const keySystemAccess = {
keySystem: 'com.widevine.alpha'
};
const done = assert.async(1);
standard5July2016({
player: this.player,
video: {},
keySystemAccess,
options,
eventBus: getMockEventBus()
}).catch((err) => {
assert.equal(
err,
'Error: Missing url/licenseUri or getLicense in com.widevine.alpha keySystem configuration.',
'correct error message'
);
done();
});
});
QUnit.test('errors when missing certificateUri and getCertificate for fairplay', function(assert) {
const options = {
keySystems: {
'com.apple.fps': {url: 'fake-url'}
}
};
const keySystemAccess = {
keySystem: 'com.apple.fps'
};
const done = assert.async();
standard5July2016({
player: this.player,
video: {},
keySystemAccess,
options
}).catch((err) => {
assert.equal(
err,
'Error: Missing getCertificate or certificateUri in com.apple.fps keySystem configuration.',
'correct error message'
);
done();
});
});
QUnit.test('rejects promise when getCertificate throws error', function(assert) {
const getCertificate = (options, callback) => {
callback('error fetching certificate');
};
const options = {
keySystems: {
'com.widevine.alpha': {
url: 'some-url',
getCertificate
}
}
};
const keySystemAccess = {
keySystem: 'com.widevine.alpha'
};
const done = assert.async(1);
const expectedError = 'error fetching certificate';
const emeError = (error, metadata) => {
assert.equal(error, expectedError, 'emeError called with expected message');
assert.equal(metadata.errorType, videojs.Error.EMEFailedToCreateMediaKeys, 'emeError called with expected type');
assert.equal(metadata.keySystem, 'com.widevine.alpha', 'emeError called with expected type');
};
standard5July2016({
player: this.player,
video: {},
keySystemAccess,
options,
eventBus: getMockEventBus(),
emeError
}).catch((err) => {
assert.equal(err, expectedError, 'correct error message');
done();
});
});
QUnit.test('rejects promise when createMediaKeys rejects', function(assert) {
const options = {
keySystems: {
'com.widevine.alpha': 'some-url'
}
};
const keySystemAccess = {
keySystem: 'com.widevine.alpha',
createMediaKeys: () => {
return Promise.reject();
}
};
const done = assert.async(1);
const emeError = (_, metadata) => {
assert.equal(metadata.errorType, videojs.Error.EMEFailedToCreateMediaKeys, 'emeError called with expected errorType');
assert.equal(metadata.keySystem, 'com.widevine.alpha', 'emeError called with expected keySystem');
};
standard5July2016({
player: this.player,
video: {},
keySystemAccess,
options,
eventBus: getMockEventBus(),
emeError
}).catch((err) => {
assert.equal(
err, 'Failed to create and initialize a MediaKeys object',
'uses generic message'
);
done();
});
});
QUnit.test('rejects promise when createMediaKeys rejects', function(assert) {
const options = {
keySystems: {
'com.widevine.alpha': 'some-url'
}
};
const keySystemAccess = {
keySystem: 'com.widevine.alpha',
createMediaKeys: () => {
return Promise.reject('failed creating mediaKeys');
}
};
const done = assert.async(1);
const expectedError = 'failed creating mediaKeys';
const emeError = (error, metadata) => {
assert.equal(error, expectedError, 'emeError called with expected error');
assert.equal(metadata.errorType, videojs.Error.EMEFailedToCreateMediaKeys, 'emeError called with expected errorType');
assert.equal(metadata.keySystem, 'com.widevine.alpha', 'emeError called with expected keySystem');
};
standard5July2016({
player: this.player,
video: {},
keySystemAccess,
options,
eventBus: getMockEventBus(),
emeError
}).catch((err) => {
assert.equal(err, expectedError, 'uses specific error when given');
done();
});
});
QUnit.test('rejects promise when addPendingSessions rejects', function(assert) {
let rejectSetServerCertificate = true;
const rejectGenerateRequest = true;
let rejectSetMediaKeys = true;
const options = {
keySystems: {
'com.widevine.alpha': {
url: 'some-url',
getCertificate: (emeOptions, callback) => {
callback(null, 'some certificate');
}
}
}
};
const keySystemAccess = {
keySystem: 'com.widevine.alpha',
createMediaKeys: () => {
return Promise.resolve({
setServerCertificate: () => resolveReject(
rejectSetServerCertificate,
'setServerCertificate failed'
),
createSession: () => {
return {
addEventListener: () => {},
generateRequest: () => resolveReject(
rejectGenerateRequest,
'generateRequest failed'
),
close: () => {}
};
}
});
}
};
const video = {
setMediaKeys: () => resolveReject(rejectSetMediaKeys, 'setMediaKeys failed')
};
const done = assert.async(3);
const callbacks = [];
const expectedErrors = [
{
error: 'setServerCertificate failed',
errorType: videojs.Error.EMEFailedToSetServerCertificate
},
{
error: 'setMediaKeys failed',
errorType: videojs.Error.EMEFailedToAttachMediaKeysToVideoElement
},
{
error: 'generateRequest failed',
errorType: videojs.Error.EMEFailedToGenerateLicenseRequest
}
];
const test = (errMessage, testDescription) => {
let expectedErrorsLength = 0;
const emeErrors = [];
video.mediaKeysObject = undefined;
standard5July2016({
player: this.player,
video,
keySystemAccess,
options,
eventBus: getMockEventBus(),
emeError: (error, metadata) => {
expectedErrorsLength++;
emeErrors.push({error, errorType: metadata.errorType });
}
}).catch((err) => {
assert.equal(err, errMessage, testDescription);
assert.equal(emeErrors.length, expectedErrorsLength, 'emeError called expected number of times');
for (let i = 0; i < expectedErrors.length; i++) {
assert.equal(emeErrors[i].error, expectedErrors[i].error, 'expected eme error');
assert.equal(emeErrors[i].errorType, expectedErrors[i].errorType, 'expected eme errorType');
}
expectedErrors.shift();
done();
if (callbacks[0]) {
callbacks.shift()();
}
});
};
callbacks.push(() => {
rejectSetServerCertificate = false;
test('Unable to create or initialize key session', 'second promise fails');
});
callbacks.push(() => {
rejectSetMediaKeys = false;
test('Unable to create or initialize key session', 'third promise fails');
});
test('Unable to create or initialize key session', 'first promise fails');
});
QUnit.test('getLicense not called for messageType that isnt license-request or license-renewal', function(assert) {
const done = assert.async();
let getLicenseCalls = 0;
const options = {
keySystems: {
'com.widevine.alpha': {
url: 'some-url',
getLicense(emeOptions, keyMessage, callback) {
getLicenseCalls++;
}
}
}
};
const keySystemAccess = {
keySystem: 'com.widevine.alpha',
createMediaKeys: () => {
return Promise.resolve({
setServerCertificate: () => Promise.resolve(),
createSession: () => {
return {
addEventListener: (event, callback) => {
if (event === 'message') {
setTimeout(() => {
callback({message: 'whatever', messageType: 'do-not-request-license'});
assert.equal(getLicenseCalls, 0, 'did not call getLicense');
done();
});
}
},
keyStatuses: [],
generateRequest: () => Promise.resolve(),
close: () => {}
};
}
});
}
};
const video = {
setMediaKeys: () => Promise.resolve()
};
standard5July2016({
player: this.player,
video,
keySystemAccess,
options,
eventBus: getMockEventBus()
});
});
QUnit.test('getLicense promise rejection', function(assert) {
const options = {
keySystems: {
'com.widevine.alpha': {
url: 'some-url',
getLicense(emeOptions, keyMessage, callback) {
callback('error getting license');
}
}
}
};
const keySystemAccess = {
keySystem: 'com.widevine.alpha',
createMediaKeys: () => {
return Promise.resolve({
setServerCertificate: () => Promise.resolve(),
createSession: () => {
return {
addEventListener: (event, callback) => {
setTimeout(() => {
callback({message: 'whatever', messageType: 'license-request'});
});
},
keyStatuses: [],
generateRequest: () => Promise.resolve(),
close: () => {}
};
}
});
}
};
const video = {
setMediaKeys: () => Promise.resolve()
};
const done = assert.async(1);
standard5July2016({
player: this.player,
video,
keySystemAccess,
options,
eventBus: getMockEventBus()
}).catch((err) => {
assert.equal(err, 'error getting license', 'correct error message');
done();
});
});
QUnit.test('getLicense calls back with error for 400 and 500 status codes', function(assert) {
const getLicenseCallback = sinon.spy();
const getLicense = defaultGetLicense('', {});
function toArrayBuffer(obj) {
const json = JSON.stringify(obj);
const buffer = new ArrayBuffer(json.length);
const bufferView = new Uint8Array(buffer);
for (let i = 0; i < json.length; i++) {
bufferView[i] = json.charCodeAt(i);
}
return buffer;
}
videojs.xhr = (params, callback) => {
return callback(null, {statusCode: 400}, toArrayBuffer({body: 'some-body'}));
};
getLicense({}, null, getLicenseCallback);
videojs.xhr = (params, callback) => {
return callback(null, {statusCode: 500}, toArrayBuffer({body: 'some-body'}));
};
getLicense({}, null, getLicenseCallback);
videojs.xhr = (params, callback) => {
return callback(null, {statusCode: 599}, toArrayBuffer({body: 'some-body'}));
};
getLicense({}, null, getLicenseCallback);
assert.equal(getLicenseCallback.callCount, 3, 'correct callcount');
assert.ok(getLicenseCallback.alwaysCalledWith({
cause: JSON.stringify({body: 'some-body'})
}), 'getLicense callback called with correct error');
});
QUnit.test('getLicense calls back with response body for non-400/500 status codes', function(assert) {
const getLicenseCallback = sinon.spy();
const getLicense = defaultGetLicense('', {});
videojs.xhr = (params, callback) => {
return callback(null, {statusCode: 200}, {body: 'some-body'});
};
getLicense({}, null, getLicenseCallback);
videojs.xhr = (params, callback) => {
return callback(null, {statusCode: 399}, {body: 'some-body'});
};
getLicense({}, null, getLicenseCallback);
videojs.xhr = (params, callback) => {
return callback(null, {statusCode: 600}, {body: 'some-body'});
};
getLicense({}, null, getLicenseCallback);
assert.equal(getLicenseCallback.callCount, 3, 'correct callcount');
assert.equal(getLicenseCallback.alwaysCalledWith(null, {body: 'some-body'}), true, 'getLicense callback called with correct args');
});
QUnit.test('keySession.update promise rejection', function(assert) {
const options = {
keySystems: {
'com.widevine.alpha': {
url: 'some-url',
getLicense(emeOptions, keyMessage, callback) {
callback(null, 'license');
}
}
}
};
const keySystemAccess = {
keySystem: 'com.widevine.alpha',
createMediaKeys: () => {
return Promise.resolve({
setServerCertificate: () => Promise.resolve(),
createSession: () => {
return {
addEventListener: (event, callback) => {
setTimeout(() => {
callback({messageType: 'license-request', message: 'whatever'});
});
},
keyStatuses: [],
generateRequest: () => Promise.resolve(),
update: () => Promise.reject('keySession update failed'),
close: () => {}
};
}
});
}
};
const video = {
setMediaKeys: () => Promise.resolve()
};
const done = assert.async(1);
const emeError = (error, metadata) => {
assert.equal(error, 'keySession update failed', 'correct error message');
assert.equal(metadata.errorType, videojs.Error.EMEFailedToUpdateSessionWithReceivedLicenseKeys, 'errorType is correct');
assert.equal(metadata.keySystem, 'com.widevine.alpha', 'keySystem is correct');
done();
};
standard5July2016({
player: this.player,
video,
keySystemAccess,
options,
eventBus: getMockEventBus(),
emeError
});
});
QUnit.test('emeHeaders option sets headers on default license xhr request', function(assert) {
const done = assert.async();
const origXhr = videojs.xhr;
const xhrCalls = [];
const mockSession = getMockSession();
const mockEventBus = getMockEventBus();
const mockMessageEvent = {
type: 'message',
message: 'the-message',
messageType: 'license-request'
};
const createSession = () => mockSession;
videojs.xhr = (options) => {
xhrCalls.push(options);
};
const keySystemAccess = {
keySystem: 'com.widevine.alpha',
createMediaKeys: () => {
return {
createSession
};
}
};
standard5July2016({
player: this.player,
keySystemAccess,
video: {
setMediaKeys: (createdMediaKeys) => Promise.resolve(createdMediaKeys)
},
initDataType: '',
initData: '',
options: {
keySystems: {
'com.widevine.alpha': 'some-url'
},
emeHeaders: {
'Some-Header': 'some-header-value'
}
},
eventBus: mockEventBus
}).catch((e) => {});
setTimeout(() => {
// Simulate 'message' event
mockSession.listeners[0].listener(mockMessageEvent);
assert.equal(mockEventBus.calls[0].type, 'keysystemaccesscomplete', 'keysystemaccesscomplete fired');
assert.deepEqual(mockEventBus.calls[0].mediaKeys, { createSession }, 'keysystemaccesscomplete payload');
assert.equal(mockEventBus.calls[1].type, 'keysessioncreated', 'keymessage fired');
assert.equal(mockEventBus.calls[2].messageEvent, mockMessageEvent, 'keymessage event is expected message event');
assert.equal(xhrCalls.length, 1, 'made one XHR');
assert.deepEqual(xhrCalls[0], {
uri: 'some-url',
method: 'POST',
responseType: 'arraybuffer',
requestType: 'license',
metadata: { keySystem: 'com.widevine.alpha' },
body: 'the-message',
headers: {
'Content-type': 'application/octet-stream',
'Some-Header': 'some-header-value'
}
}, 'made request with proper emeHeaders option value');
videojs.xhr = origXhr;
done();
});
});
QUnit.test('licenseHeaders keySystems property overrides emeHeaders value', function(assert) {
const done = assert.async();
const origXhr = videojs.xhr;
const xhrCalls = [];
const mockSession = getMockSession();
const mockEventBus = getMockEventBus();
const mockMessageEvent = {
type: 'message',
message: 'the-message',
messageType: 'license-request'
};
const createSession = () => mockSession;
videojs.xhr = (options) => {
xhrCalls.push(options);
};
const keySystemAccess = {
keySystem: 'com.widevine.alpha',
createMediaKeys: () => {
return {
createSession
};
}
};
standard5July2016({
player: this.player,
keySystemAccess,
video: {
setMediaKeys: (createdMediaKeys) => Promise.resolve(createdMediaKeys)
},
initDataType: '',
initData: '',
options: {
keySystems: {
'com.widevine.alpha': {
url: 'some-url',
licenseHeaders: {
'Some-Header': 'priority-header-value'
}
}
},
emeHeaders: {
'Some-Header': 'lower-priority-header-value'
}
},
eventBus: mockEventBus
}).catch((e) => {});
setTimeout(() => {
// Simulate 'message' event
mockSession.listeners[0].listener(mockMessageEvent);
assert.equal(mockEventBus.calls[0].type, 'keysystemaccesscomplete', 'keysystemaccesscomplete fired');
assert.deepEqual(mockEventBus.calls[0].mediaKeys, { createSession }, 'keysystemaccesscomplete payload');
assert.equal(mockEventBus.calls[1].type, 'keysessioncreated', 'keymessage fired');
assert.equal(mockEventBus.calls[1].keySession, mockSession, 'keymessage payload');
assert.equal(mockEventBus.calls[2].type, 'keymessage', 'keymessage event is expected message event');
assert.equal(mockEventBus.calls[2].messageEvent, mockMessageEvent, 'keymessage event is expected message event');
assert.equal(xhrCalls.length, 1, 'made one XHR');
assert.deepEqual(xhrCalls[0], {
uri: 'some-url',
method: 'POST',
responseType: 'arraybuffer',
requestType: 'license',
metadata: { keySystem: 'com.widevine.alpha' },
body: 'the-message',
headers: {
'Content-type': 'application/octet-stream',
'Some-Header': 'priority-header-value'
}
}, 'made request with proper licenseHeaders value');
videojs.xhr = origXhr;
done();
});
});
QUnit.test('sets required fairplay defaults if not explicitly configured', function(assert) {
const origRequestMediaKeySystemAccess = window.navigator.requestMediaKeySystemAccess;
window.navigator.requestMediaKeySystemAccess = (keySystem, systemOptions) => {
assert.ok(
systemOptions[0].initDataTypes.indexOf('sinf') !== -1,
'includes required initDataType'
);
assert.ok(
systemOptions[0].videoCapabilities[0].contentType.indexOf('video/mp4') !== -1,
'includes required video contentType'
);
};
getSupportedKeySystem({'com.apple.fps': {}});
window.requestMediaKeySystemAccess = origRequestMediaKeySystemAccess;
});
QUnit.test('makeNewRequest triggers keysessioncreated', function(assert) {
const done = assert.async();
const mockSession = getMockSession();
makeNewRequest(this.player, {
mediaKeys: {
createSession: () => mockSession
},
eventBus: {
trigger: (event) => {
if (event.type === 'keysessioncreated') {
assert.ok(true, 'got a keysessioncreated event');
done();
}
},
isDisposed: () => {
return false;
}
}
});
});
QUnit.test.skip('keySession is closed when player is disposed', function(assert) {
const mockSession = getMockSession();
const done = assert.async();
makeNewRequest(this.player, {
mediaKeys: {
createSession: () => mockSession
},
eventBus: {
trigger: (event) => {
if (event.type === 'keysessionclosed') {
assert.ok(true, 'got a keysessionclosed event');
done();
}
},
isDisposed: () => {
return false;
}
}
});
assert.equal(mockSession.numCloses, 0, 'no close() calls initially');
this.player.dispose();
assert.equal(mockSession.numCloses, 1, 'close() called once after dipose');
});
QUnit.test('emeError is called when keySession.close fails', function(assert) {
const mockSession = getMockSession();
const done = assert.async();
const expectedErrorMessage = 'Failed to close session';
mockSession.close = () => {
return Promise.reject(expectedErrorMessage);
};
makeNewRequest(this.player, {
mediaKeys: {
createSession: () => mockSession
},
eventBus: {
trigger: () => {},
isDisposed: () => {
return false;
}
},
emeError: (error, metadata) => {
assert.equal(error, expectedErrorMessage, 'expected eme error message');
assert.equal(metadata.errorType, videojs.Error.EMEFailedToCloseSession, 'expected eme error type');
done();
}
});
this.player.dispose();
});
QUnit.test('emeError called when session.generateRequest fails', function(assert) {
const mockSession = getMockSession();
const done = assert.async();
const expectedErrorMessage = 'generate request failed';
mockSession.generateRequest = () => {
return Promise.reject(expectedErrorMessage);
};
makeNewRequest(this.player, {
mediaKeys: {
createSession: () => mockSession
},
eventBus: {
trigger: () => {},
isDisposed: () => {
return false;
}
},
emeError: (error, metadata) => {
assert.equal(error, expectedErrorMessage, 'expected eme error message');
assert.equal(metadata.errorType, videojs.Error.EMEFailedToGenerateLicenseRequest, 'expected eme error type');
}
}).catch((error) => {
assert.equal(error, 'Unable to create or initialize key session', 'expected message');
done();
});
});
QUnit.test('emeError called when mediaKeys.createSession fails', function(assert) {
const done = assert.async();
const expectedError = new Error('session could not be created');
makeNewRequest(this.player, {
mediaKeys: {
createSession: () => {
throw expectedError;
}
},
eventBus: {
trigger: () => {}
},
emeError: (error, metadata) => {
assert.equal(error, expectedError, 'expected eme error message');
assert.equal(metadata.errorType, videojs.Error.EMEFailedToCreateMediaKeySession, 'expected eme error type');
done();
}
});
});
QUnit.module('session management', {
beforeEach() {
this.fixture = document.getElementById('qunit-fixture');
this.video = document.createElement('video');
this.fixture.appendChild(this.video);
this.player = videojs(this.video);
}
});
QUnit.test('addSession saves options', function(assert) {
const video = {
pendingSessionData: []
};
const initDataType = 'temporary';
const initData = new Uint8Array();
const options = { some: 'option' };
const getLicense = () => '';
const removeSession = () => '';
const eventBus = { trigger: () => {} };
const contentId = null;
const emeError = () => {};
addSession({
video,
contentId,
initDataType,
initData,
options,
getLicense,
removeSession,
eventBus,
emeError
});
assert.deepEqual(
video.pendingSessionData,
[{
initDataType,
initData,
options,
getLicense,
removeSession,
eventBus,
contentId,
emeError,
keySystem: undefined
}],
'saved options into pendingSessionData array'
);
});
QUnit.test('addPendingSessions reuses saved options', function(assert) {
assert.expect(5);
const done = assert.async();
const options = { some: 'option' };
const getLicense = (emeOptions, message) => {
assert.deepEqual(emeOptions, options, 'used pending session data options');
return Promise.resolve('license');
};
const eventListeners = [];
const pendingSessionData = [{
initDataType: 'temporary',
initData: new Uint8Array(),
options,
getLicense,
removeSession: () => '',
eventBus: {
trigger: () => {},
isDisposed: () => {
return false;
}
}
}];
const video = {
pendingSessionData,
// internal API, not used in this test
setMediaKeys: () => Promise.resolve()
};
const createdMediaKeys = {
createSession: () => {
return {
addEventListener: (event, callback) => eventListeners.push({ event, callback }),
generateRequest: (initDataType, initData) => {
assert.equal(
initDataType,
pendingSessionData[0].initDataType,
'generateRequest received correct initDataType'
);
assert.equal(
initData,
pendingSessionData[0].initData,
'generateRequest received correct initData'
);
assert.equal(eventListeners.length, 2, 'added two event listeners');
assert.equal(
eventListeners[0].event,
'message',
'added listener for message event'
);
// callback should call getLicense, which continues this test
eventListeners[0].callback({messageType: 'license-request', message: 'test message'});
return Promise.resolve();
},
// this call and everything after is beyond the scope of this test
update: () => Promise.resolve(),
close: () => {}
};
}
};
return addPendingSessions({
player: this.player,
video,
createdMediaKeys
}).then((resolve, reject) => {
done();
});
});
QUnit.module('videojs-contrib-eme getSupportedConfigurations', {
beforeEach() {
this.fixture = document.getElementById('qunit-fixture');
this.video = document.createElement('video');
this.fixture.appendChild(this.video);
this.player = videojs(this.video);
}
});
QUnit.test('includes audio and video content types', function(assert) {
assert.deepEqual(
getSupportedConfigurations('com.widevine.alpha', {
audioContentType: 'audio/mp4; codecs="mp4a.40.2"',
videoContentType: 'video/mp4; codecs="avc1.42E01E"'
}),
[{
audioCapabilities: [{
contentType: 'audio/mp4; codecs="mp4a.40.2"'
}],
videoCapabilities: [{
contentType: 'video/mp4; codecs="avc1.42E01E"'
}]
}],
'included audio and video content types'
);
});
QUnit.test('includes audio and video robustness', function(assert) {
assert.deepEqual(
getSupportedConfigurations('com.widevine.alpha', {
audioRobustness: 'SW_SECURE_CRYPTO',
videoRobustness: 'SW_SECURE_CRYPTO'
}),
[{
audioCapabilities: [{
robustness: 'SW_SECURE_CRYPTO'
}],
videoCapabilities: [{
robustness: 'SW_SECURE_CRYPTO'
}]
}],
'included audio and video robustness'
);
});
QUnit.test('includes audio and video content types and robustness', function(assert) {
assert.deepEqual(
getSupportedConfigurations('com.widevine.alpha', {
audioContentType: 'audio/mp4; codecs="mp4a.40.2"',
audioRobustness: 'SW_SECURE_CRYPTO',
videoContentType: 'video/mp4; codecs="avc1.42E01E"',
videoRobustness: 'SW_SECURE_CRYPTO'
}),
[{
audioCapabilities: [{
contentType: 'audio/mp4; codecs="mp4a.40.2"',
robustness: 'SW_SECURE_CRYPTO'
}],
videoCapabilities: [{
contentType: 'video/mp4; codecs="avc1.42E01E"',
robustness: 'SW_SECURE_CRYPTO'
}]
}],
'included audio and video content types and robustness'
);
});
QUnit.test('includes persistentState', function(assert) {
assert.deepEqual(
getSupportedConfigurations('com.widevine.alpha', { persistentState: 'optional' }),
[{ persistentState: 'optional' }],
'included persistentState'
);
});
QUnit.test('uses supportedConfigurations directly if provided', function(assert) {
assert.deepEqual(
getSupportedConfigurations('com.widevine.alpha', {
supportedConfigurations: [{
initDataTypes: ['cenc'],
audioCapabilities: [{
contentType: 'audio/mp4; codecs="mp4a.40.2"',
robustness: 'SW_SECURE_CRYPTO',
extraOption: 'audio-extra'
}],
videoCapabilities: [{
contentType: 'video/mp4; codecs="avc1.42E01E"',
robustness: 'SW_SECURE_CRYPTO',
extraOption: 'video-extra'
}]
}],
// should not be used
audioContentType: 'audio/mp4; codecs="mp4a.40.5"',
audioRobustness: 'HW_SECURE_CRYPTO',
videoContentType: 'video/mp4; codecs="avc1.42001e"',
videoRobustness: 'HW_SECURE_CRYPTO'
}),
[{
initDataTypes: ['cenc'],
audioCapabilities: [{
contentType: 'audio/mp4; codecs="mp4a.40.2"',
robustness: 'SW_SECURE_CRYPTO',
extraOption: 'audio-extra'
}],
videoCapabilities: [{
contentType: 'video/mp4; codecs="avc1.42E01E"',
robustness: 'SW_SECURE_CRYPTO',
extraOption: 'video-extra'
}]
}],
'used supportedConfigurations directly'
);
});