From 55b4aac46392ffc6f59ea1cb79ca899c438b41a3 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 23 Jan 2024 09:51:29 -0500 Subject: [PATCH] Adding AES-256 and AES-256-CTR encryption modes --- src/controller/base-stream-controller.ts | 7 +- src/controller/subtitle-stream-controller.ts | 7 +- src/crypt/aes-crypto.ts | 23 +++- src/crypt/decrypter-aes-mode.ts | 4 + src/crypt/decrypter.ts | 24 ++-- src/crypt/fast-aes-key.ts | 29 ++++- src/demux/sample-aes.ts | 2 + src/demux/transmuxer.ts | 16 ++- src/loader/fragment-loader.ts | 11 +- src/loader/key-loader.ts | 2 + src/loader/level-key.ts | 19 +-- src/utils/encryption-methods-util.ts | 21 ++++ tests/index.js | 1 + tests/test-streams.js | 5 + tests/unit/crypt/decrypter.js | 123 +++++++++++++++++++ tests/unit/loader/playlist-loader.ts | 61 ++++++++- 16 files changed, 320 insertions(+), 35 deletions(-) create mode 100644 src/crypt/decrypter-aes-mode.ts create mode 100644 src/utils/encryption-methods-util.ts create mode 100644 tests/unit/crypt/decrypter.js diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 0d50dfb3a1f..d482481d973 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -7,6 +7,10 @@ import { ErrorDetails, ErrorTypes } from '../errors'; import { ChunkMetadata } from '../types/transmuxer'; import { appendUint8Array } from '../utils/mp4-tools'; import { alignStream } from '../utils/discontinuities'; +import { + isFullSegmentEncryption, + getAesModeFromFullSegmentMethod, +} from '../utils/encryption-methods-util'; import { findFragmentByPDT, findFragmentByPTS, @@ -481,7 +485,7 @@ export default class BaseStreamController payload.byteLength > 0 && decryptData?.key && decryptData.iv && - decryptData.method === 'AES-128' + isFullSegmentEncryption(decryptData.method) ) { const startTime = self.performance.now(); // decrypt init segment data @@ -490,6 +494,7 @@ export default class BaseStreamController new Uint8Array(payload), decryptData.key.buffer, decryptData.iv.buffer, + getAesModeFromFullSegmentMethod(decryptData.method), ) .catch((err) => { hls.trigger(Events.ERROR, { diff --git a/src/controller/subtitle-stream-controller.ts b/src/controller/subtitle-stream-controller.ts index 4b08534e49e..734d8aec68f 100644 --- a/src/controller/subtitle-stream-controller.ts +++ b/src/controller/subtitle-stream-controller.ts @@ -9,6 +9,10 @@ import { PlaylistLevelType } from '../types/loader'; import { Level } from '../types/level'; import { subtitleOptionsIdentical } from '../utils/media-option-attributes'; import { ErrorDetails, ErrorTypes } from '../errors'; +import { + isFullSegmentEncryption, + getAesModeFromFullSegmentMethod, +} from '../utils/encryption-methods-util'; import type { NetworkComponentAPI } from '../types/component-api'; import type Hls from '../hls'; import type { FragmentTracker } from './fragment-tracker'; @@ -360,7 +364,7 @@ export class SubtitleStreamController payload.byteLength > 0 && decryptData?.key && decryptData.iv && - decryptData.method === 'AES-128' + isFullSegmentEncryption(decryptData.method) ) { const startTime = performance.now(); // decrypt the subtitles @@ -369,6 +373,7 @@ export class SubtitleStreamController new Uint8Array(payload), decryptData.key.buffer, decryptData.iv.buffer, + getAesModeFromFullSegmentMethod(decryptData.method), ) .catch((err) => { hls.trigger(Events.ERROR, { diff --git a/src/crypt/aes-crypto.ts b/src/crypt/aes-crypto.ts index 3e07168da37..7eb58258b29 100644 --- a/src/crypt/aes-crypto.ts +++ b/src/crypt/aes-crypto.ts @@ -1,13 +1,32 @@ +import { DecrypterAesMode } from './decrypter-aes-mode'; + export default class AESCrypto { private subtle: SubtleCrypto; private aesIV: Uint8Array; + private aesMode: DecrypterAesMode; - constructor(subtle: SubtleCrypto, iv: Uint8Array) { + constructor(subtle: SubtleCrypto, iv: Uint8Array, aesMode: DecrypterAesMode) { this.subtle = subtle; this.aesIV = iv; + this.aesMode = aesMode; } decrypt(data: ArrayBuffer, key: CryptoKey) { - return this.subtle.decrypt({ name: 'AES-CBC', iv: this.aesIV }, key, data); + switch (this.aesMode) { + case DecrypterAesMode.cbc: + return this.subtle.decrypt( + { name: 'AES-CBC', iv: this.aesIV }, + key, + data, + ); + case DecrypterAesMode.ctr: + return this.subtle.decrypt( + { name: 'AES-CTR', counter: this.aesIV, length: 64 }, //64 : NIST SP800-38A standard suggests that the counter should occupy half of the counter block + key, + data, + ); + default: + throw new Error(`[AESCrypto] invalid aes mode ${this.aesMode}`); + } } } diff --git a/src/crypt/decrypter-aes-mode.ts b/src/crypt/decrypter-aes-mode.ts new file mode 100644 index 00000000000..4b82154b282 --- /dev/null +++ b/src/crypt/decrypter-aes-mode.ts @@ -0,0 +1,4 @@ +export const enum DecrypterAesMode { + cbc = 0, + ctr = 1, +} diff --git a/src/crypt/decrypter.ts b/src/crypt/decrypter.ts index bd291a9bfd0..299dd23b64a 100644 --- a/src/crypt/decrypter.ts +++ b/src/crypt/decrypter.ts @@ -4,6 +4,7 @@ import AESDecryptor, { removePadding } from './aes-decryptor'; import { logger } from '../utils/logger'; import { appendUint8Array } from '../utils/mp4-tools'; import { sliceUint8 } from '../utils/typed-array'; +import { DecrypterAesMode } from './decrypter-aes-mode'; import type { HlsConfig } from '../config'; const CHUNK_SIZE = 16; // 16 bytes, 128 bits @@ -81,10 +82,11 @@ export default class Decrypter { data: Uint8Array | ArrayBuffer, key: ArrayBuffer, iv: ArrayBuffer, + aesMode: DecrypterAesMode, ): Promise { if (this.useSoftware) { return new Promise((resolve, reject) => { - this.softwareDecrypt(new Uint8Array(data), key, iv); + this.softwareDecrypt(new Uint8Array(data), key, iv, aesMode); const decryptResult = this.flush(); if (decryptResult) { resolve(decryptResult.buffer); @@ -93,7 +95,7 @@ export default class Decrypter { } }); } - return this.webCryptoDecrypt(new Uint8Array(data), key, iv); + return this.webCryptoDecrypt(new Uint8Array(data), key, iv, aesMode); } // Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached @@ -102,8 +104,13 @@ export default class Decrypter { data: Uint8Array, key: ArrayBuffer, iv: ArrayBuffer, + aesMode: DecrypterAesMode, ): ArrayBuffer | null { const { currentIV, currentResult, remainderData } = this; + if (aesMode !== DecrypterAesMode.cbc || key.byteLength !== 16) { + logger.warn('SoftwareDecrypt: can only handle AES-128-CBC'); + return null; + } this.logOnce('JS AES decrypt'); // The output is staggered during progressive parsing - the current result is cached, and emitted on the next call // This is done in order to strip PKCS7 padding, which is found at the end of each segment. We only know we've reached @@ -146,21 +153,22 @@ export default class Decrypter { data: Uint8Array, key: ArrayBuffer, iv: ArrayBuffer, + aesMode: DecrypterAesMode, ): Promise { const subtle = this.subtle; if (this.key !== key || !this.fastAesKey) { this.key = key; - this.fastAesKey = new FastAESKey(subtle, key); + this.fastAesKey = new FastAESKey(subtle, key, aesMode); } return this.fastAesKey .expandKey() - .then((aesKey) => { + .then((aesKey: CryptoKey) => { // decrypt using web crypto if (!subtle) { return Promise.reject(new Error('web crypto not initialized')); } this.logOnce('WebCrypto AES decrypt'); - const crypto = new AESCrypto(subtle, new Uint8Array(iv)); + const crypto = new AESCrypto(subtle, new Uint8Array(iv), aesMode); return crypto.decrypt(data.buffer, aesKey); }) .catch((err) => { @@ -168,16 +176,16 @@ export default class Decrypter { `[decrypter]: WebCrypto Error, disable WebCrypto API, ${err.name}: ${err.message}`, ); - return this.onWebCryptoError(data, key, iv); + return this.onWebCryptoError(data, key, iv, aesMode); }); } - private onWebCryptoError(data, key, iv): ArrayBuffer | never { + private onWebCryptoError(data, key, iv, aesMode): ArrayBuffer | never { const enableSoftwareAES = this.enableSoftwareAES; if (enableSoftwareAES) { this.useSoftware = true; this.logEnabled = true; - this.softwareDecrypt(data, key, iv); + this.softwareDecrypt(data, key, iv, aesMode); const decryptResult = this.flush(); if (decryptResult) { return decryptResult.buffer; diff --git a/src/crypt/fast-aes-key.ts b/src/crypt/fast-aes-key.ts index eab70e23646..277baf26419 100644 --- a/src/crypt/fast-aes-key.ts +++ b/src/crypt/fast-aes-key.ts @@ -1,16 +1,35 @@ +import { DecrypterAesMode } from './decrypter-aes-mode'; + export default class FastAESKey { private subtle: any; private key: ArrayBuffer; + private aesMode: DecrypterAesMode; - constructor(subtle, key) { + constructor(subtle, key, aesMode: DecrypterAesMode) { this.subtle = subtle; this.key = key; + this.aesMode = aesMode; } expandKey() { - return this.subtle.importKey('raw', this.key, { name: 'AES-CBC' }, false, [ - 'encrypt', - 'decrypt', - ]); + const subtleAlgoName = getSubtleAlgoName(this.aesMode); + return this.subtle.importKey( + 'raw', + this.key, + { name: subtleAlgoName }, + false, + ['encrypt', 'decrypt'], + ); + } +} + +function getSubtleAlgoName(aesMode: DecrypterAesMode) { + switch (aesMode) { + case DecrypterAesMode.cbc: + return 'AES-CBC'; + case DecrypterAesMode.ctr: + return 'AES-CTR'; + default: + throw new Error(`[FastAESKey] invalid aes mode ${aesMode}`); } } diff --git a/src/demux/sample-aes.ts b/src/demux/sample-aes.ts index 2999daa322c..20f6bcc52da 100644 --- a/src/demux/sample-aes.ts +++ b/src/demux/sample-aes.ts @@ -4,6 +4,7 @@ import { HlsConfig } from '../config'; import Decrypter from '../crypt/decrypter'; +import { DecrypterAesMode } from '../crypt/decrypter-aes-mode'; import { HlsEventEmitter } from '../events'; import type { AudioSample, @@ -30,6 +31,7 @@ class SampleAesDecrypter { encryptedData, this.keyData.key.buffer, this.keyData.iv.buffer, + DecrypterAesMode.cbc, ); } diff --git a/src/demux/transmuxer.ts b/src/demux/transmuxer.ts index 39e5c40c0e3..f3cc148d6c5 100644 --- a/src/demux/transmuxer.ts +++ b/src/demux/transmuxer.ts @@ -10,6 +10,10 @@ import { AC3Demuxer } from './audio/ac3-demuxer'; import MP4Remuxer from '../remux/mp4-remuxer'; import PassThroughRemuxer from '../remux/passthrough-remuxer'; import { logger } from '../utils/logger'; +import { + isFullSegmentEncryption, + getAesModeFromFullSegmentMethod, +} from '../utils/encryption-methods-util'; import type { Demuxer, DemuxerResult, KeyData } from '../types/demuxer'; import type { Remuxer } from '../types/remuxer'; import type { TransmuxerResult, ChunkMetadata } from '../types/transmuxer'; @@ -115,8 +119,10 @@ export default class Transmuxer { } = transmuxConfig; const keyData = getEncryptionType(uintData, decryptdata); - if (keyData && keyData.method === 'AES-128') { + if (keyData && isFullSegmentEncryption(keyData.method)) { const decrypter = this.getDecrypter(); + const aesMode = getAesModeFromFullSegmentMethod(keyData.method); + // Software decryption is synchronous; webCrypto is not if (decrypter.isSync()) { // Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached @@ -125,6 +131,7 @@ export default class Transmuxer { uintData, keyData.key.buffer, keyData.iv.buffer, + aesMode, ); // For Low-Latency HLS Parts, decrypt in place, since part parsing is expected on push progress const loadingParts = chunkMeta.part > -1; @@ -138,7 +145,12 @@ export default class Transmuxer { uintData = new Uint8Array(decryptedData); } else { this.decryptionPromise = decrypter - .webCryptoDecrypt(uintData, keyData.key.buffer, keyData.iv.buffer) + .webCryptoDecrypt( + uintData, + keyData.key.buffer, + keyData.iv.buffer, + aesMode, + ) .then((decryptedData): TransmuxerResult => { // Calling push here is important; if flush() is called while this is still resolving, this ensures that // the decrypted data has been transmuxed diff --git a/src/loader/fragment-loader.ts b/src/loader/fragment-loader.ts index fe20c586aa9..967a143e452 100644 --- a/src/loader/fragment-loader.ts +++ b/src/loader/fragment-loader.ts @@ -336,8 +336,11 @@ function createLoaderContext( if (Number.isFinite(start) && Number.isFinite(end)) { let byteRangeStart = start; let byteRangeEnd = end; - if (frag.sn === 'initSegment' && frag.decryptdata?.method === 'AES-128') { - // MAP segment encrypted with method 'AES-128', when served with HTTP Range, + if ( + frag.sn === 'initSegment' && + isMethodFullSegmentAesCbc(frag.decryptdata?.method) + ) { + // MAP segment encrypted with method 'AES-128' or 'AES-256' (cbc), when served with HTTP Range, // has the unencrypted size specified in the range. // Ref: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-6.3.6 const fragmentLen = end - start; @@ -372,6 +375,10 @@ function createGapLoadError(frag: Fragment, part?: Part): LoadError { return new LoadError(errorData); } +function isMethodFullSegmentAesCbc(method) { + return method === 'AES-128' || method === 'AES-256'; +} + export class LoadError extends Error { public readonly data: FragLoadFailResult; constructor(data: FragLoadFailResult) { diff --git a/src/loader/key-loader.ts b/src/loader/key-loader.ts index 71545063b0d..4badeed8120 100644 --- a/src/loader/key-loader.ts +++ b/src/loader/key-loader.ts @@ -194,6 +194,8 @@ export default class KeyLoader implements ComponentAPI { } return this.loadKeyEME(keyInfo, frag); case 'AES-128': + case 'AES-256': + case 'AES-256-CTR': return this.loadKeyHTTP(keyInfo, frag); default: return Promise.reject( diff --git a/src/loader/level-key.ts b/src/loader/level-key.ts index 3db1b2582cd..cf853f57118 100644 --- a/src/loader/level-key.ts +++ b/src/loader/level-key.ts @@ -2,6 +2,7 @@ import { changeEndianness, convertDataUriToArrayBytes, } from '../utils/keysystem-util'; +import { isFullSegmentEncryption } from '../utils/encryption-methods-util'; import { KeySystemFormats } from '../utils/mediakeys-helper'; import { mp4pssh } from '../utils/mp4-tools'; import { logger } from '../utils/logger'; @@ -51,13 +52,14 @@ export class LevelKey implements DecryptData { this.keyFormatVersions = formatversions; this.iv = iv; this.encrypted = method ? method !== 'NONE' : false; - this.isCommonEncryption = this.encrypted && method !== 'AES-128'; + this.isCommonEncryption = + this.encrypted && !isFullSegmentEncryption(method); } public isSupported(): boolean { // If it's Segment encryption or No encryption, just select that key system if (this.method) { - if (this.method === 'AES-128' || this.method === 'NONE') { + if (isFullSegmentEncryption(this.method) || this.method === 'NONE') { return true; } if (this.keyFormat === 'identity') { @@ -88,16 +90,15 @@ export class LevelKey implements DecryptData { return null; } - if (this.method === 'AES-128' && this.uri && !this.iv) { + if (isFullSegmentEncryption(this.method) && this.uri && !this.iv) { if (typeof sn !== 'number') { // We are fetching decryption data for a initialization segment - // If the segment was encrypted with AES-128 + // If the segment was encrypted with AES-128/256 // It must have an IV defined. We cannot substitute the Segment Number in. - if (this.method === 'AES-128' && !this.iv) { - logger.warn( - `missing IV for initialization segment with method="${this.method}" - compliance issue`, - ); - } + logger.warn( + `missing IV for initialization segment with method="${this.method}" - compliance issue`, + ); + // Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation. sn = 0; } diff --git a/src/utils/encryption-methods-util.ts b/src/utils/encryption-methods-util.ts new file mode 100644 index 00000000000..cde88677260 --- /dev/null +++ b/src/utils/encryption-methods-util.ts @@ -0,0 +1,21 @@ +import { DecrypterAesMode } from '../crypt/decrypter-aes-mode'; + +export function isFullSegmentEncryption(method: string): boolean { + return ( + method === 'AES-128' || method === 'AES-256' || method === 'AES-256-CTR' + ); +} + +export function getAesModeFromFullSegmentMethod( + method: string, +): DecrypterAesMode { + switch (method) { + case 'AES-128': + case 'AES-256': + return DecrypterAesMode.cbc; + case 'AES-256-CTR': + return DecrypterAesMode.ctr; + default: + throw new Error(`invalid full segment method ${method}`); + } +} diff --git a/tests/index.js b/tests/index.js index 17e3f85fd80..5738c47bac9 100644 --- a/tests/index.js +++ b/tests/index.js @@ -26,6 +26,7 @@ import './unit/controller/subtitle-track-controller'; import './unit/controller/timeline-controller-nonnative'; import './unit/controller/timeline-controller'; import './unit/crypt/aes-decryptor'; +import './unit/crypt/decrypter'; import './unit/demuxer/adts'; import './unit/demuxer/base-audio-demuxer'; import './unit/demuxer/id3'; diff --git a/tests/test-streams.js b/tests/test-streams.js index ba3bfd54ddc..09794bb5b5e 100644 --- a/tests/test-streams.js +++ b/tests/test-streams.js @@ -262,4 +262,9 @@ module.exports = { NAL units are not starting right at the beginning of the PES packet when using hardware accelerated decoding.`, abr: false, }, + aes256: { + url: 'https://jvaryhlstests.blob.core.windows.net/hlstestdata/playlist_encrypted.m3u8', + description: 'aes-256 and aes-256-ctr full segment encryption', + abr: false, + }, }; diff --git a/tests/unit/crypt/decrypter.js b/tests/unit/crypt/decrypter.js new file mode 100644 index 00000000000..ead7ab53516 --- /dev/null +++ b/tests/unit/crypt/decrypter.js @@ -0,0 +1,123 @@ +import Decrypter from '../../../src/crypt/decrypter'; + +describe('Decrypter', function () { + it('decripts correctly aes-128-cbc software mode', function () { + const data = get128cbcData(); + + const config = { enableSoftwareAES: true }; + const decrypter = new Decrypter(config, { removePKCS7Padding: true }); + const cbcMode = 0; + + decrypter.softwareDecrypt(data.encrypted, data.key, data.iv, cbcMode); + const decrypted = decrypter.flush(); + expect(new Uint8Array(decrypted)).to.deep.equal(data.expected); + }); + + it('decripts correctly aes-128-cbc webCrypto mode', async function () { + const data = get128cbcData(); + + const config = { enableSoftwareAES: false }; + const decrypter = new Decrypter(config); + const cbcMode = 0; + const decrypted = await decrypter.webCryptoDecrypt( + data.encrypted, + data.key, + data.iv, + cbcMode, + ); + expect(new Uint8Array(decrypted)).to.deep.equal(data.expected); + }); + + it('decripts correctly aes-128-cbc', async function () { + const data = get128cbcData(); + + const config = { enableSoftwareAES: true }; + const decrypter = new Decrypter(config); + const cbcMode = 0; + const decrypted = await decrypter.decrypt( + data.encrypted, + data.key, + data.iv, + cbcMode, + ); + expect(new Uint8Array(decrypted)).to.deep.equal(data.expected); + }); + + it('decripts correctly aes-256-cbc', async function () { + const data = get256cbcData(); + + const config = { enableSoftwareAES: false }; + const decrypter = new Decrypter(config); + const cbcMode = 0; + const decrypted = await decrypter.decrypt( + data.encrypted, + data.key, + data.iv, + cbcMode, + ); + expect(new Uint8Array(decrypted)).to.deep.equal(data.expected); + }); + + it('decripts correctly aes-256-ctr', async function () { + const data = get256ctrData(); + + const config = { enableSoftwareAES: false }; + const decrypter = new Decrypter(config); + const ctrMode = 1; + const decrypted = await decrypter.decrypt( + data.encrypted, + data.key, + data.iv, + ctrMode, + ); + expect(new Uint8Array(decrypted)).to.deep.equal(data.expected); + }); +}); + +function get128cbcData() { + const key = new Uint8Array([ + 0xe5, 0xe9, 0xfa, 0x1b, 0xa3, 0x1e, 0xcd, 0x1a, 0xe8, 0x4f, 0x75, 0xca, + 0xaa, 0x47, 0x4f, 0x3a, + ]).buffer; + const iv = new Uint8Array([ + 0x66, 0x3f, 0x05, 0xf4, 0x12, 0x02, 0x8f, 0x81, 0xda, 0x65, 0xd2, 0x6e, + 0xe5, 0x64, 0x24, 0xb2, + ]).buffer; + const encrypted = new Uint8Array([ + 0x2c, 0x94, 0xcf, 0xc0, 0x91, 0xff, 0x0e, 0xcc, 0x98, 0x66, 0xcc, 0x83, + 0x0d, 0xd7, 0xc3, 0x55, + ]); + const expected = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0x0a]); + return { key: key, iv: iv, encrypted: encrypted, expected: expected }; +} + +function get256Data() { + const key = new Uint8Array([ + 0xe5, 0xe9, 0xfa, 0x1b, 0xa3, 0x1e, 0xcd, 0x1a, 0xe8, 0x4f, 0x75, 0xca, + 0xaa, 0x47, 0x4f, 0x3a, 0x66, 0x3f, 0x05, 0xf4, 0x12, 0x02, 0x8f, 0x81, + 0xda, 0x65, 0xd2, 0x6e, 0xe5, 0x64, 0x24, 0xb2, + ]).buffer; + const iv = new Uint8Array([ + 0xf4, 0x8c, 0xef, 0xa0, 0xad, 0x59, 0xc9, 0xa5, 0x60, 0x16, 0xcf, 0xbb, + 0x26, 0x5b, 0xee, 0x8c, + ]).buffer; + const expected = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21, 0x0a]); + return { key: key, iv: iv, expected: expected }; +} + +function get256cbcData() { + const some256data = get256Data(); + const encrypted = new Uint8Array([ + 0xe7, 0x25, 0x6a, 0x77, 0x3a, 0xa5, 0x43, 0x59, 0xaf, 0x60, 0xc1, 0xd3, + 0xed, 0x31, 0xc4, 0x01, + ]); + some256data.encrypted = encrypted; + return some256data; +} + +function get256ctrData() { + const some256data = get256Data(); + const encrypted = new Uint8Array([0xb8, 0xd1, 0xcf, 0x15, 0x0d, 0x34, 0x12]); + some256data.encrypted = encrypted; + return some256data; +} diff --git a/tests/unit/loader/playlist-loader.ts b/tests/unit/loader/playlist-loader.ts index 41ced330e1e..3b9da4564e5 100644 --- a/tests/unit/loader/playlist-loader.ts +++ b/tests/unit/loader/playlist-loader.ts @@ -613,6 +613,57 @@ oceans_aes-audio=65000-video=236000-3.ts ); }); + it('parse AES-256 and AES-256-CTR encrypted URLs, with explicit IV', function () { + const level = `#EXTM3U +#EXT-X-VERSION:1 +## Created with Unified Streaming Platform(version=1.6.7) +#EXT-X-MEDIA-SEQUENCE:1 +#EXT-X-ALLOW-CACHE:NO +#EXT-X-TARGETDURATION:11 +#EXT-X-KEY:METHOD=AES-256,URI="bob1.key256",IV=0x10000000000000000000000000001234 +#EXTINF:11,no desc +bob_1.m4s +#EXT-X-KEY:METHOD=AES-256-CTR,URI="bob2.key256",IV=0x10000000000000000000000000004567 +#EXTINF:11,no desc +bob_2.m4s +#EXT-X-ENDLIST`; + const result = M3U8Parser.parseLevelPlaylist( + level, + 'http://foo.com/stream/bob.m3u8', + 0, + PlaylistLevelType.MAIN, + 0, + null, + ); + + const ivExpected = new Uint8Array(16); + ivExpected[0] = 0x10; + + expect(result.totalduration).to.equal(22); + expect(result.startSN).to.equal(1); + expect(result.targetduration).to.equal(11); + expect(result.fragments).to.have.lengthOf(2); + expect(result.fragments[0].duration).to.equal(11); + expect(result.fragments[0].url).to.equal('http://foo.com/stream/bob_1.m4s'); + expect(result.fragments[0].decryptdata?.uri).to.equal( + 'http://foo.com/stream/bob1.key256', + ); + expect(result.fragments[0].decryptdata?.method).to.equal('AES-256'); + ivExpected[14] = 0x12; + ivExpected[15] = 0x34; + expect(result.fragments[0].decryptdata?.iv).to.deep.equal(ivExpected); + + expect(result.fragments[1].duration).to.equal(11); + expect(result.fragments[1].url).to.equal('http://foo.com/stream/bob_2.m4s'); + expect(result.fragments[1].decryptdata?.uri).to.equal( + 'http://foo.com/stream/bob2.key256', + ); + expect(result.fragments[1].decryptdata?.method).to.equal('AES-256-CTR'); + ivExpected[14] = 0x45; + ivExpected[15] = 0x67; + expect(result.fragments[1].decryptdata?.iv).to.deep.equal(ivExpected); + }); + it('parse level with #EXT-X-BYTERANGE before #EXTINF', function () { const level = `#EXTM3U #EXT-X-VERSION:4 @@ -2036,7 +2087,7 @@ describe('#EXT-X-START', function () { it('parses EXT-X-START in Multivariant Playlists', function () { const manifest = `#EXTM3U #EXT-X-START:TIME-OFFSET=300.0,PRECISE=YES - + #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=836280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=848x360,NAME="480" http://proxy-62.x.com/sec(3ae40f708f79ca9471f52b86da76a3a8)/video/107/282/158282701_mp4_h264_aac_hq.m3u8#cell=core`; @@ -2047,7 +2098,7 @@ describe('#EXT-X-START', function () { it('parses negative EXT-X-START values in Multivariant Playlists', function () { const manifest = `#EXTM3U #EXT-X-START:TIME-OFFSET=-30.0 - + #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=836280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=848x360,NAME="480" http://proxy-62.x.com/sec(3ae40f708f79ca9471f52b86da76a3a8)/video/107/282/158282701_mp4_h264_aac_hq.m3u8#cell=core`; @@ -2057,7 +2108,7 @@ describe('#EXT-X-START', function () { it('result is null when EXT-X-START is not present', function () { const manifest = `#EXTM3U - + #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=836280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=848x360,NAME="480" http://proxy-62.x.com/sec(3ae40f708f79ca9471f52b86da76a3a8)/video/107/282/158282701_mp4_h264_aac_hq.m3u8#cell=core`; @@ -2072,7 +2123,7 @@ describe('#EXT-X-DEFINE', function () { #EXT-X-DEFINE:NAME="x",VALUE="1" #EXT-X-DEFINE:NAME="y",VALUE="2" #EXT-X-DEFINE:NAME="hello-var",VALUE="Hello there!" - + #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=836280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=848x360,NAME="480" http://proxy-62.x.com/sec(3ae40f708f79ca9471f52b86da76a3a8)/video/107/282/158282701_mp4_h264_aac_hq.m3u8#cell=core`; @@ -2091,7 +2142,7 @@ describe('#EXT-X-DEFINE', function () { #EXT-X-DEFINE:NAME="foo",VALUE="ok" #EXT-X-DEFINE:NAME="bar",VALUE="ok" #EXT-X-DEFINE:NAME="foo",VALUE="duped" - + #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=836280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=848x360,NAME="480" http://proxy-62.x.com/sec(3ae40f708f79ca9471f52b86da76a3a8)/video/107/282/158282701_mp4_h264_aac_hq.m3u8#cell=core`;