diff --git a/demo/common/assets.js b/demo/common/assets.js index 0394ae4ca7..11d8428d9f 100644 --- a/demo/common/assets.js +++ b/demo/common/assets.js @@ -384,6 +384,15 @@ shakaAssets.testAssets = [ .addFeature(shakaAssets.Feature.SURROUND) .addFeature(shakaAssets.Feature.OFFLINE) .addLicenseServer('com.widevine.alpha', 'https://cwip-shaka-proxy.appspot.com/no_auth'), + new ShakaDemoAssetInfo( + /* name= */ 'Angel One (HLS, MP4, SAMPLE-AES-CTR, multi-key)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/angel_one.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/angel-one-sample-aes-ctr-multiple-key/manifest.m3u8', + /* source= */ shakaAssets.Source.SHAKA) + .addKeySystem(shakaAssets.KeySystem.CLEAR_KEY) + .addFeature(shakaAssets.Feature.HLS) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.OFFLINE), new ShakaDemoAssetInfo( /* name= */ 'Sintel (HLS, TS, AES-128 key rotation)', /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 376a853587..f7e4dda377 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -4293,16 +4293,23 @@ shaka.hls.HlsParser = class { const keyUris = shaka.hls.Utils.constructSegmentUris( getUris(), drmTag.getRequiredAttrValue('URI'), variables); - const keyMapKey = keyUris.sort().join(''); - if (!this.identityKeyMap_.has(keyMapKey)) { - const requestType = shaka.net.NetworkingEngine.RequestType.KEY; - const request = shaka.net.NetworkingEngine.makeRequest( - keyUris, this.config_.retryParameters); - const keyResponse = this.makeNetworkRequest_(request, requestType); - this.identityKeyMap_.set(keyMapKey, keyResponse); - } - const keyResponse = await this.identityKeyMap_.get(keyMapKey); - const key = shaka.util.Uint8ArrayUtils.toHex(keyResponse.data); + let key; + if (keyUris[0].startsWith('data:text/plain;base64,')) { + key = shaka.util.Uint8ArrayUtils.toHex( + shaka.util.Uint8ArrayUtils.fromBase64( + keyUris[0].split('data:text/plain;base64,').pop())); + } else { + const keyMapKey = keyUris.sort().join(''); + if (!this.identityKeyMap_.has(keyMapKey)) { + const requestType = shaka.net.NetworkingEngine.RequestType.KEY; + const request = shaka.net.NetworkingEngine.makeRequest( + keyUris, this.config_.retryParameters); + const keyResponse = this.makeNetworkRequest_(request, requestType); + this.identityKeyMap_.set(keyMapKey, keyResponse); + } + const keyResponse = await this.identityKeyMap_.get(keyMapKey); + key = shaka.util.Uint8ArrayUtils.toHex(keyResponse.data); + } // 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 diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index a0bd598a79..e3a84bfb8b 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -2314,9 +2314,17 @@ shaka.media.DrmEngine = class { shaka.util.BufferUtils.equal(a.initData, b.initData); }; + const clearkeyDataStart = 'data:application/json;base64,'; + const clearKeyLicenseServers = []; + for (const drmInfo of drmInfos) { // Build an array of unique license servers. - if (!licenseServers.includes(drmInfo.licenseServerUri)) { + if (drmInfo.keySystem == 'org.w3.clearkey' && + drmInfo.licenseServerUri.startsWith(clearkeyDataStart)) { + if (!clearKeyLicenseServers.includes(drmInfo.licenseServerUri)) { + clearKeyLicenseServers.push(drmInfo.licenseServerUri); + } + } else if (!licenseServers.includes(drmInfo.licenseServerUri)) { licenseServers.push(drmInfo.licenseServerUri); } @@ -2353,6 +2361,21 @@ shaka.media.DrmEngine = class { } } } + + if (clearKeyLicenseServers.length == 1) { + licenseServers.push(clearKeyLicenseServers[0]); + } else if (clearKeyLicenseServers.length > 0) { + const keys = []; + for (const clearKeyLicenseServer of clearKeyLicenseServers) { + const license = window.atob( + clearKeyLicenseServer.split(clearkeyDataStart).pop()); + const jwkSet = /** @type {{keys: !Array}} */(JSON.parse(license)); + keys.push(...jwkSet.keys); + } + const newJwkSet = {keys: keys}; + const newLicense = JSON.stringify(newJwkSet); + licenseServers.push(clearkeyDataStart + window.btoa(newLicense)); + } } /** diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index 61560d0034..f25d469305 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -3732,6 +3732,51 @@ describe('HlsParser', () => { expect(newDrmInfoSpy).toHaveBeenCalled(); }); + it('constructs DrmInfo for ClearKey with raw key', async () => { + const master = [ + '#EXTM3U\n', + '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1.4d401f",', + '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="data:text/plain;base64,Pj6hFgt5iFZtfBLN6oq8Eg==",\n', + '#EXT-X-MAP:URI="init.mp4"\n', + '#EXTINF:5,\n', + 'main.mp4', + ].join(''); + + const initDataBase64 = 'eyJraWRzIjpbIkFBQUFBQUFBQUFBQUFBQUFBQUFBQUEiXX0='; + const keyId = '00000000000000000000000000000000'; + + 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', (drmInfo) => { + drmInfo.licenseServerUri = 'data:application/json;base64,eyJrZXl' + + 'zIjpbeyJrdHkiOiJvY3QiLCJraWQiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFB' + + 'IiwiayI6IlBqNmhGZ3Q1aUZadGZCTE42b3E4RWcifV19'; + drmInfo.keyIds.add(keyId); + drmInfo.addKeyIdsData(initDataBase64); + }); + }); + }); + manifest.sequenceMode = sequenceMode; + manifest.type = shaka.media.ManifestParser.HLS; + }); + + await testHlsParser(master, media, manifest); + expect(newDrmInfoSpy).toHaveBeenCalled(); + }); + describe('constructs DrmInfo with EXT-X-SESSION-KEY', () => { it('for Widevine', async () => { const initDataBase64 =