From 3bd032c9d67fc8f7ddf4fbc4cc68760491a07d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Wed, 10 Jul 2024 07:13:56 +0200 Subject: [PATCH] fix(HLS): Fix load AES media playlist (#7012) --- lib/hls/hls_parser.js | 23 ++++++++---- lib/media/segment_utils.js | 41 +++++++++++++++++++++ lib/media/streaming_engine.js | 47 ++----------------------- test/hls/hls_parser_integration.js | 2 +- test/test/assets/hls-aes-256/index.m3u8 | 6 ---- 5 files changed, 61 insertions(+), 58 deletions(-) delete mode 100644 test/test/assets/hls-aes-256/index.m3u8 diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index f691e5cfcd..7a66bcd575 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -1025,7 +1025,7 @@ shaka.hls.HlsParser = class { if (!segments.length) { return defaultBasicInfo; } - const segment = this.getAvailableSegment_(segments); + const {segment, segmentIndex} = this.getAvailableSegment_(segments); const segmentUris = segment.getUris(); const segmentUri = segmentUris[0]; const parsedUri = new goog.Uri(segmentUri); @@ -1050,6 +1050,10 @@ shaka.hls.HlsParser = class { const initResponse = await this.makeNetworkRequest_( initSegmentRequest, requestType, {type: initType}); initData = initResponse.data; + if (initSegmentRef.aesKey) { + initData = await shaka.media.SegmentUtils.aesDecrypt( + initData, initSegmentRef.aesKey, 0); + } initMimeType = initResponse.headers['content-type']; if (initMimeType) { @@ -1064,6 +1068,11 @@ shaka.hls.HlsParser = class { const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT; const response = await this.makeNetworkRequest_( segmentRequest, requestType, {type}); + let data = response.data; + if (segment.aesKey) { + data = await shaka.media.SegmentUtils.aesDecrypt( + data, segment.aesKey, segmentIndex); + } let contentMimeType = response.headers['content-type']; if (contentMimeType) { @@ -1091,9 +1100,9 @@ shaka.hls.HlsParser = class { ]; if (shaka.util.TsParser.probe( - shaka.util.BufferUtils.toUint8(response.data))) { + shaka.util.BufferUtils.toUint8(data))) { const basicInfo = - shaka.media.SegmentUtils.getBasicInfoFromTs(response.data); + shaka.media.SegmentUtils.getBasicInfoFromTs(data); if (basicInfo) { return basicInfo; } @@ -1101,7 +1110,7 @@ shaka.hls.HlsParser = class { validMp4MimeType.includes(contentMimeType) || (initMimeType && validMp4MimeType.includes(initMimeType))) { const basicInfo = shaka.media.SegmentUtils.getBasicInfoFromMp4( - initData, response.data); + initData, data); if (basicInfo) { return basicInfo; } @@ -4266,7 +4275,7 @@ shaka.hls.HlsParser = class { /** * @param {!Array.} segments - * @return {!shaka.media.SegmentReference} + * @return {{segment: !shaka.media.SegmentReference, segmentIndex: number}} * @private */ getAvailableSegment_(segments) { @@ -4283,7 +4292,7 @@ shaka.hls.HlsParser = class { segmentIndex ++; segment = segments[segmentIndex]; } - return segment; + return {segment, segmentIndex}; } /** @@ -4299,7 +4308,7 @@ shaka.hls.HlsParser = class { const HlsParser = shaka.hls.HlsParser; const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT; - const segment = this.getAvailableSegment_(segments); + const {segment} = this.getAvailableSegment_(segments); if (segment.status == shaka.media.SegmentReference.Status.MISSING) { return this.guessMimeTypeFallback_(contentType); diff --git a/lib/media/segment_utils.js b/lib/media/segment_utils.js index 56354fb938..2868988e04 100644 --- a/lib/media/segment_utils.js +++ b/lib/media/segment_utils.js @@ -539,6 +539,47 @@ shaka.media.SegmentUtils = class { .parse(data, /* partialOkay= */ true); return defaultKID; } + + /** + * @param {!BufferSource} rawResult + * @param {shaka.extern.aesKey} aesKey + * @param {number} position + * @return {!Promise.} + */ + static async aesDecrypt(rawResult, aesKey, position) { + const key = aesKey; + if (!key.cryptoKey) { + goog.asserts.assert(key.fetchKey, 'If AES cryptoKey was not ' + + 'preloaded, fetchKey function should be provided'); + await key.fetchKey(); + goog.asserts.assert(key.cryptoKey, 'AES cryptoKey should now be set'); + } + let iv = key.iv; + if (!iv) { + iv = shaka.util.BufferUtils.toUint8(new ArrayBuffer(16)); + let sequence = key.firstMediaSequenceNumber + position; + for (let i = iv.byteLength - 1; i >= 0; i--) { + iv[i] = sequence & 0xff; + sequence >>= 8; + } + } + let algorithm; + if (aesKey.blockCipherMode == 'CBC') { + algorithm = { + name: 'AES-CBC', + iv, + }; + } else { + algorithm = { + name: 'AES-CTR', + counter: iv, + // NIST SP800-38A standard suggests that the counter should occupy half + // of the counter block + length: 64, + }; + } + return window.crypto.subtle.decrypt(algorithm, key.cryptoKey, rawResult); + } }; diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js index 5cd04d297b..d643cef62f 100644 --- a/lib/media/streaming_engine.js +++ b/lib/media/streaming_engine.js @@ -18,9 +18,9 @@ goog.require('shaka.media.MediaSourceEngine'); goog.require('shaka.media.SegmentIterator'); goog.require('shaka.media.SegmentReference'); goog.require('shaka.media.SegmentPrefetch'); +goog.require('shaka.media.SegmentUtils'); goog.require('shaka.net.Backoff'); goog.require('shaka.net.NetworkingEngine'); -goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.DelayedTick'); goog.require('shaka.util.Destroyer'); goog.require('shaka.util.Error'); @@ -1749,48 +1749,6 @@ shaka.media.StreamingEngine = class { } } - /** - * @param {!BufferSource} rawResult - * @param {shaka.extern.aesKey} aesKey - * @param {number} position - * @return {!Promise.} finalResult - * @private - */ - async aesDecrypt_(rawResult, aesKey, position) { - const key = aesKey; - if (!key.cryptoKey) { - goog.asserts.assert(key.fetchKey, 'If AES cryptoKey was not ' + - 'preloaded, fetchKey function should be provided'); - await key.fetchKey(); - goog.asserts.assert(key.cryptoKey, 'AES cryptoKey should now be set'); - } - let iv = key.iv; - if (!iv) { - iv = shaka.util.BufferUtils.toUint8(new ArrayBuffer(16)); - let sequence = key.firstMediaSequenceNumber + position; - for (let i = iv.byteLength - 1; i >= 0; i--) { - iv[i] = sequence & 0xff; - sequence >>= 8; - } - } - let algorithm; - if (aesKey.blockCipherMode == 'CBC') { - algorithm = { - name: 'AES-CBC', - iv, - }; - } else { - algorithm = { - name: 'AES-CTR', - counter: iv, - // NIST SP800-38A standard suggests that the counter should occupy half - // of the counter block - length: 64, - }; - } - return window.crypto.subtle.decrypt(algorithm, key.cryptoKey, rawResult); - } - /** * Clear per-stream error states and retry any failed streams. @@ -2567,7 +2525,8 @@ shaka.media.StreamingEngine = class { mediaState.operation = null; let result = response.data; if (reference.aesKey) { - result = await this.aesDecrypt_(result, reference.aesKey, position); + result = await shaka.media.SegmentUtils.aesDecrypt( + result, reference.aesKey, position); } return result; } diff --git a/test/hls/hls_parser_integration.js b/test/hls/hls_parser_integration.js index d94e63cf03..d73dbc9936 100644 --- a/test/hls/hls_parser_integration.js +++ b/test/hls/hls_parser_integration.js @@ -72,7 +72,7 @@ describe('HlsParser', () => { keyRequests++; } }); - await player.load('/base/test/test/assets/hls-aes-256/index.m3u8'); + await player.load('/base/test/test/assets/hls-aes-256/media.m3u8'); await video.play(); expect(player.isLive()).toBe(false); diff --git a/test/test/assets/hls-aes-256/index.m3u8 b/test/test/assets/hls-aes-256/index.m3u8 deleted file mode 100644 index ccfa7567d4..0000000000 --- a/test/test/assets/hls-aes-256/index.m3u8 +++ /dev/null @@ -1,6 +0,0 @@ -#EXTM3U -#EXT-X-VERSION:6 -#EXT-X-INDEPENDENT-SEGMENTS - -#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=829339,BANDWIDTH=901770,CODECS="avc1.4D400D",RESOLUTION=320x180 -media.m3u8 \ No newline at end of file