From b1e81a684afe086b7a37ea29bbbfc972575ba332 Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Tue, 30 Aug 2022 11:49:06 -0700 Subject: [PATCH] feat: Add limited support for HLS "identity" key format (#4451) This feature is not entirely automatic. The ClearKey CDM requires a key-id to key mapping. HLS doesn't provide a key ID anywhere. So although we could use the 'URI' attribute to fetch the actual 16-byte key, without a key ID, we can't provide this automatically to the ClearKey CDM. Instead, the application will have to use `player.configure('drm.clearKeys', { ... })` to provide the key IDs and keys or `player.configure('drm.servers.org\.w3\.clearkey', ...)` to provide a ClearKey license server URI. Closes #2146 --- lib/hls/hls_parser.js | 37 ++++++++++++++- test/hls/hls_parser_unit.js | 68 +++++++++++++++++++++++++++- test/player_integration.js | 10 ++++ test/test/util/manifest_generator.js | 9 ++-- test/test/util/test_scheme.js | 34 ++++++++------ 5 files changed, 135 insertions(+), 23 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index c25c693c23..278478139b 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -1610,7 +1610,11 @@ shaka.hls.HlsParser = class { // These keys are handled separately. aesEncrypted = true; } else { - const keyFormat = drmTag.getRequiredAttrValue('KEYFORMAT'); + // According to the HLS spec, KEYFORMAT is optional and implicitly + // defaults to "identity". + // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-4.4.4.4 + const keyFormat = + drmTag.getAttributeValue('KEYFORMAT') || 'identity'; const drmParser = shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat]; @@ -2660,7 +2664,7 @@ shaka.hls.HlsParser = class { */ const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo( 'com.apple.fps', [ - {initDataType: 'sinf', initData: new Uint8Array(0)}, + {initDataType: 'sinf', initData: new Uint8Array(0), keyId: null}, ]); return drmInfo; @@ -2739,6 +2743,33 @@ shaka.hls.HlsParser = class { return drmInfo; } + + /** + * See: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-11#section-5.1 + * + * @param {!shaka.hls.Tag} drmTag + * @return {?shaka.extern.DrmInfo} + * @private + */ + static identityDrmParser_(drmTag) { + const method = drmTag.getRequiredAttrValue('METHOD'); + const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR']; + if (!VALID_METHODS.includes(method)) { + shaka.log.error('Identity (ClearKey) in HLS is only supported with [', + VALID_METHODS.join(', '), '], not', method); + return null; + } + + // NOTE: The ClearKey CDM requires a key-id to key mapping. HLS doesn't + // provide a key ID anywhere. So although we could use the 'URI' attribute + // to fetch the actual 16-byte key, without a key ID, we can't provide this + // automatically to the ClearKey CDM. Instead, the application will have + // to use player.configure('drm.clearKeys', { ... }) to provide the key IDs + // and keys or player.configure('drm.servers.org\.w3\.clearkey', ...) to + // provide a ClearKey license server URI. + return shaka.util.ManifestParserUtils.createDrmInfo( + 'org.w3.clearkey', /* initDatas= */ null); + } }; @@ -2886,6 +2917,8 @@ shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_ = { shaka.hls.HlsParser.widevineDrmParser_, 'com.microsoft.playready': shaka.hls.HlsParser.playreadyDrmParser_, + 'identity': + shaka.hls.HlsParser.identityDrmParser_, }; diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index 47de05391f..b7c582074a 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -2590,7 +2590,6 @@ describe('HlsParser', () => { } }); - it('constructs DrmInfo for Widevine', async () => { const master = [ '#EXTM3U\n', @@ -2713,6 +2712,73 @@ describe('HlsParser', () => { await testHlsParser(master, media, manifest); }); + it('constructs DrmInfo for ClearKey with explicit KEYFORMAT', async () => { + const master = [ + '#EXTM3U\n', + '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",', + 'RESOLUTION=960x540,FRAME-RATE=60\n', + 'video\n', + ].join(''); + + const media = [ + '#EXTM3U\n', + '#EXT-X-TARGETDURATION:6\n', + '#EXT-X-PLAYLIST-TYPE:VOD\n', + '#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,', + 'KEYFORMAT="identity",', + 'URI="key.bin",\n', + '#EXT-X-MAP:URI="init.mp4"\n', + '#EXTINF:5,\n', + 'main.mp4', + ].join(''); + + const manifest = shaka.test.ManifestGenerator.generate((manifest) => { + manifest.anyTimeline(); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.encrypted = true; + stream.addDrmInfo('org.w3.clearkey'); + }); + }); + manifest.sequenceMode = true; + }); + + await testHlsParser(master, media, manifest); + }); + + it('constructs DrmInfo for ClearKey without explicit KEYFORMAT', async () => { + const master = [ + '#EXTM3U\n', + '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",', + 'RESOLUTION=960x540,FRAME-RATE=60\n', + 'video\n', + ].join(''); + + const media = [ + '#EXTM3U\n', + '#EXT-X-TARGETDURATION:6\n', + '#EXT-X-PLAYLIST-TYPE:VOD\n', + '#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,', + 'URI="key.bin",\n', + '#EXT-X-MAP:URI="init.mp4"\n', + '#EXTINF:5,\n', + 'main.mp4', + ].join(''); + + const manifest = shaka.test.ManifestGenerator.generate((manifest) => { + manifest.anyTimeline(); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.encrypted = true; + stream.addDrmInfo('org.w3.clearkey'); + }); + }); + manifest.sequenceMode = true; + }); + + await testHlsParser(master, media, manifest); + }); + it('falls back to mp4 if HEAD request fails', async () => { const master = [ '#EXTM3U\n', diff --git a/test/player_integration.js b/test/player_integration.js index 5411ed14ea..2140f6d21e 100644 --- a/test/player_integration.js +++ b/test/player_integration.js @@ -1152,4 +1152,14 @@ describe('Player', () => { expect(chapter3.endTime).toBe(61.349); }); }); // describe('addChaptersTrack') + + it('requires a license server for HLS ClearKey content', async () => { + const expectedError = Util.jasmineError(new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.DRM, + shaka.util.Error.Code.NO_LICENSE_SERVER_GIVEN, + 'org.w3.clearkey')); + await expectAsync(player.load('test:sintel-hls-clearkey')) + .toBeRejectedWith(expectedError); + }); }); diff --git a/test/test/util/manifest_generator.js b/test/test/util/manifest_generator.js index 3357d01c66..07ac9be353 100644 --- a/test/test/util/manifest_generator.js +++ b/test/test/util/manifest_generator.js @@ -393,8 +393,8 @@ shaka.test.ManifestGenerator.DrmInfo = class { this.videoRobustness = ''; /** @type {Uint8Array} */ this.serverCertificate = null; - /** @type {Array.} */ - this.initData = null; + /** @type {!Array.} */ + this.initData = []; /** @type {Set.} */ this.keyIds = new Set(); /** @type {string} */ @@ -422,10 +422,7 @@ shaka.test.ManifestGenerator.DrmInfo = class { * @param {!Uint8Array} buffer */ addInitData(type, buffer) { - if (!this.initData) { - this.initData = []; - } - this.initData.push({initData: buffer, initDataType: type}); + this.initData.push({initData: buffer, initDataType: type, keyId: null}); } /** diff --git a/test/test/util/test_scheme.js b/test/test/util/test_scheme.js index 4907b7defb..a443075900 100644 --- a/test/test/util/test_scheme.js +++ b/test/test/util/test_scheme.js @@ -52,7 +52,8 @@ let ExtraMetadataType; * duration: number, * licenseServers: (!Object.|undefined), * licenseRequestHeaders: (!Object.|undefined), - * sequenceMode: boolean + * customizeStream: (function(shaka.test.ManifestGenerator.Stream)|undefined), + * sequenceMode: (boolean|undefined) * }} */ let MetadataType; @@ -243,6 +244,10 @@ shaka.test.TestScheme = class { }); } } + + if (data.customizeStream) { + data.customizeStream(stream); + } } /** @@ -279,7 +284,7 @@ shaka.test.TestScheme = class { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.presentationTimeline.setDuration(data.duration); - manifest.sequenceMode = data.sequenceMode; + manifest.sequenceMode = data.sequenceMode || false; const videoResolutions = data.videoResolutions || [undefined]; const audioLanguages = data.audioLanguages || @@ -513,7 +518,6 @@ shaka.test.TestScheme.DATA = { audio: sintelAudioSegment, text: vttSegment, duration: 30, - sequenceMode: false, }, // Like 'sintel', but flagged as sequence mode. @@ -530,7 +534,6 @@ shaka.test.TestScheme.DATA = { video: sintelVideoSegment, audio: sintelAudioSegment, duration: 300, - sequenceMode: false, }, // Like 'sintel' above, but with languages and delayed setup. @@ -547,7 +550,6 @@ shaka.test.TestScheme.DATA = { language: 'fa', // Necessary to repro #1696 }), duration: 30, - sequenceMode: false, }, 'sintel_multi_lingual_multi_res': { @@ -561,20 +563,17 @@ shaka.test.TestScheme.DATA = { audioLanguages: ['en', 'es'], textLanguages: ['zh', 'fr'], duration: 30, - sequenceMode: false, }, 'sintel_audio_only': { audio: sintelAudioSegment, duration: 30, - sequenceMode: false, }, 'sintel_no_text': { video: sintelVideoSegment, audio: sintelAudioSegment, duration: 30, - sequenceMode: false, }, // https://github.com/shaka-project/shaka-player/issues/2553 @@ -583,7 +582,6 @@ shaka.test.TestScheme.DATA = { text: vttSegment, textLanguages: ['de', 'de'], // one of these is the "forced subs" track duration: 30, - sequenceMode: false, }, 'sintel-enc': { @@ -592,7 +590,19 @@ shaka.test.TestScheme.DATA = { text: vttSegment, licenseServers: widevineDrmServers, duration: 30, - sequenceMode: false, + }, + + // Equivalent to what you get with HLS METHOD=SAMPLE-AES, KEYFORMAT=identity. + // Requires explicit clear keys or license server configuration. + 'sintel-hls-clearkey': { + video: sintelEncryptedVideo, + audio: sintelEncryptedAudio, + duration: 30, + sequenceMode: true, + customizeStream: (stream) => { + stream.encrypted = true; + stream.addDrmInfo('org.w3.clearkey'); + }, }, 'multidrm': { @@ -602,7 +612,6 @@ shaka.test.TestScheme.DATA = { licenseServers: axinomDrmServers, licenseRequestHeaders: axinomDrmHeaders, duration: 30, - sequenceMode: false, }, 'multidrm_no_init_data': { @@ -615,7 +624,6 @@ shaka.test.TestScheme.DATA = { licenseServers: axinomDrmServers, licenseRequestHeaders: axinomDrmHeaders, duration: 30, - sequenceMode: false, }, 'cea-708_ts': { @@ -629,7 +637,6 @@ shaka.test.TestScheme.DATA = { mimeType: 'application/cea-608', }, duration: 30, - sequenceMode: false, }, 'cea-708_mp4': { @@ -644,7 +651,6 @@ shaka.test.TestScheme.DATA = { closedCaptions: new Map([['CC1', 'en']]), }, duration: 30, - sequenceMode: false, }, };