From e8ac57f36c8d0a5e82e9f603e20e93d306754120 Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Fri, 3 Apr 2020 16:59:44 -0700 Subject: [PATCH] Move drmInfos array to Stream Period-flattening will concatenate Stream objects, so this information should be available per-Stream instead of at the Variant level. Issue #1339 Change-Id: I96195fea48cab1e4a349b2ab0b16064a443e928a --- docs/tutorials/manifest-parser.md | 23 +- externs/shaka/manifest.js | 10 +- lib/dash/dash_parser.js | 6 +- lib/hls/hls_parser.js | 46 ++-- lib/media/drm_engine.js | 114 ++++++---- lib/offline/manifest_converter.js | 9 +- lib/offline/storage.js | 9 +- lib/player.js | 9 +- .../dash_parser_content_protection_unit.js | 21 +- test/hls/hls_parser_unit.js | 6 +- test/media/adaptation_set_unit.js | 2 +- test/media/drm_engine_integration.js | 6 +- test/media/drm_engine_unit.js | 204 +++++++++++------- test/media/streaming_engine_unit.js | 1 - test/offline/manifest_convert_unit.js | 11 +- test/player_unit.js | 4 +- test/test/util/manifest_generator.js | 41 ++-- test/test/util/streaming_engine_util.js | 4 +- test/test/util/test_scheme.js | 3 +- 19 files changed, 296 insertions(+), 233 deletions(-) diff --git a/docs/tutorials/manifest-parser.md b/docs/tutorials/manifest-parser.md index c43318284e..c779a218b1 100644 --- a/docs/tutorials/manifest-parser.md +++ b/docs/tutorials/manifest-parser.md @@ -281,7 +281,6 @@ MyManifestParser.prototype.loadVariant_ = function(hasVideo, hasAudio) { audio: hasAudio ? this.loadStream_('audio') : null, video: hasVideo ? this.loadStream_('video') : null, bandwidth: 8000, // bits/sec, audio+video combined - drmInfos: [], allowedByApplication: true, // always initially true allowedByKeySystem: true // always initially true }; @@ -313,6 +312,7 @@ MyManifestParser.prototype.loadStream_ = function(type) { kind: type == 'text' ? 'subtitles' : undefined, channelsCount: type == 'audio' ? 2 : undefined, encrypted: false, + drmInfos: [], keyId: null, language: 'en', label: 'my_stream', @@ -342,17 +342,16 @@ MyManifestParser.prototype.loadReference_ = ## Encrypted Content If your content is encrypted, there are a few changes to the manifest you need -to do. First, for each Variant that contains encrypted content, you need to set -`variant.drmInfos` to an array of {@link shaka.extern.DrmInfo} objects. All the -fields (except the key-system name) can be set to the default and will be -replaced by settings from the Player configuration. If the `drmInfos` array -is empty, the content is expected to be clear. - -In each stream that is encrypted, set `stream.encrypted` to `true` and -optionally set `stream.keyId` to the key ID that the stream is encrypted with. -The `keyId` field is optional, but it allows the player to choose streams more -intelligently based on which keys are available. If `keyId` is omitted, missing -keys may cause playback to stall. +to do. First, for each Stream that contains encrypted content, you need to set +`stream.encrypted` to true and set `stream.keyId` to the key ID that the stream +is encrypted with. The `keyId` field is technically optional, but it allows the +player to choose streams more intelligently based on which keys are available. +If `keyId` is omitted, missing keys may cause playback to stall. + +You must also set `stream.drmInfos` to an array of {@link shaka.extern.DrmInfo} +objects. All the fields (except the key-system name) can be set to the default +and will be replaced by settings from the Player configuration. If the +`drmInfos` array is empty, the content is expected to be clear. If you set `drmInfo.initData` to a non-empty array, we will use that to initialize EME. We will override any encryption info in the media (e.g. diff --git a/externs/shaka/manifest.js b/externs/shaka/manifest.js index 466f9f3762..a4db64c187 100644 --- a/externs/shaka/manifest.js +++ b/externs/shaka/manifest.js @@ -162,7 +162,6 @@ shaka.extern.DrmInfo; * audio: ?shaka.extern.Stream, * video: ?shaka.extern.Stream, * bandwidth: number, - * drmInfos: !Array., * allowedByApplication: boolean, * allowedByKeySystem: boolean * }} @@ -191,10 +190,6 @@ shaka.extern.DrmInfo; * The video stream of the variant. * @property {number} bandwidth * The variant's required bandwidth in bits per second. - * @property {!Array.} drmInfos - * Defaults to [] (i.e., no DRM).
- * An array of DrmInfo objects which describe DRM schemes are compatible with - * the content. * @property {boolean} allowedByApplication * Defaults to true.
* Set by the Player to indicate whether the variant is allowed to be played @@ -234,6 +229,7 @@ shaka.extern.CreateSegmentIndexFunction; * height: (number|undefined), * kind: (string|undefined), * encrypted: boolean, + * drmInfos: !Array., * keyIds: !Array., * language: string, * label: ?string, @@ -294,6 +290,10 @@ shaka.extern.CreateSegmentIndexFunction; * @property {boolean} encrypted * Defaults to false.
* True if the stream is encrypted. + * @property {!Array.} drmInfos + * Defaults to [] (i.e., no DRM).
+ * An array of DrmInfo objects which describe DRM schemes are compatible with + * the content. * @property {!Array.} keyIds * Defaults to empty (i.e., unencrypted or key ID unknown).
* The stream's key IDs as lowercase hex strings. These key IDs identify the diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 38ca0d7812..b676a78a2b 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -686,9 +686,6 @@ shaka.dash.DashParser = class { // Audio+video variants. const DrmEngine = shaka.media.DrmEngine; if (DrmEngine.areDrmCompatible(audio.drmInfos, video.drmInfos)) { - const drmInfos = DrmEngine.getCommonDrmInfos(audio.drmInfos, - video.drmInfos); - for (const audioStream of audio.streams) { for (const videoStream of video.streams) { bandwidth = @@ -701,7 +698,6 @@ shaka.dash.DashParser = class { audio: audioStream, video: videoStream, bandwidth: bandwidth, - drmInfos: drmInfos, allowedByApplication: true, allowedByKeySystem: true, }; @@ -722,7 +718,6 @@ shaka.dash.DashParser = class { audio: audio ? stream : null, video: video ? stream : null, bandwidth: bandwidth, - drmInfos: set.drmInfos, allowedByApplication: true, allowedByKeySystem: true, }; @@ -1072,6 +1067,7 @@ shaka.dash.DashParser = class { height: context.representation.height, kind, encrypted: contentProtection.drmInfos.length > 0, + drmInfos: contentProtection.drmInfos, keyIds, language, label, diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 80ee5de206..eaa446d165 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -866,28 +866,20 @@ shaka.hls.HlsParser = class { for (const videoInfo of videoInfos) { const audioStream = audioInfo ? audioInfo.stream : null; const videoStream = videoInfo ? videoInfo.stream : null; - const audioDrmInfos = audioInfo ? audioInfo.drmInfos : null; - const videoDrmInfos = videoInfo ? videoInfo.drmInfos : null; + const audioDrmInfos = audioInfo ? audioInfo.stream.drmInfos : null; + const videoDrmInfos = videoInfo ? videoInfo.stream.drmInfos : null; const videoStreamUri = - videoInfo ? videoInfo.verbatimMediaPlaylistUri : ''; + videoInfo ? videoInfo.verbatimMediaPlaylistUri : ''; const audioStreamUri = - audioInfo ? audioInfo.verbatimMediaPlaylistUri : ''; + audioInfo ? audioInfo.verbatimMediaPlaylistUri : ''; const variantUriKey = videoStreamUri + ' - ' + audioStreamUri; - let drmInfos; if (audioStream && videoStream) { - if (DrmEngine.areDrmCompatible(audioDrmInfos, videoDrmInfos)) { - drmInfos = - DrmEngine.getCommonDrmInfos(audioDrmInfos, videoDrmInfos); - } else { + if (!DrmEngine.areDrmCompatible(audioDrmInfos, videoDrmInfos)) { shaka.log.warning( 'Incompatible DRM info in HLS variant. Skipping.'); continue; } - } else if (audioStream) { - drmInfos = audioDrmInfos; - } else if (videoStream) { - drmInfos = videoDrmInfos; } if (this.variantUriSet_.has(variantUriKey)) { @@ -911,8 +903,7 @@ shaka.hls.HlsParser = class { (!!videoStream && videoStream.primary), audio: audioStream, video: videoStream, - bandwidth: bandwidth, - drmInfos: drmInfos, + bandwidth, allowedByApplication: true, allowedByKeySystem: true, }; @@ -1182,16 +1173,17 @@ shaka.hls.HlsParser = class { id: this.globalId_++, originalId: name, createSegmentIndex: () => Promise.resolve(), - segmentIndex: segmentIndex, - mimeType: mimeType, - codecs: codecs, - kind: kind, - encrypted: encrypted, + segmentIndex, + mimeType, + codecs, + kind, + encrypted, + drmInfos, keyIds: [keyId], - language: language, + language, label: name, // For historical reasons, since before "originalId". - type: type, - primary: primary, + type, + primary, // TODO: trick mode trickModeVideo: null, emsgSchemeIdUris: null, @@ -1201,14 +1193,13 @@ shaka.hls.HlsParser = class { height: undefined, bandwidth: undefined, roles: [], - channelsCount: channelsCount, + channelsCount, audioSamplingRate: null, - closedCaptions: closedCaptions, + closedCaptions, }; return { stream, - drmInfos, verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, minTimestamp, @@ -2213,7 +2204,6 @@ shaka.hls.HlsParser = class { /** * @typedef {{ * stream: !shaka.extern.Stream, - * drmInfos: !Array., * verbatimMediaPlaylistUri: string, * absoluteMediaPlaylistUri: string, * minTimestamp: number, @@ -2226,8 +2216,6 @@ shaka.hls.HlsParser = class { * * @property {!shaka.extern.Stream} stream * The Stream itself. - * @property {!Array.} drmInfos - * DrmInfos of the stream. There may be multiple for multi-DRM content. * @property {string} verbatimMediaPlaylistUri * The verbatim media playlist URI, as it appeared in the master playlist. * This has not been canonicalized into an absolute URI. This gives us a diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index e3dec0f895..d2dd805770 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -17,6 +17,7 @@ goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.Iterables'); goog.require('shaka.util.Lazy'); +goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MapUtils'); goog.require('shaka.util.MimeUtils'); goog.require('shaka.util.Platform'); @@ -286,11 +287,24 @@ shaka.media.DrmEngine = class { const clearKeyDrmInfo = this.configureClearKey_(); if (clearKeyDrmInfo) { for (const variant of variants) { - variant.drmInfos = [clearKeyDrmInfo]; + if (variant.video && variant.video.encrypted) { + variant.video.drmInfos = [clearKeyDrmInfo]; + } + if (variant.audio && variant.audio.encrypted) { + variant.audio.drmInfos = [clearKeyDrmInfo]; + } } } - const hadDrmInfo = variants.some((v) => v.drmInfos.length > 0); + const hadDrmInfo = variants.some((variant) => { + if (variant.video && variant.video.drmInfos.length) { + return true; + } + if (variant.audio && variant.audio.drmInfos.length) { + return true; + } + return false; + }); // When preparing to play live streams, it is possible that we won't know // about some upcoming encrypted content. If we initialize the drm engine @@ -306,7 +320,10 @@ shaka.media.DrmEngine = class { // Make sure all the drm infos are valid and filled in correctly. for (const variant of variants) { - for (const info of variant.drmInfos) { + const videoDrmInfos = variant.video ? variant.video.drmInfos : []; + const audioDrmInfos = variant.audio ? variant.audio.drmInfos : []; + const drmInfos = videoDrmInfos.concat(audioDrmInfos); + for (const info of drmInfos) { shaka.media.DrmEngine.fillInDrmInfoDefaults_( info, shaka.util.MapUtils.asMap(this.config_.servers), @@ -626,11 +643,16 @@ shaka.media.DrmEngine = class { * @private */ prepareMediaKeyConfigsForVariants_(variants) { + const ContentType = shaka.util.ManifestParserUtils.ContentType; + // Get all the drm info so that we can avoid using nested loops when we just // need the drm info. const allDrmInfo = new Set(); for (const variant of variants) { - for (const info of variant.drmInfos) { + const videoDrmInfos = variant.video ? variant.video.drmInfos : []; + const audioDrmInfos = variant.audio ? variant.audio.drmInfos : []; + const drmInfos = videoDrmInfos.concat(audioDrmInfos); + for (const info of drmInfos) { allDrmInfo.add(info); } } @@ -675,55 +697,47 @@ shaka.media.DrmEngine = class { /** @type {?shaka.extern.Stream} */ const video = variant.video; - /** @type {string} */ - const audioMimeType = - audio ? - shaka.util.MimeUtils.getFullType(audio.mimeType, audio.codecs) : - ''; - /** @type {string} */ - const videoMimeType = - video ? - shaka.util.MimeUtils.getFullType(video.mimeType, video.codecs) : - ''; - // Add the last bit of information to each config; - for (const info of variant.drmInfos) { - const config = configs.get(info.keySystem); - goog.asserts.assert( - config, - 'Any missing configs should have be filled in before.'); + for (const stream of [audio, video]) { + if (!stream) { + continue; + } - config.drmInfos.push(info); + const mimeType = shaka.util.MimeUtils.getFullType( + stream.mimeType, stream.codecs); - if (info.distinctiveIdentifierRequired) { - config.distinctiveIdentifier = 'required'; - } + for (const info of stream.drmInfos) { + const config = configs.get(info.keySystem); + goog.asserts.assert( + config, + 'Any missing configs should have be filled in before.'); - if (info.persistentStateRequired) { - config.persistentState = 'required'; - } + config.drmInfos.push(info); - if (audio) { - /** @type {MediaKeySystemMediaCapability} */ - const capability = { - robustness: info.audioRobustness || '', - contentType: audioMimeType, - }; + if (info.distinctiveIdentifierRequired) { + config.distinctiveIdentifier = 'required'; + } + if (info.persistentStateRequired) { + config.persistentState = 'required'; + } - config.audioCapabilities.push(capability); - } + const robustness = (stream.type == ContentType.AUDIO) ? + info.audioRobustness : info.videoRobustness; - if (video) { /** @type {MediaKeySystemMediaCapability} */ const capability = { - robustness: info.videoRobustness || '', - contentType: videoMimeType, + robustness: robustness || '', + contentType: mimeType, }; - config.videoCapabilities.push(capability); - } - } - } + if (stream.type == ContentType.AUDIO) { + config.audioCapabilities.push(capability); + } else { + config.videoCapabilities.push(capability); + } + } // for (const info of stream.drmInfos) + } // for (const stream of [audio, video]) + } // for (const variant of variants) return configs; } @@ -1567,8 +1581,13 @@ shaka.media.DrmEngine = class { } const keySystem = shaka.media.DrmEngine.keySystem(this.currentDrmInfo_); - return variant.drmInfos.length == 0 || - variant.drmInfos.some((drmInfo) => drmInfo.keySystem == keySystem); + + const videoDrmInfos = video ? video.drmInfos : []; + const audioDrmInfos = audio ? audio.drmInfos : []; + const drmInfos = videoDrmInfos.concat(audioDrmInfos); + + return drmInfos.length == 0 || + drmInfos.some((drmInfo) => drmInfo.keySystem == keySystem); } /** @@ -1697,7 +1716,12 @@ shaka.media.DrmEngine = class { }); for (const variant of variants) { - variant.drmInfos = drmInfos; + if (variant.video && variant.video.encrypted) { + variant.video.drmInfos = drmInfos; + } + if (variant.audio && variant.audio.encrypted) { + variant.audio.drmInfos = drmInfos; + } } } diff --git a/lib/offline/manifest_converter.js b/lib/offline/manifest_converter.js index 6aa5939e11..50e8baeefc 100644 --- a/lib/offline/manifest_converter.js +++ b/lib/offline/manifest_converter.js @@ -65,7 +65,12 @@ shaka.offline.ManifestConverter = class { const drmInfos = manifestDB.drmInfo ? [manifestDB.drmInfo] : []; if (manifestDB.drmInfo) { for (const variant of variants.values()) { - variant.drmInfos = drmInfos; + if (variant.audio && variant.audio.encrypted) { + variant.audio.drmInfos = drmInfos; + } + if (variant.video && variant.video.encrypted) { + variant.video.drmInfos = drmInfos; + } } } @@ -173,6 +178,7 @@ shaka.offline.ManifestConverter = class { pixelAspectRatio: streamDB.pixelAspectRatio, kind: streamDB.kind, encrypted: streamDB.encrypted, + drmInfos: [], keyIds: streamDB.keyIds, language: streamDB.language, label: streamDB.label, @@ -276,7 +282,6 @@ shaka.offline.ManifestConverter = class { audio: null, video: null, bandwidth: 0, - drmInfos: [], allowedByApplication: true, allowedByKeySystem: true, }; diff --git a/lib/offline/storage.js b/lib/offline/storage.js index f686130792..9b903f528b 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -519,10 +519,15 @@ shaka.offline.Storage = class { uri, manifest, /* size= */ 0, metadata); const isEncrypted = manifest.variants.some((variant) => { - return variant.drmInfos && variant.drmInfos.length; + const videoEncrypted = variant.video && variant.video.encrypted; + const audioEncrypted = variant.audio && variant.audio.encrypted; + return videoEncrypted || audioEncrypted; }); const includesInitData = manifest.variants.some((variant) => { - return variant.drmInfos.some((drmInfos) => { + const videoDrmInfos = variant.video ? variant.video.drmInfos : []; + const audioDrmInfos = variant.audio ? variant.audio.drmInfos : []; + const drmInfos = videoDrmInfos.concat(audioDrmInfos); + return drmInfos.some((drmInfos) => { return drmInfos.initData && drmInfos.initData.length; }); }); diff --git a/lib/player.js b/lib/player.js index f885c0f93e..6b5177497f 100644 --- a/lib/player.js +++ b/lib/player.js @@ -1888,6 +1888,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { mimeType: 'video/mp4', codecs: '', encrypted: true, + drmInfos: [], // Filled in by DrmEngine config. keyIds: [], language: 'und', label: null, @@ -1901,7 +1902,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget { closedCaptions: null, }, bandwidth: 100, - drmInfos: [], // Filled in by DrmEngine config. allowedByApplication: true, allowedByKeySystem: true, }; @@ -3449,6 +3449,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { codecs: codec || '', kind: kind, encrypted: false, + drmInfos: [], keyIds: [], language: language, label: label || null, @@ -3609,6 +3610,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { codecs: '', kind: TextStreamKind.CLOSED_CAPTION, encrypted: false, + drmInfos: [], keyIds: [], language: video.closedCaptions.get(id), label: null, @@ -3670,7 +3672,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const curDrmInfo = this.drmEngine_ ? this.drmEngine_.getDrmInfo() : null; if (curDrmInfo) { for (const variant of manifest.variants) { - for (const drmInfo of variant.drmInfos) { + const videoDrmInfos = variant.video ? variant.video.drmInfos : []; + const audioDrmInfos = variant.audio ? variant.audio.drmInfos : []; + const drmInfos = videoDrmInfos.concat(audioDrmInfos); + for (const drmInfo of drmInfos) { // Ignore any data for different key systems. if (drmInfo.keySystem == curDrmInfo.keySystem) { for (const initData of (drmInfo.initData || [])) { diff --git a/test/dash/dash_parser_content_protection_unit.js b/test/dash/dash_parser_content_protection_unit.js index 5022086706..4efbdcfa07 100644 --- a/test/dash/dash_parser_content_protection_unit.js +++ b/test/dash/dash_parser_content_protection_unit.js @@ -95,9 +95,9 @@ describe('DashParser ContentProtection', () => { const variants = []; for (const i of shaka.util.Iterables.range(numVariants)) { const variant = jasmine.objectContaining({ - drmInfos: drmInfos, video: jasmine.objectContaining({ keyIds: keyIds[i] ? [keyIds[i]] : [], + drmInfos, }), }); variants.push(variant); @@ -493,25 +493,6 @@ describe('DashParser ContentProtection', () => { await testDashParser(source, expected); }); - it('only keeps key systems common to all Representations', async () => { - const source = buildManifestText([ - // AdaptationSet lines - ], [ - // Representation 1 lines - '', - '', - ], [ - // Representation 2 lines - '', - ]); - const expected = buildExpectedManifest( - [buildDrmInfo('com.widevine.alpha')]); - await testDashParser(source, expected); - }); - it('still keeps per-Representation key IDs when merging', async () => { const source = buildManifestText([ // AdaptationSet lines diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index 9249f3ddf8..cdd05fafcf 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -1693,11 +1693,11 @@ describe('HlsParser', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.anyTimeline(); manifest.addPartialVariant((variant) => { - variant.addDrmInfo('com.widevine.alpha', (drmInfo) => { - drmInfo.addCencInitData(initDataBase64); - }); variant.addPartialStream(ContentType.VIDEO, (stream) => { stream.encrypted = true; + stream.addDrmInfo('com.widevine.alpha', (drmInfo) => { + drmInfo.addCencInitData(initDataBase64); + }); }); }); }); diff --git a/test/media/adaptation_set_unit.js b/test/media/adaptation_set_unit.js index de196c58df..ee08b149bb 100644 --- a/test/media/adaptation_set_unit.js +++ b/test/media/adaptation_set_unit.js @@ -142,7 +142,6 @@ describe('AdaptationSet', () => { allowedByKeySystem: true, audio: audio, bandwidth: 1024, - drmInfos: [], id: id, language: '', primary: false, @@ -166,6 +165,7 @@ describe('AdaptationSet', () => { createSegmentIndex: () => Promise.resolve(), emsgSchemeIdUris: null, encrypted: false, + drmInfos: [], segmentIndex: null, id: id, keyIds: [], diff --git a/test/media/drm_engine_integration.js b/test/media/drm_engine_integration.js index 222d0ceeb7..9351faf2cd 100644 --- a/test/media/drm_engine_integration.js +++ b/test/media/drm_engine_integration.js @@ -105,13 +105,15 @@ describe('DrmEngine', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { - variant.addDrmInfo('com.widevine.alpha'); - variant.addDrmInfo('com.microsoft.playready'); variant.addVideo(1, (stream) => { stream.encrypted = true; + stream.addDrmInfo('com.widevine.alpha'); + stream.addDrmInfo('com.microsoft.playready'); }); variant.addAudio(2, (stream) => { stream.encrypted = true; + stream.addDrmInfo('com.widevine.alpha'); + stream.addDrmInfo('com.microsoft.playready'); }); }); }); diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index 079e11b162..845ee9ad74 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -66,14 +66,16 @@ describe('DrmEngine', () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { - variant.addDrmInfo('drm.abc'); - variant.addDrmInfo('drm.def'); variant.addVideo(1, (stream) => { stream.encrypted = true; + stream.addDrmInfo('drm.abc'); + stream.addDrmInfo('drm.def'); stream.mime('video/foo', 'vbar'); }); variant.addAudio(2, (stream) => { stream.encrypted = true; + stream.addDrmInfo('drm.abc'); + stream.addDrmInfo('drm.def'); stream.mime('audio/foo', 'abar'); }); }); @@ -135,10 +137,10 @@ describe('DrmEngine', () => { it('supports all clear variants', async () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { - variant.addDrmInfo('drm.abc'); - variant.addDrmInfo('drm.def'); variant.addVideo(1, (stream) => { stream.encrypted = false; + stream.addDrmInfo('drm.abc'); + stream.addDrmInfo('drm.def'); stream.mime('video/foo', 'vbar'); }); }); @@ -213,18 +215,20 @@ describe('DrmEngine', () => { setRequestMediaKeySystemAccessSpy(['drm.abc', 'drm.def']); // Add manifest-supplied license servers for both. - for (const drmInfo of manifest.variants[0].drmInfos) { - if (drmInfo.keySystem == 'drm.abc') { - drmInfo.licenseServerUri = 'http://foo.bar/abc'; - } else if (drmInfo.keySystem == 'drm.def') { - drmInfo.licenseServerUri = 'http://foo.bar/def'; + tweakDrmInfos((drmInfos) => { + for (const drmInfo of drmInfos) { + if (drmInfo.keySystem == 'drm.abc') { + drmInfo.licenseServerUri = 'http://foo.bar/abc'; + } else if (drmInfo.keySystem == 'drm.def') { + drmInfo.licenseServerUri = 'http://foo.bar/def'; + } + + // Make sure we didn't somehow choose manifest-supplied values that + // match the config. This would invalidate parts of the test. + const configServer = config.servers[drmInfo.keySystem]; + expect(drmInfo.licenseServerUri).not.toBe(configServer); } - - // Make sure we didn't somehow choose manifest-supplied values that - // match the config. This would invalidate parts of the test. - const configServer = config.servers[drmInfo.keySystem]; - expect(drmInfo.licenseServerUri).not.toBe(configServer); - } + }); // Remove the server URI for drm.abc from the config, so that only drm.def // could be used, in spite of the manifest-supplied license server URI. @@ -305,7 +309,7 @@ describe('DrmEngine', () => { .toHaveBeenCalledWith('drm.def', jasmine.any(Object)); }); - it('silences errors for unencrypted assets', async () => { + it('does not error for unencrypted assets with no EME', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { variant.addVideo(1, (stream) => { @@ -317,25 +321,23 @@ describe('DrmEngine', () => { }); }); - // Accept no key systems. + // Accept no key systems, simulating a lack of EME. setRequestMediaKeySystemAccessSpy([]); const variants = manifest.variants; - await drmEngine.initForPlayback(variants, manifest.offlineSessionIds); - - // Both key systems were tried, since the first one failed. - expect(requestMediaKeySystemAccessSpy).toHaveBeenCalledTimes(2); - expect(requestMediaKeySystemAccessSpy) - .toHaveBeenCalledWith('drm.abc', jasmine.any(Object)); - expect(requestMediaKeySystemAccessSpy) - .toHaveBeenCalledWith('drm.def', jasmine.any(Object)); + // All that matters here is that we don't throw. + await expectAsync( + drmEngine.initForPlayback(variants, manifest.offlineSessionIds)) + .not.toBeRejected(); }); it('fails to initialize if no key systems are recognized', async () => { // Simulate the DASH parser inserting a blank placeholder when only // unrecognized custom schemes are found. - manifest.variants[0].drmInfos[0].keySystem = ''; - manifest.variants[0].drmInfos[1].keySystem = ''; + tweakDrmInfos((drmInfos) => { + drmInfos[0].keySystem = ''; + drmInfos[1].keySystem = ''; + }); const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -433,8 +435,10 @@ describe('DrmEngine', () => { it('honors distinctive identifier and persistent state', async () => { setRequestMediaKeySystemAccessSpy([]); - manifest.variants[0].drmInfos[0].distinctiveIdentifierRequired = true; - manifest.variants[0].drmInfos[1].persistentStateRequired = true; + tweakDrmInfos((drmInfos) => { + drmInfos[0].distinctiveIdentifierRequired = true; + drmInfos[1].persistentStateRequired = true; + }); const variants = manifest.variants; await expectAsync( @@ -459,7 +463,8 @@ describe('DrmEngine', () => { it('makes no queries for clear content if no key config', async () => { setRequestMediaKeySystemAccessSpy([]); - manifest.variants[0].drmInfos = []; + manifest.variants[0].video.drmInfos = []; + manifest.variants[0].audio.drmInfos = []; config.servers = {}; config.advanced = {}; @@ -473,7 +478,8 @@ describe('DrmEngine', () => { it('makes queries for clear content if key is configured', async () => { setRequestMediaKeySystemAccessSpy(['drm.abc']); - manifest.variants[0].drmInfos = []; + manifest.variants[0].video.drmInfos = []; + manifest.variants[0].audio.drmInfos = []; config.servers = { 'drm.abc': 'http://abc.drm/license', }; @@ -491,11 +497,14 @@ describe('DrmEngine', () => { // Leave only one drmInfo manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { - variant.addDrmInfo('drm.abc'); variant.addVideo(1, (stream) => { stream.encrypted = true; + stream.addDrmInfo('drm.abc'); + }); + variant.addAudio(2, (stream) => { + stream.encrypted = true; + stream.addDrmInfo('drm.abc'); }); - variant.addAudio(2); }); }); @@ -535,22 +544,26 @@ describe('DrmEngine', () => { // Leave only one drmInfo manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { - variant.addDrmInfo('drm.abc'); variant.addVideo(1, (stream) => { stream.encrypted = true; + stream.addDrmInfo('drm.abc'); + }); + variant.addAudio(2, (stream) => { + stream.encrypted = true; + stream.addDrmInfo('drm.abc'); }); - variant.addAudio(2); }); }); setRequestMediaKeySystemAccessSpy([]); // DrmInfo directly sets advanced settings. - manifest.variants[0].drmInfos[0].distinctiveIdentifierRequired = true; - manifest.variants[0].drmInfos[0].persistentStateRequired = true; - manifest.variants[0].drmInfos[0].audioRobustness = 'good'; - manifest.variants[0].drmInfos[0] - .videoRobustness = 'really_really_ridiculously_good'; + tweakDrmInfos((drmInfos) => { + drmInfos[0].distinctiveIdentifierRequired = true; + drmInfos[0].persistentStateRequired = true; + drmInfos[0].audioRobustness = 'good'; + drmInfos[0].videoRobustness = 'really_really_ridiculously_good'; + }); config.advanced['drm.abc'] = { audioRobustness: 'bad', @@ -605,12 +618,13 @@ describe('DrmEngine', () => { // Both audio and video with the same key system: manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { - variant.addDrmInfo('drm.abc'); variant.addVideo(1, (stream) => { stream.encrypted = true; + stream.addDrmInfo('drm.abc'); }); variant.addAudio(2, (stream) => { stream.encrypted = true; + stream.addDrmInfo('drm.abc'); }); }); }); @@ -618,7 +632,8 @@ describe('DrmEngine', () => { it('does nothing for unencrypted content', async () => { setRequestMediaKeySystemAccessSpy([]); - manifest.variants[0].drmInfos = []; + manifest.variants[0].video.drmInfos = []; + manifest.variants[0].audio.drmInfos = []; config.servers = {}; config.advanced = {}; @@ -643,7 +658,9 @@ describe('DrmEngine', () => { it('prefers server certificate from DrmInfo', async () => { const cert1 = new Uint8Array(5); const cert2 = new Uint8Array(1); - manifest.variants[0].drmInfos[0].serverCertificate = cert1; + tweakDrmInfos((drmInfos) => { + drmInfos[0].serverCertificate = cert1; + }); config.advanced['drm.abc'] = createAdvancedConfig(cert2); drmEngine.configure(config); @@ -665,11 +682,14 @@ describe('DrmEngine', () => { const initData2 = new Uint8Array(0); /** @type {!Uint8Array} */ const initData3 = new Uint8Array(10); - manifest.variants[0].drmInfos[0].initData = [ - {initData: initData1, initDataType: 'cenc', keyId: null}, - {initData: initData2, initDataType: 'webm', keyId: null}, - {initData: initData3, initDataType: 'cenc', keyId: null}, - ]; + + tweakDrmInfos((drmInfos) => { + drmInfos[0].initData = [ + {initData: initData1, initDataType: 'cenc', keyId: null}, + {initData: initData2, initDataType: 'webm', keyId: null}, + {initData: initData3, initDataType: 'cenc', keyId: null}, + ]; + }); await initAndAttach(); expect(mockMediaKeys.createSession).toHaveBeenCalledTimes(3); @@ -692,11 +712,14 @@ describe('DrmEngine', () => { const initData1 = new Uint8Array(1); const initData2 = new Uint8Array(1); const initData3 = new Uint8Array(10); - manifest.variants[0].drmInfos[0].initData = [ - {initData: initData1, initDataType: 'cenc', keyId: 'abc'}, - {initData: initData2, initDataType: 'cenc', keyId: 'def'}, - {initData: initData3, initDataType: 'cenc', keyId: 'abc'}, - ]; + + tweakDrmInfos((drmInfos) => { + drmInfos[0].initData = [ + {initData: initData1, initDataType: 'cenc', keyId: 'abc'}, + {initData: initData2, initDataType: 'cenc', keyId: 'def'}, + {initData: initData3, initDataType: 'cenc', keyId: 'abc'}, + ]; + }); await initAndAttach(); expect(mockMediaKeys.createSession).toHaveBeenCalledTimes(1); @@ -705,8 +728,9 @@ describe('DrmEngine', () => { }); it('uses clearKeys config to override DrmInfo', async () => { - manifest.variants[0].drmInfos[0].keySystem = - 'com.fake.NOT.clearkey'; + tweakDrmInfos((drmInfos) => { + drmInfos[0].keySystem = 'com.fake.NOT.clearkey'; + }); setRequestMediaKeySystemAccessSpy(['org.w3.clearkey']); @@ -726,9 +750,10 @@ describe('DrmEngine', () => { await initAndAttach(); const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; - expect(manifest.variants[0].drmInfos.length).toBe(1); - expect(manifest.variants[0].drmInfos[0].keySystem) - .toBe('org.w3.clearkey'); + tweakDrmInfos((drmInfos) => { + expect(drmInfos.length).toBe(1); + expect(drmInfos[0].keySystem).toBe('org.w3.clearkey'); + }); expect(session.generateRequest) .toHaveBeenCalledWith('keyids', jasmine.any(Uint8Array)); @@ -746,7 +771,8 @@ describe('DrmEngine', () => { // Regression test for #2139, in which we suppressed errors if drmInfos was // empty and clearKeys config was given it('fails if clearKeys config fails', async () => { - manifest.variants[0].drmInfos = []; + manifest.variants[0].video.drmInfos = []; + manifest.variants[0].audio.drmInfos = []; // Make it so that clear key setup fails by pretending we don't have it. // In reality, it was failing because of missing codec info, but any @@ -806,9 +832,12 @@ describe('DrmEngine', () => { // Set up an init data override in the manifest to get an immediate call // to generateRequest: const initData1 = new Uint8Array(5); - manifest.variants[0].drmInfos[0].initData = [ - {initData: initData1, initDataType: 'cenc', keyId: null}, - ]; + + tweakDrmInfos((drmInfos) => { + drmInfos[0].initData = [ + {initData: initData1, initDataType: 'cenc', keyId: null}, + ]; + }); // Fail generateRequest. const session1 = createMockSession(); @@ -870,9 +899,11 @@ describe('DrmEngine', () => { it('is ignored when init data is in DrmInfo', async () => { // Set up an init data override in the manifest: - manifest.variants[0].drmInfos[0].initData = [ - {initData: new Uint8Array(0), initDataType: 'cenc', keyId: null}, - ]; + tweakDrmInfos((drmInfos) => { + drmInfos[0].initData = [ + {initData: new Uint8Array(0), initDataType: 'cenc', keyId: null}, + ]; + }); await initAndAttach(); // We already created a session for the init data override. @@ -900,7 +931,8 @@ describe('DrmEngine', () => { }); it('dispatches an error if manifest says unencrypted', async () => { - manifest.variants[0].drmInfos = []; + manifest.variants[0].video.drmInfos = []; + manifest.variants[0].audio.drmInfos = []; config.servers = {}; config.advanced = {}; @@ -936,8 +968,9 @@ describe('DrmEngine', () => { }); it('prefers a license server URI from configuration', async () => { - manifest.variants[0].drmInfos[0].licenseServerUri = - 'http://foo.bar/drm'; + tweakDrmInfos((drmInfos) => { + drmInfos[0].licenseServerUri = 'http://foo.bar/drm'; + }); await sendMessageTest('http://abc.drm/license'); }); @@ -1105,10 +1138,13 @@ describe('DrmEngine', () => { // sessions. const initData1 = new Uint8Array(10); const initData2 = new Uint8Array(11); - manifest.variants[0].drmInfos[0].initData = [ - {initData: initData1, initDataType: 'cenc', keyId: null}, - {initData: initData2, initDataType: 'cenc', keyId: null}, - ]; + + tweakDrmInfos((drmInfos) => { + drmInfos[0].initData = [ + {initData: initData1, initDataType: 'cenc', keyId: null}, + {initData: initData2, initDataType: 'cenc', keyId: null}, + ]; + }); const keyId1 = makeKeyId(1); const keyId2 = makeKeyId(2); @@ -1250,7 +1286,9 @@ describe('DrmEngine', () => { }); it('uses clearKeys config to override DrmInfo', async () => { - manifest.variants[0].drmInfos[0].keySystem = 'com.fake.NOT.clearkey'; + tweakDrmInfos((drmInfos) => { + drmInfos[0].keySystem = 'com.fake.NOT.clearkey'; + }); setRequestMediaKeySystemAccessSpy(['org.w3.clearkey']); // Configure clear keys (map of hex key IDs to keys) @@ -1719,20 +1757,22 @@ describe('DrmEngine', () => { // Leave only one drmInfo manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { - variant.addDrmInfo('drm.abc'); variant.addVideo(1, (stream) => { stream.encrypted = true; + stream.addDrmInfo('drm.abc'); }); variant.addAudio(2, (stream) => { stream.encrypted = true; + stream.addDrmInfo('drm.abc'); }); }); }); setRequestMediaKeySystemAccessSpy(['drm.abc']); // Key IDs in manifest - manifest.variants[0].drmInfos[0].keyIds[0] = - 'deadbeefdeadbeefdeadbeefdeadbeef'; + tweakDrmInfos((drmInfos) => { + drmInfos[0].keyIds[0] = 'deadbeefdeadbeefdeadbeefdeadbeef'; + }); config.advanced['drm.abc'] = { audioRobustness: 'good', @@ -2094,4 +2134,16 @@ describe('DrmEngine', () => { function makeKeyId(id) { return shaka.util.BufferUtils.toArrayBuffer(new Uint8Array([id])); } + + /** + * @param {function(!Array.)} callback + */ + function tweakDrmInfos(callback) { + if (manifest.variants[0].video.encrypted) { + callback(manifest.variants[0].video.drmInfos); + } + if (manifest.variants[0].audio.encrypted) { + callback(manifest.variants[0].audio.drmInfos); + } + } }); diff --git a/test/media/streaming_engine_unit.js b/test/media/streaming_engine_unit.js index a9a24367ae..d3a3e11867 100644 --- a/test/media/streaming_engine_unit.js +++ b/test/media/streaming_engine_unit.js @@ -333,7 +333,6 @@ describe('StreamingEngine', () => { language: 'und', primary: false, bandwidth: 0, - drmInfos: [], allowedByApplication: true, allowedByKeySystem: true, }; diff --git a/test/offline/manifest_convert_unit.js b/test/offline/manifest_convert_unit.js index b20c8d9bb0..8f132e7c37 100644 --- a/test/offline/manifest_convert_unit.js +++ b/test/offline/manifest_convert_unit.js @@ -119,10 +119,9 @@ describe('ManifestConverter', () => { expect(variant.bandwidth).toEqual(jasmine.any(Number)); expect(variant.allowedByApplication).toBe(true); expect(variant.allowedByKeySystem).toBe(true); - expect(variant.drmInfos).toEqual([manifestDb.drmInfo]); - verifyStream(variant.video, manifestDb.streams[0]); - verifyStream(variant.audio, manifestDb.streams[1]); + verifyStream(variant.video, manifestDb.streams[0], manifestDb.drmInfo); + verifyStream(variant.audio, manifestDb.streams[1], manifestDb.drmInfo); }); it('supports video-only content', () => { @@ -442,13 +441,16 @@ describe('ManifestConverter', () => { /** * @param {?shaka.extern.Stream} stream * @param {?shaka.extern.StreamDB} streamDb + * @param {(?shaka.extern.DrmInfo)=} drmInfo */ - function verifyStream(stream, streamDb) { + function verifyStream(stream, streamDb, drmInfo = null) { if (!streamDb) { expect(stream).toBeFalsy(); return; } + const expectedDrmInfos = streamDb.encrypted ? [drmInfo] : []; + const expectedStream = { id: jasmine.any(Number), originalId: jasmine.any(String), @@ -461,6 +463,7 @@ describe('ManifestConverter', () => { width: streamDb.width || undefined, height: streamDb.height || undefined, kind: streamDb.kind, + drmInfos: expectedDrmInfos, encrypted: streamDb.encrypted, keyIds: streamDb.keyIds, language: streamDb.language, diff --git a/test/player_unit.js b/test/player_unit.js index 5c5f1e244b..9f6dac1fa6 100644 --- a/test/player_unit.js +++ b/test/player_unit.js @@ -2516,16 +2516,16 @@ describe('Player', () => { it('removes if key system does not support codec', async () => { manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(0, (variant) => { - variant.addDrmInfo('foo.bar'); variant.addVideo(1, (stream) => { stream.encrypted = true; stream.mimeType = 'video/unsupported'; + stream.addDrmInfo('foo.bar'); }); }); manifest.addVariant(1, (variant) => { - variant.addDrmInfo('foo.bar'); variant.addVideo(2, (stream) => { stream.encrypted = true; + stream.addDrmInfo('foo.bar'); }); }); }); diff --git a/test/test/util/manifest_generator.js b/test/test/util/manifest_generator.js index d73f7bda47..6330904926 100644 --- a/test/test/util/manifest_generator.js +++ b/test/test/util/manifest_generator.js @@ -252,8 +252,6 @@ shaka.test.ManifestGenerator.Variant = class { this.bandwidth = 0; /** @type {boolean} */ this.primary = false; - /** @type {!Array.} */ - this.drmInfos = []; /** @type {boolean} */ this.allowedByApplication = true; /** @type {boolean} */ @@ -273,24 +271,6 @@ shaka.test.ManifestGenerator.Variant = class { return shaka.test.ManifestGenerator.buildCommon_(this); } - /** - * Adds a new DrmInfo to the current variant. - * - * @param {string} keySystem - * @param {function(!shaka.test.ManifestGenerator.DrmInfo)=} func - */ - addDrmInfo(keySystem, func) { - const drmInfo = - new shaka.test.ManifestGenerator.DrmInfo(this.manifest_, keySystem); - if (func) { - func(drmInfo); - } - if (!this.drmInfos) { - this.drmInfos = []; - } - this.drmInfos.push(drmInfo.build_()); - } - /** * Sets video stream of the current variant. * @@ -509,6 +489,8 @@ shaka.test.ManifestGenerator.Stream = class { this.kind = undefined; /** @type {boolean} */ this.encrypted = false; + /** @type {!Array.} */ + this.drmInfos = []; /** @type {!Array.} */ this.keyIds = []; /** @type {string} */ @@ -544,6 +526,25 @@ shaka.test.ManifestGenerator.Stream = class { return shaka.test.ManifestGenerator.buildCommon_(this); } + /** + * Adds a new DrmInfo to the current Stream. + * + * @param {string} keySystem + * @param {function(!shaka.test.ManifestGenerator.DrmInfo)=} func + */ + addDrmInfo(keySystem, func) { + const drmInfo = + new shaka.test.ManifestGenerator.DrmInfo(this.manifest_, keySystem); + if (func) { + func(drmInfo); + } + if (!this.drmInfos) { + // This may be the case if this was created through addPartialStream. + this.drmInfos = []; + } + this.drmInfos.push(drmInfo.build_()); + } + /** * Sets the current stream to use segment template to create segments. * diff --git a/test/test/util/streaming_engine_util.js b/test/test/util/streaming_engine_util.js index 7898aaa4b6..5e28eff0fb 100644 --- a/test/test/util/streaming_engine_util.js +++ b/test/test/util/streaming_engine_util.js @@ -310,7 +310,6 @@ shaka.test.StreamingEngineUtil = class { allowedByApplication: true, allowedByKeySystem: true, bandwidth: 0, - drmInfos: [], id: 0, language: 'und', primary: false, @@ -413,6 +412,7 @@ shaka.test.StreamingEngineUtil = class { codecs: 'mp4a.40.2', bandwidth: 192000, type: ContentType.AUDIO, + drmInfos: [], }; } @@ -434,6 +434,7 @@ shaka.test.StreamingEngineUtil = class { width: 600, height: 400, type: ContentType.VIDEO, + drmInfos: [], }; } @@ -452,6 +453,7 @@ shaka.test.StreamingEngineUtil = class { mimeType: 'text/vtt', kind: ManifestParserUtils.TextStreamKind.SUBTITLE, type: ManifestParserUtils.ContentType.TEXT, + drmInfos: [], }; } }; diff --git a/test/test/util/test_scheme.js b/test/test/util/test_scheme.js index 010dc0d48e..457a1e9011 100644 --- a/test/test/util/test_scheme.js +++ b/test/test/util/test_scheme.js @@ -160,7 +160,8 @@ shaka.test.TestScheme = class { if (data.licenseServers) { for (const keySystem in data.licenseServers) { - variant.addDrmInfo(keySystem, (drmInfo) => { + stream.encrypted = true; + stream.addDrmInfo(keySystem, (drmInfo) => { drmInfo.licenseServerUri = data.licenseServers[keySystem]; if (data[contentType].initData) { drmInfo.addCencInitData(data[contentType].initData);