videojs-contrib-eme
Version:
Supports Encrypted Media Extensions for playback of encrypted content in Video.js
635 lines (561 loc) • 16.9 kB
JavaScript
import QUnit from 'qunit';
import videojs from 'video.js';
import window from 'global/window';
import {
createMessageBuffer,
challengeElement
} from './playready-message';
import {
PLAYREADY_KEY_SYSTEM,
createSession,
default as msPrefixed
} from '../src/ms-prefixed';
import {
stringToArrayBuffer,
getMockEventBus
} from './utils';
QUnit.module('videojs-contrib-eme ms-prefixed', {
beforeEach() {
this.origMSMediaKeys = window.MSMediaKeys;
window.MSMediaKeys = function() {};
const session = new videojs.EventTarget();
session.keys = [];
session.update = (key) => session.keys.push(key);
// mock the video since the APIs won't be available on non IE11 browsers
const video = {
msSetMediaKeys: () => {
video.msKeys = {
createSession: () => this.session
};
}
};
this.session = session;
this.video = video;
},
afterEach() {
window.MSMediaKeys = this.origMSMediaKeys;
}
});
QUnit.test('overwrites msKeys', function(assert) {
const origMsKeys = {};
this.video.msKeys = origMsKeys;
msPrefixed({
video: this.video,
initData: '',
options: {
keySystems: {
'com.microsoft.playready': true
}
},
eventBus: getMockEventBus()
});
assert.notEqual(this.video.msKeys, origMsKeys, 'overwrote msKeys');
});
QUnit.test('error thrown when creating keys bubbles up', function(assert) {
const emeError = (_, metadata) => {
assert.equal(metadata.errorType, videojs.Error.EMEFailedToCreateMediaKeys, 'errorType is expected value');
assert.equal(metadata.keySystem, PLAYREADY_KEY_SYSTEM, 'errorType is expected value');
};
window.MSMediaKeys = function() {
throw new Error('error');
};
assert.throws(
() => msPrefixed({video: this.video, emeError}),
new Error('Unable to create media keys for PlayReady key system. Error: error'),
'error is thrown with proper message'
);
});
QUnit.test('createSession throws unknown error', function(assert) {
const video = {
msSetMediaKeys: () => {
video.msKeys = {
createSession: () => {
throw new Error('whatever');
}
};
}
};
assert.throws(
() => msPrefixed({video}),
new Error('whatever'),
'error is thrown with proper message'
);
});
QUnit.test('throws error if session was not created', function(assert) {
const video = {
msSetMediaKeys: () => {
video.msKeys = {
createSession: () => null
};
}
};
const emeError = (_, metadata) => {
assert.equal(metadata.errorType, videojs.Error.EMEFailedToCreateMediaKeySession, 'errorType is expected value');
assert.equal(metadata.keySystem, PLAYREADY_KEY_SYSTEM, 'errorType is expected value');
};
assert.throws(
() => msPrefixed({video, emeError}),
new Error('Could not create key session.'),
'error is thrown with proper message'
);
});
QUnit.test('throws error on keysession mskeyerror event', function(assert) {
let errorMessage;
const emeError = (_, metadata) => {
assert.equal(metadata.errorType, videojs.Error.EMEFailedToCreateMediaKeySession, 'errorType is expected value');
assert.equal(metadata.keySystem, PLAYREADY_KEY_SYSTEM, 'keySystem is expected value');
};
msPrefixed({
video: this.video,
initData: '',
options: {
keySystems: {
'com.microsoft.playready': true
}
},
eventBus: {
trigger: (event) => {
errorMessage = typeof event === 'string' ? event : event.message;
},
isDisposed: () => {
return false;
}
},
emeError
});
this.session.error = {
code: 5,
systemCode: 9
};
this.session.trigger('mskeyerror');
assert.equal(
errorMessage,
'Unexpected key error from key session with code: 5 and systemCode: 9',
'error is thrown with proper message'
);
});
QUnit.test('calls getKey when provided on key message', function(assert) {
let passedOptions = null;
let passedDestinationURL = null;
let passedBuffer = null;
let passedCallback = null;
let getKeyCallback = (callback) => {
callback(null, 'a key');
};
let errorMessage;
const emeOptions = {
keySystems: {
'com.microsoft.playready': {
getKey: (options, destinationURL, buffer, callback) => {
passedOptions = options;
passedDestinationURL = destinationURL;
passedBuffer = buffer;
passedCallback = callback;
getKeyCallback(callback);
}
}
}
};
const emeError = (_, metadata) => {
assert.equal(metadata.errorType, videojs.Error.EMEFailedToRequestMediaKeySystemAccess, 'errorType is expected value');
assert.deepEqual(metadata.config, [{}], 'keySystem is expected value');
};
msPrefixed({
video: this.video,
initData: '',
options: emeOptions,
eventBus: {
trigger: (event) => {
errorMessage = typeof event === 'string' ? event : event.message;
},
isDisposed: () => {
return false;
}
},
emeError
});
assert.notOk(passedOptions, 'getKey not called');
this.session.trigger({
type: 'mskeymessage',
destinationURL: 'url',
message: {
buffer: 'buffer'
}
});
assert.equal(passedOptions, emeOptions, 'getKey called with options');
assert.equal(passedDestinationURL, 'url', 'getKey called with destinationURL');
assert.equal(passedBuffer, 'buffer', 'getKey called with buffer');
assert.equal(typeof passedCallback, 'function', 'getKey called with callback');
assert.equal(this.session.keys.length, 1, 'added key to session');
assert.equal(this.session.keys[0], 'a key', 'added correct key to session');
getKeyCallback = (callback) => {
callback('an error', 'an errored key');
};
this.session.trigger({
type: 'mskeymessage',
destinationURL: 'url',
message: {
buffer: 'buffer'
}
});
assert.equal(
errorMessage,
'Unable to get key: an error',
'fires mskeyerror on eventBus when callback has an error'
);
assert.equal(this.session.keys.length, 1, 'did not add a new key');
});
QUnit.test('makes request when nothing provided on key message', function(assert) {
const origXhr = videojs.xhr;
const xhrCalls = [];
let errorMessage;
const emeError = (_, metadata) => {
assert.equal(metadata.errorType, videojs.Error.EMEFailedToGenerateLicenseRequest, 'errorType is expected value');
assert.equal(metadata.keySystem, PLAYREADY_KEY_SYSTEM, 'keySystem is expected value');
};
videojs.xhr = (config, callback) => xhrCalls.push({config, callback});
msPrefixed({
video: this.video,
initData: '',
options: {
keySystems: {
'com.microsoft.playready': true
}
},
eventBus: {
trigger: (event) => {
if (typeof event === 'object' && event.type === 'mskeyerror') {
errorMessage = event.message;
}
},
isDisposed: () => {
return false;
}
},
emeError
});
this.session.trigger({
type: 'mskeymessage',
destinationURL: 'destination-url',
message: {
buffer: createMessageBuffer()
}
});
assert.equal(xhrCalls.length, 1, 'one xhr request');
assert.equal(
xhrCalls[0].config.uri,
'destination-url',
'made request to destinationURL'
);
assert.deepEqual(
xhrCalls[0].config.headers,
{
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': '"http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"'
},
'uses headers from message'
);
assert.equal(xhrCalls[0].config.body, challengeElement, 'sends the challenge element');
assert.equal(xhrCalls[0].config.method, 'post', 'request is a post');
assert.equal(
xhrCalls[0].config.responseType,
'arraybuffer',
'responseType is an arraybuffer'
);
const response = {
body: stringToArrayBuffer('key value')
};
xhrCalls[0].callback('an error', response, response.body);
assert.equal(
errorMessage,
'Unable to request key from url: destination-url',
'triggers mskeyerror on event bus when callback has an error'
);
assert.equal(this.session.keys.length, 0, 'no key added to session');
xhrCalls[0].callback(null, response, response.body);
assert.equal(this.session.keys.length, 1, 'key added to session');
assert.deepEqual(
this.session.keys[0],
new Uint8Array(response.body),
'correct key added to session'
);
videojs.xhr = origXhr;
});
QUnit.test(
'makes request on key message when empty object provided in options',
function(assert) {
const origXhr = videojs.xhr;
const xhrCalls = [];
videojs.xhr = (config, callback) => xhrCalls.push({config, callback});
msPrefixed({
video: this.video,
initData: '',
options: {
keySystems: {
'com.microsoft.playready': {}
}
},
eventBus: getMockEventBus()
});
this.session.trigger({
type: 'mskeymessage',
destinationURL: 'destination-url',
message: {
buffer: createMessageBuffer()
}
});
assert.equal(xhrCalls.length, 1, 'one xhr request');
assert.equal(
xhrCalls[0].config.uri,
'destination-url',
'made request to destinationURL'
);
assert.deepEqual(
xhrCalls[0].config.headers,
{
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': '"http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"'
},
'uses headers from message'
);
assert.equal(xhrCalls[0].config.body, challengeElement, 'sends the challenge element');
assert.equal(xhrCalls[0].config.method, 'post', 'request is a post');
assert.equal(
xhrCalls[0].config.responseType,
'arraybuffer',
'responseType is an arraybuffer'
);
videojs.xhr = origXhr;
}
);
QUnit.test('makes request with provided url string on key message', function(assert) {
const origXhr = videojs.xhr;
const xhrCalls = [];
let errorMessage;
const emeError = (_, metadata) => {
assert.equal(metadata.errorType, videojs.Error.EMEFailedToGenerateLicenseRequest, 'errorType is expected value');
assert.equal(metadata.keySystem, PLAYREADY_KEY_SYSTEM, 'keySystem is expected value');
};
videojs.xhr = (config, callback) => xhrCalls.push({config, callback});
msPrefixed({
video: this.video,
initData: '',
options: {
keySystems: {
'com.microsoft.playready': 'provided-url'
}
},
eventBus: {
trigger: (event) => {
if (typeof event === 'object' && event.type === 'mskeyerror') {
errorMessage = event.message;
}
},
isDisposed: () => {
return false;
}
},
emeError
});
this.session.trigger({
type: 'mskeymessage',
destinationURL: 'destination-url',
message: {
buffer: createMessageBuffer([{
name: 'Content-Type',
value: 'text/xml; charset=utf-8'
}, {
name: 'SOAPAction',
value: '"http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"'
}])
}
});
assert.equal(xhrCalls.length, 1, 'one xhr request');
assert.equal(
xhrCalls[0].config.uri,
'provided-url',
'made request to provided-url'
);
assert.deepEqual(
xhrCalls[0].config.headers,
{
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': '"http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"'
},
'uses headers from message'
);
assert.equal(xhrCalls[0].config.body, challengeElement, 'sends the challenge element');
assert.equal(xhrCalls[0].config.method, 'post', 'request is a post');
assert.equal(
xhrCalls[0].config.responseType,
'arraybuffer',
'responseType is an arraybuffer'
);
const response = {
body: stringToArrayBuffer('key value')
};
xhrCalls[0].callback('an error', response, response.body);
assert.equal(
errorMessage,
'Unable to request key from url: provided-url',
'triggers mskeyerror on event bus when callback has an error'
);
assert.equal(this.session.keys.length, 0, 'no key added to session');
xhrCalls[0].callback(null, response, response.body);
assert.equal(this.session.keys.length, 1, 'key added to session');
assert.deepEqual(
this.session.keys[0],
new Uint8Array(response.body),
'correct key added to session'
);
videojs.xhr = origXhr;
});
QUnit.test('makes request with provided url on key message', function(assert) {
const origXhr = videojs.xhr;
const xhrCalls = [];
const callCounts = {
licenseRequestAttempts: 0
};
let errorMessage;
const emeError = (_, metadata) => {
assert.equal(metadata.errorType, videojs.Error.EMEFailedToGenerateLicenseRequest, 'errorType is expected value');
assert.equal(metadata.keySystem, PLAYREADY_KEY_SYSTEM, 'keySystem is expected value');
};
videojs.xhr = (config, callback) => xhrCalls.push({config, callback});
msPrefixed({
video: this.video,
initData: '',
options: {
keySystems: {
'com.microsoft.playready': {
url: 'provided-url'
}
}
},
eventBus: {
trigger: (event) => {
if (event.type === 'licenserequestattempted') {
callCounts.licenseRequestAttempts++;
} else if (typeof event === 'object' && event.type === 'mskeyerror') {
errorMessage = event.message;
}
},
isDisposed: () => {
return false;
}
},
emeError
});
this.session.trigger({
type: 'mskeymessage',
destinationURL: 'destination-url',
message: {
buffer: createMessageBuffer([{
name: 'Content-Type',
value: 'text/xml; charset=utf-8'
}, {
name: 'SOAPAction',
value: '"http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"'
}])
}
});
assert.equal(xhrCalls.length, 1, 'one xhr request');
assert.equal(
xhrCalls[0].config.uri,
'provided-url',
'made request to provided-url'
);
assert.deepEqual(
xhrCalls[0].config.headers,
{
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': '"http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"'
},
'uses headers from message'
);
assert.equal(xhrCalls[0].config.body, challengeElement, 'sends the challenge element');
assert.equal(xhrCalls[0].config.method, 'post', 'request is a post');
assert.equal(
xhrCalls[0].config.responseType,
'arraybuffer',
'responseType is an arraybuffer'
);
assert.equal(
callCounts.licenseRequestAttempts, 0,
'license request event not triggered (since no callback yet)'
);
const response = {
body: stringToArrayBuffer('key value')
};
xhrCalls[0].callback('an error', response, response.body);
assert.equal(callCounts.licenseRequestAttempts, 1, 'license request event triggered');
assert.equal(
errorMessage,
'Unable to request key from url: provided-url',
'triggers mskeyerror on event bus when callback has an error'
);
assert.equal(this.session.keys.length, 0, 'no key added to session');
xhrCalls[0].callback(null, response, response.body);
assert.equal(
callCounts.licenseRequestAttempts, 2,
'second license request event triggered'
);
assert.equal(this.session.keys.length, 1, 'key added to session');
assert.deepEqual(
this.session.keys[0],
new Uint8Array(response.body),
'correct key added to session'
);
videojs.xhr = origXhr;
});
QUnit.test('will use a custom getLicense method if one is provided', function(assert) {
let callCount = 0;
msPrefixed({
video: this.video,
initData: '',
options: {
keySystems: {
'com.microsoft.playready': {
getLicense() {
callCount++;
}
}
}
},
eventBus: getMockEventBus()
});
const buffer = createMessageBuffer([{
name: 'Content-Type',
value: 'text/xml; charset=utf-8'
}, {
name: 'SOAPAction',
value: '"http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"'
}]);
this.session.trigger({
type: 'mskeymessage',
destinationURL: 'destination-url',
message: {buffer}
});
assert.equal(callCount, 1, 'getLicense was called');
});
QUnit.test('createSession triggers keysessioncreated', function(assert) {
const addEventListener = () => {};
const video = {
msKeys: {
createSession: () => {
return {
addEventListener
};
}
}
};
const eventBus = getMockEventBus();
createSession(video, '', {}, eventBus);
assert.equal(eventBus.calls.length, 1, 'one event triggered');
assert.equal(
eventBus.calls[0].type,
'keysessioncreated',
'triggered keysessioncreated event'
);
assert.deepEqual(eventBus.calls[0].keySession, { addEventListener }, 'keysessioncreated payload');
});