diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 5720c799fb1..7787531c47b 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -1438,6 +1438,8 @@ export class LevelDetails { // (undocumented) get edge(): number; // (undocumented) + encryptedFragments: Fragment[]; + // (undocumented) endCC: number; // (undocumented) endSN: number; diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 0272668237e..1b4a52ff9f5 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -573,6 +573,8 @@ export default class BaseStreamController }); this.hls.trigger(Events.KEY_LOADING, { frag }); this.throwIfFragContextChanged('KEY_LOADING'); + } else if (!frag.encrypted && details.encryptedFragments.length) { + this.keyLoader.loadClear(frag, details.encryptedFragments); } targetBufferTime = Math.max(frag.start, targetBufferTime || 0); diff --git a/src/controller/eme-controller.ts b/src/controller/eme-controller.ts index 7683971d7ac..18d6ea61dbc 100644 --- a/src/controller/eme-controller.ts +++ b/src/controller/eme-controller.ts @@ -724,7 +724,7 @@ class EMEController implements ComponentAPI { `key status change "${status}" for keyStatuses keyId: ${Hex.hexDump( keyId )} session keyId: ${Hex.hexDump( - mediaKeySessionContext.decryptdata.keyId + mediaKeySessionContext.decryptdata.keyId || [] )} uri: ${mediaKeySessionContext.decryptdata.uri}` ); mediaKeySessionContext.keyStatus = status; diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts index 8e7d65681f5..e5265345c7f 100644 --- a/src/loader/fragment.ts +++ b/src/loader/fragment.ts @@ -213,7 +213,7 @@ export class Fragment extends BaseSegment { setKeyFormat(keyFormat: KeySystemFormats) { if (this.levelkeys) { const key = this.levelkeys[keyFormat]; - if (key) { + if (key && !this._decryptdata) { this._decryptdata = key.getDecryptData(this.sn); } } diff --git a/src/loader/key-loader.ts b/src/loader/key-loader.ts index f4ceeed888a..f6ed0eedb31 100644 --- a/src/loader/key-loader.ts +++ b/src/loader/key-loader.ts @@ -80,6 +80,24 @@ export default class KeyLoader implements ComponentAPI { }); } + loadClear(loadingFrag: Fragment, encryptedFragments: Fragment[]): void | Promise { + if (this.emeController && this.config.emeEnabled) { + // access key-system with nearest key on start (loaidng frag is unencrypted) + const { sn, cc } = loadingFrag; + for (let i = 0; i < encryptedFragments.length; i++) { + const frag = encryptedFragments[i]; + if (cc <= frag.cc && (sn === 'initSegment' || sn < frag.sn)) { + this.emeController + .selectKeySystemFormat(frag) + .then((keySystemFormat) => { + frag.setKeyFormat(keySystemFormat); + }); + break; + } + } + } + } + load(frag: Fragment): Promise { if (!frag.decryptdata && frag.encrypted && this.emeController) { // Multiple keys, but none selected, resolve in eme-controller diff --git a/src/loader/level-details.ts b/src/loader/level-details.ts index a39ecfe7578..269d7eff89e 100644 --- a/src/loader/level-details.ts +++ b/src/loader/level-details.ts @@ -48,9 +48,11 @@ export class LevelDetails { public driftEndTime: number = 0; public driftStart: number = 0; public driftEnd: number = 0; + public encryptedFragments: Fragment[]; constructor(baseUrl) { this.fragments = []; + this.encryptedFragments = []; this.dateRanges = {}; this.url = baseUrl; } diff --git a/src/loader/m3u8-parser.ts b/src/loader/m3u8-parser.ts index 76b0466d356..7c27146150a 100644 --- a/src/loader/m3u8-parser.ts +++ b/src/loader/m3u8-parser.ts @@ -245,6 +245,18 @@ export default class M3U8Parser { frag.start = totalduration; if (levelkeys) { frag.levelkeys = levelkeys; + const { encryptedFragments } = level; + if ( + frag.levelkeys && + Object.keys(frag.levelkeys).some( + (format) => frag.levelkeys![format].isCommonEncryption + ) && + (!encryptedFragments.length || + encryptedFragments[encryptedFragments.length - 1].levelkeys !== + levelkeys) + ) { + encryptedFragments.push(frag); + } } frag.sn = currentSN; frag.level = id; @@ -406,7 +418,11 @@ export default class M3U8Parser { .filter(Number.isFinite); if (isKeyTagSupported(decryptkeyformat, decryptmethod)) { - if (decryptmethod === 'NONE' || !levelkeys) { + if (decryptmethod === 'NONE') { + levelkeys = undefined; + break; + } + if (!levelkeys) { levelkeys = {}; } if (levelkeys[decryptkeyformat]) { diff --git a/tests/unit/loader/playlist-loader.ts b/tests/unit/loader/playlist-loader.ts index 6d7367e435f..db72ba6ecab 100644 --- a/tests/unit/loader/playlist-loader.ts +++ b/tests/unit/loader/playlist-loader.ts @@ -1722,6 +1722,94 @@ media_1638278.m4s`; pdt += frag.duration * 1000; } }); + + it('parse clear->enc->clear->enc playlist', function () { + const level = `#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-TARGETDURATION:6 + +#EXT-X-MAP:URI="init.mp4" +#EXTINF:5.5, +1.mp4 +#EXTINF:5.0, +2.mp4 + +#EXT-X-DISCONTINUITY +#EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://a",KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1" +#EXT-X-KEY:METHOD=SAMPLE-AES,URI="data:text/plain;base64,YQo=",KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",KEYFORMATVERSIONS="1" +#EXT-X-MAP:URI="init.mp4" +#EXTINF:5.5, +3.mp4 +#EXTINF:5.0, +4.mp4 + +#EXT-X-DISCONTINUITY +#EXT-X-KEY:METHOD=NONE +#EXT-X-MAP:URI="init.mp4" +#EXTINF:5.5, +5.mp4 +#EXTINF:5.0, +6.mp4 + +#EXT-X-DISCONTINUITY +#EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://b",KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1" +#EXT-X-KEY:METHOD=SAMPLE-AES,URI="data:text/plain;base64,Yg==",KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",KEYFORMATVERSIONS="1" +#EXT-X-MAP:URI="init.mp4" +#EXTINF:5.0, +7.mp4 +#EXTINF:4.0, +8.mp4 +#EXT-X-ENDLIST`; + const result = M3U8Parser.parseLevelPlaylist( + level, + 'http://foo.com/adaptive/test.m3u8', + 0, + PlaylistLevelType.MAIN, + 0 + ); + expect(result.fragments.length).to.equal(8); + expect(result.fragments[0].levelkeys, 'first segment has no keys').to.equal( + undefined + ); + expect( + result.fragments[1].levelkeys, + 'second segment has no keys' + ).to.equal(undefined); + expect(result.fragments[2].levelkeys, 'third segment has two keys') + .to.be.an('object') + .with.keys([ + 'com.apple.streamingkeydelivery', + 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + ]); + expect(result.fragments[3].levelkeys, 'forth segment has two keys') + .to.be.an('object') + .with.keys([ + 'com.apple.streamingkeydelivery', + 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + ]); + expect(result.fragments[4].levelkeys, 'fifth segment has no keys').to.equal( + undefined + ); + expect(result.fragments[5].levelkeys, 'sixth segment has no keys').to.equal( + undefined + ); + expect(result.fragments[6].levelkeys, 'seventh segment has two keys') + .to.be.an('object') + .with.keys([ + 'com.apple.streamingkeydelivery', + 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + ]); + expect(result.fragments[7].levelkeys, 'eighth segment has two keys') + .to.be.an('object') + .with.keys([ + 'com.apple.streamingkeydelivery', + 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + ]); + expect(result) + .to.have.property('encryptedFragments') + .which.is.an('array') + .which.has.members([result.fragments[2], result.fragments[6]]); + }); }); function expectWithJSONMessage(value: any, msg?: string) {