Browse Source

fix: initialize EME for all playlists and PSSH values (#872)

pull/893/head
Garrett Singer 5 years ago
committed by GitHub
parent
commit
e0e497f7ca
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 98
      src/videojs-http-streaming.js
  2. 12
      test/master-playlist-controller.test.js
  3. 225
      test/videojs-http-streaming.test.js

98
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
};

12
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');

225
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'
);
});
Loading…
Cancel
Save