diff --git a/README.md b/README.md index 0d990dd..1cd6979 100644 --- a/README.md +++ b/README.md @@ -617,8 +617,10 @@ This event is triggered directly from the underlying `keystatuseschange` event, When the key session is created, an event of type `keysessioncreated` will be triggered on the Video.js playback tech. ``` -player.tech().on('keysessioncreated', function(event) { - // note that there is no event data for keysessioncreated +player.tech().on('keysessioncreated', function(keySession) { + // Event data: + // keySession: the mediaKeySession object + // https://www.w3.org/TR/encrypted-media/#mediakeysession-interface }); ``` diff --git a/src/eme.js b/src/eme.js index 0dee1d8..66c0a6c 100644 --- a/src/eme.js +++ b/src/eme.js @@ -109,11 +109,17 @@ export const makeNewRequest = (player, requestOptions) => { try { const keySession = mediaKeys.createSession(); - eventBus.trigger('keysessioncreated'); + eventBus.trigger({ + type: 'keysessioncreated', + keySession + }); player.on('dispose', () => { keySession.close().then(() => { - eventBus.trigger('keysessionclosed'); + eventBus.trigger({ + type: 'keysessionclosed', + keySession + }); }).catch((error) => { const metadata = { errorType: videojs.Error.EMEFailedToCloseSession, @@ -128,7 +134,7 @@ export const makeNewRequest = (player, requestOptions) => { keySession.addEventListener('message', (event) => { eventBus.trigger({ type: 'keymessage', - event + messageEvent: event }); // all other types will be handled by keystatuseschange if (event.messageType !== 'license-request' && event.messageType !== 'license-renewal') { @@ -138,7 +144,10 @@ export const makeNewRequest = (player, requestOptions) => { getLicense(options, event.message, contentId) .then((license) => { resolve(keySession.update(license).then(() => { - eventBus.trigger('keysessionupdated'); + eventBus.trigger({ + type: 'keysessionupdated', + keySession + }); }).catch((error) => { const metadata = { errorType: videojs.Error.EMEFailedToUpdateSessionWithReceivedLicenseKeys, @@ -153,9 +162,18 @@ export const makeNewRequest = (player, requestOptions) => { }); }, false); - keySession.addEventListener('keystatuseschange', (event) => { + const KEY_STATUSES_CHANGE = 'keystatuseschange'; + + keySession.addEventListener(KEY_STATUSES_CHANGE, (event) => { let expired = false; + // Re-emit the keystatuseschange event with the entire keyStatusesMap + eventBus.trigger({ + type: KEY_STATUSES_CHANGE, + keyStatuses: keySession.keyStatuses + }); + + // Keep 'keystatuschange' for backward compatibility. // based on https://www.w3.org/TR/encrypted-media/#example-using-all-events keySession.keyStatuses.forEach((status, keyId) => { // Trigger an event so that outside listeners can take action if appropriate. @@ -191,7 +209,10 @@ export const makeNewRequest = (player, requestOptions) => { // session can be created. videojs.log.debug('Session expired, closing the session.'); keySession.close().then(() => { - eventBus.trigger('keysessionclosed'); + eventBus.trigger({ + type: 'keysessionclosed', + keySession + }); removeSession(initData); makeNewRequest(player, requestOptions); }).catch((error) => { @@ -504,7 +525,10 @@ export const standard5July2016 = ({ }).then(() => { return keySystemAccess.createMediaKeys(); }).then((createdMediaKeys) => { - eventBus.trigger('keysystemaccesscomplete'); + eventBus.trigger({ + type: 'keysystemaccesscomplete', + mediaKeys: createdMediaKeys + }); return addPendingSessions({ player, video, diff --git a/src/fairplay.js b/src/fairplay.js index d85788e..3b56644 100644 --- a/src/fairplay.js +++ b/src/fairplay.js @@ -84,11 +84,18 @@ const addKey = ({video, contentId, initData, cert, options, getLicense, eventBus return; } - eventBus.trigger('keysessioncreated'); + eventBus.trigger({ + type: 'keysessioncreated', + keySession + }); keySession.contentId = contentId; keySession.addEventListener('webkitkeymessage', (event) => { + eventBus.trigger({ + type: 'keymessage', + messageEvent: event + }); getLicense(options, contentId, event.message, (err, license) => { if (eventBus) { eventBus.trigger('licenserequestattempted'); @@ -105,6 +112,11 @@ const addKey = ({video, contentId, initData, cert, options, getLicense, eventBus } keySession.update(new Uint8Array(license)); + + eventBus.trigger({ + type: 'keysessionupdated', + keySession + }); }); }); diff --git a/src/ms-prefixed.js b/src/ms-prefixed.js index 201f336..968a3f5 100644 --- a/src/ms-prefixed.js +++ b/src/ms-prefixed.js @@ -9,6 +9,7 @@ import window from 'global/window'; import { requestPlayreadyLicense } from './playready'; import videojs from 'video.js'; +import { getMediaKeySystemConfigurations } from './utils'; export const PLAYREADY_KEY_SYSTEM = 'com.microsoft.playready'; @@ -20,7 +21,7 @@ export const addKeyToSession = (options, session, event, eventBus, emeError) => if (err) { const metadata = { errorType: videojs.Error.EMEFailedToRequestMediaKeySystemAccess, - keySystem: PLAYREADY_KEY_SYSTEM + config: getMediaKeySystemConfigurations(options.keySystems) }; emeError(err, metadata); @@ -33,6 +34,11 @@ export const addKeyToSession = (options, session, event, eventBus, emeError) => } session.update(key); + + eventBus.trigger({ + type: 'keysessionupdated', + keySession: session + }); }); return; } @@ -92,7 +98,10 @@ export const createSession = (video, initData, options, eventBus, emeError) => { throw error; } - eventBus.trigger('keysessioncreated'); + eventBus.trigger({ + type: 'keysessioncreated', + keySession: session + }); // Note that mskeymessage may not always be called for PlayReady: // @@ -104,6 +113,10 @@ export const createSession = (video, initData, options, eventBus, emeError) => { // eslint-disable-next-line max-len // @see [PlayReady License Acquisition]{@link https://msdn.microsoft.com/en-us/library/dn468979.aspx} session.addEventListener('mskeymessage', (event) => { + eventBus.trigger({ + type: 'keymessage', + messageEvent: event + }); addKeyToSession(options, session, event, eventBus, emeError); }); diff --git a/src/plugin.js b/src/plugin.js index fbfaae9..df33ff4 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -10,7 +10,7 @@ import { PLAYREADY_KEY_SYSTEM } from './ms-prefixed'; import {detectSupportedCDMs } from './cdm.js'; -import { arrayBuffersEqual, arrayBufferFrom, merge } from './utils'; +import { arrayBuffersEqual, arrayBufferFrom, merge, getMediaKeySystemConfigurations } from './utils'; import {version as VERSION} from '../package.json'; export const hasSession = (sessions, initData) => { @@ -103,7 +103,7 @@ export const handleEncryptedEvent = (player, event, options, sessions, eventBus, }).catch((error) => { const metadata = { errorType: videojs.Error.EMEFailedToRequestMediaKeySystemAccess, - config: options.keySystems + config: getMediaKeySystemConfigurations(options.keySystems) }; emeError(error, metadata); diff --git a/src/utils.js b/src/utils.js index 37e102e..45432b7 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,6 @@ import document from 'global/document'; import videojs from 'video.js'; +import { getSupportedConfigurations } from './eme'; export const stringToUint16Array = (string) => { // 2 bytes for each char @@ -78,3 +79,20 @@ export const mergeAndRemoveNull = (...args) => { return result; }; + +/** + * Transforms the keySystems object into a MediaKeySystemConfiguration Object array. + * + * @param {Object} keySystems object from the options. + * @return {Array} of MediaKeySystemConfiguration objects. + */ +export const getMediaKeySystemConfigurations = (keySystems) => { + const config = []; + + Object.keys(keySystems).forEach((keySystem) => { + const mediaKeySystemConfig = getSupportedConfigurations(keySystem, keySystems[keySystem])[0]; + + config.push(mediaKeySystemConfig); + }); + return config; +}; diff --git a/test/eme.test.js b/test/eme.test.js index 5c424ff..a450c42 100644 --- a/test/eme.test.js +++ b/test/eme.test.js @@ -70,7 +70,7 @@ QUnit.test('keystatuseschange triggers keystatuschange on eventBus for each key' const mockSession = getMockSession(); const eventBus = { trigger: (event) => { - if (typeof event === 'string') { + if (typeof event === 'string' || event.type !== 'keystatuschange') { return; } @@ -91,7 +91,8 @@ QUnit.test('keystatuseschange triggers keystatuschange on eventBus for each key' options: {}, getLicense() {}, removeSession() {}, - eventBus + eventBus, + emeError() {} }); assert.equal(mockSession.listeners.length, 2, 'added listeners'); @@ -325,11 +326,12 @@ QUnit.test('accepts a license URL as an option', function(assert) { xhrCalls.push(options); }; + const createSession = () => mockSession; const keySystemAccess = { keySystem: 'com.widevine.alpha', createMediaKeys: () => { return { - createSession: () => mockSession + createSession }; } }; @@ -361,9 +363,12 @@ QUnit.test('accepts a license URL as an option', function(assert) { // Simulate 'message' event mockSession.listeners[0].listener(mockMessageEvent); - assert.equal(mockEventBus.calls[0], 'keysystemaccesscomplete', 'keysystemaccesscomplete fired'); - assert.equal(mockEventBus.calls[1], 'keysessioncreated', 'keymessage fired'); - assert.equal(mockEventBus.calls[2].event, mockMessageEvent, 'keymessage event is expected message event'); + 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', @@ -388,6 +393,7 @@ QUnit.test('accepts a license URL as property', function(assert) { const xhrCalls = []; const mockSession = getMockSession(); const mockEventBus = getMockEventBus(); + const createSession = () => mockSession; const mockMessageEvent = { type: 'message', message: 'the-message', @@ -397,7 +403,7 @@ QUnit.test('accepts a license URL as property', function(assert) { keySystem: 'com.widevine.alpha', createMediaKeys: () => { return { - createSession: () => mockSession + createSession }; } }; @@ -435,9 +441,10 @@ QUnit.test('accepts a license URL as property', function(assert) { // Simulate 'message' event mockSession.listeners[0].listener(mockMessageEvent); - assert.equal(mockEventBus.calls[0], 'keysystemaccesscomplete', 'keysystemaccesscomplete fired'); - assert.equal(mockEventBus.calls[1], 'keysessioncreated', 'keymessage fired'); - assert.equal(mockEventBus.calls[2].event, mockMessageEvent, 'keymessage event is expected message event'); + 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', @@ -500,7 +507,9 @@ QUnit.test('5 July 2016 lifecycle', function(assert) { }; const eventBus = { - trigger: (name) => { + trigger: (event) => { + const name = typeof event === 'string' ? event : event.type; + if (name === 'licenserequestattempted') { callCounts.licenseRequestAttempts++; } @@ -1111,6 +1120,7 @@ QUnit.test('emeHeaders option sets headers on default license xhr request', func message: 'the-message', messageType: 'license-request' }; + const createSession = () => mockSession; videojs.xhr = (options) => { xhrCalls.push(options); @@ -1120,7 +1130,7 @@ QUnit.test('emeHeaders option sets headers on default license xhr request', func keySystem: 'com.widevine.alpha', createMediaKeys: () => { return { - createSession: () => mockSession + createSession }; } }; @@ -1148,9 +1158,10 @@ QUnit.test('emeHeaders option sets headers on default license xhr request', func // Simulate 'message' event mockSession.listeners[0].listener(mockMessageEvent); - assert.equal(mockEventBus.calls[0], 'keysystemaccesscomplete', 'keysystemaccesscomplete fired'); - assert.equal(mockEventBus.calls[1], 'keysessioncreated', 'keymessage fired'); - assert.equal(mockEventBus.calls[2].event, mockMessageEvent, 'keymessage event is expected message event'); + 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', @@ -1181,6 +1192,7 @@ QUnit.test('licenseHeaders keySystems property overrides emeHeaders value', func message: 'the-message', messageType: 'license-request' }; + const createSession = () => mockSession; videojs.xhr = (options) => { xhrCalls.push(options); @@ -1190,7 +1202,7 @@ QUnit.test('licenseHeaders keySystems property overrides emeHeaders value', func keySystem: 'com.widevine.alpha', createMediaKeys: () => { return { - createSession: () => mockSession + createSession }; } }; @@ -1223,9 +1235,12 @@ QUnit.test('licenseHeaders keySystems property overrides emeHeaders value', func // Simulate 'message' event mockSession.listeners[0].listener(mockMessageEvent); - assert.equal(mockEventBus.calls[0], 'keysystemaccesscomplete', 'keysystemaccesscomplete fired'); - assert.equal(mockEventBus.calls[1], 'keysessioncreated', 'keymessage fired'); - assert.equal(mockEventBus.calls[2].event, mockMessageEvent, 'keymessage event is expected message event'); + 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', @@ -1273,8 +1288,8 @@ QUnit.test('makeNewRequest triggers keysessioncreated', function(assert) { createSession: () => mockSession }, eventBus: { - trigger: (eventName) => { - if (eventName === 'keysessioncreated') { + trigger: (event) => { + if (event.type === 'keysessioncreated') { assert.ok(true, 'got a keysessioncreated event'); done(); } @@ -1292,8 +1307,8 @@ QUnit.test('keySession is closed when player is disposed', function(assert) { createSession: () => mockSession }, eventBus: { - trigger: (eventName) => { - if (eventName === 'keysessionclosed') { + trigger: (event) => { + if (event.type === 'keysessionclosed') { assert.ok(true, 'got a keysessionclosed event'); done(); } diff --git a/test/fairplay.test.js b/test/fairplay.test.js index e311af7..15a8eee 100644 --- a/test/fairplay.test.js +++ b/test/fairplay.test.js @@ -336,13 +336,14 @@ QUnit.test('keysessioncreated fired on key session created', function(assert) { 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: () => {} + addEventListener }; } }; @@ -350,8 +351,9 @@ QUnit.test('keysessioncreated fired on key session created', function(assert) { }; const eventBus = { trigger: (event) => { - if (event === 'keysessioncreated') { + if (event.type === 'keysessioncreated') { assert.ok(sessionCreated, 'keysessioncreated fired after session created'); + assert.deepEqual(event.keySession, { addEventListener }, 'keySession payload passed with event'); done(); } } diff --git a/test/ms-prefixed.test.js b/test/ms-prefixed.test.js index f070241..702eaf1 100644 --- a/test/ms-prefixed.test.js +++ b/test/ms-prefixed.test.js @@ -178,7 +178,7 @@ QUnit.test('calls getKey when provided on key message', function(assert) { }; const emeError = (_, metadata) => { assert.equal(metadata.errorType, videojs.Error.EMEFailedToRequestMediaKeySystemAccess, 'errorType is expected value'); - assert.equal(metadata.keySystem, PLAYREADY_KEY_SYSTEM, 'keySystem is expected value'); + assert.deepEqual(metadata.config, [{}], 'keySystem is expected value'); }; msPrefixed({ @@ -595,11 +595,12 @@ QUnit.test('will use a custom getLicense method if one is provided', function(as }); QUnit.test('createSession triggers keysessioncreated', function(assert) { + const addEventListener = () => {}; const video = { msKeys: { createSession: () => { return { - addEventListener: () => {} + addEventListener }; } } @@ -610,8 +611,9 @@ QUnit.test('createSession triggers keysessioncreated', function(assert) { assert.equal(eventBus.calls.length, 1, 'one event triggered'); assert.equal( - eventBus.calls[0], + eventBus.calls[0].type, 'keysessioncreated', 'triggered keysessioncreated event' ); + assert.deepEqual(eventBus.calls[0].keySession, { addEventListener }, 'keysessioncreated payload'); }); diff --git a/test/utils.test.js b/test/utils.test.js index 5e5d9aa..6718085 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -3,7 +3,8 @@ import QUnit from 'qunit'; import { arrayBuffersEqual, arrayBufferFrom, - mergeAndRemoveNull + mergeAndRemoveNull, + getMediaKeySystemConfigurations } from '../src/utils'; QUnit.module('utils'); @@ -72,3 +73,31 @@ QUnit.test('mergeAndRemoveNull removes property if value is null', function(asse c: 'c' }, 'successfully merged and removed null property'); }); + +QUnit.test('getMediaKeySystemConfigurations returns MediaKeySystemConfiguration array', function(assert) { + const config = getMediaKeySystemConfigurations({ + 'com.widevine.alpha': { + audioContentType: 'audio/mp4; codecs="mp4a.40.2"', + audioRobustness: 'SW_SECURE_CRYPTO', + videoContentType: 'video/mp4; codecs="avc1.42E01E"', + videoRobustness: 'SW_SECURE_CRYPTO' + } + }); + + const expectedConfig = [{ + audioCapabilities: [ + { + contentType: 'audio/mp4; codecs=\"mp4a.40.2\"', + robustness: 'SW_SECURE_CRYPTO' + } + ], + videoCapabilities: [ + { + contentType: 'video/mp4; codecs=\"avc1.42E01E\"', + robustness: 'SW_SECURE_CRYPTO' + } + ] + }]; + + assert.deepEqual(config, expectedConfig, 'getMediaKeysystemConfigurations returns expected values'); +});