diff --git a/CHANGELOG.md b/CHANGELOG.md
index 390f762..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)
@@ -6,14 +22,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)
diff --git a/README.md b/README.md
index 91f787b..0c39e03 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],
@@ -177,6 +179,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
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",
diff --git a/src/parse-stream.js b/src/parse-stream.js
index c1bd19a..0c10a35 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;
@@ -623,12 +624,23 @@ export default class ParseStream extends Stream {
});
return;
}
+
match = (/^#EXT-X-I-FRAMES-ONLY/).exec(newLine);
if (match) {
this.trigger('data', {
type: 'tag',
tagType: 'i-frames-only'
});
+
+ 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;
}
diff --git a/src/parser.js b/src/parser.js
index 954260e..921040f 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();
@@ -685,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);
}
@@ -693,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'() {
@@ -709,6 +727,14 @@ export default class Parser extends Stream {
this.manifest.iFramesOnly = true;
this.requiredCompatibilityversion(this.manifest.version, 4);
+ },
+ '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/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 2090245..4b61e7d 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) {
@@ -690,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 61cc509..0f80d09 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',
@@ -1201,6 +1227,35 @@ QUnit.module('m3u8s', function(hooks) {
);
});
+ 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) {