UNPKG

videojs-contrib-eme

Version:

Supports Encrypted Media Extensions for playback of encrypted content in Video.js

635 lines (561 loc) 16.9 kB
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'); });