Compare commits

...

12 commits

Author SHA1 Message Date
Harisha Rajam Swaminathan
6a80bba013
fix typo
Co-authored-by: Adam Waldron <acwald@gmail.com>
2023-07-07 14:57:36 -04:00
hswaminathan
dc8394fe20 update Readme 2023-07-07 10:31:59 -04:00
hswaminathan
42608b7145 fix tests 2023-07-06 17:58:42 -04:00
hswaminathan
0f37308b39 add default dateRanges array 2023-07-06 17:41:22 -04:00
hswaminathan
a57e3d7bef programDateTimeString and move daterange warning 2023-07-06 17:16:43 -04:00
hswaminathan
12bc3039ab remove dateTimeObject and rename dateTimeString 2023-07-06 15:41:05 -04:00
hswaminathan
c6aa7e62bd remove PDT as a manifest object 2023-07-06 15:07:15 -04:00
Dzianis Dashkevich
0aa6179dc3 Simplify explicit extrapolation by making it to be a part of the forward extrapolation 2023-07-06 11:51:51 -04:00
Dzianis Dashkevich
8bdc0d0e30 fix blank line lint issue 2023-07-05 19:20:31 -04:00
Dzianis Dashkevich
600dff9956 refactor forward/backward program date time extrapolation 2023-07-05 19:18:01 -04:00
hswaminathan
95eff0da82 call addPDTToSegment_ for each segment 2023-06-30 22:46:54 -04:00
hswaminathan
3e4ee65efc feat: Add PDT to each segment 2023-06-30 13:12:06 -04:00
63 changed files with 168 additions and 43 deletions

View file

