hls.js 1.5.12-0.canary.10340 → 1.5.12-0.canary.10343

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.
@@ -1,9 +1,9 @@
1
- import type { Fragment } from '../loader/fragment';
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: Fragment;
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;
@@ -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(attrs: string | Record<string, any>) {
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(input: string): Record<string, any> {
88
- let match;
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: Fragment[],
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: Fragment, fragTo: Fragment) {
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: Fragment,
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 as number; // 'initSegment'
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 as number;
224
+ newDetails.startSN = newDetails.fragments[0].sn;
224
225
  newDetails.startCC = newDetails.fragments[0].cc;
225
- } else if (newDetails.canSkipDateRanges) {
226
- newDetails.dateRanges = mergeDateRanges(
227
- oldDetails.dateRanges,
228
- newDetails.dateRanges,
229
- newDetails.recentlyRemovedDateranges,
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
- deltaDateRanges: Record<string, DateRange>,
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(deltaDateRanges).forEach((id) => {
306
- const dateRange = new DateRange(deltaDateRanges[id].attr, dateRanges[id]);
307
- if (dateRange.isValid) {
308
- dateRanges[id] = dateRange;
309
- } else {
310
- logger.warn(
311
- `Ignoring invalid Playlist Delta Update DATERANGE tag: "${JSON.stringify(
312
- deltaDateRanges[id].attr,
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 && i < skippedSegments) {
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
- ): Fragment | null {
440
- if (!level?.details) {
467
+ ): MediaFragment | null {
468
+ const details = level?.details;
469
+ if (!details) {
441
470
  return null;
442
471
  }
443
- const levelDetails = level.details;
444
- let fragment: Fragment | undefined =
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 = levelDetails.fragmentHint;
477
+ fragment = details.fragmentHint;
450
478
  if (fragment && fragment.sn === sn) {
451
479
  return fragment;
452
480
  }
453
- if (sn < levelDetails.startSN && fragCurrent && fragCurrent.sn === 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,