UNPKG

videojs-contrib-eme

Version:

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

541 lines (468 loc) 15.1 kB
import QUnit from 'qunit'; import { default as fairplay, defaultGetLicense, defaultGetCertificate, LEGACY_FAIRPLAY_KEY_SYSTEM } from '../src/fairplay'; import videojs from 'video.js'; import window from 'global/window'; import { getMockEventBus } from './utils'; QUnit.module('videojs-contrib-eme fairplay', { beforeEach() { this.origXhr = videojs.xhr; videojs.xhr = (params, callback) => { return callback(null, {statusCode: 200}, new Uint8Array([0, 1, 2, 3]).buffer); }; }, afterEach() { videojs.xhr = this.origXhr; } }); QUnit.test('lifecycle', function(assert) { assert.expect(23); const done = assert.async(); const initData = new Uint8Array([1, 2, 3, 4]).buffer; const callbacks = {}; const callCounts = { getCertificate: 0, getLicense: 0, updateKeySession: 0, createSession: 0, licenseRequestAttempts: 0 }; const getCertificate = (emeOptions, callback) => { callCounts.getCertificate++; callbacks.getCertificate = callback; }; const getLicense = (emeOptions, contentId, keyMessage, callback) => { callCounts.getLicense++; callbacks.getLicense = callback; }; const options = { keySystems: { 'com.apple.fps.1_0': { getCertificate, getLicense, // not needed due to mocking getContentId: () => 'some content id' } } }; const eventBus = { trigger: (event) => { if (event.type === 'licenserequestattempted') { callCounts.licenseRequestAttempts++; } }, isDisposed: () => { return false; } }; // trap event listeners const keySessionEventListeners = {}; const updateKeySession = (key) => { callCounts.updateKeySession++; }; let onKeySessionCreated; const createSession = (type, concatenatedData) => { callCounts.createSession++; return { addEventListener: (name, callback) => { keySessionEventListeners[name] = callback; if (name === 'webkitkeyerror') { // Since we don't have a way of executing code at the end of addKey's promise, // we assume that adding the listener for webkitkeyerror is the last run code // within the promise. onKeySessionCreated(); } }, update: updateKeySession }; }; // mock webkitKeys to avoid browser specific calls and enable us to verify ordering const video = { webkitKeys: { createSession } }; fairplay({ video, initData, options, eventBus }) .then(() => { done(); }); // Step 1: getCertificate assert.equal(callCounts.getCertificate, 1, 'getCertificate has been called'); assert.equal(callCounts.createSession, 0, 'a key session has not been created'); assert.equal(callCounts.getLicense, 0, 'getLicense has not been called'); assert.equal(callCounts.updateKeySession, 0, 'updateKeySession has not been called'); assert.equal( callCounts.licenseRequestAttempts, 0, 'license request event not triggered (since no callback yet)' ); callbacks.getCertificate(null, new Uint16Array([4, 5, 6, 7]).buffer); onKeySessionCreated = () => { // Step 2: create a key session assert.equal(callCounts.getCertificate, 1, 'getCertificate has been called'); assert.equal(callCounts.createSession, 1, 'a key session has been created'); assert.equal(callCounts.getLicense, 0, 'getLicense has not been called'); assert.equal(callCounts.updateKeySession, 0, 'updateKeySession has not been called'); assert.equal( callCounts.licenseRequestAttempts, 0, 'license request event not triggered (since no callback yet)' ); assert.ok( keySessionEventListeners.webkitkeymessage, 'added an event listener for webkitkeymessage' ); assert.ok( keySessionEventListeners.webkitkeyadded, 'added an event listener for webkitkeyadded' ); assert.ok( keySessionEventListeners.webkitkeyerror, 'added an event listener for webkitkeyerror' ); keySessionEventListeners.webkitkeymessage({}); // Step 3: get the key on webkitkeymessage assert.equal(callCounts.getCertificate, 1, 'getCertificate has been called'); assert.equal(callCounts.createSession, 1, 'a key session has been created'); assert.equal(callCounts.getLicense, 1, 'getLicense has been called'); assert.equal(callCounts.updateKeySession, 0, 'updateKeySession has not been called'); assert.equal( callCounts.licenseRequestAttempts, 0, 'license request event not triggered (since no callback yet)' ); callbacks.getLicense(null, []); // Step 4: update the key session with the key assert.equal(callCounts.getCertificate, 1, 'getCertificate has been called'); assert.equal(callCounts.createSession, 1, 'a key session has been created'); assert.equal(callCounts.getLicense, 1, 'getLicense has been called'); assert.equal(callCounts.updateKeySession, 1, 'updateKeySession has been called'); assert.equal( callCounts.licenseRequestAttempts, 1, 'license request event triggered' ); keySessionEventListeners.webkitkeyadded(); }; }); QUnit.test('error in getCertificate rejects promise', function(assert) { const keySystems = {}; const done = assert.async(1); const emeError = (_, metadata) => { assert.equal(metadata.errorType, videojs.Error.EMEFailedToSetServerCertificate, 'errorType is expected value'); assert.equal(metadata.keySystem, LEGACY_FAIRPLAY_KEY_SYSTEM, 'keySystem is expected value'); }; keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] = { getCertificate: (options, callback) => { callback('error in getCertificate'); } }; fairplay({options: {keySystems}, eventBus: getMockEventBus(), emeError}).catch((err) => { assert.equal(err, 'error in getCertificate', 'message is good'); done(); }); }); QUnit.test('error in WebKitMediaKeys rejects promise', function(assert) { const keySystems = {}; const done = assert.async(1); const initData = new Uint8Array([1, 2, 3, 4]).buffer; const video = { webkitSetMediaKeys: () => {} }; const emeError = (_, metadata) => { assert.equal(metadata.errorType, videojs.Error.EMEFailedToCreateMediaKeys, 'errorType is expected value'); assert.equal(metadata.keySystem, LEGACY_FAIRPLAY_KEY_SYSTEM, 'keySystem is expected value'); }; window.WebKitMediaKeys = () => { throw new Error('unsupported keySystem'); }; keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] = {}; fairplay({ video, initData, options: {keySystems}, eventBus: getMockEventBus(), emeError }).catch(err => { assert.equal(err, 'Could not create MediaKeys', 'message is good'); done(); }); }); QUnit.test('error in webkitSetMediaKeys rejects promise', function(assert) { const keySystems = {}; const done = assert.async(1); const initData = new Uint8Array([1, 2, 3, 4]).buffer; const video = { webkitSetMediaKeys: () => { throw new Error('MediaKeys unusable'); } }; const emeError = (_, metadata) => { assert.equal(metadata.errorType, videojs.Error.EMEFailedToCreateMediaKeys, 'errorType is expected value'); assert.equal(metadata.keySystem, LEGACY_FAIRPLAY_KEY_SYSTEM, 'keySystem is expected value'); }; window.WebKitMediaKeys = function() {}; keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] = {}; fairplay({ video, initData, options: {keySystems}, eventBus: getMockEventBus(), emeError }).catch(err => { assert.equal(err, 'Could not create MediaKeys', 'message is good'); done(); }); }); QUnit.test('error in webkitKeys.createSession rejects promise', function(assert) { const keySystems = {}; const done = assert.async(1); const initData = new Uint8Array([1, 2, 3, 4]).buffer; const video = { webkitSetMediaKeys: () => { video.webkitKeys = { createSession: () => { throw new Error('invalid mimeType or initData'); } }; } }; const emeError = (_, metadata) => { assert.equal(metadata.errorType, videojs.Error.EMEFailedToCreateMediaKeySession, 'errorType is expected value'); assert.equal(metadata.keySystem, LEGACY_FAIRPLAY_KEY_SYSTEM, 'keySystem is expected value'); }; window.WebKitMediaKeys = function() {}; keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] = {}; fairplay({ video, initData, options: {keySystems}, eventBus: getMockEventBus(), emeError }).catch(err => { assert.equal( err, 'Could not create key session', 'message is good' ); done(); }); }); QUnit.test('error in getLicense rejects promise', function(assert) { const keySystems = {}; const done = assert.async(1); const initData = new Uint8Array([1, 2, 3, 4]).buffer; const video = { webkitSetMediaKeys: () => { video.webkitKeys = { createSession: () => { return { addEventListener: (event, callback) => { if (event === 'webkitkeymessage') { callback({message: 'whatever'}); } } }; } }; } }; const emeError = (_, metadata) => { assert.equal(metadata.keySystem, LEGACY_FAIRPLAY_KEY_SYSTEM, 'keySystem is expected value'); }; window.WebKitMediaKeys = function() {}; keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] = { getLicense: (options, contentId, message, callback) => { callback('error in getLicense'); } }; fairplay({ video, initData, options: {keySystems}, eventBus: getMockEventBus(), emeError }).catch(err => { assert.equal(err, 'error in getLicense', 'message is good'); done(); }); }); QUnit.test('keysessioncreated fired on key session created', function(assert) { const keySystems = {}; const done = assert.async(); const initData = new Uint8Array([1, 2, 3, 4]).buffer; let sessionCreated = false; const addEventListener = () => {}; const video = { webkitSetMediaKeys: () => { video.webkitKeys = { createSession: () => { sessionCreated = true; return { addEventListener }; } }; } }; const eventBus = { trigger: (event) => { if (event.type === 'keysessioncreated') { assert.ok(sessionCreated, 'keysessioncreated fired after session created'); assert.deepEqual(event.keySession, { addEventListener }, 'keySession payload passed with event'); done(); } }, isDisposed: () => { return false; } }; window.WebKitMediaKeys = function() {}; keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] = { licenseUri: 'some-url', certificateUri: 'some-other-url' }; fairplay({ video, initData, options: { keySystems }, eventBus }); }); QUnit.test('a webkitkeyerror rejects promise', function(assert) { let keySession; const keySystems = {}; const done = assert.async(1); const initData = new Uint8Array([1, 2, 3, 4]).buffer; const video = { webkitSetMediaKeys: () => { video.webkitKeys = { createSession: () => { return { addEventListener: (event, callback) => { if (event === 'webkitkeyerror') { callback('webkitkeyerror'); } }, error: { code: 0, systemCode: 1 } }; } }; } }; const emeError = (_, metadata) => { assert.equal(metadata.errorType, videojs.Error.EMEFailedToUpdateSessionWithReceivedLicenseKeys, 'errorType is expected value'); assert.equal(metadata.keySystem, LEGACY_FAIRPLAY_KEY_SYSTEM, 'keySystem is expected value'); }; window.WebKitMediaKeys = function() {}; keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] = { getLicense: (options, contentId, message, callback) => { callback(null); keySession.trigger('webkitkeyerror'); } }; fairplay({ video, initData, options: {keySystems}, eventBus: getMockEventBus(), emeError }).catch(err => { assert.equal(err, 'KeySession error: code 0, systemCode 1', 'message is good'); done(); }); }); QUnit.test('emeHeaders sent with license and certificate requests', function(assert) { const origXhr = videojs.xhr; const emeOptions = { emeHeaders: { 'Some-Header': 'some-header-value' } }; const fairplayOptions = { licenseUri: 'some-url', certificateUri: 'some-other-url' }; const xhrCalls = []; videojs.xhr = (xhrOptions) => { xhrCalls.push(xhrOptions); }; const getLicense = defaultGetLicense('', fairplayOptions); const getCertificate = defaultGetCertificate('', fairplayOptions); getLicense(emeOptions, 'contentId'); getCertificate(emeOptions); assert.equal(xhrCalls.length, 2, 'made two XHR requests'); assert.deepEqual(xhrCalls[0], { uri: 'some-url', method: 'POST', responseType: 'arraybuffer', requestType: 'license', metadata: { keySystem: '', contentId: 'contentId' }, body: undefined, headers: { 'Content-type': 'application/octet-stream', 'Some-Header': 'some-header-value' } }, 'made license request with proper emeHeaders value'); assert.deepEqual(xhrCalls[1], { uri: 'some-other-url', responseType: 'arraybuffer', requestType: 'license', metadata: { keySystem: '' }, headers: { 'Some-Header': 'some-header-value' } }, 'made certificate request with proper emeHeaders value'); videojs.xhr = origXhr; }); QUnit.test('licenseHeaders and certificateHeaders properties override emeHeaders value', function(assert) { const origXhr = videojs.xhr; const emeOptions = { emeHeaders: { 'Some-Header': 'some-header-value' } }; const fairplayOptions = { licenseUri: 'some-url', certificateUri: 'some-other-url', licenseHeaders: { 'Some-Header': 'higher-priority-license-header' }, certificateHeaders: { 'Some-Header': 'higher-priority-cert-header' } }; const xhrCalls = []; videojs.xhr = (xhrOptions) => { xhrCalls.push(xhrOptions); }; const getLicense = defaultGetLicense('', fairplayOptions); const getCertificate = defaultGetCertificate('', fairplayOptions); getLicense(emeOptions, 'contentId'); getCertificate(emeOptions); assert.equal(xhrCalls.length, 2, 'made two XHR requests'); assert.deepEqual(xhrCalls[0], { uri: 'some-url', method: 'POST', responseType: 'arraybuffer', requestType: 'license', metadata: { keySystem: '', contentId: 'contentId' }, body: undefined, headers: { 'Content-type': 'application/octet-stream', 'Some-Header': 'higher-priority-license-header' } }, 'made license request with proper licenseHeaders value'); assert.deepEqual(xhrCalls[1], { uri: 'some-other-url', responseType: 'arraybuffer', requestType: 'license', metadata: { keySystem: '' }, headers: { 'Some-Header': 'higher-priority-cert-header' } }, 'made certificate request with proper certificateHeaders value'); videojs.xhr = origXhr; });