@ -57,6 +57,7 @@ var manifest = [
'0.ts',
'#EXTINF:6,',
'1.ts',
'#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:14:00.106Z'
'#EXTINF:6,',
'2.ts',
'#EXT-X-ENDLIST'
@ -79,6 +80,7 @@ Manifest {
allowCache: boolean,
endList: boolean,
mediaSequence: number,
dateRanges: [],
discontinuitySequence: number,
playlistType: string,
custom: {},
@ -106,8 +108,6 @@ Manifest {
'CLOSED-CAPTIONS': {},
SUBTITLES: {}
},
dateTimeString: string,
dateTimeObject: Date,
targetDuration: number,
totalDuration: number,
discontinuityStarts: [number],
@ -118,6 +118,7 @@ Manifest {
offset: number
},
duration: number,
programDateTime: number,
attributes: {},
discontinuity: number,
uri: string,

View file

@ -356,7 +356,6 @@ export default class ParseStream extends Stream {
};
if (match[1]) {
event.dateTimeString = match[1];
event.dateTimeObject = new Date(match[1]);
}
this.trigger('data', event);
return;

View file

@ -97,6 +97,8 @@ export default class Parser extends Stream {
this.parseStream = new ParseStream();
this.lineStream.pipe(this.parseStream);
this.lastProgramDateTime = null;
/* eslint-disable consistent-this */
const self = this;
/* eslint-enable consistent-this */
@ -124,6 +126,7 @@ export default class Parser extends Stream {
this.manifest = {
allowCache: true,
discontinuityStarts: [],
dateRanges: [],
segments: []
};
// keep track of the last seen segment's byte range end, as segments are not required
@ -451,17 +454,21 @@ 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;
}
const { lastProgramDateTime } = this;
currentUri.dateTimeString = entry.dateTimeString;
currentUri.dateTimeObject = entry.dateTimeObject;
this.lastProgramDateTime = new Date(entry.dateTimeString).getTime();
// We should extrapolate Program Date Time backward only during first program date time occurrence.
// Once we have at least one program date time point, we can always extrapolate it forward using lastProgramDateTime reference.
if (lastProgramDateTime === null) {
// Extrapolate Program Date Time backward
// Since it is first program date time occurrence we're assuming that
// all this.manifest.segments have no program date time info
this.manifest.segments.reduceRight((programDateTime, segment) => {
segment.programDateTime = programDateTime - (segment.duration * 1000);
return segment.programDateTime;
}, this.lastProgramDateTime);
}
},
targetduration() {
if (!isFinite(entry.duration) || entry.duration < 0) {
@ -634,7 +641,6 @@ export default class Parser extends Stream {
setHoldBack.call(this, this.manifest);
},
'daterange'() {
this.manifest.dateRanges = this.manifest.dateRanges || [];
this.manifest.dateRanges.push(camelCaseKeys(entry.attributes));
const index = this.manifest.dateRanges.length - 1;
@ -678,11 +684,6 @@ export default class Parser extends Stream {
this.manifest.dateRanges[index].endDate = new Date(newDateInSeconds);
}
if (dateRange && !this.manifest.dateTimeString) {
this.trigger('warn', {
message: 'A playlist with EXT-X-DATERANGE tag must contain atleast one EXT-X-PROGRAM-DATE-TIME tag'
});
}
if (!dateRangeTags[dateRange.id]) {
dateRangeTags[dateRange.id] = dateRange;
} else {
@ -725,6 +726,12 @@ export default class Parser extends Stream {
// reset the last byterange end as it needs to be 0 between parts
lastPartByterangeEnd = 0;
// Once we have at least one program date time we can always extrapolate it forward
if (this.lastProgramDateTime !== null) {
currentUri.programDateTime = this.lastProgramDateTime;
this.lastProgramDateTime += currentUri.duration * 1000;
}
// prepare for the next URI
currentUri = {};
},
@ -777,7 +784,13 @@ export default class Parser extends Stream {
end() {
// flush any buffered input
this.lineStream.push('\n');
if (this.manifest.dateRanges.length && this.lastProgramDateTime === null) {
this.trigger('warn', {
message: 'A playlist with EXT-X-DATERANGE tag must contain atleast one EXT-X-PROGRAM-DATE-TIME tag'
});
}
this.lastProgramDateTime = null;
this.trigger('end');
}
/**

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,6 +1,7 @@
module.exports = {
allowCache: true,
mediaSequence: 0,
dateRanges: [],
playlistType: 'VOD',
segments: [
{

View file

@ -1,6 +1,7 @@
module.exports = {
allowCache: true,
mediaSequence: 0,
dateRanges: [],
playlistType: 'VOD',
segments: [
{

View file

@ -1,6 +1,7 @@
module.exports = {
allowCache: true,
discontinuityStarts: [],
dateRanges: [],
mediaGroups: {
// TYPE
'AUDIO': {

View file

@ -1,6 +1,7 @@
module.exports = {
allowCache: true,
discontinuityStarts: [],
dateRanges: [],
mediaGroups: {
'AUDIO': {
aac: {

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
playlists: [
{
attributes: {

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,18 +1,17 @@
module.exports = {
allowCache: false,
mediaSequence: 0,
dateRanges: [],
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,
uri: 'hls_450k_video.ts'
@ -20,8 +19,6 @@ 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: []
};

View file

@ -2,6 +2,7 @@ module.exports = {
allowCache: true,
discontinuitySequence: 0,
discontinuityStarts: [],
dateRanges: [],
mediaSequence: 7794,
segments: [
{

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: false,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
discontinuitySequence: 3,
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
discontinuitySequence: 0,
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
discontinuityStarts: [],
segments: []
};

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
segments: [
{

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
playlists: [
{
attributes: {

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 7794,
discontinuitySequence: 0,
discontinuityStarts: [],

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'EVENT',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 1,
segments: [
{

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 1,
playlistType: 'VOD',
targetDuration: 6,

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
discontinuityStarts: [],
segments: []
};

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
segments: [
{

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
segments: [
{

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
discontinuitySequence: 0,
discontinuityStarts: [],
mediaSequence: 0,

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
discontinuitySequence: 0,
discontinuityStarts: [],
mediaSequence: 0,

View file

@ -1,7 +1,6 @@
module.exports = {
allowCache: true,
dateTimeObject: new Date('2019-02-14T02:13:36.106Z'),
dateTimeString: '2019-02-14T02:13:36.106Z',
dateRanges: [],
discontinuitySequence: 0,
discontinuityStarts: [],
mediaSequence: 266,
@ -38,8 +37,7 @@ 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: {
uri: 'init.mp4'
@ -52,6 +50,7 @@ module.exports = {
map: {
uri: 'init.mp4'
},
programDateTime: 1550110420106.08,
timeline: 0,
uri: 'fileSequence267.mp4'
},
@ -60,6 +59,7 @@ module.exports = {
map: {
uri: 'init.mp4'
},
programDateTime: 1550110424106.1602,
timeline: 0,
uri: 'fileSequence268.mp4'
},
@ -68,6 +68,7 @@ module.exports = {
map: {
uri: 'init.mp4'
},
programDateTime: 1550110428106.2402,
timeline: 0,
uri: 'fileSequence269.mp4'
},
@ -76,6 +77,7 @@ module.exports = {
map: {
uri: 'init.mp4'
},
programDateTime: 1550110432106.3203,
timeline: 0,
uri: 'fileSequence270.mp4'
},
@ -84,6 +86,7 @@ module.exports = {
map: {
uri: 'init.mp4'
},
programDateTime: 1550110436106.4004,
timeline: 0,
uri: 'fileSequence271.mp4',
parts: [
@ -140,12 +143,11 @@ 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'
},
programDateTime: 1550110440106,
timeline: 0,
uri: 'fileSequence272.mp4',
parts: [

View file

@ -1,7 +1,6 @@
module.exports = {
allowCache: true,
dateTimeObject: new Date('2019-02-14T02:14:00.106Z'),
dateTimeString: '2019-02-14T02:14:00.106Z',
dateRanges: [],
discontinuitySequence: 0,
discontinuityStarts: [],
mediaSequence: 266,
@ -42,16 +41,19 @@ module.exports = {
segments: [
{
duration: 4.00008,
programDateTime: 1550110428105.7598,
timeline: 0,
uri: 'fileSequence269.mp4'
},
{
duration: 4.00008,
programDateTime: 1550110432105.8398,
timeline: 0,
uri: 'fileSequence270.mp4'
},
{
duration: 4.00008,
programDateTime: 1550110436105.92,
timeline: 0,
uri: 'fileSequence271.mp4',
parts: [
@ -108,9 +110,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,
uri: 'fileSequence272.mp4',
parts: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
segments: [
{

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
segments: [
{

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
segments: [
{

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
discontinuityStarts: [],
mediaGroups: {
'AUDIO': {

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
playlists: [
{
attributes: {

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
segments: [
{

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
discontinuityStarts: [],
mediaGroups: {
'AUDIO': {

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
discontinuityStarts: [],
mediaGroups: {
'AUDIO': {

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
targetDuration: 10,
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
discontinuityStarts: [],
mediaGroups: {
'AUDIO': {

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: -11,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 17,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
playlists: [
{
attributes: {

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 11,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,5 +1,6 @@
module.exports = {
allowCache: true,
dateRanges: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

@ -1,6 +1,7 @@
module.exports = {
allowCache: true,
mediaSequence: 0,
dateRanges: [],
playlistType: 'VOD',
segments: [
{

View file

@ -635,10 +635,6 @@ 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);
@ -650,10 +646,6 @@ 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) {

View file

@ -845,6 +845,71 @@ QUnit.module('m3u8s', function(hooks) {
);
});
QUnit.test('PDT value is assigned to segments with explicit #EXT-X-PROGRAM-DATE-TIME tags', function(assert) {
this.parser.push([
'#EXTM3U',
'#EXT-X-VERSION:6',
'#EXT-X-TARGETDURATION:8',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXTINF:8.0',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'https://example.com/playlist1.m3u8',
'#EXTINF:8.0,',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T22:14:10.053+00:00',
'https://example.com/playlist2.m3u8',
'#EXT-X-ENDLIST'
].join('\n'));
this.parser.end();
assert.equal(this.parser.manifest.segments[0].programDateTime, new Date('2017-07-31T20:35:35.053+00:00').getTime());
assert.equal(this.parser.manifest.segments[1].programDateTime, new Date('2017-07-31T22:14:10.053+00:00').getTime());
});
QUnit.test('backfill PDT values when the first EXT-X-PROGRAM-DATE-TIME tag appears after one or more Media Segment URIs', function(assert) {
this.parser.push([
'#EXTM3U',
'#EXT-X-VERSION:6',
'#EXT-X-TARGETDURATION:8',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXTINF:8.0',
'https://example.com/playlist1.m3u8',
'#EXTINF:8.0,',
'https://example.com/playlist2.m3u8',
'#EXTINF:8.0',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'https://example.com/playlist3.m3u8',
'#EXT-X-ENDLIST'
].join('\n'));
this.parser.end();
const segments = this.parser.manifest.segments;
assert.equal(segments[2].programDateTime, new Date('2017-07-31T20:35:35.053+00:00').getTime());
assert.equal(segments[1].programDateTime, segments[2].programDateTime - (segments[1].duration * 1000));
assert.equal(segments[0].programDateTime, segments[1].programDateTime - (segments[0].duration * 1000));
});
QUnit.test('extrapolates forward when subsequent fragments do not have explicit PDT tags', function(assert) {
this.parser.push([
'#EXTM3U',
'#EXT-X-VERSION:6',
'#EXT-X-TARGETDURATION:8',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXTINF:8.0',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'https://example.com/playlist1.m3u8',
'#EXTINF:8.0,',
'https://example.com/playlist2.m3u8',
'#EXTINF:8.0',
'https://example.com/playlist3.m3u8',
'#EXT-X-ENDLIST'
].join('\n'));
this.parser.end();
const segments = this.parser.manifest.segments;
assert.equal(segments[0].programDateTime, new Date('2017-07-31T20:35:35.053+00:00').getTime());
assert.equal(segments[1].programDateTime, segments[0].programDateTime + segments[1].duration * 1000);
assert.equal(segments[2].programDateTime, segments[1].programDateTime + segments[2].duration * 1000);
});
QUnit.test('warns when #EXT-X-DATERANGE missing attribute', function(assert) {
this.parser.push([
'#EXT-X-VERSION:3',