hls.js 1.5.12-0.canary.10340 → 1.5.12-0.canary.10341
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hls.js +431 -288
- package/dist/hls.js.d.ts +49 -16
- package/dist/hls.js.map +1 -1
- package/dist/hls.light.js +364 -178
- package/dist/hls.light.js.map +1 -1
- package/dist/hls.light.min.js +1 -1
- package/dist/hls.light.min.js.map +1 -1
- package/dist/hls.light.mjs +356 -179
- package/dist/hls.light.mjs.map +1 -1
- package/dist/hls.min.js +1 -1
- package/dist/hls.min.js.map +1 -1
- package/dist/hls.mjs +422 -288
- package/dist/hls.mjs.map +1 -1
- package/dist/hls.worker.js +1 -1
- package/dist/hls.worker.js.map +1 -1
- package/package.json +1 -1
- package/src/controller/audio-stream-controller.ts +13 -5
- package/src/controller/base-stream-controller.ts +28 -26
- package/src/controller/fragment-finders.ts +12 -14
- package/src/controller/fragment-tracker.ts +14 -14
- package/src/controller/id3-track-controller.ts +38 -28
- package/src/controller/stream-controller.ts +9 -4
- package/src/controller/subtitle-stream-controller.ts +3 -3
- package/src/demux/transmuxer-interface.ts +4 -4
- package/src/hls.ts +3 -1
- package/src/loader/date-range.ts +71 -5
- package/src/loader/fragment.ts +6 -2
- package/src/loader/level-details.ts +7 -6
- package/src/loader/m3u8-parser.ts +138 -144
- package/src/types/events.ts +2 -5
- package/src/types/fragment-tracker.ts +2 -2
- package/src/utils/attr-list.ts +96 -9
- package/src/utils/level-helper.ts +68 -40
- package/src/utils/variable-substitution.ts +0 -19
package/src/loader/date-range.ts
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
import { AttrList } from '../utils/attr-list';
|
2
2
|
import { logger } from '../utils/logger';
|
3
|
+
import type { Fragment } from './fragment';
|
3
4
|
|
4
5
|
// Avoid exporting const enum so that these values can be inlined
|
5
6
|
const enum DateRangeAttribute {
|
6
7
|
ID = 'ID',
|
7
8
|
CLASS = 'CLASS',
|
9
|
+
CUE = 'CUE',
|
8
10
|
START_DATE = 'START-DATE',
|
9
11
|
DURATION = 'DURATION',
|
10
12
|
END_DATE = 'END-DATE',
|
@@ -12,12 +14,22 @@ const enum DateRangeAttribute {
|
|
12
14
|
PLANNED_DURATION = 'PLANNED-DURATION',
|
13
15
|
SCTE35_OUT = 'SCTE35-OUT',
|
14
16
|
SCTE35_IN = 'SCTE35-IN',
|
17
|
+
SCTE35_CMD = 'SCTE35-CMD',
|
15
18
|
}
|
16
19
|
|
20
|
+
export type DateRangeCue = {
|
21
|
+
pre: boolean;
|
22
|
+
post: boolean;
|
23
|
+
once: boolean;
|
24
|
+
};
|
25
|
+
|
26
|
+
const CLASS_INTERSTITIAL = 'com.apple.hls.interstitial';
|
27
|
+
|
17
28
|
export function isDateRangeCueAttribute(attrName: string): boolean {
|
18
29
|
return (
|
19
30
|
attrName !== DateRangeAttribute.ID &&
|
20
31
|
attrName !== DateRangeAttribute.CLASS &&
|
32
|
+
attrName !== DateRangeAttribute.CUE &&
|
21
33
|
attrName !== DateRangeAttribute.START_DATE &&
|
22
34
|
attrName !== DateRangeAttribute.DURATION &&
|
23
35
|
attrName !== DateRangeAttribute.END_DATE &&
|
@@ -28,17 +40,27 @@ export function isDateRangeCueAttribute(attrName: string): boolean {
|
|
28
40
|
export function isSCTE35Attribute(attrName: string): boolean {
|
29
41
|
return (
|
30
42
|
attrName === DateRangeAttribute.SCTE35_OUT ||
|
31
|
-
attrName === DateRangeAttribute.SCTE35_IN
|
43
|
+
attrName === DateRangeAttribute.SCTE35_IN ||
|
44
|
+
attrName === DateRangeAttribute.SCTE35_CMD
|
32
45
|
);
|
33
46
|
}
|
34
47
|
|
35
48
|
export class DateRange {
|
36
49
|
public attr: AttrList;
|
50
|
+
public tagAnchor: Fragment | null;
|
51
|
+
public tagOrder: number;
|
37
52
|
private _startDate: Date;
|
38
53
|
private _endDate?: Date;
|
54
|
+
private _cue?: DateRangeCue;
|
39
55
|
private _badValueForSameId?: string;
|
40
56
|
|
41
|
-
constructor(
|
57
|
+
constructor(
|
58
|
+
dateRangeAttr: AttrList,
|
59
|
+
dateRangeWithSameId?: DateRange | undefined,
|
60
|
+
tagCount: number = 0,
|
61
|
+
) {
|
62
|
+
this.tagAnchor = dateRangeWithSameId?.tagAnchor || null;
|
63
|
+
this.tagOrder = dateRangeWithSameId?.tagOrder ?? tagCount;
|
42
64
|
if (dateRangeWithSameId) {
|
43
65
|
const previousAttr = dateRangeWithSameId.attr;
|
44
66
|
for (const key in previousAttr) {
|
@@ -61,9 +83,13 @@ export class DateRange {
|
|
61
83
|
);
|
62
84
|
}
|
63
85
|
this.attr = dateRangeAttr;
|
64
|
-
this._startDate =
|
86
|
+
this._startDate = dateRangeWithSameId
|
87
|
+
? dateRangeWithSameId.startDate
|
88
|
+
: new Date(dateRangeAttr[DateRangeAttribute.START_DATE]);
|
65
89
|
if (DateRangeAttribute.END_DATE in this.attr) {
|
66
|
-
const endDate =
|
90
|
+
const endDate =
|
91
|
+
dateRangeWithSameId?.endDate ||
|
92
|
+
new Date(this.attr[DateRangeAttribute.END_DATE]);
|
67
93
|
if (Number.isFinite(endDate.getTime())) {
|
68
94
|
this._endDate = endDate;
|
69
95
|
}
|
@@ -78,6 +104,36 @@ export class DateRange {
|
|
78
104
|
return this.attr.CLASS;
|
79
105
|
}
|
80
106
|
|
107
|
+
get cue(): DateRangeCue {
|
108
|
+
const _cue = this._cue;
|
109
|
+
if (_cue === undefined) {
|
110
|
+
return (this._cue = this.attr.enumeratedStringList(
|
111
|
+
this.attr.CUE ? 'CUE' : 'X-CUE',
|
112
|
+
{
|
113
|
+
pre: false,
|
114
|
+
post: false,
|
115
|
+
once: false,
|
116
|
+
},
|
117
|
+
));
|
118
|
+
}
|
119
|
+
return _cue;
|
120
|
+
}
|
121
|
+
|
122
|
+
get startTime(): number {
|
123
|
+
const { tagAnchor } = this;
|
124
|
+
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
125
|
+
if (tagAnchor === null || tagAnchor.programDateTime === null) {
|
126
|
+
logger.warn(
|
127
|
+
`Expected tagAnchor Fragment with PDT set for DateRange "${this.id}": ${tagAnchor}`,
|
128
|
+
);
|
129
|
+
return NaN;
|
130
|
+
}
|
131
|
+
return (
|
132
|
+
tagAnchor.start +
|
133
|
+
(this.startDate.getTime() - tagAnchor.programDateTime) / 1000
|
134
|
+
);
|
135
|
+
}
|
136
|
+
|
81
137
|
get startDate(): Date {
|
82
138
|
return this._startDate;
|
83
139
|
}
|
@@ -120,13 +176,23 @@ export class DateRange {
|
|
120
176
|
return this.attr.bool(DateRangeAttribute.END_ON_NEXT);
|
121
177
|
}
|
122
178
|
|
179
|
+
get isInterstitial(): boolean {
|
180
|
+
return this.class === CLASS_INTERSTITIAL;
|
181
|
+
}
|
182
|
+
|
123
183
|
get isValid(): boolean {
|
124
184
|
return (
|
125
185
|
!!this.id &&
|
126
186
|
!this._badValueForSameId &&
|
127
187
|
Number.isFinite(this.startDate.getTime()) &&
|
128
188
|
(this.duration === null || this.duration >= 0) &&
|
129
|
-
(!this.endOnNext || !!this.class)
|
189
|
+
(!this.endOnNext || !!this.class) &&
|
190
|
+
(!this.attr.CUE ||
|
191
|
+
(!this.cue.pre && !this.cue.post) ||
|
192
|
+
this.cue.pre !== this.cue.post) &&
|
193
|
+
(!this.isInterstitial ||
|
194
|
+
'X-ASSET-URI' in this.attr ||
|
195
|
+
'X-ASSET-LIST' in this.attr)
|
130
196
|
);
|
131
197
|
}
|
132
198
|
}
|
package/src/loader/fragment.ts
CHANGED
@@ -90,6 +90,10 @@ export class BaseSegment {
|
|
90
90
|
}
|
91
91
|
}
|
92
92
|
|
93
|
+
export interface MediaFragment extends Fragment {
|
94
|
+
sn: number;
|
95
|
+
}
|
96
|
+
|
93
97
|
/**
|
94
98
|
* Object representing parsed data from an HLS Segment. Found in {@link hls.js#LevelDetails.fragments}.
|
95
99
|
*/
|
@@ -274,13 +278,13 @@ export class Part extends BaseSegment {
|
|
274
278
|
public readonly gap: boolean = false;
|
275
279
|
public readonly independent: boolean = false;
|
276
280
|
public readonly relurl: string;
|
277
|
-
public readonly fragment:
|
281
|
+
public readonly fragment: MediaFragment;
|
278
282
|
public readonly index: number;
|
279
283
|
public stats: LoadStats = new LoadStats();
|
280
284
|
|
281
285
|
constructor(
|
282
286
|
partAttrs: AttrList,
|
283
|
-
frag:
|
287
|
+
frag: MediaFragment,
|
284
288
|
baseurl: string,
|
285
289
|
index: number,
|
286
290
|
previous?: Part,
|
@@ -1,7 +1,7 @@
|
|
1
|
-
import { Part } from './fragment';
|
2
|
-
import type { Fragment } from './fragment';
|
3
|
-
import type { AttrList } from '../utils/attr-list';
|
1
|
+
import type { Part } from './fragment';
|
4
2
|
import type { DateRange } from './date-range';
|
3
|
+
import type { Fragment, MediaFragment } from './fragment';
|
4
|
+
import type { AttrList } from '../utils/attr-list';
|
5
5
|
import type { VariableMap } from '../types/level';
|
6
6
|
|
7
7
|
const DEFAULT_TARGET_DURATION = 10;
|
@@ -15,10 +15,11 @@ export class LevelDetails {
|
|
15
15
|
public averagetargetduration?: number;
|
16
16
|
public endCC: number = 0;
|
17
17
|
public endSN: number = 0;
|
18
|
-
public fragments:
|
19
|
-
public fragmentHint?:
|
18
|
+
public fragments: MediaFragment[];
|
19
|
+
public fragmentHint?: MediaFragment;
|
20
20
|
public partList: Part[] | null = null;
|
21
21
|
public dateRanges: Record<string, DateRange>;
|
22
|
+
public dateRangeTagCount: number = 0;
|
22
23
|
public live: boolean = true;
|
23
24
|
public ageHeader: number = 0;
|
24
25
|
public advancedDateTime?: number;
|
@@ -148,7 +149,7 @@ export class LevelDetails {
|
|
148
149
|
|
149
150
|
get lastPartSn(): number {
|
150
151
|
if (this.partList?.length) {
|
151
|
-
return this.partList[this.partList.length - 1].fragment.sn
|
152
|
+
return this.partList[this.partList.length - 1].fragment.sn;
|
152
153
|
}
|
153
154
|
return this.endSN;
|
154
155
|
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { buildAbsoluteURL } from 'url-toolkit';
|
2
2
|
import { DateRange } from './date-range';
|
3
|
-
import { Fragment, Part } from './fragment';
|
3
|
+
import { Fragment, MediaFragment, Part } from './fragment';
|
4
4
|
import { LevelDetails } from './level-details';
|
5
5
|
import { LevelKey } from './level-key';
|
6
6
|
import { AttrList } from '../utils/attr-list';
|
@@ -10,7 +10,6 @@ import {
|
|
10
10
|
hasVariableReferences,
|
11
11
|
importVariableDefinition,
|
12
12
|
substituteVariables,
|
13
|
-
substituteVariablesInAttributes,
|
14
13
|
} from '../utils/variable-substitution';
|
15
14
|
import { isCodecType } from '../utils/codecs';
|
16
15
|
import type { CodecType } from '../utils/codecs';
|
@@ -120,21 +119,7 @@ export default class M3U8Parser {
|
|
120
119
|
while ((result = MASTER_PLAYLIST_REGEX.exec(string)) != null) {
|
121
120
|
if (result[1]) {
|
122
121
|
// '#EXT-X-STREAM-INF' is found, parse level tag in group 1
|
123
|
-
const attrs = new AttrList(result[1]) as LevelAttributes;
|
124
|
-
if (__USE_VARIABLE_SUBSTITUTION__) {
|
125
|
-
substituteVariablesInAttributes(parsed, attrs, [
|
126
|
-
'CODECS',
|
127
|
-
'SUPPLEMENTAL-CODECS',
|
128
|
-
'ALLOWED-CPC',
|
129
|
-
'PATHWAY-ID',
|
130
|
-
'STABLE-VARIANT-ID',
|
131
|
-
'AUDIO',
|
132
|
-
'VIDEO',
|
133
|
-
'SUBTITLES',
|
134
|
-
'CLOSED-CAPTIONS',
|
135
|
-
'NAME',
|
136
|
-
]);
|
137
|
-
}
|
122
|
+
const attrs = new AttrList(result[1], parsed) as LevelAttributes;
|
138
123
|
const uri = __USE_VARIABLE_SUBSTITUTION__
|
139
124
|
? substituteVariables(parsed, result[2])
|
140
125
|
: result[2];
|
@@ -166,15 +151,7 @@ export default class M3U8Parser {
|
|
166
151
|
switch (tag) {
|
167
152
|
case 'SESSION-DATA': {
|
168
153
|
// #EXT-X-SESSION-DATA
|
169
|
-
const sessionAttrs = new AttrList(attributes);
|
170
|
-
if (__USE_VARIABLE_SUBSTITUTION__) {
|
171
|
-
substituteVariablesInAttributes(parsed, sessionAttrs, [
|
172
|
-
'DATA-ID',
|
173
|
-
'LANGUAGE',
|
174
|
-
'VALUE',
|
175
|
-
'URI',
|
176
|
-
]);
|
177
|
-
}
|
154
|
+
const sessionAttrs = new AttrList(attributes, parsed);
|
178
155
|
const dataId = sessionAttrs['DATA-ID'];
|
179
156
|
if (dataId) {
|
180
157
|
if (parsed.sessionData === null) {
|
@@ -202,26 +179,14 @@ export default class M3U8Parser {
|
|
202
179
|
case 'DEFINE': {
|
203
180
|
// #EXT-X-DEFINE
|
204
181
|
if (__USE_VARIABLE_SUBSTITUTION__) {
|
205
|
-
const variableAttributes = new AttrList(attributes);
|
206
|
-
substituteVariablesInAttributes(parsed, variableAttributes, [
|
207
|
-
'NAME',
|
208
|
-
'VALUE',
|
209
|
-
'QUERYPARAM',
|
210
|
-
]);
|
182
|
+
const variableAttributes = new AttrList(attributes, parsed);
|
211
183
|
addVariableDefinition(parsed, variableAttributes, baseurl);
|
212
184
|
}
|
213
185
|
break;
|
214
186
|
}
|
215
187
|
case 'CONTENT-STEERING': {
|
216
188
|
// #EXT-X-CONTENT-STEERING
|
217
|
-
const contentSteeringAttributes = new AttrList(attributes);
|
218
|
-
if (__USE_VARIABLE_SUBSTITUTION__) {
|
219
|
-
substituteVariablesInAttributes(
|
220
|
-
parsed,
|
221
|
-
contentSteeringAttributes,
|
222
|
-
['SERVER-URI', 'PATHWAY-ID'],
|
223
|
-
);
|
224
|
-
}
|
189
|
+
const contentSteeringAttributes = new AttrList(attributes, parsed);
|
225
190
|
parsed.contentSteering = {
|
226
191
|
uri: M3U8Parser.resolve(
|
227
192
|
contentSteeringAttributes['SERVER-URI'],
|
@@ -278,26 +243,13 @@ export default class M3U8Parser {
|
|
278
243
|
let id = 0;
|
279
244
|
MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0;
|
280
245
|
while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) !== null) {
|
281
|
-
const attrs = new AttrList(result[1]) as MediaAttributes;
|
246
|
+
const attrs = new AttrList(result[1], parsed) as MediaAttributes;
|
282
247
|
const type = attrs.TYPE;
|
283
248
|
if (type) {
|
284
249
|
const groups: (typeof groupsByType)[keyof typeof groupsByType] =
|
285
250
|
groupsByType[type];
|
286
251
|
const medias: MediaPlaylist[] = results[type] || [];
|
287
252
|
results[type] = medias;
|
288
|
-
if (__USE_VARIABLE_SUBSTITUTION__) {
|
289
|
-
substituteVariablesInAttributes(parsed, attrs, [
|
290
|
-
'URI',
|
291
|
-
'GROUP-ID',
|
292
|
-
'LANGUAGE',
|
293
|
-
'ASSOC-LANGUAGE',
|
294
|
-
'STABLE-RENDITION-ID',
|
295
|
-
'NAME',
|
296
|
-
'INSTREAM-ID',
|
297
|
-
'CHARACTERISTICS',
|
298
|
-
'CHANNELS',
|
299
|
-
]);
|
300
|
-
}
|
301
253
|
const lang = attrs.LANGUAGE;
|
302
254
|
const assocLang = attrs['ASSOC-LANGUAGE'];
|
303
255
|
const channels = attrs.CHANNELS;
|
@@ -355,6 +307,7 @@ export default class M3U8Parser {
|
|
355
307
|
): LevelDetails {
|
356
308
|
const level = new LevelDetails(baseurl);
|
357
309
|
const fragments: M3U8ParserFragments = level.fragments;
|
310
|
+
const programDateTimes: MediaFragment[] = [];
|
358
311
|
// The most recent init segment seen (applies to all subsequent segments)
|
359
312
|
let currentInitSegment: Fragment | null = null;
|
360
313
|
let currentSN = 0;
|
@@ -420,7 +373,11 @@ export default class M3U8Parser {
|
|
420
373
|
frag.relurl = __USE_VARIABLE_SUBSTITUTION__
|
421
374
|
? substituteVariables(level, uri)
|
422
375
|
: uri;
|
423
|
-
assignProgramDateTime(
|
376
|
+
assignProgramDateTime(
|
377
|
+
frag as MediaFragment,
|
378
|
+
prevFrag as MediaFragment,
|
379
|
+
programDateTimes,
|
380
|
+
);
|
424
381
|
prevFrag = frag;
|
425
382
|
totalduration += frag.duration;
|
426
383
|
currentSN++;
|
@@ -468,19 +425,19 @@ export default class M3U8Parser {
|
|
468
425
|
currentSN = level.startSN = parseInt(value1);
|
469
426
|
break;
|
470
427
|
case 'SKIP': {
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
]);
|
428
|
+
if (level.skippedSegments) {
|
429
|
+
level.playlistParsingError = new Error(
|
430
|
+
`#EXT-X-SKIP MUST NOT appear more than once in a Playlist`,
|
431
|
+
);
|
476
432
|
}
|
433
|
+
const skipAttrs = new AttrList(value1, level);
|
477
434
|
const skippedSegments =
|
478
435
|
skipAttrs.decimalInteger('SKIPPED-SEGMENTS');
|
479
436
|
if (Number.isFinite(skippedSegments)) {
|
480
|
-
level.skippedSegments
|
437
|
+
level.skippedSegments += skippedSegments;
|
481
438
|
// This will result in fragments[] containing undefined values, which we will fill in with `mergeDetails`
|
482
439
|
for (let i = skippedSegments; i--; ) {
|
483
|
-
fragments.
|
440
|
+
fragments.push(null);
|
484
441
|
}
|
485
442
|
currentSN += skippedSegments;
|
486
443
|
}
|
@@ -488,8 +445,9 @@ export default class M3U8Parser {
|
|
488
445
|
'RECENTLY-REMOVED-DATERANGES',
|
489
446
|
);
|
490
447
|
if (recentlyRemovedDateranges) {
|
491
|
-
level.recentlyRemovedDateranges =
|
492
|
-
recentlyRemovedDateranges
|
448
|
+
level.recentlyRemovedDateranges = (
|
449
|
+
level.recentlyRemovedDateranges || []
|
450
|
+
).concat(recentlyRemovedDateranges.split('\t'));
|
493
451
|
}
|
494
452
|
break;
|
495
453
|
}
|
@@ -522,27 +480,13 @@ export default class M3U8Parser {
|
|
522
480
|
frag.tagList.push([tag, value1]);
|
523
481
|
break;
|
524
482
|
case 'DATERANGE': {
|
525
|
-
const dateRangeAttr = new AttrList(value1);
|
526
|
-
if (__USE_VARIABLE_SUBSTITUTION__) {
|
527
|
-
substituteVariablesInAttributes(level, dateRangeAttr, [
|
528
|
-
'ID',
|
529
|
-
'CLASS',
|
530
|
-
'START-DATE',
|
531
|
-
'END-DATE',
|
532
|
-
'SCTE35-CMD',
|
533
|
-
'SCTE35-OUT',
|
534
|
-
'SCTE35-IN',
|
535
|
-
]);
|
536
|
-
substituteVariablesInAttributes(
|
537
|
-
level,
|
538
|
-
dateRangeAttr,
|
539
|
-
dateRangeAttr.clientAttrs,
|
540
|
-
);
|
541
|
-
}
|
483
|
+
const dateRangeAttr = new AttrList(value1, level);
|
542
484
|
const dateRange = new DateRange(
|
543
485
|
dateRangeAttr,
|
544
486
|
level.dateRanges[dateRangeAttr.ID],
|
487
|
+
level.dateRangeTagCount,
|
545
488
|
);
|
489
|
+
level.dateRangeTagCount++;
|
546
490
|
if (dateRange.isValid || level.skippedSegments) {
|
547
491
|
level.dateRanges[dateRange.id] = dateRange;
|
548
492
|
} else {
|
@@ -554,13 +498,7 @@ export default class M3U8Parser {
|
|
554
498
|
}
|
555
499
|
case 'DEFINE': {
|
556
500
|
if (__USE_VARIABLE_SUBSTITUTION__) {
|
557
|
-
const variableAttributes = new AttrList(value1);
|
558
|
-
substituteVariablesInAttributes(level, variableAttributes, [
|
559
|
-
'NAME',
|
560
|
-
'VALUE',
|
561
|
-
'IMPORT',
|
562
|
-
'QUERYPARAM',
|
563
|
-
]);
|
501
|
+
const variableAttributes = new AttrList(value1, level);
|
564
502
|
if ('IMPORT' in variableAttributes) {
|
565
503
|
importVariableDefinition(
|
566
504
|
level,
|
@@ -600,13 +538,7 @@ export default class M3U8Parser {
|
|
600
538
|
level.startTimeOffset = parseStartTimeOffset(value1);
|
601
539
|
break;
|
602
540
|
case 'MAP': {
|
603
|
-
const mapAttrs = new AttrList(value1);
|
604
|
-
if (__USE_VARIABLE_SUBSTITUTION__) {
|
605
|
-
substituteVariablesInAttributes(level, mapAttrs, [
|
606
|
-
'BYTERANGE',
|
607
|
-
'URI',
|
608
|
-
]);
|
609
|
-
}
|
541
|
+
const mapAttrs = new AttrList(value1, level);
|
610
542
|
if (frag.duration) {
|
611
543
|
// Initial segment tag is after segment duration tag.
|
612
544
|
// #EXTINF: 6.0
|
@@ -635,6 +567,7 @@ export default class M3U8Parser {
|
|
635
567
|
currentInitSegment = frag;
|
636
568
|
createNextFrag = true;
|
637
569
|
}
|
570
|
+
currentInitSegment.cc = discontinuityCounter;
|
638
571
|
break;
|
639
572
|
}
|
640
573
|
case 'SERVER-CONTROL': {
|
@@ -667,16 +600,10 @@ export default class M3U8Parser {
|
|
667
600
|
const previousFragmentPart =
|
668
601
|
currentPart > 0 ? partList[partList.length - 1] : undefined;
|
669
602
|
const index = currentPart++;
|
670
|
-
const partAttrs = new AttrList(value1);
|
671
|
-
if (__USE_VARIABLE_SUBSTITUTION__) {
|
672
|
-
substituteVariablesInAttributes(level, partAttrs, [
|
673
|
-
'BYTERANGE',
|
674
|
-
'URI',
|
675
|
-
]);
|
676
|
-
}
|
603
|
+
const partAttrs = new AttrList(value1, level);
|
677
604
|
const part = new Part(
|
678
605
|
partAttrs,
|
679
|
-
frag,
|
606
|
+
frag as MediaFragment,
|
680
607
|
baseurl,
|
681
608
|
index,
|
682
609
|
previousFragmentPart,
|
@@ -686,20 +613,12 @@ export default class M3U8Parser {
|
|
686
613
|
break;
|
687
614
|
}
|
688
615
|
case 'PRELOAD-HINT': {
|
689
|
-
const preloadHintAttrs = new AttrList(value1);
|
690
|
-
if (__USE_VARIABLE_SUBSTITUTION__) {
|
691
|
-
substituteVariablesInAttributes(level, preloadHintAttrs, ['URI']);
|
692
|
-
}
|
616
|
+
const preloadHintAttrs = new AttrList(value1, level);
|
693
617
|
level.preloadHint = preloadHintAttrs;
|
694
618
|
break;
|
695
619
|
}
|
696
620
|
case 'RENDITION-REPORT': {
|
697
|
-
const renditionReportAttrs = new AttrList(value1);
|
698
|
-
if (__USE_VARIABLE_SUBSTITUTION__) {
|
699
|
-
substituteVariablesInAttributes(level, renditionReportAttrs, [
|
700
|
-
'URI',
|
701
|
-
]);
|
702
|
-
}
|
621
|
+
const renditionReportAttrs = new AttrList(value1, level);
|
703
622
|
level.renditionReports = level.renditionReports || [];
|
704
623
|
level.renditionReports.push(renditionReportAttrs);
|
705
624
|
break;
|
@@ -714,12 +633,16 @@ export default class M3U8Parser {
|
|
714
633
|
fragments.pop();
|
715
634
|
totalduration -= prevFrag.duration;
|
716
635
|
if (level.partList) {
|
717
|
-
level.fragmentHint = prevFrag;
|
636
|
+
level.fragmentHint = prevFrag as MediaFragment;
|
718
637
|
}
|
719
638
|
} else if (level.partList) {
|
720
|
-
assignProgramDateTime(
|
639
|
+
assignProgramDateTime(
|
640
|
+
frag as MediaFragment,
|
641
|
+
prevFrag as MediaFragment,
|
642
|
+
programDateTimes,
|
643
|
+
);
|
721
644
|
frag.cc = discontinuityCounter;
|
722
|
-
level.fragmentHint = frag;
|
645
|
+
level.fragmentHint = frag as MediaFragment;
|
723
646
|
if (levelkeys) {
|
724
647
|
setFragLevelKeys(frag, levelkeys, level);
|
725
648
|
}
|
@@ -738,6 +661,21 @@ export default class M3U8Parser {
|
|
738
661
|
if (firstFragment) {
|
739
662
|
level.startCC = firstFragment.cc;
|
740
663
|
}
|
664
|
+
/**
|
665
|
+
* Backfill any missing PDT values
|
666
|
+
* "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after
|
667
|
+
* one or more Media Segment URIs, the client SHOULD extrapolate
|
668
|
+
* backward from that tag (using EXTINF durations and/or media
|
669
|
+
* timestamps) to associate dates with those segments."
|
670
|
+
* We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs
|
671
|
+
* computed.
|
672
|
+
*/
|
673
|
+
if (firstPdtIndex > 0) {
|
674
|
+
backfillProgramDateTimes(fragments, firstPdtIndex);
|
675
|
+
if (firstFragment) {
|
676
|
+
programDateTimes.unshift(firstFragment as MediaFragment);
|
677
|
+
}
|
678
|
+
}
|
741
679
|
} else {
|
742
680
|
level.endSN = 0;
|
743
681
|
level.startCC = 0;
|
@@ -746,23 +684,83 @@ export default class M3U8Parser {
|
|
746
684
|
totalduration += level.fragmentHint.duration;
|
747
685
|
}
|
748
686
|
level.totalduration = totalduration;
|
687
|
+
if (programDateTimes.length && level.dateRangeTagCount && firstFragment) {
|
688
|
+
mapDateRanges(programDateTimes, level);
|
689
|
+
}
|
690
|
+
|
749
691
|
level.endCC = discontinuityCounter;
|
750
692
|
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
693
|
+
return level;
|
694
|
+
}
|
695
|
+
}
|
696
|
+
|
697
|
+
export function mapDateRanges(
|
698
|
+
programDateTimes: MediaFragment[],
|
699
|
+
details: LevelDetails,
|
700
|
+
) {
|
701
|
+
// Make sure DateRanges are mapped to a ProgramDateTime tag that applies a date to a segment that overlaps with its start date
|
702
|
+
const programDateTimeCount = programDateTimes.length;
|
703
|
+
const lastProgramDateTime = programDateTimes[programDateTimeCount - 1];
|
704
|
+
const playlistEnd = details.live ? Infinity : details.totalduration;
|
705
|
+
const dateRangeIds = Object.keys(details.dateRanges);
|
706
|
+
for (let i = dateRangeIds.length; i--; ) {
|
707
|
+
const dateRange = details.dateRanges[dateRangeIds[i]];
|
708
|
+
const startDateTime = dateRange.startDate.getTime();
|
709
|
+
dateRange.tagAnchor = lastProgramDateTime;
|
710
|
+
for (let j = programDateTimeCount; j--; ) {
|
711
|
+
const fragIndex = findFragmentWithStartDate(
|
712
|
+
details,
|
713
|
+
startDateTime,
|
714
|
+
programDateTimes,
|
715
|
+
j,
|
716
|
+
playlistEnd,
|
717
|
+
);
|
718
|
+
if (fragIndex !== -1) {
|
719
|
+
dateRange.tagAnchor = details.fragments[fragIndex];
|
720
|
+
break;
|
721
|
+
}
|
762
722
|
}
|
723
|
+
}
|
724
|
+
}
|
763
725
|
|
764
|
-
|
726
|
+
function findFragmentWithStartDate(
|
727
|
+
details: LevelDetails,
|
728
|
+
startDateTime: number,
|
729
|
+
programDateTimes: MediaFragment[],
|
730
|
+
index: number,
|
731
|
+
endTime: number,
|
732
|
+
): number {
|
733
|
+
const pdtFragment = programDateTimes[index];
|
734
|
+
if (pdtFragment) {
|
735
|
+
// find matching range between PDT tags
|
736
|
+
const durationBetweenPdt =
|
737
|
+
(programDateTimes[index + 1]?.start || endTime) - pdtFragment.start;
|
738
|
+
const pdtStart = pdtFragment.programDateTime as number;
|
739
|
+
if (
|
740
|
+
(startDateTime >= pdtStart || index === 0) &&
|
741
|
+
startDateTime <= pdtStart + durationBetweenPdt * 1000
|
742
|
+
) {
|
743
|
+
// map to fragment with date-time range
|
744
|
+
const startIndex = programDateTimes[index].sn - details.startSN;
|
745
|
+
const fragments = details.fragments;
|
746
|
+
if (fragments.length > programDateTimes.length) {
|
747
|
+
const endSegment =
|
748
|
+
programDateTimes[index + 1] || fragments[fragments.length - 1];
|
749
|
+
const endIndex = endSegment.sn - details.startSN;
|
750
|
+
for (let i = endIndex; i > startIndex; i--) {
|
751
|
+
const fragStartDateTime = fragments[i].programDateTime as number;
|
752
|
+
if (
|
753
|
+
startDateTime >= fragStartDateTime &&
|
754
|
+
startDateTime < fragStartDateTime + fragments[i].duration * 1000
|
755
|
+
) {
|
756
|
+
return i;
|
757
|
+
}
|
758
|
+
}
|
759
|
+
}
|
760
|
+
return startIndex;
|
761
|
+
}
|
765
762
|
}
|
763
|
+
return -1;
|
766
764
|
}
|
767
765
|
|
768
766
|
function parseKey(
|
@@ -771,16 +769,7 @@ function parseKey(
|
|
771
769
|
parsed: ParsedMultivariantPlaylist | LevelDetails,
|
772
770
|
): LevelKey {
|
773
771
|
// https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
774
|
-
const keyAttrs = new AttrList(keyTagAttributes);
|
775
|
-
if (__USE_VARIABLE_SUBSTITUTION__) {
|
776
|
-
substituteVariablesInAttributes(parsed, keyAttrs, [
|
777
|
-
'KEYFORMAT',
|
778
|
-
'KEYFORMATVERSIONS',
|
779
|
-
'URI',
|
780
|
-
'IV',
|
781
|
-
'URI',
|
782
|
-
]);
|
783
|
-
}
|
772
|
+
const keyAttrs = new AttrList(keyTagAttributes, parsed);
|
784
773
|
const decryptmethod = keyAttrs.METHOD ?? '';
|
785
774
|
const decrypturi = keyAttrs.URI;
|
786
775
|
const decryptiv = keyAttrs.hexadecimalInteger('IV');
|
@@ -864,17 +853,22 @@ function backfillProgramDateTimes(
|
|
864
853
|
}
|
865
854
|
}
|
866
855
|
|
867
|
-
function assignProgramDateTime(
|
856
|
+
export function assignProgramDateTime(
|
857
|
+
frag: MediaFragment,
|
858
|
+
prevFrag: MediaFragment | null,
|
859
|
+
programDateTimes: MediaFragment[],
|
860
|
+
) {
|
868
861
|
if (frag.rawProgramDateTime) {
|
869
862
|
frag.programDateTime = Date.parse(frag.rawProgramDateTime);
|
863
|
+
if (!Number.isFinite(frag.programDateTime)) {
|
864
|
+
frag.programDateTime = null;
|
865
|
+
frag.rawProgramDateTime = null;
|
866
|
+
return;
|
867
|
+
}
|
868
|
+
programDateTimes.push(frag);
|
870
869
|
} else if (prevFrag?.programDateTime) {
|
871
870
|
frag.programDateTime = prevFrag.endProgramDateTime;
|
872
871
|
}
|
873
|
-
|
874
|
-
if (!Number.isFinite(frag.programDateTime)) {
|
875
|
-
frag.programDateTime = null;
|
876
|
-
frag.rawProgramDateTime = null;
|
877
|
-
}
|
878
872
|
}
|
879
873
|
|
880
874
|
function setInitSegment(
|
package/src/types/events.ts
CHANGED
@@ -1,7 +1,4 @@
|
|
1
|
-
|
2
|
-
import type { Fragment } from '../loader/fragment';
|
3
|
-
// eslint-disable-next-line import/no-duplicates
|
4
|
-
import type { Part } from '../loader/fragment';
|
1
|
+
import type { Fragment, MediaFragment, Part } from '../loader/fragment';
|
5
2
|
import type { LevelDetails } from '../loader/level-details';
|
6
3
|
import type {
|
7
4
|
HdcpLevel,
|
@@ -321,7 +318,7 @@ export interface NonNativeTextTracksData {
|
|
321
318
|
|
322
319
|
export interface InitPTSFoundData {
|
323
320
|
id: string;
|
324
|
-
frag:
|
321
|
+
frag: MediaFragment;
|
325
322
|
initPTS: number;
|
326
323
|
timescale: number;
|
327
324
|
}
|