From 64f13c45868776754400f8249f98a6d713a6d6d5 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Tue, 29 Jan 2019 00:40:02 -0400 Subject: [PATCH 01/12] Update Fragment and LevelKey to typescript. --- src/loader/fragment.js | 150 ----------------------------------- src/loader/fragment.ts | 171 ++++++++++++++++++++++++++++++++++++++++ src/loader/level-key.js | 18 ----- src/loader/level-key.ts | 25 ++++++ 4 files changed, 196 insertions(+), 168 deletions(-) delete mode 100644 src/loader/fragment.js create mode 100644 src/loader/fragment.ts delete mode 100644 src/loader/level-key.js create mode 100644 src/loader/level-key.ts diff --git a/src/loader/fragment.js b/src/loader/fragment.js deleted file mode 100644 index ef898c50236..00000000000 --- a/src/loader/fragment.js +++ /dev/null @@ -1,150 +0,0 @@ - -import * as URLToolkit from 'url-toolkit'; - -import LevelKey from './level-key'; - -export default class Fragment { - constructor () { - this._url = null; - this._byteRange = null; - this._decryptdata = null; - this.tagList = []; - this.programDateTime = null; - this.rawProgramDateTime = null; - - // Holds the types of data this fragment supports - this._elementaryStreams = { - [Fragment.ElementaryStreamTypes.AUDIO]: false, - [Fragment.ElementaryStreamTypes.VIDEO]: false - }; - } - - /** - * `type` property for this._elementaryStreams - * - * @enum - */ - static get ElementaryStreamTypes () { - return { - AUDIO: 'audio', - VIDEO: 'video' - }; - } - - get url () { - if (!this._url && this.relurl) { - this._url = URLToolkit.buildAbsoluteURL(this.baseurl, this.relurl, { alwaysNormalize: true }); - } - - return this._url; - } - - set url (value) { - this._url = value; - } - - get byteRange () { - if (!this._byteRange && !this.rawByteRange) { - return []; - } - - if (this._byteRange) { - return this._byteRange; - } - - let byteRange = []; - if (this.rawByteRange) { - const params = this.rawByteRange.split('@', 2); - if (params.length === 1) { - const lastByteRangeEndOffset = this.lastByteRangeEndOffset; - byteRange[0] = lastByteRangeEndOffset || 0; - } else { - byteRange[0] = parseInt(params[1]); - } - byteRange[1] = parseInt(params[0]) + byteRange[0]; - this._byteRange = byteRange; - } - return byteRange; - } - - /** - * @type {number} - */ - get byteRangeStartOffset () { - return this.byteRange[0]; - } - - get byteRangeEndOffset () { - return this.byteRange[1]; - } - - get decryptdata () { - if (!this._decryptdata) { - this._decryptdata = this.fragmentDecryptdataFromLevelkey(this.levelkey, this.sn); - } - - return this._decryptdata; - } - - get endProgramDateTime () { - if (!Number.isFinite(this.programDateTime)) { - return null; - } - - let duration = !Number.isFinite(this.duration) ? 0 : this.duration; - - return this.programDateTime + (duration * 1000); - } - - get encrypted () { - return !!((this.decryptdata && this.decryptdata.uri !== null) && (this.decryptdata.key === null)); - } - - /** - * @param {ElementaryStreamType} type - */ - addElementaryStream (type) { - this._elementaryStreams[type] = true; - } - - /** - * @param {ElementaryStreamType} type - */ - hasElementaryStream (type) { - return this._elementaryStreams[type] === true; - } - - /** - * Utility method for parseLevelPlaylist to create an initialization vector for a given segment - * @returns {Uint8Array} - */ - createInitializationVector (segmentNumber) { - let uint8View = new Uint8Array(16); - - for (let i = 12; i < 16; i++) { - uint8View[i] = (segmentNumber >> 8 * (15 - i)) & 0xff; - } - - return uint8View; - } - - /** - * Utility method for parseLevelPlaylist to get a fragment's decryption data from the currently parsed encryption key data - * @param levelkey - a playlist's encryption info - * @param segmentNumber - the fragment's segment number - * @returns {*} - an object to be applied as a fragment's decryptdata - */ - fragmentDecryptdataFromLevelkey (levelkey, segmentNumber) { - let decryptdata = levelkey; - - if (levelkey && levelkey.method && levelkey.uri && !levelkey.iv) { - decryptdata = new LevelKey(); - decryptdata.method = levelkey.method; - decryptdata.baseuri = levelkey.baseuri; - decryptdata.reluri = levelkey.reluri; - decryptdata.iv = this.createInitializationVector(segmentNumber); - } - - return decryptdata; - } -} diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts new file mode 100644 index 00000000000..208a23013e1 --- /dev/null +++ b/src/loader/fragment.ts @@ -0,0 +1,171 @@ + +import { buildAbsoluteURL } from 'url-toolkit'; +import { logger } from '../utils/logger'; +import LevelKey from './level-key'; + +export enum ElementaryStreamTypes { + AUDIO = 'audio', + VIDEO = 'video', +} + +export default class Fragment { + private _url: string | null = null; + private _byteRange: number[] | null = null; + private _decryptdata: LevelKey | null = null; + + // Holds the types of data this fragment supports + private _elementaryStreams: Record = { + [ElementaryStreamTypes.AUDIO]: false, + [ElementaryStreamTypes.VIDEO]: false + }; + + public rawProgramDateTime: string | null = null; + public programDateTime: number | null = null; + + public tagList: Array; + + // TODO: Move at least baseurl to constructor. + // Currently we do a two-pass construction as use the Fragment class almost like a object for holding parsing state. + // It may make more sense to just use a POJO to keep state during the parsing phase. + // Have Fragment be the representation once we have a known state? + // Something to think on. + + // relurl is the portion of the URL that comes from inside the playlist. + public relurl!: string; + // baseurl is the URL to the playlist + public baseurl!: string; + // EXTINF has to be present for a m3u8 to be considered valid + public duration!: number; + // sn notates the sequence number for a segment, and if set to a string can be 'initSegment' + public sn: number | string = 0; + public levelkey?: LevelKey; + + constructor () { + this.tagList = []; + } + + // parseByteRange converts a EXT-X-BYTERANGE attribute into a two element array + parseByteRange (value: string, previousFrag?: Fragment): number[] { + const params = value.split('@', 2); + const byteRange: number[] = []; + if (params.length === 1) { + byteRange[0] = previousFrag ? previousFrag.byteRangeEndOffset : 0; + } else { + byteRange[0] = parseInt(params[1]); + } + byteRange[1] = parseInt(params[0]) + byteRange[0]; + this._byteRange = byteRange; + return this._byteRange; + } + + get url () { + if (!this._url && this.relurl) { + this._url = buildAbsoluteURL(this.baseurl, this.relurl, { alwaysNormalize: true }); + } + + return this._url; + } + + set url (value) { + this._url = value; + } + + get byteRange (): number[] { + if (!this._byteRange) { + return []; + } + + return this._byteRange; + } + + /** + * @type {number} + */ + get byteRangeStartOffset () { + return this.byteRange[0]; + } + + get byteRangeEndOffset () { + return this.byteRange[1]; + } + + get decryptdata (): LevelKey | null { + if (!this.levelkey && !this._decryptdata) { + return null; + } + + if (!this._decryptdata && this.levelkey) { + // TODO look for this warning for 'initSegment' sn getting used in decryption IV + if (typeof this.sn !== 'number') { + logger.warn(`undefined behaviour for sn="${this.sn}"`); + } + this._decryptdata = this.fragmentDecryptdataFromLevelkey(this.levelkey, this.sn as number); + } + + return this._decryptdata; + } + + get endProgramDateTime () { + if (!this.programDateTime) { + return null; + } + + if (!Number.isFinite(this.programDateTime)) { + return null; + } + + let duration = !Number.isFinite(this.duration) ? 0 : this.duration; + + return this.programDateTime + (duration * 1000); + } + + get encrypted () { + return !!((this.decryptdata && this.decryptdata.uri !== null) && (this.decryptdata.key === null)); + } + + /** + * @param {ElementaryStreamTypes} type + */ + addElementaryStream (type: ElementaryStreamTypes) { + this._elementaryStreams[type] = true; + } + + /** + * @param {ElementaryStreamTypes} type + */ + hasElementaryStream (type: ElementaryStreamTypes) { + return this._elementaryStreams[type] === true; + } + + /** + * Utility method for parseLevelPlaylist to create an initialization vector for a given segment + * @returns {Uint8Array} + */ + createInitializationVector (segmentNumber): Uint8Array { + let uint8View = new Uint8Array(16); + + for (let i = 12; i < 16; i++) { + uint8View[i] = (segmentNumber >> 8 * (15 - i)) & 0xff; + } + + return uint8View; + } + + /** + * Utility method for parseLevelPlaylist to get a fragment's decryption data from the currently parsed encryption key data + * @param levelkey - a playlist's encryption info + * @param segmentNumber - the fragment's segment number + * @returns {*} - an object to be applied as a fragment's decryptdata + */ + fragmentDecryptdataFromLevelkey (levelkey: LevelKey, segmentNumber: number): LevelKey { + let decryptdata = levelkey; + + if (levelkey && levelkey.method && levelkey.uri && !levelkey.iv) { + decryptdata = new LevelKey(levelkey.baseuri, levelkey.reluri); + decryptdata.method = levelkey.method; + decryptdata.iv = this.createInitializationVector(segmentNumber); + } + + return decryptdata; + } +} diff --git a/src/loader/level-key.js b/src/loader/level-key.js deleted file mode 100644 index 94238f627f1..00000000000 --- a/src/loader/level-key.js +++ /dev/null @@ -1,18 +0,0 @@ -import * as URLToolkit from 'url-toolkit'; - -export default class LevelKey { - constructor () { - this.method = null; - this.key = null; - this.iv = null; - this._uri = null; - } - - get uri () { - if (!this._uri && this.reluri) { - this._uri = URLToolkit.buildAbsoluteURL(this.baseuri, this.reluri, { alwaysNormalize: true }); - } - - return this._uri; - } -} diff --git a/src/loader/level-key.ts b/src/loader/level-key.ts new file mode 100644 index 00000000000..de29f98b30e --- /dev/null +++ b/src/loader/level-key.ts @@ -0,0 +1,25 @@ +import { buildAbsoluteURL } from 'url-toolkit'; + +export default class LevelKey { + private _uri: string | null = null; + + public baseuri: string; + public reluri: string; + + method: string | null = null; + key: Uint8Array | null = null; + iv: Uint8Array | null = null; + + constructor (baseURI: string, relativeURI: string) { + this.baseuri = baseURI; + this.reluri = relativeURI; + } + + get uri () { + if (!this._uri && this.reluri) { + this._uri = buildAbsoluteURL(this.baseuri, this.reluri, { alwaysNormalize: true }); + } + + return this._uri; + } +} From af437da8460bb4b0c4ba0550926d9cf0dff8cb83 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Tue, 29 Jan 2019 00:40:38 -0400 Subject: [PATCH 02/12] Update usage of Fragment and LevelKey. --- src/controller/audio-stream-controller.js | 4 ++-- src/controller/stream-controller.js | 8 ++++---- src/loader/fragment.ts | 2 +- src/loader/m3u8-parser.js | 11 +++++------ src/loader/playlist-loader.js | 4 ++-- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/controller/audio-stream-controller.js b/src/controller/audio-stream-controller.js index 0a13ce03e6b..f7489cb3561 100644 --- a/src/controller/audio-stream-controller.js +++ b/src/controller/audio-stream-controller.js @@ -12,7 +12,7 @@ import { ErrorTypes, ErrorDetails } from '../errors'; import { logger } from '../utils/logger'; import { findFragWithCC } from '../utils/discontinuities'; import { FragmentState } from './fragment-tracker'; -import Fragment from '../loader/fragment'; +import Fragment, { ElementaryStreamTypes } from '../loader/fragment'; import BaseStreamController, { State } from './base-stream-controller'; const { performance } = window; @@ -615,7 +615,7 @@ class AudioStreamController extends BaseStreamController { data.endDTS = data.startDTS + fragCurrent.duration; } - fragCurrent.addElementaryStream(Fragment.ElementaryStreamTypes.AUDIO); + fragCurrent.addElementaryStream(ElementaryStreamTypes.AUDIO); logger.log(`parsed ${data.type},PTS:[${data.startPTS.toFixed(3)},${data.endPTS.toFixed(3)}],DTS:[${data.startDTS.toFixed(3)}/${data.endDTS.toFixed(3)}],nb:${data.nb}`); LevelHelper.updateFragPTSDTS(track.details, fragCurrent, data.startPTS, data.endPTS); diff --git a/src/controller/stream-controller.js b/src/controller/stream-controller.js index 3624ae25fbb..5975475c914 100644 --- a/src/controller/stream-controller.js +++ b/src/controller/stream-controller.js @@ -7,7 +7,7 @@ import { BufferHelper } from '../utils/buffer-helper'; import Demuxer from '../demux/demuxer'; import Event from '../events'; import { FragmentState } from './fragment-tracker'; -import Fragment from '../loader/fragment'; +import Fragment, { ElementaryStreamTypes } from '../loader/fragment'; import PlaylistLoader from '../loader/playlist-loader'; import * as LevelHelper from './level-helper'; import TimeRanges from '../utils/time-ranges'; @@ -1000,11 +1000,11 @@ class StreamController extends BaseStreamController { } if (data.hasAudio === true) { - frag.addElementaryStream(Fragment.ElementaryStreamTypes.AUDIO); + frag.addElementaryStream(ElementaryStreamTypes.AUDIO); } if (data.hasVideo === true) { - frag.addElementaryStream(Fragment.ElementaryStreamTypes.VIDEO); + frag.addElementaryStream(ElementaryStreamTypes.VIDEO); } logger.log(`Parsed ${data.type},PTS:[${data.startPTS.toFixed(3)},${data.endPTS.toFixed(3)}],DTS:[${data.startDTS.toFixed(3)}/${data.endDTS.toFixed(3)}],nb:${data.nb},dropped:${data.dropped || 0}`); @@ -1305,7 +1305,7 @@ class StreamController extends BaseStreamController { const media = this.mediaBuffer ? this.mediaBuffer : this.media; if (media) { // filter fragments potentially evicted from buffer. this is to avoid memleak on live streams - this.fragmentTracker.detectEvictedFragments(Fragment.ElementaryStreamTypes.VIDEO, media.buffered); + this.fragmentTracker.detectEvictedFragments(ElementaryStreamTypes.VIDEO, media.buffered); } // move to IDLE once flush complete. this should trigger new fragment loading this.state = State.IDLE; diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts index 208a23013e1..604d6691d85 100644 --- a/src/loader/fragment.ts +++ b/src/loader/fragment.ts @@ -106,7 +106,7 @@ export default class Fragment { } get endProgramDateTime () { - if (!this.programDateTime) { + if (this.programDateTime === null) { return null; } diff --git a/src/loader/m3u8-parser.js b/src/loader/m3u8-parser.js index f499a9edafd..e99239dafa9 100644 --- a/src/loader/m3u8-parser.js +++ b/src/loader/m3u8-parser.js @@ -189,12 +189,11 @@ export default class M3U8Parser { frag = new Fragment(); } } else if (result[4]) { // X-BYTERANGE - frag.rawByteRange = (' ' + result[4]).slice(1); + const data = (' ' + result[4]).slice(1); if (prevFrag) { - const lastByteRangeEndOffset = prevFrag.byteRangeEndOffset; - if (lastByteRangeEndOffset) { - frag.lastByteRangeEndOffset = lastByteRangeEndOffset; - } + frag.parseByteRange(data, prevFrag); + } else { + frag.parseByteRange(data); } } else if (result[5]) { // PROGRAM-DATE-TIME // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 @@ -276,7 +275,7 @@ export default class M3U8Parser { case 'MAP': let mapAttrs = new AttrList(value1); frag.relurl = mapAttrs.URI; - frag.rawByteRange = mapAttrs.BYTERANGE; + frag.parseByteRange(mapAttrs.BYTERANGE, prevFrag); frag.baseurl = baseurl; frag.level = id; frag.type = type; diff --git a/src/loader/playlist-loader.js b/src/loader/playlist-loader.js index 03ac882720d..ac7ac0f6802 100644 --- a/src/loader/playlist-loader.js +++ b/src/loader/playlist-loader.js @@ -407,10 +407,10 @@ class PlaylistLoader extends EventHandler { const frag = levelDetails.fragments[index]; if (frag.byteRange.length === 0) { - frag.rawByteRange = String(1 + segRefInfo.end - segRefInfo.start) + '@' + String(segRefInfo.start); + frag.parseByteRange(String(1 + segRefInfo.end - segRefInfo.start) + '@' + String(segRefInfo.start)); } }); - levelDetails.initSegment.rawByteRange = String(sidxInfo.moovEndOffset) + '@0'; + levelDetails.initSegment.parseByteRange(String(sidxInfo.moovEndOffset) + '@0'); } _handleManifestParsingError (response, context, reason, networkDetails) { From 40e81b854389c23a77fbc025d88e9ef82e5267ee Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Tue, 29 Jan 2019 01:12:53 -0400 Subject: [PATCH 03/12] Don't pass prevFrag into EXT-X-MAP handling. --- src/loader/m3u8-parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loader/m3u8-parser.js b/src/loader/m3u8-parser.js index e99239dafa9..21903445cfd 100644 --- a/src/loader/m3u8-parser.js +++ b/src/loader/m3u8-parser.js @@ -275,7 +275,7 @@ export default class M3U8Parser { case 'MAP': let mapAttrs = new AttrList(value1); frag.relurl = mapAttrs.URI; - frag.parseByteRange(mapAttrs.BYTERANGE, prevFrag); + frag.parseByteRange(mapAttrs.BYTERANGE); frag.baseurl = baseurl; frag.level = id; frag.type = type; From 2a80ff24006cb55bb5e815a90487166919a1b524 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Tue, 29 Jan 2019 01:26:27 -0400 Subject: [PATCH 04/12] Add tests for parsing byte range as seperate call. --- tests/unit/loader/fragment.js | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/unit/loader/fragment.js b/tests/unit/loader/fragment.js index c172c264f76..d9b1b75b9ab 100644 --- a/tests/unit/loader/fragment.js +++ b/tests/unit/loader/fragment.js @@ -1,12 +1,37 @@ import Fragment from '../../../src/loader/fragment'; describe('Fragment class tests', function () { + /** + * @type {Fragment} + */ let frag; - describe('endProgramDateTime getter', function () { - beforeEach(function () { - frag = new Fragment(); + beforeEach(function () { + frag = new Fragment(); + }); + + describe('parseByteRange', function () { + it('parses byte range string with length@offset', function () { + frag.parseByteRange('1000@10000'); + expect(frag.byteRangeStartOffset).to.equal(10000); + expect(frag.byteRangeEndOffset).to.equal(11000); + }); + + it('parses byte range with no offset and uses 0 as offset', function () { + frag.parseByteRange('5000'); + expect(frag.byteRangeStartOffset).to.equal(0); + expect(frag.byteRangeEndOffset).to.equal(5000); }); + it('parses byte range with no offset and uses 0 as offset', function () { + const prevFrag = new Fragment(); + prevFrag.parseByteRange('1000@10000'); + frag.parseByteRange('5000', prevFrag); + expect(frag.byteRangeStartOffset).to.equal(11000); + expect(frag.byteRangeEndOffset).to.equal(16000); + }); + }); + + describe('endProgramDateTime getter', function () { it('computes endPdt when pdt and duration are valid', function () { frag.programDateTime = 1000; frag.duration = 1; From b37a351c3f52689d2bd38a680fa88f684054758e Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Tue, 29 Jan 2019 16:35:53 -0400 Subject: [PATCH 05/12] Only parse byte range if it exists. --- src/loader/m3u8-parser.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/loader/m3u8-parser.js b/src/loader/m3u8-parser.js index 21903445cfd..4ede889fd34 100644 --- a/src/loader/m3u8-parser.js +++ b/src/loader/m3u8-parser.js @@ -275,7 +275,9 @@ export default class M3U8Parser { case 'MAP': let mapAttrs = new AttrList(value1); frag.relurl = mapAttrs.URI; - frag.parseByteRange(mapAttrs.BYTERANGE); + if (mapAttrs.BYTERANGE) { + frag.parseByteRange(mapAttrs.BYTERANGE); + } frag.baseurl = baseurl; frag.level = id; frag.type = type; From a742c301f1000708b1740fd88437a36254a8fbe0 Mon Sep 17 00:00:00 2001 From: Tom Jenkinson Date: Tue, 29 Jan 2019 17:31:31 -0400 Subject: [PATCH 06/12] Apply suggestions from code review Co-Authored-By: itsjamie <1956521+itsjamie@users.noreply.github.com> --- src/loader/fragment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts index 604d6691d85..e10b8fb4eda 100644 --- a/src/loader/fragment.ts +++ b/src/loader/fragment.ts @@ -37,7 +37,7 @@ export default class Fragment { // EXTINF has to be present for a m3u8 to be considered valid public duration!: number; // sn notates the sequence number for a segment, and if set to a string can be 'initSegment' - public sn: number | string = 0; + public sn: number | 'initSegment' = 0; public levelkey?: LevelKey; constructor () { From 2e2cb12e0e1f015c4913e85f64c4b83fb4316006 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Tue, 29 Jan 2019 17:36:50 -0400 Subject: [PATCH 07/12] Review changes. parseByteRange -> setByteRange - removed it returning the parsed value since its usage was like a setter. --- src/loader/fragment.ts | 5 ++--- src/loader/level-key.ts | 7 +++---- src/loader/m3u8-parser.js | 6 +++--- src/loader/playlist-loader.js | 4 ++-- tests/unit/loader/fragment.js | 8 ++++---- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts index e10b8fb4eda..5139cf43c44 100644 --- a/src/loader/fragment.ts +++ b/src/loader/fragment.ts @@ -44,8 +44,8 @@ export default class Fragment { this.tagList = []; } - // parseByteRange converts a EXT-X-BYTERANGE attribute into a two element array - parseByteRange (value: string, previousFrag?: Fragment): number[] { + // setByteRange converts a EXT-X-BYTERANGE attribute into a two element array + setByteRange (value: string, previousFrag?: Fragment) { const params = value.split('@', 2); const byteRange: number[] = []; if (params.length === 1) { @@ -55,7 +55,6 @@ export default class Fragment { } byteRange[1] = parseInt(params[0]) + byteRange[0]; this._byteRange = byteRange; - return this._byteRange; } get url () { diff --git a/src/loader/level-key.ts b/src/loader/level-key.ts index de29f98b30e..f5abc95e79e 100644 --- a/src/loader/level-key.ts +++ b/src/loader/level-key.ts @@ -5,10 +5,9 @@ export default class LevelKey { public baseuri: string; public reluri: string; - - method: string | null = null; - key: Uint8Array | null = null; - iv: Uint8Array | null = null; + public method: string | null = null; + public key: Uint8Array | null = null; + public iv: Uint8Array | null = null; constructor (baseURI: string, relativeURI: string) { this.baseuri = baseURI; diff --git a/src/loader/m3u8-parser.js b/src/loader/m3u8-parser.js index 4ede889fd34..943c84a6856 100644 --- a/src/loader/m3u8-parser.js +++ b/src/loader/m3u8-parser.js @@ -191,9 +191,9 @@ export default class M3U8Parser { } else if (result[4]) { // X-BYTERANGE const data = (' ' + result[4]).slice(1); if (prevFrag) { - frag.parseByteRange(data, prevFrag); + frag.setByteRange(data, prevFrag); } else { - frag.parseByteRange(data); + frag.setByteRange(data); } } else if (result[5]) { // PROGRAM-DATE-TIME // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 @@ -276,7 +276,7 @@ export default class M3U8Parser { let mapAttrs = new AttrList(value1); frag.relurl = mapAttrs.URI; if (mapAttrs.BYTERANGE) { - frag.parseByteRange(mapAttrs.BYTERANGE); + frag.setByteRange(mapAttrs.BYTERANGE); } frag.baseurl = baseurl; frag.level = id; diff --git a/src/loader/playlist-loader.js b/src/loader/playlist-loader.js index ac7ac0f6802..7bcc111d005 100644 --- a/src/loader/playlist-loader.js +++ b/src/loader/playlist-loader.js @@ -407,10 +407,10 @@ class PlaylistLoader extends EventHandler { const frag = levelDetails.fragments[index]; if (frag.byteRange.length === 0) { - frag.parseByteRange(String(1 + segRefInfo.end - segRefInfo.start) + '@' + String(segRefInfo.start)); + frag.setByteRange(String(1 + segRefInfo.end - segRefInfo.start) + '@' + String(segRefInfo.start)); } }); - levelDetails.initSegment.parseByteRange(String(sidxInfo.moovEndOffset) + '@0'); + levelDetails.initSegment.setByteRange(String(sidxInfo.moovEndOffset) + '@0'); } _handleManifestParsingError (response, context, reason, networkDetails) { diff --git a/tests/unit/loader/fragment.js b/tests/unit/loader/fragment.js index d9b1b75b9ab..36549505cc9 100644 --- a/tests/unit/loader/fragment.js +++ b/tests/unit/loader/fragment.js @@ -9,20 +9,20 @@ describe('Fragment class tests', function () { frag = new Fragment(); }); - describe('parseByteRange', function () { - it('parses byte range string with length@offset', function () { + describe('setByteRange', function () { + it('set byte range with length@offset', function () { frag.parseByteRange('1000@10000'); expect(frag.byteRangeStartOffset).to.equal(10000); expect(frag.byteRangeEndOffset).to.equal(11000); }); - it('parses byte range with no offset and uses 0 as offset', function () { + it('set byte range with no offset and uses 0 as offset', function () { frag.parseByteRange('5000'); expect(frag.byteRangeStartOffset).to.equal(0); expect(frag.byteRangeEndOffset).to.equal(5000); }); - it('parses byte range with no offset and uses 0 as offset', function () { + it('set byte range with no offset and uses 0 as offset', function () { const prevFrag = new Fragment(); prevFrag.parseByteRange('1000@10000'); frag.parseByteRange('5000', prevFrag); From c8b00488703a3894e7f60f1ba8aa15318eee9c20 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Tue, 29 Jan 2019 17:43:34 -0400 Subject: [PATCH 08/12] Review comments pt.2 --- src/loader/fragment.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts index 5139cf43c44..e03539467b8 100644 --- a/src/loader/fragment.ts +++ b/src/loader/fragment.ts @@ -21,8 +21,7 @@ export default class Fragment { public rawProgramDateTime: string | null = null; public programDateTime: number | null = null; - - public tagList: Array; + public tagList: Array = []; // TODO: Move at least baseurl to constructor. // Currently we do a two-pass construction as use the Fragment class almost like a object for holding parsing state. @@ -38,12 +37,11 @@ export default class Fragment { public duration!: number; // sn notates the sequence number for a segment, and if set to a string can be 'initSegment' public sn: number | 'initSegment' = 0; + // levelkey is the EXT-X-KEY that applies to this segment for decryption + // core difference from the private field _decryptdata is the lack of the initialized IV + // _decryptdata will set the IV for this segment based on the segment number in the fragment public levelkey?: LevelKey; - constructor () { - this.tagList = []; - } - // setByteRange converts a EXT-X-BYTERANGE attribute into a two element array setByteRange (value: string, previousFrag?: Fragment) { const params = value.split('@', 2); @@ -96,7 +94,7 @@ export default class Fragment { if (!this._decryptdata && this.levelkey) { // TODO look for this warning for 'initSegment' sn getting used in decryption IV if (typeof this.sn !== 'number') { - logger.warn(`undefined behaviour for sn="${this.sn}"`); + logger.warn(`undefined behaviour for sn="${this.sn}" in IV generation`); } this._decryptdata = this.fragmentDecryptdataFromLevelkey(this.levelkey, this.sn as number); } @@ -140,7 +138,7 @@ export default class Fragment { * Utility method for parseLevelPlaylist to create an initialization vector for a given segment * @returns {Uint8Array} */ - createInitializationVector (segmentNumber): Uint8Array { + createInitializationVector (segmentNumber: number): Uint8Array { let uint8View = new Uint8Array(16); for (let i = 12; i < 16; i++) { From 150d1e62b38e065109f5db3f45057686020a70e4 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Tue, 29 Jan 2019 17:49:08 -0400 Subject: [PATCH 09/12] Change tests to call setByteRange --- tests/unit/loader/fragment.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/loader/fragment.js b/tests/unit/loader/fragment.js index 36549505cc9..41c359aa9c6 100644 --- a/tests/unit/loader/fragment.js +++ b/tests/unit/loader/fragment.js @@ -11,21 +11,21 @@ describe('Fragment class tests', function () { describe('setByteRange', function () { it('set byte range with length@offset', function () { - frag.parseByteRange('1000@10000'); + frag.setByteRange('1000@10000'); expect(frag.byteRangeStartOffset).to.equal(10000); expect(frag.byteRangeEndOffset).to.equal(11000); }); it('set byte range with no offset and uses 0 as offset', function () { - frag.parseByteRange('5000'); + frag.setByteRange('5000'); expect(frag.byteRangeStartOffset).to.equal(0); expect(frag.byteRangeEndOffset).to.equal(5000); }); it('set byte range with no offset and uses 0 as offset', function () { const prevFrag = new Fragment(); - prevFrag.parseByteRange('1000@10000'); - frag.parseByteRange('5000', prevFrag); + prevFrag.setByteRange('1000@10000'); + frag.setByteRange('5000', prevFrag); expect(frag.byteRangeStartOffset).to.equal(11000); expect(frag.byteRangeEndOffset).to.equal(16000); }); From 9329b554c04216f4d8c9021d13d9506959b4a8c7 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Tue, 29 Jan 2019 23:09:51 -0400 Subject: [PATCH 10/12] Explicit handling of 'initSegment' conversion for bitwise operation. --- src/loader/fragment.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts index e03539467b8..ea16d8b8004 100644 --- a/src/loader/fragment.ts +++ b/src/loader/fragment.ts @@ -92,11 +92,18 @@ export default class Fragment { } if (!this._decryptdata && this.levelkey) { - // TODO look for this warning for 'initSegment' sn getting used in decryption IV - if (typeof this.sn !== 'number') { - logger.warn(`undefined behaviour for sn="${this.sn}" in IV generation`); + let sn = this.sn; + if (typeof sn !== 'number') { + /* + Be converted to a Number. + 'initSegment' will become NaN. + NaN, which when converted through ToInt32() -> +0. + --- + Explicitly set sn to expected value for 'initSegment' values for IV generation. + */ + sn = 0; } - this._decryptdata = this.fragmentDecryptdataFromLevelkey(this.levelkey, this.sn as number); + this._decryptdata = this.fragmentDecryptdataFromLevelkey(this.levelkey, sn); } return this._decryptdata; From 3902e1f9e48bb4646a8ce949e8f075ecef7322e7 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Wed, 30 Jan 2019 19:22:37 -0400 Subject: [PATCH 11/12] Add handling for AES-128 encrypted initialization segments needing IV. --- src/loader/fragment.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts index ea16d8b8004..71dd15fcb8e 100644 --- a/src/loader/fragment.ts +++ b/src/loader/fragment.ts @@ -94,12 +94,21 @@ export default class Fragment { if (!this._decryptdata && this.levelkey) { let sn = this.sn; if (typeof sn !== 'number') { + // We are fetching decryption data for a initialization segment + // If the segment was encrypted with AES-128 + // It must have an IV defined. We cannot substitute the Segment Number in. + if (this.levelkey && this.levelkey.method === 'AES-128') { + if (!this.levelkey.iv) { + throw new Error(`missing IV for initialization segment with method="${this.levelkey.method}"`); + } + } + /* Be converted to a Number. 'initSegment' will become NaN. NaN, which when converted through ToInt32() -> +0. --- - Explicitly set sn to expected value for 'initSegment' values for IV generation. + Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation. */ sn = 0; } From a31f1be6ac1bda989acd705d1b0235bbf29e0008 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Sat, 2 Feb 2019 08:39:08 -0400 Subject: [PATCH 12/12] Change IV handling for initSegments to just log a warning. --- src/loader/fragment.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts index 71dd15fcb8e..f32dc2b09b5 100644 --- a/src/loader/fragment.ts +++ b/src/loader/fragment.ts @@ -97,10 +97,8 @@ export default class Fragment { // We are fetching decryption data for a initialization segment // If the segment was encrypted with AES-128 // It must have an IV defined. We cannot substitute the Segment Number in. - if (this.levelkey && this.levelkey.method === 'AES-128') { - if (!this.levelkey.iv) { - throw new Error(`missing IV for initialization segment with method="${this.levelkey.method}"`); - } + if (this.levelkey && this.levelkey.method === 'AES-128' && !this.levelkey.iv) { + logger.warn(`missing IV for initialization segment with method="${this.levelkey.method}" - compliance issue`); } /* @@ -112,7 +110,7 @@ export default class Fragment { */ sn = 0; } - this._decryptdata = this.fragmentDecryptdataFromLevelkey(this.levelkey, sn); + this._decryptdata = this.setDecryptDataFromLevelKey(this.levelkey, sn); } return this._decryptdata; @@ -152,6 +150,7 @@ export default class Fragment { /** * Utility method for parseLevelPlaylist to create an initialization vector for a given segment + * @param {number} segmentNumber - segment number to generate IV with * @returns {Uint8Array} */ createInitializationVector (segmentNumber: number): Uint8Array { @@ -168,9 +167,9 @@ export default class Fragment { * Utility method for parseLevelPlaylist to get a fragment's decryption data from the currently parsed encryption key data * @param levelkey - a playlist's encryption info * @param segmentNumber - the fragment's segment number - * @returns {*} - an object to be applied as a fragment's decryptdata + * @returns {LevelKey} - an object to be applied as a fragment's decryptdata */ - fragmentDecryptdataFromLevelkey (levelkey: LevelKey, segmentNumber: number): LevelKey { + setDecryptDataFromLevelKey (levelkey: LevelKey, segmentNumber: number): LevelKey { let decryptdata = levelkey; if (levelkey && levelkey.method && levelkey.uri && !levelkey.iv) {