From af4da1aca7929861639a74949f5da615f5dc04a1 Mon Sep 17 00:00:00 2001 From: Brandon Casey <2381475+brandonocasey@users.noreply.github.com> Date: Fri, 14 Feb 2020 16:45:38 -0500 Subject: [PATCH] feat: setup EME key systems for HLS as well as DASH (#657) Port of #629 against master. --- src/util/codecs.js | 22 +++ src/videojs-http-streaming.js | 33 +++-- test/manifests/demuxed-two.m3u8 | 6 + test/videojs-http-streaming.test.js | 202 ++++++++++++++++++++++------ 4 files changed, 213 insertions(+), 50 deletions(-) create mode 100644 test/manifests/demuxed-two.m3u8 diff --git a/src/util/codecs.js b/src/util/codecs.js index 1325cd3d..02823330 100644 --- a/src/util/codecs.js +++ b/src/util/codecs.js @@ -99,3 +99,25 @@ export const codecsForPlaylist = function(master, media) { export const isLikelyFmp4Data = (bytes) => { return findBox(bytes, ['moof']).length > 0; }; + +/* + * Check if a codec string refers to an audio codec. + * + * @param {String} codec codec string to check + * @return {Boolean} if this is an audio codec + * @private + */ +export const isAudioCodec = function(codec) { + return (/mp4a\.\d+.\d+/i).test(codec); +}; + +/** + * Check if a codec string refers to a video codec. + * + * @param {string} codec codec string to check + * @return {boolean} if this is a video codec + * @private + */ +export const isVideoCodec = function(codec) { + return (/avc1\.[\da-f]+/i).test(codec); +}; diff --git a/src/videojs-http-streaming.js b/src/videojs-http-streaming.js index bc82c355..dcd0cd6b 100644 --- a/src/videojs-http-streaming.js +++ b/src/videojs-http-streaming.js @@ -36,6 +36,7 @@ import {version as m3u8Version} from 'm3u8-parser/package.json'; import {version as aesVersion} from 'aes-decrypter/package.json'; // import needed to register middleware import './middleware-set-current-time'; +import {isAudioCodec, isVideoCodec} from './util/codecs'; const Hls = { PlaylistLoader, @@ -134,14 +135,30 @@ const emeKeySystems = (keySystemOptions, videoPlaylist, audioPlaylist) => { return keySystemOptions; } + const codecs = { + video: videoPlaylist && videoPlaylist.attributes && videoPlaylist.attributes.CODECS, + audio: audioPlaylist && audioPlaylist.attributes && audioPlaylist.attributes.CODECS + }; + + if (!codecs.audio && codecs.video.split(',').length > 1) { + codecs.video.split(',').forEach(function(codec) { + codec = codec.trim(); + + if (isAudioCodec(codec)) { + codecs.audio = codec; + } else if (isVideoCodec(codec)) { + codecs.video = codec; + } + }); + } + const videoContentType = codecs.video ? `video/mp4;codecs="${codecs.video}"` : null; + const audioContentType = codecs.audio ? `audio/mp4;codecs="${codecs.audio}"` : null; + // upsert the content types based on the selected playlist const keySystemContentTypes = {}; for (const keySystem in keySystemOptions) { - keySystemContentTypes[keySystem] = { - audioContentType: `audio/mp4; codecs="${audioPlaylist.attributes.CODECS}"`, - videoContentType: `video/mp4; codecs="${videoPlaylist.attributes.CODECS}"` - }; + keySystemContentTypes[keySystem] = {audioContentType, videoContentType}; if (videoPlaylist.contentProtection && videoPlaylist.contentProtection[keySystem] && @@ -161,16 +178,14 @@ const emeKeySystems = (keySystemOptions, videoPlaylist, audioPlaylist) => { }; const setupEmeOptions = (hlsHandler) => { - if (hlsHandler.options_.sourceType !== 'dash') { - return; - } - const player = videojs.players[hlsHandler.tech_.options_.playerId]; + const player = hlsHandler.player_; if (player.eme) { + const audioPlaylistLoader = hlsHandler.masterPlaylistController_.mediaTypes_.AUDIO.activePlaylistLoader; const sourceOptions = emeKeySystems( hlsHandler.source_.keySystems, hlsHandler.playlists.media(), - hlsHandler.masterPlaylistController_.mediaTypes_.AUDIO.activePlaylistLoader.media() + audioPlaylistLoader && audioPlaylistLoader.media() ); if (sourceOptions) { diff --git a/test/manifests/demuxed-two.m3u8 b/test/manifests/demuxed-two.m3u8 new file mode 100644 index 00000000..466433a4 --- /dev/null +++ b/test/manifests/demuxed-two.m3u8 @@ -0,0 +1,6 @@ +#EXTM3U +#EXT-X-VERSION:4 +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="en",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",URI="media.m3u8" +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=564300,CODECS="mp4a.40.2,avc1.420015",AUDIO="audio" +media1.m3u8 + diff --git a/test/videojs-http-streaming.test.js b/test/videojs-http-streaming.test.js index e247ff65..01d0b543 100644 --- a/test/videojs-http-streaming.test.js +++ b/test/videojs-http-streaming.test.js @@ -3739,7 +3739,7 @@ QUnit.test('populates quality levels list when available', function(assert) { ); }); -QUnit.test('configures eme if present on selectedinitialmedia', function(assert) { +QUnit.test('configures eme for DASH if present on selectedinitialmedia', function(assert) { this.player.eme = { options: { previousSetting: 1 @@ -3758,23 +3758,18 @@ QUnit.test('configures eme if present on selectedinitialmedia', function(assert) this.clock.tick(1); this.player.tech_.hls.playlists = { - media: () => { - return { - attributes: { - CODECS: 'video-codec' - }, - contentProtection: { - keySystem1: { - pssh: 'test' - } + media: () => ({ + attributes: { + CODECS: 'avc1.420015' + }, + contentProtection: { + keySystem1: { + pssh: 'test' } - }; - }, - // mocked for renditions mixin - master: { - playlists: [] - } + } + }) }; + this.player.tech_.hls.masterPlaylistController_.mediaTypes_ = { SUBTITLES: {}, AUDIO: { @@ -3782,13 +3777,14 @@ QUnit.test('configures eme if present on selectedinitialmedia', function(assert) media: () => { return { attributes: { - CODECS: 'audio-codec' + CODECS: 'mp4a.40.2c' } }; } } } }; + this.player.tech_.hls.masterPlaylistController_.trigger('selectedinitialmedia'); assert.deepEqual(this.player.eme.options, { @@ -3801,15 +3797,66 @@ QUnit.test('configures eme if present on selectedinitialmedia', function(assert) keySystems: { keySystem1: { url: 'url1', - audioContentType: 'audio/mp4; codecs="audio-codec"', - videoContentType: 'video/mp4; codecs="video-codec"', + audioContentType: 'audio/mp4;codecs="mp4a.40.2c"', + videoContentType: 'video/mp4;codecs="avc1.420015"', + pssh: 'test' + } + } + }, 'set source eme options'); +}); + +QUnit.test('configures eme for HLS if present on selectedinitialmedia', function(assert) { + this.player.eme = { + options: { + previousSetting: 1 + } + }; + this.player.src({ + src: 'manifest/master.m3u8', + type: 'application/x-mpegURL', + keySystems: { + keySystem1: { + url: 'url1' + } + } + }); + + this.clock.tick(1); + + this.player.tech_.hls.playlists = { + media: () => ({ + attributes: { + CODECS: 'avc1.420015, mp4a.40.2c' + }, + contentProtection: { + keySystem1: { + pssh: 'test' + } + } + }) + }; + + this.player.tech_.hls.masterPlaylistController_.trigger('selectedinitialmedia'); + + assert.deepEqual(this.player.eme.options, { + previousSetting: 1 + }, 'did not modify plugin options'); + + assert.deepEqual(this.player.currentSource(), { + src: 'manifest/master.m3u8', + type: 'application/x-mpegURL', + keySystems: { + keySystem1: { + url: 'url1', + audioContentType: 'audio/mp4;codecs="mp4a.40.2c"', + videoContentType: 'video/mp4;codecs="avc1.420015"', pssh: 'test' } } }, 'set source eme options'); }); -QUnit.test('integration: configures eme if present on selectedinitialmedia', function(assert) { +QUnit.test('integration: configures eme for DASH if present on selectedinitialmedia', function(assert) { assert.timeout(3000); const done = assert.async(); @@ -3840,8 +3887,8 @@ QUnit.test('integration: configures eme if present on selectedinitialmedia', fun keySystems: { keySystem1: { url: 'url1', - audioContentType: 'audio/mp4; codecs="mp4a.40.2"', - videoContentType: 'video/mp4; codecs="avc1.420015"' + audioContentType: 'audio/mp4;codecs="mp4a.40.2"', + videoContentType: 'video/mp4;codecs="avc1.420015"' } } }, 'set source eme options'); @@ -3854,6 +3901,59 @@ QUnit.test('integration: configures eme if present on selectedinitialmedia', fun this.clock.tick(1); }); +QUnit.test('integration: configures eme for HLS if present on selectedinitialmedia', function(assert) { + assert.timeout(3000); + const done = assert.async(); + + this.player.eme = { + options: { + previousSetting: 1 + } + }; + this.player.src({ + src: 'demuxed-two.m3u8', + type: 'application/x-mpegURL', + keySystems: { + keySystem1: { + url: 'url1' + } + } + }); + this.clock.tick(1); + + this.player.tech_.hls.masterPlaylistController_.on('selectedinitialmedia', () => { + assert.deepEqual(this.player.eme.options, { + previousSetting: 1 + }, 'did not modify plugin options'); + + assert.deepEqual(this.player.currentSource(), { + src: 'demuxed-two.m3u8', + type: 'application/x-mpegURL', + keySystems: { + keySystem1: { + url: 'url1', + audioContentType: 'audio/mp4;codecs="mp4a.40.2"', + videoContentType: 'video/mp4;codecs="avc1.420015"' + } + } + }, 'set source eme options'); + + done(); + }); + + // master manifest + this.standardXHRResponse(this.requests.shift()); + + // video manifest + this.standardXHRResponse(this.requests.shift()); + + // audio manifest + this.standardXHRResponse(this.requests.shift()); + + // this allows the audio playlist loader to load + this.clock.tick(1); +}); + QUnit.test( 'does not set source keySystems if keySystems not provided by source', function(assert) { @@ -4409,7 +4509,8 @@ QUnit.module('HLS Integration', { this.env = useFakeEnvironment(assert); this.requests = this.env.requests; this.mse = useFakeMediaSource(); - this.tech = new (videojs.getTech('Html5'))({}); + this.player = createPlayer(); + this.tech = this.player.tech_; this.clock = this.env.clock; this.standardXHRResponse = (request, data) => { @@ -4427,6 +4528,7 @@ QUnit.module('HLS Integration', { this.env.restore(); this.mse.restore(); window.localStorage.clear(); + this.player.dispose(); videojs.HlsHandler.prototype.setupQualityLevels_ = ogHlsHandlerSetupQualityLevels; } }); @@ -4864,20 +4966,40 @@ QUnit.test( QUnit.module('videojs-contrib-hls isolated functions'); QUnit.test('emeKeySystems adds content types for all keySystems', function(assert) { + // muxed content + assert.deepEqual( + emeKeySystems( + { keySystem1: {}, keySystem2: {} }, + { attributes: { CODECS: 'avc1.420015, mp4a.40.2c' } }, + ), + { + keySystem1: { + audioContentType: 'audio/mp4;codecs="mp4a.40.2c"', + videoContentType: 'video/mp4;codecs="avc1.420015"' + }, + keySystem2: { + audioContentType: 'audio/mp4;codecs="mp4a.40.2c"', + videoContentType: 'video/mp4;codecs="avc1.420015"' + } + }, + 'added content types' + ); + + // unmuxed content assert.deepEqual( emeKeySystems( { keySystem1: {}, keySystem2: {} }, - { attributes: { CODECS: 'some-video-codec' } }, - { attributes: { CODECS: 'some-audio-codec' } } + { attributes: { CODECS: 'avc1.420015' } }, + { attributes: { CODECS: 'mp4a.40.2c' } }, ), { keySystem1: { - audioContentType: 'audio/mp4; codecs="some-audio-codec"', - videoContentType: 'video/mp4; codecs="some-video-codec"' + audioContentType: 'audio/mp4;codecs="mp4a.40.2c"', + videoContentType: 'video/mp4;codecs="avc1.420015"' }, keySystem2: { - audioContentType: 'audio/mp4; codecs="some-audio-codec"', - videoContentType: 'video/mp4; codecs="some-video-codec"' + audioContentType: 'audio/mp4;codecs="mp4a.40.2c"', + videoContentType: 'video/mp4;codecs="avc1.420015"' } }, 'added content types' @@ -4888,19 +5010,18 @@ QUnit.test('emeKeySystems retains non content type properties', function(assert) assert.deepEqual( emeKeySystems( { keySystem1: { url: '1' }, keySystem2: { url: '2'} }, - { attributes: { CODECS: 'some-video-codec' } }, - { attributes: { CODECS: 'some-audio-codec' } } + { attributes: { CODECS: 'avc1.420015, mp4a.40.2c' } }, ), { keySystem1: { url: '1', - audioContentType: 'audio/mp4; codecs="some-audio-codec"', - videoContentType: 'video/mp4; codecs="some-video-codec"' + audioContentType: 'audio/mp4;codecs="mp4a.40.2c"', + videoContentType: 'video/mp4;codecs="avc1.420015"' }, keySystem2: { url: '2', - audioContentType: 'audio/mp4; codecs="some-audio-codec"', - videoContentType: 'video/mp4; codecs="some-video-codec"' + audioContentType: 'audio/mp4;codecs="mp4a.40.2c"', + videoContentType: 'video/mp4;codecs="avc1.420015"' } }, 'retained options' @@ -4920,17 +5041,16 @@ QUnit.test('emeKeySystems overwrites content types', function(assert) { videoContentType: 'd' } }, - { attributes: { CODECS: 'some-video-codec' } }, - { attributes: { CODECS: 'some-audio-codec' } } + { attributes: { CODECS: 'avc1.420015, mp4a.40.2c' } }, ), { keySystem1: { - audioContentType: 'audio/mp4; codecs="some-audio-codec"', - videoContentType: 'video/mp4; codecs="some-video-codec"' + audioContentType: 'audio/mp4;codecs="mp4a.40.2c"', + videoContentType: 'video/mp4;codecs="avc1.420015"' }, keySystem2: { - audioContentType: 'audio/mp4; codecs="some-audio-codec"', - videoContentType: 'video/mp4; codecs="some-video-codec"' + audioContentType: 'audio/mp4;codecs="mp4a.40.2c"', + videoContentType: 'video/mp4;codecs="avc1.420015"' } }, 'overwrote content types'