From f29329f583ac63ee1e58443eb319042247ed6d9a Mon Sep 17 00:00:00 2001 From: Garrett Singer Date: Fri, 19 Jun 2020 11:51:40 -0400 Subject: [PATCH 1/2] fix: initialize EME for all playlists and PSSH values --- src/videojs-http-streaming.js | 98 +++++++++++- test/videojs-http-streaming.test.js | 225 ++++++++++++++++++++++++---- 2 files changed, 286 insertions(+), 37 deletions(-) diff --git a/src/videojs-http-streaming.js b/src/videojs-http-streaming.js index 7b9ee4105..e5de91055 100644 --- a/src/videojs-http-streaming.js +++ b/src/videojs-http-streaming.js @@ -172,11 +172,75 @@ const emeKeySystems = (keySystemOptions, videoPlaylist, audioPlaylist) => { return videojs.mergeOptions(keySystemOptions, keySystemContentTypes); }; +/** + * @typedef {Object} KeySystems + * + * keySystems configuration for https://github.com/videojs/videojs-contrib-eme + * Note: not all options are listed here. + * + * @property {Uint8Array} [pssh] + * Protection System Specific Header + */ + +/** + * Goes through all the playlists and collects an array of KeySystems options objects + * containing each playlist's keySystems and their pssh values, if available. + * + * @param {Object[]} playlists + * The playlists to look through + * @param {string[]} keySystems + * The keySystems to collect pssh values for + * + * @return {KeySystems[]} + * An array of KeySystems objects containing available key systems and their + * pssh values + */ +const getAllPsshKeySystemsOptions = (playlists, keySystems) => { + return playlists.reduce((keySystemsArr, playlist) => { + if (!playlist.contentProtection) { + return keySystemsArr; + } + + const keySystemsOptions = keySystems.reduce((keySystemsObj, keySystem) => { + const keySystemOptions = playlist.contentProtection[keySystem]; + + if (keySystemOptions && keySystemOptions.pssh) { + keySystemsObj[keySystem] = { pssh: keySystemOptions.pssh }; + } + + return keySystemsObj; + }, {}); + + if (Object.keys(keySystemsOptions).length) { + keySystemsArr.push(keySystemsOptions); + } + + return keySystemsArr; + }, []); +}; + +/** + * If the [eme](https://github.com/videojs/videojs-contrib-eme) plugin is available, and + * there are keySystems on the source, sets up source options to prepare the source for + * eme and tries to initialize it early via eme's initializeMediaKeys API (if available). + * + * @param {Object} player + * The player instance + * @param {Object[]} sourceKeySystems + * The key systems options from the player source + * @param {Object} media + * The active media playlist + * @param {Object} [audioMedia] + * The active audio media playlist (optional) + * @param {Object[]} mainPlaylists + * The playlists found on the master playlist object + */ const setupEmeOptions = ({ player, sourceKeySystems, media, - audioMedia + audioMedia, + mainPlaylists }) => { if (!player.eme) { return; @@ -196,7 +260,31 @@ const setupEmeOptions = ({ return; } - player.eme.initializeMediaKeys(); + // TODO should all audio PSSH values be initialized for DRM? + // + // All unique video rendition pssh values are initialized for DRM, but here only + // the initial audio playlist license is initialized. In theory, an encrypted + // event should be fired if the user switches to an alternative audio playlist + // where a license is required, but this case hasn't yet been tested. In addition, there + // may be many alternate audio playlists unlikely to be used (e.g., multiple different + // languages). + const playlists = audioMedia ? mainPlaylists.concat([audioMedia]) : mainPlaylists; + + const keySystemsOptionsArr = getAllPsshKeySystemsOptions( + playlists, + Object.keys(sourceKeySystems) + ); + + // Since PSSH values are interpreted as initData, EME will dedupe any duplicates. The + // only place where it should not be deduped is for ms-prefixed APIs, but the early + // return for IE11 above, and the existence of modern EME APIs in addition to + // ms-prefixed APIs on Edge should prevent this from being a concern. + // initializeMediaKeys also won't use the webkit-prefixed APIs. + keySystemsOptionsArr.forEach((keySystemsOptions) => { + player.eme.initializeMediaKeys({ + keySystems: keySystemsOptions + }); + }); }; const getVhsLocalStorage = () => { @@ -738,7 +826,8 @@ class VhsHandler extends Component { player: this.player_, sourceKeySystems: this.source_.keySystems, media: this.playlists.media(), - audioMedia: audioPlaylistLoader && audioPlaylistLoader.media() + audioMedia: audioPlaylistLoader && audioPlaylistLoader.media(), + mainPlaylists: this.playlists.master.playlists }); }); @@ -1003,5 +1092,6 @@ export { emeKeySystems, simpleTypeFromSourceType, expandDataUri, - setupEmeOptions + setupEmeOptions, + getAllPsshKeySystemsOptions }; diff --git a/test/videojs-http-streaming.test.js b/test/videojs-http-streaming.test.js index f697431c1..6169ce579 100644 --- a/test/videojs-http-streaming.test.js +++ b/test/videojs-http-streaming.test.js @@ -37,7 +37,8 @@ import { emeKeySystems, LOCAL_STORAGE_KEY, expandDataUri, - setupEmeOptions + setupEmeOptions, + getAllPsshKeySystemsOptions } from '../src/videojs-http-streaming'; import window from 'global/window'; // we need this so the plugin registers itself @@ -4341,17 +4342,20 @@ QUnit.test('configures eme for DASH if present on sourceUpdater ready', function this.clock.tick(1); - this.player.tech_.vhs.playlists = { - media: () => ({ - attributes: { - CODECS: 'avc1.420015' - }, - contentProtection: { - keySystem1: { - pssh: 'test' - } + const media = { + attributes: { + CODECS: 'avc1.420015' + }, + contentProtection: { + keySystem1: { + pssh: 'test' } - }) + } + }; + + this.player.tech_.vhs.playlists = { + master: { playlists: [media] }, + media: () => media }; this.player.tech_.vhs.masterPlaylistController_.mediaTypes_ = { @@ -4407,17 +4411,20 @@ QUnit.test('configures eme for HLS if present on sourceUpdater ready', function( this.clock.tick(1); - this.player.tech_.vhs.playlists = { - media: () => ({ - attributes: { - CODECS: 'avc1.420015, mp4a.40.2c' - }, - contentProtection: { - keySystem1: { - pssh: 'test' - } + const media = { + attributes: { + CODECS: 'avc1.420015, mp4a.40.2c' + }, + contentProtection: { + keySystem1: { + pssh: 'test' } - }) + } + }; + + this.player.tech_.vhs.playlists = { + master: { playlists: [media] }, + media: () => media }; this.player.tech_.vhs.masterPlaylistController_.sourceUpdater_.trigger('ready'); @@ -5741,8 +5748,9 @@ QUnit.test('no error if no eme', function(assert) { const sourceKeySystems = {}; const media = {}; const audioMedia = {}; + const mainPlaylists = []; - setupEmeOptions({ player, sourceKeySystems, media, audioMedia }); + setupEmeOptions({ player, sourceKeySystems, media, audioMedia, mainPlaylists }); assert.ok(true, 'no exception'); }); @@ -5756,8 +5764,9 @@ QUnit.test('no initialize calls if no source key systems', function(assert) { contentProtection: { 'com.widevine.alpha': { pssh: new Uint8Array() } } }; const audioMedia = null; + const mainPlaylists = [media]; - setupEmeOptions({ player, sourceKeySystems, media, audioMedia }); + setupEmeOptions({ player, sourceKeySystems, media, audioMedia, mainPlaylists }); assert.equal(numInitializeCalls, 0, 'no initialize calls'); }); @@ -5780,13 +5789,14 @@ QUnit.test('initializes for muxed playlist', function(assert) { contentProtection: { 'com.widevine.alpha': { pssh: new Uint8Array() } } }; const audioMedia = null; + const mainPlaylists = [media]; - setupEmeOptions({ player, sourceKeySystems, media, audioMedia }); + setupEmeOptions({ player, sourceKeySystems, media, audioMedia, mainPlaylists }); assert.equal(numInitializeCalls, 1, 'one initialize call'); }); -QUnit.test('initializes for demuxed playlist', function(assert) { +QUnit.test('initializes for each playlist for demuxed playlist', function(assert) { let numInitializeCalls = 0; const player = { eme: { initializeMediaKeys: () => numInitializeCalls++ }, @@ -5807,10 +5817,11 @@ QUnit.test('initializes for demuxed playlist', function(assert) { attributes: {}, contentProtection: { 'com.widevine.alpha': { pssh: new Uint8Array() } } }; + const mainPlaylists = [media]; - setupEmeOptions({ player, sourceKeySystems, media, audioMedia }); + setupEmeOptions({ player, sourceKeySystems, media, audioMedia, mainPlaylists }); - assert.equal(numInitializeCalls, 1, 'one initialize call'); + assert.equal(numInitializeCalls, 2, 'two initialize calls'); }); QUnit.test('does not initialize if IE11', function(assert) { @@ -5835,13 +5846,14 @@ QUnit.test('does not initialize if IE11', function(assert) { attributes: {}, contentProtection: { 'com.widevine.alpha': { pssh: new Uint8Array() } } }; + const mainPlaylists = [media]; - setupEmeOptions({ player, sourceKeySystems, media, audioMedia }); + setupEmeOptions({ player, sourceKeySystems, media, audioMedia, mainPlaylists }); assert.equal(numInitializeCalls, 0, 'no initialize calls'); }); -QUnit.test('only initializes once even for different pssh values', function(assert) { +QUnit.test('initializes for each playlist', function(assert) { let numInitializeCalls = 0; const player = { eme: { initializeMediaKeys: () => numInitializeCalls++ }, @@ -5856,14 +5868,161 @@ QUnit.test('only initializes once even for different pssh values', function(asse }; const media = { attributes: { CODECS: 'avc1.4d400d,mp4a.40.2' }, - contentProtection: { 'com.widevine.alpha': { pssh: new Uint8Array([0]) } } + contentProtection: { 'com.widevine.alpha': { pssh: new Uint8Array() } } + }; + const media1 = { + attributes: { CODECS: 'avc1.4d400d,mp4a.40.2' }, + contentProtection: { 'com.widevine.alpha': { pssh: new Uint8Array() } } }; const audioMedia = { attributes: {}, - contentProtection: { 'com.widevine.alpha': { pssh: new Uint8Array([1]) } } + contentProtection: { 'com.widevine.alpha': { pssh: new Uint8Array() } } }; + const mainPlaylists = [media, media1]; - setupEmeOptions({ player, sourceKeySystems, media, audioMedia }); + setupEmeOptions({ player, sourceKeySystems, media, audioMedia, mainPlaylists }); - assert.equal(numInitializeCalls, 1, 'one initialize call'); + assert.equal(numInitializeCalls, 3, 'three initialize calls'); +}); + +QUnit.test('initializes with correct options for each playlist', function(assert) { + const initializeCallOptions = []; + const player = { + eme: { initializeMediaKeys: (options) => initializeCallOptions.push(options) }, + currentSource: () => { + return {}; + } + }; + const sourceKeySystems = { + 'com.widevine.alpha': { + url: 'license-url' + }, + 'com.microsoft.playready': { + url: 'license-url' + } + }; + const media = { + attributes: { CODECS: 'avc1.4d400d,mp4a.40.2' }, + contentProtection: { + 'com.widevine.alpha': { pssh: new Uint8Array([0]) }, + 'com.microsoft.playready': { pssh: new Uint8Array([1]) } + } + }; + const media1 = { + attributes: { CODECS: 'avc1.4d400d,mp4a.40.2' }, + contentProtection: { + 'com.widevine.alpha': { pssh: new Uint8Array([2]) }, + 'com.microsoft.playready': { pssh: new Uint8Array([3]) } + } + }; + const audioMedia = { + attributes: {}, + contentProtection: { + 'com.widevine.alpha': { pssh: new Uint8Array([4]) }, + 'com.microsoft.playready': { pssh: new Uint8Array([5]) } + } + }; + const mainPlaylists = [media, media1]; + + setupEmeOptions({ player, sourceKeySystems, media, audioMedia, mainPlaylists }); + + assert.deepEqual( + initializeCallOptions, + [{ + keySystems: media.contentProtection + }, { + keySystems: media1.contentProtection + }, { + keySystems: audioMedia.contentProtection + }], + 'called with correct values' + ); +}); + +QUnit.module('getAllPsshKeySystemsOptions'); + +QUnit.test('empty array if no content proteciton in playlists', function(assert) { + assert.deepEqual( + getAllPsshKeySystemsOptions( + [{}, {}], + ['com.widevine.alpha', 'com.microsoft.playready'] + ), + [], + 'returned an empty array' + ); +}); + +QUnit.test('empty array if no matching key systems in playlists', function(assert) { + assert.deepEqual( + getAllPsshKeySystemsOptions( + [{ + contentProtection: { 'com.widevine.alpha': { pssh: new Uint8Array() } } + }, { + contentProtection: { 'com.widevine.alpha': { pssh: new Uint8Array() } } + }], + ['com.microsoft.playready'] + ), + [], + 'returned an empty array' + ); +}); + +QUnit.test('empty array if no pssh in playlist contentProtection', function(assert) { + assert.deepEqual( + getAllPsshKeySystemsOptions( + [{ + contentProtection: { + 'com.widevine.alpha': {}, + 'com.microsoft.playready': {} + } + }, { + contentProtection: { + 'com.widevine.alpha': {}, + 'com.microsoft.playready': {} + } + }], + ['com.widevine.alpha', 'com.microsoft.playready'] + ), + [], + 'returned an empty array' + ); +}); + +QUnit.test('returns all key systems and pssh values', function(assert) { + assert.deepEqual( + getAllPsshKeySystemsOptions( + [{ + contentProtection: { + 'com.widevine.alpha': { + pssh: new Uint8Array([0]), + otherProperty: true + }, + 'com.microsoft.playready': { + pssh: new Uint8Array([1]), + otherProperty: true + } + } + }, { + contentProtection: { + 'com.widevine.alpha': { + pssh: new Uint8Array([2]), + otherProperty: true + }, + 'com.microsoft.playready': { + pssh: new Uint8Array([3]), + otherProperty: true + } + } + }], + ['com.widevine.alpha', 'com.microsoft.playready'] + ), + [{ + 'com.widevine.alpha': { pssh: new Uint8Array([0]) }, + 'com.microsoft.playready': { pssh: new Uint8Array([1]) } + }, { + 'com.widevine.alpha': { pssh: new Uint8Array([2]) }, + 'com.microsoft.playready': { pssh: new Uint8Array([3]) } + }], + 'returned key systems and pssh values without other properties' + ); }); From 7fe56901cef6eebebbef91f9f497e29431fd77b6 Mon Sep 17 00:00:00 2001 From: Garrett Singer Date: Mon, 6 Jul 2020 15:48:39 -0400 Subject: [PATCH 2/2] Fix tests where playlist should be downloaded before source buffers are created --- test/master-playlist-controller.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/master-playlist-controller.test.js b/test/master-playlist-controller.test.js index e686f487a..eedb749e3 100644 --- a/test/master-playlist-controller.test.js +++ b/test/master-playlist-controller.test.js @@ -2639,13 +2639,13 @@ QUnit.test('trigger event when a video fMP4 stream is detected', function(assert const mpc = this.player.tech(true).vhs.masterPlaylistController_; const loader = mpc.mainSegmentLoader_; + // media + this.standardXHRResponse(this.requests.shift()); + return setupMediaSource(loader.mediaSource_, loader.sourceUpdater_, { videoEl: this.player.tech_.el_, isVideoOnly: true }).then(() => { - // media - this.standardXHRResponse(this.requests.shift()); - assert.equal(hlsFmp4Events, 0, 'an fMP4 stream is not detected'); const initSegmentRequest = this.requests.shift(); @@ -2727,13 +2727,13 @@ QUnit.test('trigger event when an audio fMP4 stream is detected', function(asser const mpc = this.player.tech(true).vhs.masterPlaylistController_; const loader = mpc.mainSegmentLoader_; + // media + this.standardXHRResponse(this.requests.shift()); + return setupMediaSource(loader.mediaSource_, loader.sourceUpdater_, { videoEl: this.player.tech_.el_, isAudioOnly: true }).then(() => { - // media - this.standardXHRResponse(this.requests.shift()); - assert.equal(vhsFmp4Events, 0, 'an fMP4 stream is not detected'); assert.equal(hlsFmp4Events, 0, 'an fMP4 stream is not detected');