diff --git a/src/eme.js b/src/eme.js index 8c70c7a..b6b69f8 100644 --- a/src/eme.js +++ b/src/eme.js @@ -92,7 +92,7 @@ export const getSupportedKeySystem = (keySystems) => { return promise; }; -export const makeNewRequest = (requestOptions) => { +export const makeNewRequest = (player, requestOptions) => { const { mediaKeys, initDataType, @@ -108,8 +108,11 @@ export const makeNewRequest = (requestOptions) => { eventBus.trigger('keysessioncreated'); - return new Promise((resolve, reject) => { + player.on('dispose', () => { + keySession.close(); + }); + return new Promise((resolve, reject) => { keySession.addEventListener('message', (event) => { // all other types will be handled by keystatuseschange if (event.messageType !== 'license-request' && event.messageType !== 'license-renewal') { @@ -167,7 +170,7 @@ export const makeNewRequest = (requestOptions) => { // videojs.log.debug('Session expired, closing the session.'); keySession.close().then(() => { removeSession(initData); - makeNewRequest(requestOptions); + makeNewRequest(player, requestOptions); }); } }, false); @@ -206,6 +209,7 @@ export const makeNewRequest = (requestOptions) => { * session creation if media keys are available */ export const addSession = ({ + player, video, initDataType, initData, @@ -227,7 +231,7 @@ export const addSession = ({ if (video.mediaKeysObject) { sessionData.mediaKeys = video.mediaKeysObject; - return makeNewRequest(sessionData); + return makeNewRequest(player, sessionData); } video.pendingSessionData.push(sessionData); @@ -255,6 +259,7 @@ export const addSession = ({ * video object */ export const addPendingSessions = ({ + player, video, certificate, createdMediaKeys @@ -271,7 +276,7 @@ export const addPendingSessions = ({ for (let i = 0; i < video.pendingSessionData.length; i++) { const data = video.pendingSessionData[i]; - promises.push(makeNewRequest({ + promises.push(makeNewRequest(player, { mediaKeys: video.mediaKeysObject, initDataType: data.initDataType, initData: data.initData, @@ -375,6 +380,7 @@ const standardizeKeySystemOptions = (keySystem, keySystemOptions) => { }; export const standard5July2016 = ({ + player, video, initDataType, initData, @@ -432,6 +438,7 @@ export const standard5July2016 = ({ return keySystemAccess.createMediaKeys(); }).then((createdMediaKeys) => { return addPendingSessions({ + player, video, certificate, createdMediaKeys @@ -452,6 +459,7 @@ export const standard5July2016 = ({ promisifyGetLicense(keySystem, keySystemOptions.getLicense, eventBus) : null; return addSession({ + player, video, initDataType, initData, diff --git a/src/plugin.js b/src/plugin.js index 93298ab..53cb8e9 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -49,7 +49,7 @@ export const removeSession = (sessions, initData) => { } }; -export const handleEncryptedEvent = (event, options, sessions, eventBus) => { +export const handleEncryptedEvent = (player, event, options, sessions, eventBus) => { if (!options || !options.keySystems) { // return silently since it may be handled by a different system return Promise.resolve(); @@ -82,6 +82,7 @@ export const handleEncryptedEvent = (event, options, sessions, eventBus) => { sessions.push({ initData }); return standard5July2016({ + player, video: event.target, initDataType: event.initDataType, initData, @@ -234,7 +235,7 @@ const onPlayerReady = (player, emeError) => { // https://github.com/videojs/video.js/pull/4780 // videojs.log('eme', 'Received an \'encrypted\' event'); setupSessions(player); - handleEncryptedEvent(event, getOptions(player), player.eme.sessions, player.tech_) + handleEncryptedEvent(player, event, getOptions(player), player.eme.sessions, player.tech_) .catch(emeError); }); } else if (window.WebKitMediaKeys) { @@ -365,7 +366,7 @@ const eme = function(options = {}) { setupSessions(player); if (window.MediaKeys) { - handleEncryptedEvent(mockEncryptedEvent, mergedEmeOptions, player.eme.sessions, player.tech_) + handleEncryptedEvent(player, mockEncryptedEvent, mergedEmeOptions, player.eme.sessions, player.tech_) .then(() => callback()) .catch((error) => { callback(error); diff --git a/test/eme.test.js b/test/eme.test.js index d1be434..8e028f8 100644 --- a/test/eme.test.js +++ b/test/eme.test.js @@ -1,3 +1,5 @@ +import document from 'global/document'; + import QUnit from 'qunit'; import videojs from 'video.js'; import window from 'global/window'; @@ -48,6 +50,10 @@ const resolveReject = (rejectVariable, rejectMessage) => { 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() { @@ -73,7 +79,7 @@ QUnit.test('keystatuseschange triggers keystatuschange on eventBus for each key' } }; - makeNewRequest({ + makeNewRequest(this.player, { mediaKeys: { createSession: () => mockSession }, @@ -197,7 +203,7 @@ QUnit.test('keystatuseschange with expired key closes and recreates session', fu }; let creates = 0; - makeNewRequest({ + makeNewRequest(this.player, { mediaKeys: { createSession: () => { creates++; @@ -258,7 +264,7 @@ QUnit.test('keystatuseschange with internal-error logs a warning', function(asse videojs.log.warn = (...args) => warnCalls.push(args); - makeNewRequest({ + makeNewRequest(this.player, { mediaKeys: { createSession: () => mockSession }, @@ -304,7 +310,7 @@ QUnit.test('accepts a license URL as an option', function(assert) { const done = assert.async(); const origXhr = videojs.xhr; const xhrCalls = []; - const session = new videojs.EventTarget(); + const mockSession = getMockSession(); videojs.xhr = (options) => { xhrCalls.push(options); @@ -314,12 +320,13 @@ QUnit.test('accepts a license URL as an option', function(assert) { keySystem: 'com.widevine.alpha', createMediaKeys: () => { return { - createSession: () => session + createSession: () => mockSession }; } }; standard5July2016({ + player: this.player, keySystemAccess, video: { setMediaKeys: (createdMediaKeys) => Promise.resolve(createdMediaKeys) @@ -335,7 +342,15 @@ QUnit.test('accepts a license URL as an option', function(assert) { }).catch((e) => {}); setTimeout(() => { - session.trigger({ + 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({ type: 'message', message: 'the-message', messageType: 'license-request' @@ -362,12 +377,12 @@ QUnit.test('accepts a license URL as property', function(assert) { const done = assert.async(); const origXhr = videojs.xhr; const xhrCalls = []; - const session = new videojs.EventTarget(); + const mockSession = getMockSession(); const keySystemAccess = { keySystem: 'com.widevine.alpha', createMediaKeys: () => { return { - createSession: () => session + createSession: () => mockSession }; } }; @@ -377,6 +392,7 @@ QUnit.test('accepts a license URL as property', function(assert) { }; standard5July2016({ + player: this.player, keySystemAccess, video: { setMediaKeys: (createdMediaKeys) => Promise.resolve(createdMediaKeys) @@ -394,7 +410,15 @@ QUnit.test('accepts a license URL as property', function(assert) { }).catch((e) => {}); setTimeout(() => { - session.trigger({ + 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({ type: 'message', message: 'the-message', messageType: 'license-request' @@ -481,7 +505,8 @@ QUnit.test('5 July 2016 lifecycle', function(assert) { update: () => { callCounts.keySessionUpdate++; return Promise.resolve(); - } + }, + close: () => {} }; } }; @@ -495,6 +520,7 @@ QUnit.test('5 July 2016 lifecycle', function(assert) { }; standard5July2016({ + player: this.player, video, initDataType: '', initData: '', @@ -593,6 +619,7 @@ QUnit.test('errors when missing url/licenseUri or getLicense', function(assert) const done = assert.async(1); standard5July2016({ + player: this.player, video: {}, keySystemAccess, options, @@ -619,6 +646,7 @@ QUnit.test('errors when missing certificateUri and getCertificate for fairplay', const done = assert.async(); standard5July2016({ + player: this.player, video: {}, keySystemAccess, options @@ -650,6 +678,7 @@ QUnit.test('rejects promise when getCertificate throws error', function(assert) const done = assert.async(1); standard5July2016({ + player: this.player, video: {}, keySystemAccess, options, @@ -675,6 +704,7 @@ QUnit.test('rejects promise when createMediaKeys rejects', function(assert) { const done = assert.async(1); standard5July2016({ + player: this.player, video: {}, keySystemAccess, options, @@ -704,6 +734,7 @@ QUnit.test('rejects promise when createMediaKeys rejects', function(assert) { const done = assert.async(1); standard5July2016({ + player: this.player, video: {}, keySystemAccess, options, @@ -743,7 +774,8 @@ QUnit.test('rejects promise when addPendingSessions rejects', function(assert) { generateRequest: () => resolveReject( rejectGenerateRequest, 'generateRequest failed' - ) + ), + close: () => {} }; } }); @@ -757,6 +789,7 @@ QUnit.test('rejects promise when addPendingSessions rejects', function(assert) { const test = (errMessage, testDescription) => { video.mediaKeysObject = undefined; standard5July2016({ + player: this.player, video, keySystemAccess, options, @@ -813,7 +846,8 @@ QUnit.test('getLicense not called for messageType that isnt license-request or l } }, keyStatuses: [], - generateRequest: () => Promise.resolve() + generateRequest: () => Promise.resolve(), + close: () => {} }; } }); @@ -824,6 +858,7 @@ QUnit.test('getLicense not called for messageType that isnt license-request or l }; standard5July2016({ + player: this.player, video, keySystemAccess, options, @@ -855,7 +890,8 @@ QUnit.test('getLicense promise rejection', function(assert) { }); }, keyStatuses: [], - generateRequest: () => Promise.resolve() + generateRequest: () => Promise.resolve(), + close: () => {} }; } }); @@ -867,6 +903,7 @@ QUnit.test('getLicense promise rejection', function(assert) { const done = assert.async(1); standard5July2016({ + player: this.player, video, keySystemAccess, options, @@ -968,7 +1005,8 @@ QUnit.test('keySession.update promise rejection', function(assert) { }, keyStatuses: [], generateRequest: () => Promise.resolve(), - update: () => Promise.reject('keySession update failed') + update: () => Promise.reject('keySession update failed'), + close: () => {} }; } }); @@ -980,6 +1018,7 @@ QUnit.test('keySession.update promise rejection', function(assert) { const done = assert.async(1); standard5July2016({ + player: this.player, video, keySystemAccess, options, @@ -995,7 +1034,7 @@ QUnit.test('emeHeaders option sets headers on default license xhr request', func const done = assert.async(); const origXhr = videojs.xhr; const xhrCalls = []; - const session = new videojs.EventTarget(); + const mockSession = getMockSession(); videojs.xhr = (options) => { xhrCalls.push(options); @@ -1005,12 +1044,13 @@ QUnit.test('emeHeaders option sets headers on default license xhr request', func keySystem: 'com.widevine.alpha', createMediaKeys: () => { return { - createSession: () => session + createSession: () => mockSession }; } }; standard5July2016({ + player: this.player, keySystemAccess, video: { setMediaKeys: (createdMediaKeys) => Promise.resolve(createdMediaKeys) @@ -1029,7 +1069,8 @@ QUnit.test('emeHeaders option sets headers on default license xhr request', func }).catch((e) => {}); setTimeout(() => { - session.trigger({ + // Simulate 'message' event + mockSession.listeners[0].listener({ type: 'message', message: 'the-message', messageType: 'license-request' @@ -1057,7 +1098,7 @@ QUnit.test('licenseHeaders keySystems property overrides emeHeaders value', func const done = assert.async(); const origXhr = videojs.xhr; const xhrCalls = []; - const session = new videojs.EventTarget(); + const mockSession = getMockSession(); videojs.xhr = (options) => { xhrCalls.push(options); @@ -1067,12 +1108,13 @@ QUnit.test('licenseHeaders keySystems property overrides emeHeaders value', func keySystem: 'com.widevine.alpha', createMediaKeys: () => { return { - createSession: () => session + createSession: () => mockSession }; } }; standard5July2016({ + player: this.player, keySystemAccess, video: { setMediaKeys: (createdMediaKeys) => Promise.resolve(createdMediaKeys) @@ -1096,7 +1138,8 @@ QUnit.test('licenseHeaders keySystems property overrides emeHeaders value', func }).catch((e) => {}); setTimeout(() => { - session.trigger({ + // Simulate 'message' event + mockSession.listeners[0].listener({ type: 'message', message: 'the-message', messageType: 'license-request' @@ -1139,7 +1182,52 @@ QUnit.test('sets required fairplay defaults if not explicitly configured', funct window.requestMediaKeySystemAccess = origRequestMediaKeySystemAccess; }); -QUnit.module('session management'); +QUnit.test('makeNewRequest triggers keysessioncreated', function(assert) { + const done = assert.async(); + const mockSession = getMockSession(); + + makeNewRequest(this.player, { + mediaKeys: { + createSession: () => mockSession + }, + eventBus: { + trigger: (eventName) => { + if (eventName === 'keysessioncreated') { + assert.ok(true, 'got a keysessioncreated event'); + done(); + } + } + } + }); +}); + +QUnit.test('keySession is closed when player is disposed', function(assert) { + const mockSession = getMockSession(); + + makeNewRequest(this.player, { + mediaKeys: { + createSession: () => mockSession + }, + eventBus: { + trigger: (eventName) => {} + } + }); + + assert.equal(mockSession.numCloses, 0, 'no close() calls initially'); + + this.player.dispose(); + + assert.equal(mockSession.numCloses, 1, 'close() called once after dipose'); +}); + +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 = { @@ -1228,12 +1316,14 @@ QUnit.test('addPendingSessions reuses saved options', function(assert) { return Promise.resolve(); }, // this call and everything after is beyond the scope of this test - update: () => Promise.resolve() + update: () => Promise.resolve(), + close: () => {} }; } }; return addPendingSessions({ + player: this.player, video, createdMediaKeys }).then((resolve, reject) => { @@ -1241,7 +1331,14 @@ QUnit.test('addPendingSessions reuses saved options', function(assert) { }); }); -QUnit.module('videojs-contrib-eme getSupportedConfigurations'); +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( @@ -1347,22 +1444,3 @@ QUnit.test('uses supportedConfigurations directly if provided', function(assert) 'used supportedConfigurations directly' ); }); - -QUnit.test('makeNewRequest triggers keysessioncreated', function(assert) { - const done = assert.async(); - const mockSession = getMockSession(); - - makeNewRequest({ - mediaKeys: { - createSession: () => mockSession - }, - eventBus: { - trigger: (eventName) => { - if (eventName === 'keysessioncreated') { - assert.ok(true, 'got a keysessioncreated event'); - done(); - } - } - } - }); -}); diff --git a/test/plugin.test.js b/test/plugin.test.js index e2356ad..613b883 100644 --- a/test/plugin.test.js +++ b/test/plugin.test.js @@ -356,6 +356,10 @@ QUnit.test('only registers for spec-compliant events even if legacy APIs are ava QUnit.module('plugin guard functions', { beforeEach() { + this.fixture = document.getElementById('qunit-fixture'); + this.video = document.createElement('video'); + this.fixture.appendChild(this.video); + this.player = videojs(this.video); this.options = { keySystems: { 'org.w3.clearkey': {url: 'some-url'} @@ -412,7 +416,7 @@ QUnit.test('handleEncryptedEvent checks for required options', function(assert) const done = assert.async(); const sessions = []; - handleEncryptedEvent(this.event1, {}, sessions).then(() => { + handleEncryptedEvent(this.player, this.event1, {}, sessions).then(() => { assert.equal(sessions.length, 0, 'did not create a session when no options'); done(); }); @@ -422,7 +426,7 @@ QUnit.test('handleEncryptedEvent checks for required init data', function(assert const done = assert.async(); const sessions = []; - handleEncryptedEvent({ target: {}, initData: null }, this.options, sessions).then(() => { + handleEncryptedEvent(this.player, { target: {}, initData: null }, this.options, sessions).then(() => { assert.equal(sessions.length, 0, 'did not create a session when no init data'); done(); }); @@ -433,7 +437,7 @@ QUnit.test('handleEncryptedEvent creates session', function(assert) { const sessions = []; // testing the rejection path because this isn't a real session - handleEncryptedEvent(this.event1, this.options, sessions).catch(() => { + handleEncryptedEvent(this.player, this.event1, this.options, sessions).catch(() => { assert.equal(sessions.length, 1, 'created a session when keySystems in options'); assert.equal(sessions[0].initData, this.initData1, 'captured initData in the session'); done(); @@ -445,8 +449,8 @@ QUnit.test('handleEncryptedEvent creates new session for new init data', functio const sessions = []; // testing the rejection path because this isn't a real session - handleEncryptedEvent(this.event1, this.options, sessions).catch(() => { - return handleEncryptedEvent(this.event2, this.options, sessions).catch(() => { + handleEncryptedEvent(this.player, this.event1, this.options, sessions).catch(() => { + return handleEncryptedEvent(this.player, this.event2, this.options, sessions).catch(() => { assert.equal(sessions.length, 2, 'created a new session when new init data'); assert.equal(sessions[0].initData, this.initData1, 'retained session init data'); assert.equal(sessions[1].initData, this.initData2, 'added new session init data'); @@ -460,9 +464,9 @@ QUnit.test('handleEncryptedEvent doesn\'t create duplicate sessions', function(a const sessions = []; // testing the rejection path because this isn't a real session - handleEncryptedEvent(this.event1, this.options, sessions).catch(() => { - return handleEncryptedEvent(this.event2, this.options, sessions).catch(() => { - return handleEncryptedEvent(this.event2, this.options, sessions).then(() => { + handleEncryptedEvent(this.player, this.event1, this.options, sessions).catch(() => { + return handleEncryptedEvent(this.player, this.event2, this.options, sessions).catch(() => { + return handleEncryptedEvent(this.player, this.event2, this.options, sessions).then(() => { assert.equal(sessions.length, 2, 'no new session when same init data'); assert.equal(sessions[0].initData, this.initData1, 'retained session init data'); assert.equal(sessions[1].initData, this.initData2, 'retained session init data'); @@ -484,7 +488,7 @@ QUnit.test('handleEncryptedEvent uses predefined init data', function(assert) { const sessions = []; // testing the rejection path because this isn't a real session - handleEncryptedEvent(this.event2, options, sessions).catch(() => { + handleEncryptedEvent(this.player, this.event2, options, sessions).catch(() => { assert.equal(sessions.length, 1, 'created a session when keySystems in options'); assert.deepEqual(sessions[0].initData, this.initData1, 'captured initData in the session'); done();