From 72da994469518443495b1d57b3057909b6912895 Mon Sep 17 00:00:00 2001 From: Harisha Rajam Swaminathan <35213866+harisha-swaminathan@users.noreply.github.com> Date: Mon, 24 Jul 2023 18:09:50 -0400 Subject: [PATCH 1/6] chore: update v7.0.0 documentation (#172) --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 390f762..280d6c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,15 @@ * Add PDT to each segment ([#168](https://github.com/videojs/m3u8-parser/issues/168)) ([e7c683f](https://github.com/videojs/m3u8-parser/commit/e7c683f)) * output segment title from EXTINF ([#158](https://github.com/videojs/m3u8-parser/issues/158)) ([4adaa2c](https://github.com/videojs/m3u8-parser/commit/4adaa2c)) -### Bug Fixes - -* rename daterange to dateRanges ([#166](https://github.com/videojs/m3u8-parser/issues/166)) ([516ab67](https://github.com/videojs/m3u8-parser/commit/516ab67)) - ### Documentation * correct `customType` option name ([#147](https://github.com/videojs/m3u8-parser/issues/147)) ([4d3e6ce](https://github.com/videojs/m3u8-parser/commit/4d3e6ce)) +### BREAKING CHANGES + +* rename `daterange` to `dateRanges` +* remove `dateTimeObject` and `dateTimeString` from parsed segment and replaces it with `programDateTime` which represents the timestamp in milliseconds + # [6.2.0](https://github.com/videojs/m3u8-parser/compare/v6.1.0...v6.2.0) (2023-05-25) From 6944bb1b2fa2611b5acc318322f820d35eb9b760 Mon Sep 17 00:00:00 2001 From: Harisha Rajam Swaminathan <35213866+harisha-swaminathan@users.noreply.github.com> Date: Mon, 7 Aug 2023 11:57:27 -0400 Subject: [PATCH 2/6] fix: add dateTimeObject and dateTimeString for backward compatibility (#174) --- README.md | 2 ++ src/parse-stream.js | 1 + src/parser.js | 11 +++++++++++ test/fixtures/integration/dateTime.js | 6 ++++++ test/fixtures/integration/llhls.js | 6 ++++++ test/fixtures/integration/llhlsDelta.js | 4 ++++ test/parse-stream.test.js | 8 ++++++++ 7 files changed, 38 insertions(+) diff --git a/README.md b/README.md index 2dc6550..98b661a 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,8 @@ Manifest { 'CLOSED-CAPTIONS': {}, SUBTITLES: {} }, + dateTimeString: string, + dateTimeObject: Date, targetDuration: number, totalDuration: number, discontinuityStarts: [number], diff --git a/src/parse-stream.js b/src/parse-stream.js index 7d8cf72..2b6a691 100644 --- a/src/parse-stream.js +++ b/src/parse-stream.js @@ -356,6 +356,7 @@ export default class ParseStream extends Stream { }; if (match[1]) { event.dateTimeString = match[1]; + event.dateTimeObject = new Date(match[1]); } this.trigger('data', event); return; diff --git a/src/parser.js b/src/parser.js index 5f32e55..157603f 100644 --- a/src/parser.js +++ b/src/parser.js @@ -459,6 +459,17 @@ export default class Parser extends Stream { this.manifest.discontinuityStarts.push(uris.length); }, 'program-date-time'() { + if (typeof this.manifest.dateTimeString === 'undefined') { + // PROGRAM-DATE-TIME is a media-segment tag, but for backwards + // compatibility, we add the first occurence of the PROGRAM-DATE-TIME tag + // to the manifest object + // TODO: Consider removing this in future major version + this.manifest.dateTimeString = entry.dateTimeString; + this.manifest.dateTimeObject = entry.dateTimeObject; + } + currentUri.dateTimeString = entry.dateTimeString; + currentUri.dateTimeObject = entry.dateTimeObject; + const { lastProgramDateTime } = this; this.lastProgramDateTime = new Date(entry.dateTimeString).getTime(); diff --git a/test/fixtures/integration/dateTime.js b/test/fixtures/integration/dateTime.js index 3e58f3d..a2e4dbf 100644 --- a/test/fixtures/integration/dateTime.js +++ b/test/fixtures/integration/dateTime.js @@ -5,12 +5,16 @@ module.exports = { playlistType: 'VOD', segments: [ { + dateTimeString: '2016-06-22T09:20:16.166-04:00', + dateTimeObject: new Date('2016-06-22T09:20:16.166-04:00'), programDateTime: 1466601616166, duration: 10, timeline: 0, uri: 'hls_450k_video.ts' }, { + dateTimeString: '2016-06-22T09:20:26.166-04:00', + dateTimeObject: new Date('2016-06-22T09:20:26.166-04:00'), programDateTime: 1466601626166, duration: 10, timeline: 0, @@ -19,6 +23,8 @@ module.exports = { ], targetDuration: 10, endList: true, + dateTimeString: '2016-06-22T09:20:16.166-04:00', + dateTimeObject: new Date('2016-06-22T09:20:16.166-04:00'), discontinuitySequence: 0, discontinuityStarts: [] }; diff --git a/test/fixtures/integration/llhls.js b/test/fixtures/integration/llhls.js index b394cb3..ae93cbc 100644 --- a/test/fixtures/integration/llhls.js +++ b/test/fixtures/integration/llhls.js @@ -1,5 +1,7 @@ module.exports = { allowCache: true, + dateTimeObject: new Date('2019-02-14T02:13:36.106Z'), + dateTimeString: '2019-02-14T02:13:36.106Z', dateRanges: [], discontinuitySequence: 0, discontinuityStarts: [], @@ -37,6 +39,8 @@ module.exports = { partTargetDuration: 0.33334, segments: [ { + dateTimeObject: new Date('2019-02-14T02:13:36.106Z'), + dateTimeString: '2019-02-14T02:13:36.106Z', programDateTime: 1550110416106, duration: 4.00008, map: { @@ -143,6 +147,8 @@ module.exports = { ] }, { + dateTimeObject: new Date('2019-02-14T02:14:00.106Z'), + dateTimeString: '2019-02-14T02:14:00.106Z', duration: 4.00008, map: { uri: 'init.mp4' diff --git a/test/fixtures/integration/llhlsDelta.js b/test/fixtures/integration/llhlsDelta.js index 78a32a1..e965d03 100644 --- a/test/fixtures/integration/llhlsDelta.js +++ b/test/fixtures/integration/llhlsDelta.js @@ -1,5 +1,7 @@ module.exports = { allowCache: true, + dateTimeObject: new Date('2019-02-14T02:14:00.106Z'), + dateTimeString: '2019-02-14T02:14:00.106Z', dateRanges: [], discontinuitySequence: 0, discontinuityStarts: [], @@ -110,6 +112,8 @@ module.exports = { ] }, { + dateTimeObject: new Date('2019-02-14T02:14:00.106Z'), + dateTimeString: '2019-02-14T02:14:00.106Z', duration: 4.00008, programDateTime: 1550110440106, timeline: 0, diff --git a/test/parse-stream.test.js b/test/parse-stream.test.js index 22f62de..9763985 100644 --- a/test/parse-stream.test.js +++ b/test/parse-stream.test.js @@ -635,6 +635,10 @@ QUnit.test( element.dateTimeString, '2016-06-22T09:20:16.166-04:00', 'dateTimeString is parsed' ); + assert.deepEqual( + element.dateTimeObject, new Date('2016-06-22T09:20:16.166-04:00'), + 'dateTimeObject is parsed' + ); manifest = '#EXT-X-PROGRAM-DATE-TIME:2016-06-22T09:20:16.16389Z\n'; this.lineStream.push(manifest); @@ -646,6 +650,10 @@ QUnit.test( element.dateTimeString, '2016-06-22T09:20:16.16389Z', 'dateTimeString is parsed' ); + assert.deepEqual( + element.dateTimeObject, new Date('2016-06-22T09:20:16.16389Z'), + 'dateTimeObject is parsed' + ); } ); QUnit.test('parses #EXT-X-STREAM-INF with common attributes', function(assert) { From 73d934ce5812798e709aa6a510f9812027a7c602 Mon Sep 17 00:00:00 2001 From: Harisha Rajam Swaminathan <35213866+harisha-swaminathan@users.noreply.github.com> Date: Mon, 7 Aug 2023 11:58:23 -0400 Subject: [PATCH 3/6] fix: merge dateRange tags with same IDs and no conflicting attributes (#175) --- src/parser.js | 13 ++++++++++--- test/parser.test.js | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/parser.js b/src/parser.js index 157603f..79ffdd4 100644 --- a/src/parser.js +++ b/src/parser.js @@ -696,7 +696,7 @@ export default class Parser extends Stream { } if (dateRange.duration && dateRange.endDate) { const startDate = dateRange.startDate; - const newDateInSeconds = startDate.setSeconds(startDate.getSeconds() + dateRange.duration); + const newDateInSeconds = startDate.getTime() + (dateRange.duration * 1000); this.manifest.dateRanges[index].endDate = new Date(newDateInSeconds); } @@ -704,13 +704,20 @@ export default class Parser extends Stream { dateRangeTags[dateRange.id] = dateRange; } else { for (const attribute in dateRangeTags[dateRange.id]) { - if (dateRangeTags[dateRange.id][attribute] !== dateRange[attribute]) { + if (!!dateRange[attribute] && JSON.stringify(dateRangeTags[dateRange.id][attribute]) !== JSON.stringify(dateRange[attribute])) { this.trigger('warn', { - message: 'EXT-X-DATERANGE tags with the same ID in a playlist must have the same attributes and same attribute values' + message: 'EXT-X-DATERANGE tags with the same ID in a playlist must have the same attributes values' }); break; } } + // if tags with the same ID do not have conflicting attributes, merge them + const dateRangeWithSameId = this.manifest.dateRanges.findIndex((dateRangeToFind) => dateRangeToFind.id === dateRange.id); + + this.manifest.dateRanges[dateRangeWithSameId] = Object.assign(this.manifest.dateRanges[dateRangeWithSameId], dateRange); + dateRangeTags[dateRange.id] = Object.assign(dateRangeTags[dateRange.id], dateRange); + // after merging, delete the duplicate dateRange that was added last + this.manifest.dateRanges.pop(); } }, 'independent-segments'() { diff --git a/test/parser.test.js b/test/parser.test.js index 25f6c14..7565b32 100644 --- a/test/parser.test.js +++ b/test/parser.test.js @@ -1031,7 +1031,7 @@ QUnit.module('m3u8s', function(hooks) { ); }); - QUnit.test('warns when playlist has multiple #EXT-X-DATERANGE tag same ID but different attribute names and values', function(assert) { + QUnit.test('warns when playlist has multiple #EXT-X-DATERANGE tag same ID but different attribute values', function(assert) { this.parser.push([ '#EXT-X-VERSION:3', '#EXT-X-MEDIA-SEQUENCE:0', @@ -1041,12 +1041,12 @@ QUnit.module('m3u8s', function(hooks) { '#EXT-X-ENDLIST', '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00', '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",END-ON-NEXT=YES,CLASS="CLASSATTRIBUTE"', - '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:20.840000Z"' + '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",CLASS="CLASSATTRIBUTE1"' ].join('\n')); this.parser.end(); const warnings = [ - 'EXT-X-DATERANGE tags with the same ID in a playlist must have the same attributes and same attribute values' + 'EXT-X-DATERANGE tags with the same ID in a playlist must have the same attributes values' ]; assert.deepEqual( @@ -1095,7 +1095,33 @@ QUnit.module('m3u8s', function(hooks) { ); }); - QUnit.test(' playlist with multiple ext-x-daterange ', function(assert) { + QUnit.test('playlist with multiple ext-x-daterange with same ID but no conflicting attributes', function(assert) { + const expectedDateRange = { + id: '12345', + scte35In: '0xFC30200FFF2', + scte35Out: '0xFC30200FFF2', + startDate: new Date('2023-04-13T18:16:15.840000Z'), + class: 'CLASSATTRIBUTE' + }; + + this.parser.push([ + '#EXT-X-VERSION:3', + '#EXT-X-MEDIA-SEQUENCE:0', + '#EXT-X-DISCONTINUITY-SEQUENCE:0', + '#EXTINF:10,', + 'media-00001.ts', + '#EXT-X-ENDLIST', + '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00', + '#EXT-X-DATERANGE:ID="12345",SCTE35-IN=0xFC30200FFF2,START-DATE="2023-04-13T18:16:15.840000Z",CLASS="CLASSATTRIBUTE"', + '#EXT-X-DATERANGE:ID="12345",SCTE35-OUT=0xFC30200FFF2,START-DATE="2023-04-13T18:16:15.840000Z"' + ].join('\n')); + this.parser.end(); + assert.equal(this.parser.manifest.dateRanges.length, 1, 'two dateranges with same ID are merged'); + assert.deepEqual(this.parser.manifest.dateRanges[0], expectedDateRange); + + }); + + QUnit.test('playlist with multiple ext-x-daterange ', function(assert) { this.parser.push([ ' #EXTM3U', '#EXT-X-VERSION:6', From 42472c597964c65c1fd22528f04e2cd5a21f2683 Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Mon, 7 Aug 2023 09:19:35 -0700 Subject: [PATCH 4/6] feat: parse content steering tags and attributes (#176) --- src/parse-stream.js | 10 ++++++++++ src/parser.js | 8 ++++++++ test/parse-stream.test.js | 8 ++++++++ test/parser.test.js | 29 +++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+) diff --git a/src/parse-stream.js b/src/parse-stream.js index 2b6a691..b09489a 100644 --- a/src/parse-stream.js +++ b/src/parse-stream.js @@ -624,6 +624,16 @@ export default class ParseStream extends Stream { }); return; } + match = (/^#EXT-X-CONTENT-STEERING:(.*)$/).exec(newLine); + if (match) { + event = { + type: 'tag', + tagType: 'content-steering' + }; + event.attributes = parseAttributes(match[1]); + this.trigger('data', event); + return; + } // unknown tag type this.trigger('data', { diff --git a/src/parser.js b/src/parser.js index 79ffdd4..77cb1a5 100644 --- a/src/parser.js +++ b/src/parser.js @@ -722,6 +722,14 @@ export default class Parser extends Stream { }, 'independent-segments'() { this.manifest.independentSegments = true; + }, + 'content-steering'() { + this.manifest.contentSteering = camelCaseKeys(entry.attributes); + this.warnOnMissingAttributes_( + '#EXT-X-CONTENT-STEERING', + entry.attributes, + ['SERVER-URI'] + ); } })[entry.tagType] || noop).call(self); }, diff --git a/test/parse-stream.test.js b/test/parse-stream.test.js index 9763985..ed51fcb 100644 --- a/test/parse-stream.test.js +++ b/test/parse-stream.test.js @@ -698,6 +698,14 @@ QUnit.test('parses #EXT-X-STREAM-INF with common attributes', function(assert) { 'avc1.4d400d, mp4a.40.2', 'codecs are parsed' ); + + manifest = '#EXT-X-STREAM-INF:PATHWAY-ID="CDN-A"\n'; + this.lineStream.push(manifest); + + assert.ok(element, 'an event was triggered'); + assert.strictEqual(element.type, 'tag', 'the line type is tag'); + assert.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf'); + assert.strictEqual(element.attributes['PATHWAY-ID'], 'CDN-A', 'pathway-id is parsed'); }); QUnit.test('parses #EXT-X-STREAM-INF with arbitrary attributes', function(assert) { const manifest = '#EXT-X-STREAM-INF:NUMERIC=24,ALPHA=Value,MIXED=123abc\n'; diff --git a/test/parser.test.js b/test/parser.test.js index 7565b32..230d093 100644 --- a/test/parser.test.js +++ b/test/parser.test.js @@ -1158,6 +1158,35 @@ QUnit.module('m3u8s', function(hooks) { assert.equal(this.parser.manifest.independentSegments, true); }); + QUnit.test('parses #EXT-X-CONTENT-STEERING', function(assert) { + const expectedContentSteeringObject = { + serverUri: '/foo?bar=00012', + pathwayId: 'CDN-A' + }; + + this.parser.push('#EXT-X-CONTENT-STEERING:SERVER-URI="/foo?bar=00012",PATHWAY-ID="CDN-A"'); + this.parser.end(); + assert.deepEqual(this.parser.manifest.contentSteering, expectedContentSteeringObject); + }); + + QUnit.test('parses #EXT-X-CONTENT-STEERING without PATHWAY-ID', function(assert) { + const expectedContentSteeringObject = { + serverUri: '/bar?foo=00012' + }; + + this.parser.push('#EXT-X-CONTENT-STEERING:SERVER-URI="/bar?foo=00012"'); + this.parser.end(); + assert.deepEqual(this.parser.manifest.contentSteering, expectedContentSteeringObject); + }); + + QUnit.test('warns on #EXT-X-CONTENT-STEERING missing SERVER-URI', function(assert) { + const warning = ['#EXT-X-CONTENT-STEERING lacks required attribute(s): SERVER-URI']; + + this.parser.push('#EXT-X-CONTENT-STEERING:PATHWAY-ID="CDN-A"'); + this.parser.end(); + assert.deepEqual(this.warnings, warning, 'warnings as expected'); + }); + QUnit.module('integration'); for (const key in testDataExpected) { From b2d44f204264ff13ede4810ea80fa03156010e26 Mon Sep 17 00:00:00 2001 From: hswaminathan Date: Mon, 7 Aug 2023 12:26:05 -0400 Subject: [PATCH 5/6] 7.1.0 --- CHANGELOG.md | 16 ++++++++++++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 280d6c6..bb0db02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ + +# [7.1.0](https://github.com/videojs/m3u8-parser/compare/v7.0.0...v7.1.0) (2023-08-07) + +### Features + +* parse content steering tags and attributes ([#176](https://github.com/videojs/m3u8-parser/issues/176)) ([42472c5](https://github.com/videojs/m3u8-parser/commit/42472c5)) + +### Bug Fixes + +* add dateTimeObject and dateTimeString for backward compatibility ([#174](https://github.com/videojs/m3u8-parser/issues/174)) ([6944bb1](https://github.com/videojs/m3u8-parser/commit/6944bb1)) +* merge dateRange tags with same IDs and no conflicting attributes ([#175](https://github.com/videojs/m3u8-parser/issues/175)) ([73d934c](https://github.com/videojs/m3u8-parser/commit/73d934c)) + +### Chores + +* update v7.0.0 documentation ([#172](https://github.com/videojs/m3u8-parser/issues/172)) ([72da994](https://github.com/videojs/m3u8-parser/commit/72da994)) + # [7.0.0](https://github.com/videojs/m3u8-parser/compare/v6.2.0...v7.0.0) (2023-07-10) diff --git a/package-lock.json b/package-lock.json index 942b10d..81c8a93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "m3u8-parser", - "version": "7.0.0", + "version": "7.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index aaa4f79..3ca5ad3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "m3u8-parser", - "version": "7.0.0", + "version": "7.1.0", "description": "m3u8 parser", "main": "dist/m3u8-parser.cjs.js", "module": "dist/m3u8-parser.es.js", From f8c9817a95da39ee2b8ec10b889df325daaa846b Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Tue, 15 Aug 2023 15:39:09 -0700 Subject: [PATCH 6/6] chore: add content-steering tag to readme (#177) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 98b661a..c4b58a0 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,7 @@ Manifest { * [EXT-X-MEDIA](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.1) * [EXT-X-STREAM-INF](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.2) +* [EXT-X-CONTENT-STEERING](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.6.6) ### Experimental Tags