Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/loader/date-range.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AttrList } from '../utils/attr-list';
import { parseDateTime } from '../utils/date-time';
import { logger } from '../utils/logger';
import type { MediaFragmentRef } from './fragment';

Expand Down Expand Up @@ -90,12 +91,14 @@ export class DateRange {
this._endDate = dateRangeWithSameId._endDate;
this._dateAtEnd = dateRangeWithSameId._dateAtEnd;
} else {
this._startDate = new Date(dateRangeAttr[DateRangeAttribute.START_DATE]);
this._startDate = new Date(
parseDateTime(dateRangeAttr[DateRangeAttribute.START_DATE]),
);
}
if (DateRangeAttribute.END_DATE in dateRangeAttr) {
const endDate =
dateRangeWithSameId?.endDate ||
new Date(dateRangeAttr[DateRangeAttribute.END_DATE]);
new Date(parseDateTime(dateRangeAttr[DateRangeAttribute.END_DATE]));
if (Number.isFinite(endDate.getTime())) {
this._endDate = endDate;
}
Expand Down
3 changes: 2 additions & 1 deletion src/loader/fragment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { buildAbsoluteURL } from 'url-toolkit';
import { LoadStats } from './load-stats';
import { PlaylistLevelType } from '../types/loader';
import { parseDateTime } from '../utils/date-time';
import type { LevelKey } from './level-key';
import type {
FragmentLoaderContext,
Expand Down Expand Up @@ -337,7 +338,7 @@ export class Fragment extends BaseSegment {

get programDateTime(): number | null {
if (this._programDateTime === null && this.rawProgramDateTime) {
this.programDateTime = Date.parse(this.rawProgramDateTime);
this.programDateTime = parseDateTime(this.rawProgramDateTime);
}
return this._programDateTime;
}
Expand Down
24 changes: 24 additions & 0 deletions src/utils/date-time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { logger } from './logger';

// 'Z' or a UTC offset (+05:00, -0500, +05) at the end of a date-time string
const ZONE_DESIGNATOR = /(?:[Zz]|[+-]\d{2}(?::?\d{2})?)$/;

let warnedZoneless = false;

// ISO 8601 date-times without a time zone represent local time, and
// `Date.parse` follows that. HLS supersedes it: clients SHOULD treat a
// date-time without a time zone as UTC (rfc8216bis-17 Section 4.4.4.6),
// matching Apple's clients. Playlists SHOULD indicate a time zone, so the
// first zone-less value parsed logs a warning.
export function parseDateTime(value: string): number {
if (/[Tt]/.test(value) && !ZONE_DESIGNATOR.test(value)) {
Comment thread
itsjamie marked this conversation as resolved.
Outdated
if (!warnedZoneless) {
warnedZoneless = true;
logger.warn(
`Date/time "${value}" has no time zone. Parsing as UTC (playlists SHOULD indicate a time zone).`,
);
}
return Date.parse(value + 'Z');
}
return Date.parse(value);
}
1 change: 1 addition & 0 deletions tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import './unit/utils/binary-search';
import './unit/utils/buffer-helper';
import './unit/utils/cea-608-parser';
import './unit/utils/codecs';
import './unit/utils/date-time';
import './unit/utils/error-helper';
import './unit/utils/fetch-loader';
import './unit/utils/discontinuities';
Expand Down
14 changes: 14 additions & 0 deletions tests/unit/loader/date-range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ describe('DateRange class', function () {
expect(dateRangeEndDate.duration).to.equal(60.001);
});

it('parses zone-less START-DATE and END-DATE as UTC', function () {
const zonelessDates = new AttrList(
'ID="ad5",START-DATE="2020-01-02T21:55:44.000",END-DATE="2020-01-02T21:56:44.001"',
);
const dateRange = new DateRange(zonelessDates);
expect(dateRange.isValid).to.be.true;
expect(dateRange.startDate.toISOString()).to.equal(
'2020-01-02T21:55:44.000Z',
);
expect((dateRange.endDate as Date).toISOString()).to.equal(
'2020-01-02T21:56:44.001Z',
);
});

it('merges tags with matching ID attributes', function () {
const scteOut = new DateRange(sctePlanned);
const scteIn = new DateRange(scteDurationUpdate, scteOut);
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/loader/m3u8-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1349,6 +1349,28 @@ Rollover38803/20160525T064049-01-69844069.ts
expect(result.fragments[2].programDateTime).to.equal(1464366904000);
});

it('parses #EXT-X-PROGRAM-DATE-TIME without a time zone as UTC', function () {
const level = `#EXTM3U
#EXT-X-VERSION:2
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:69844067
#EXTINF:10, no desc
#EXT-X-PROGRAM-DATE-TIME:2016-05-27T16:34:44.000
Rollover38803/20160525T064049-01-69844067.ts
`;
const result = M3U8Parser.parseLevelPlaylist(
level,
'http://video.example.com/disc.m3u8',
0,
PlaylistLevelType.MAIN,
0,
null,
);
expect(result.playlistParsingError).to.be.null;
expect(result.hasProgramDateTime).to.be.true;
expect(result.fragments[0].programDateTime).to.equal(1464366884000);
});

it('parses delta playlists with one #EXT-X-PROGRAM-DATE-TIME after segments', function () {
const level = `#EXTM3U
#EXT-X-TARGETDURATION:6
Expand Down
24 changes: 24 additions & 0 deletions tests/unit/utils/date-time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect } from 'chai';
import { parseDateTime } from '../../../src/utils/date-time';

describe('parseDateTime', function () {
it('parses date-times with a time zone as-is', function () {
expect(parseDateTime('2016-05-27T16:34:44Z')).to.equal(1464366884000);
expect(parseDateTime('2016-05-27T16:34:44.000Z')).to.equal(1464366884000);
expect(parseDateTime('2016-05-27T19:34:44+03:00')).to.equal(1464366884000);
expect(parseDateTime('2016-05-27T13:34:44-03:00')).to.equal(1464366884000);
});

it('parses zone-less date-times as UTC', function () {
expect(parseDateTime('2016-05-27T16:34:44')).to.equal(1464366884000);
expect(parseDateTime('2016-05-27T16:34:44.000')).to.equal(1464366884000);
});

it('leaves date-only values alone (already UTC per ECMA-262)', function () {
expect(parseDateTime('2016-05-27')).to.equal(1464307200000);
});

it('returns NaN for invalid input', function () {
expect(parseDateTime('not a date')).to.be.NaN;
});
});
Loading