hls.js 1.5.12-0.canary.10338 → 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 +2 -2
- 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
@@ -1,9 +1,9 @@
|
|
1
|
-
import type {
|
1
|
+
import type { MediaFragment } from '../loader/fragment';
|
2
2
|
import type { SourceBufferName } from './buffer';
|
3
3
|
import type { FragLoadedData } from './events';
|
4
4
|
|
5
5
|
export interface FragmentEntity {
|
6
|
-
body:
|
6
|
+
body: MediaFragment;
|
7
7
|
// appendedPTS is the latest buffered presentation time within the fragment's time range.
|
8
8
|
// It is used to determine: which fragment is appended at any given position, and hls.currentLevel.
|
9
9
|
appendedPTS: number | null;
|
package/src/utils/attr-list.ts
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
import type { LevelDetails } from '../loader/level-details';
|
2
|
+
import type { ParsedMultivariantPlaylist } from '../loader/m3u8-parser';
|
3
|
+
import { logger } from './logger';
|
4
|
+
import { substituteVariables } from './variable-substitution';
|
5
|
+
|
1
6
|
const DECIMAL_RESOLUTION_REGEX = /^(\d+)x(\d+)$/;
|
2
7
|
const ATTR_LIST_REGEX = /(.+?)=(".*?"|.*?)(?:,|$)/g;
|
3
8
|
|
@@ -5,9 +10,15 @@ const ATTR_LIST_REGEX = /(.+?)=(".*?"|.*?)(?:,|$)/g;
|
|
5
10
|
export class AttrList {
|
6
11
|
[key: string]: any;
|
7
12
|
|
8
|
-
constructor(
|
13
|
+
constructor(
|
14
|
+
attrs: string | Record<string, any>,
|
15
|
+
parsed?: Pick<
|
16
|
+
ParsedMultivariantPlaylist | LevelDetails,
|
17
|
+
'variableList' | 'hasVariableRefs' | 'playlistParsingError'
|
18
|
+
>,
|
19
|
+
) {
|
9
20
|
if (typeof attrs === 'string') {
|
10
|
-
attrs = AttrList.parseAttrList(attrs);
|
21
|
+
attrs = AttrList.parseAttrList(attrs, parsed);
|
11
22
|
}
|
12
23
|
Object.assign(this, attrs);
|
13
24
|
}
|
@@ -63,6 +74,20 @@ export class AttrList {
|
|
63
74
|
return this[attrName];
|
64
75
|
}
|
65
76
|
|
77
|
+
enumeratedStringList<T extends { [key: string]: boolean }>(
|
78
|
+
attrName: string,
|
79
|
+
dict: T,
|
80
|
+
): { [key in keyof T]: boolean } {
|
81
|
+
const attrValue = this[attrName];
|
82
|
+
return (attrValue ? attrValue.split(/[ ,]+/) : []).reduce(
|
83
|
+
(result: { [key in keyof T]: boolean }, identifier: string) => {
|
84
|
+
result[identifier.toLowerCase() as keyof T] = true;
|
85
|
+
return result;
|
86
|
+
},
|
87
|
+
dict,
|
88
|
+
);
|
89
|
+
}
|
90
|
+
|
66
91
|
bool(attrName: string): boolean {
|
67
92
|
return this[attrName] === 'YES';
|
68
93
|
}
|
@@ -84,21 +109,83 @@ export class AttrList {
|
|
84
109
|
};
|
85
110
|
}
|
86
111
|
|
87
|
-
static parseAttrList(
|
88
|
-
|
112
|
+
static parseAttrList(
|
113
|
+
input: string,
|
114
|
+
parsed?: Pick<
|
115
|
+
ParsedMultivariantPlaylist | LevelDetails,
|
116
|
+
'variableList' | 'hasVariableRefs' | 'playlistParsingError'
|
117
|
+
>,
|
118
|
+
): Record<string, string> {
|
119
|
+
let match: RegExpExecArray | null;
|
89
120
|
const attrs = {};
|
90
121
|
const quote = '"';
|
91
122
|
ATTR_LIST_REGEX.lastIndex = 0;
|
92
123
|
while ((match = ATTR_LIST_REGEX.exec(input)) !== null) {
|
124
|
+
const name = match[1].trim();
|
93
125
|
let value = match[2];
|
94
|
-
|
95
|
-
if (
|
126
|
+
const quotedString =
|
96
127
|
value.indexOf(quote) === 0 &&
|
97
|
-
value.lastIndexOf(quote) === value.length - 1
|
98
|
-
|
128
|
+
value.lastIndexOf(quote) === value.length - 1;
|
129
|
+
let hexadecimalSequence = false;
|
130
|
+
if (quotedString) {
|
99
131
|
value = value.slice(1, -1);
|
132
|
+
} else {
|
133
|
+
switch (name) {
|
134
|
+
case 'IV':
|
135
|
+
case 'SCTE35-CMD':
|
136
|
+
case 'SCTE35-IN':
|
137
|
+
case 'SCTE35-OUT':
|
138
|
+
hexadecimalSequence = true;
|
139
|
+
}
|
140
|
+
}
|
141
|
+
if (parsed && (quotedString || hexadecimalSequence)) {
|
142
|
+
if (__USE_VARIABLE_SUBSTITUTION__) {
|
143
|
+
value = substituteVariables(parsed, value);
|
144
|
+
}
|
145
|
+
} else if (!hexadecimalSequence && !quotedString) {
|
146
|
+
switch (name) {
|
147
|
+
case 'CLOSED-CAPTIONS':
|
148
|
+
if (value === 'NONE') {
|
149
|
+
break;
|
150
|
+
}
|
151
|
+
// falls through
|
152
|
+
case 'ALLOWED-CPC':
|
153
|
+
case 'CLASS':
|
154
|
+
case 'ASSOC-LANGUAGE':
|
155
|
+
case 'AUDIO':
|
156
|
+
case 'BYTERANGE':
|
157
|
+
case 'CHANNELS':
|
158
|
+
case 'CHARACTERISTICS':
|
159
|
+
case 'CODECS':
|
160
|
+
case 'DATA-ID':
|
161
|
+
case 'END-DATE':
|
162
|
+
case 'GROUP-ID':
|
163
|
+
case 'ID':
|
164
|
+
case 'IMPORT':
|
165
|
+
case 'INSTREAM-ID':
|
166
|
+
case 'KEYFORMAT':
|
167
|
+
case 'KEYFORMATVERSIONS':
|
168
|
+
case 'LANGUAGE':
|
169
|
+
case 'NAME':
|
170
|
+
case 'PATHWAY-ID':
|
171
|
+
case 'QUERYPARAM':
|
172
|
+
case 'RECENTLY-REMOVED-DATERANGES':
|
173
|
+
case 'SERVER-URI':
|
174
|
+
case 'STABLE-RENDITION-ID':
|
175
|
+
case 'STABLE-VARIANT-ID':
|
176
|
+
case 'START-DATE':
|
177
|
+
case 'SUBTITLES':
|
178
|
+
case 'SUPPLEMENTAL-CODECS':
|
179
|
+
case 'URI':
|
180
|
+
case 'VALUE':
|
181
|
+
case 'VIDEO':
|
182
|
+
case 'X-ASSET-LIST':
|
183
|
+
case 'X-ASSET-URI':
|
184
|
+
// Since we are not checking tag:attribute combination, just warn rather than ignoring attribute
|
185
|
+
logger.warn(`${input}: attribute ${name} is missing quotes`);
|
186
|
+
// continue;
|
187
|
+
}
|
100
188
|
}
|
101
|
-
const name = match[1].trim();
|
102
189
|
attrs[name] = value;
|
103
190
|
}
|
104
191
|
return attrs;
|
@@ -3,16 +3,17 @@
|
|
3
3
|
*/
|
4
4
|
|
5
5
|
import { logger } from './logger';
|
6
|
-
import { Fragment, Part } from '../loader/fragment';
|
7
|
-
import { LevelDetails } from '../loader/level-details';
|
8
|
-
import type { Level } from '../types/level';
|
9
6
|
import { DateRange } from '../loader/date-range';
|
7
|
+
import { assignProgramDateTime, mapDateRanges } from '../loader/m3u8-parser';
|
8
|
+
import type { Fragment, MediaFragment, Part } from '../loader/fragment';
|
9
|
+
import type { LevelDetails } from '../loader/level-details';
|
10
|
+
import type { Level } from '../types/level';
|
10
11
|
|
11
12
|
type FragmentIntersection = (oldFrag: Fragment, newFrag: Fragment) => void;
|
12
13
|
type PartIntersection = (oldPart: Part, newPart: Part) => void;
|
13
14
|
|
14
15
|
export function updatePTS(
|
15
|
-
fragments:
|
16
|
+
fragments: MediaFragment[],
|
16
17
|
fromIdx: number,
|
17
18
|
toIdx: number,
|
18
19
|
): void {
|
@@ -21,7 +22,7 @@ export function updatePTS(
|
|
21
22
|
updateFromToPTS(fragFrom, fragTo);
|
22
23
|
}
|
23
24
|
|
24
|
-
function updateFromToPTS(fragFrom:
|
25
|
+
function updateFromToPTS(fragFrom: MediaFragment, fragTo: MediaFragment) {
|
25
26
|
const fragToPTS = fragTo.startPTS as number;
|
26
27
|
// if we know startPTS[toIdx]
|
27
28
|
if (Number.isFinite(fragToPTS)) {
|
@@ -55,7 +56,7 @@ function updateFromToPTS(fragFrom: Fragment, fragTo: Fragment) {
|
|
55
56
|
|
56
57
|
export function updateFragPTSDTS(
|
57
58
|
details: LevelDetails | undefined,
|
58
|
-
frag:
|
59
|
+
frag: MediaFragment,
|
59
60
|
startPTS: number,
|
60
61
|
endPTS: number,
|
61
62
|
startDTS: number,
|
@@ -82,11 +83,11 @@ export function updateFragPTSDTS(
|
|
82
83
|
|
83
84
|
maxStartPTS = Math.max(startPTS, fragStartPts);
|
84
85
|
startPTS = Math.min(startPTS, fragStartPts);
|
85
|
-
startDTS = Math.min(startDTS, frag.startDTS);
|
86
|
+
startDTS = Math.min(startDTS, frag.startDTS as number);
|
86
87
|
|
87
88
|
minEndPTS = Math.min(endPTS, fragEndPts);
|
88
89
|
endPTS = Math.max(endPTS, fragEndPts);
|
89
|
-
endDTS = Math.max(endDTS, frag.endDTS);
|
90
|
+
endDTS = Math.max(endDTS, frag.endDTS as number);
|
90
91
|
}
|
91
92
|
|
92
93
|
const drift = startPTS - frag.start;
|
@@ -101,7 +102,7 @@ export function updateFragPTSDTS(
|
|
101
102
|
frag.minEndPTS = minEndPTS;
|
102
103
|
frag.endDTS = endDTS;
|
103
104
|
|
104
|
-
const sn = frag.sn
|
105
|
+
const sn = frag.sn;
|
105
106
|
// exit if sn out of range
|
106
107
|
if (!details || sn < details.startSN || sn > details.endSN) {
|
107
108
|
return 0;
|
@@ -196,10 +197,10 @@ export function mergeDetails(
|
|
196
197
|
},
|
197
198
|
);
|
198
199
|
|
200
|
+
const fragmentsToCheck = newDetails.fragmentHint
|
201
|
+
? newDetails.fragments.concat(newDetails.fragmentHint)
|
202
|
+
: newDetails.fragments;
|
199
203
|
if (currentInitSegment) {
|
200
|
-
const fragmentsToCheck = newDetails.fragmentHint
|
201
|
-
? newDetails.fragments.concat(newDetails.fragmentHint)
|
202
|
-
: newDetails.fragments;
|
203
204
|
fragmentsToCheck.forEach((frag) => {
|
204
205
|
if (
|
205
206
|
frag &&
|
@@ -220,14 +221,30 @@ export function mergeDetails(
|
|
220
221
|
for (let i = newDetails.skippedSegments; i--; ) {
|
221
222
|
newDetails.fragments.shift();
|
222
223
|
}
|
223
|
-
newDetails.startSN = newDetails.fragments[0].sn
|
224
|
+
newDetails.startSN = newDetails.fragments[0].sn;
|
224
225
|
newDetails.startCC = newDetails.fragments[0].cc;
|
225
|
-
} else
|
226
|
-
newDetails.
|
227
|
-
|
228
|
-
|
229
|
-
|
226
|
+
} else {
|
227
|
+
if (newDetails.canSkipDateRanges) {
|
228
|
+
newDetails.dateRanges = mergeDateRanges(
|
229
|
+
oldDetails.dateRanges,
|
230
|
+
newDetails,
|
231
|
+
);
|
232
|
+
}
|
233
|
+
const programDateTimes = oldDetails.fragments.filter(
|
234
|
+
(frag) => frag.rawProgramDateTime,
|
230
235
|
);
|
236
|
+
if (oldDetails.hasProgramDateTime && !newDetails.hasProgramDateTime) {
|
237
|
+
for (let i = 1; i < fragmentsToCheck.length; i++) {
|
238
|
+
if (fragmentsToCheck[i].programDateTime === null) {
|
239
|
+
assignProgramDateTime(
|
240
|
+
fragmentsToCheck[i],
|
241
|
+
fragmentsToCheck[i - 1],
|
242
|
+
programDateTimes,
|
243
|
+
);
|
244
|
+
}
|
245
|
+
}
|
246
|
+
}
|
247
|
+
mapDateRanges(programDateTimes, newDetails);
|
231
248
|
}
|
232
249
|
}
|
233
250
|
|
@@ -293,27 +310,38 @@ export function mergeDetails(
|
|
293
310
|
|
294
311
|
function mergeDateRanges(
|
295
312
|
oldDateRanges: Record<string, DateRange>,
|
296
|
-
|
297
|
-
recentlyRemovedDateranges: string[] | undefined,
|
313
|
+
newDetails: LevelDetails,
|
298
314
|
): Record<string, DateRange> {
|
315
|
+
const { dateRanges: deltaDateRanges, recentlyRemovedDateranges } = newDetails;
|
299
316
|
const dateRanges = Object.assign({}, oldDateRanges);
|
300
317
|
if (recentlyRemovedDateranges) {
|
301
318
|
recentlyRemovedDateranges.forEach((id) => {
|
302
319
|
delete dateRanges[id];
|
303
320
|
});
|
304
321
|
}
|
305
|
-
Object.keys(
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
)}"`,
|
322
|
+
const mergeIds = Object.keys(dateRanges);
|
323
|
+
const mergeCount = mergeIds.length;
|
324
|
+
if (mergeCount) {
|
325
|
+
Object.keys(deltaDateRanges).forEach((id) => {
|
326
|
+
const mergedDateRange = dateRanges[id];
|
327
|
+
const dateRange = new DateRange(
|
328
|
+
deltaDateRanges[id].attr,
|
329
|
+
mergedDateRange,
|
314
330
|
);
|
315
|
-
|
316
|
-
|
331
|
+
if (dateRange.isValid) {
|
332
|
+
dateRanges[id] = dateRange;
|
333
|
+
if (!mergedDateRange) {
|
334
|
+
dateRange.tagOrder += mergeCount;
|
335
|
+
}
|
336
|
+
} else {
|
337
|
+
logger.warn(
|
338
|
+
`Ignoring invalid Playlist Delta Update DATERANGE tag: "${JSON.stringify(
|
339
|
+
deltaDateRanges[id].attr,
|
340
|
+
)}"`,
|
341
|
+
);
|
342
|
+
}
|
343
|
+
});
|
344
|
+
}
|
317
345
|
return dateRanges;
|
318
346
|
}
|
319
347
|
|
@@ -366,7 +394,7 @@ export function mapFragmentIntersection(
|
|
366
394
|
for (let i = start; i <= end; i++) {
|
367
395
|
const oldFrag = oldFrags[delta + i];
|
368
396
|
let newFrag = newFrags[i];
|
369
|
-
if (skippedSegments && !newFrag &&
|
397
|
+
if (skippedSegments && !newFrag && oldFrag) {
|
370
398
|
// Fill in skipped segments in delta playlist
|
371
399
|
newFrag = newDetails.fragments[i] = oldFrag;
|
372
400
|
}
|
@@ -436,22 +464,22 @@ export function getFragmentWithSN(
|
|
436
464
|
level: Level,
|
437
465
|
sn: number,
|
438
466
|
fragCurrent: Fragment | null,
|
439
|
-
):
|
440
|
-
|
467
|
+
): MediaFragment | null {
|
468
|
+
const details = level?.details;
|
469
|
+
if (!details) {
|
441
470
|
return null;
|
442
471
|
}
|
443
|
-
|
444
|
-
|
445
|
-
levelDetails.fragments[sn - levelDetails.startSN];
|
472
|
+
let fragment: MediaFragment | undefined =
|
473
|
+
details.fragments[sn - details.startSN];
|
446
474
|
if (fragment) {
|
447
475
|
return fragment;
|
448
476
|
}
|
449
|
-
fragment =
|
477
|
+
fragment = details.fragmentHint;
|
450
478
|
if (fragment && fragment.sn === sn) {
|
451
479
|
return fragment;
|
452
480
|
}
|
453
|
-
if (sn <
|
454
|
-
return fragCurrent;
|
481
|
+
if (sn < details.startSN && fragCurrent && fragCurrent.sn === sn) {
|
482
|
+
return fragCurrent as MediaFragment;
|
455
483
|
}
|
456
484
|
return null;
|
457
485
|
}
|
@@ -9,25 +9,6 @@ export function hasVariableReferences(str: string): boolean {
|
|
9
9
|
return VARIABLE_REPLACEMENT_REGEX.test(str);
|
10
10
|
}
|
11
11
|
|
12
|
-
export function substituteVariablesInAttributes(
|
13
|
-
parsed: Pick<
|
14
|
-
ParsedMultivariantPlaylist | LevelDetails,
|
15
|
-
'variableList' | 'hasVariableRefs' | 'playlistParsingError'
|
16
|
-
>,
|
17
|
-
attr: AttrList,
|
18
|
-
attributeNames: string[],
|
19
|
-
) {
|
20
|
-
if (parsed.variableList !== null || parsed.hasVariableRefs) {
|
21
|
-
for (let i = attributeNames.length; i--; ) {
|
22
|
-
const name = attributeNames[i];
|
23
|
-
const value = attr[name];
|
24
|
-
if (value) {
|
25
|
-
attr[name] = substituteVariables(parsed, value);
|
26
|
-
}
|
27
|
-
}
|
28
|
-
}
|
29
|
-
}
|
30
|
-
|
31
12
|
export function substituteVariables(
|
32
13
|
parsed: Pick<
|
33
14
|
ParsedMultivariantPlaylist | LevelDetails,
|