hls.js 1.5.14-0.canary.10515 → 1.5.14

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.
Files changed (107) hide show
  1. package/README.md +3 -4
  2. package/dist/hls-demo.js +38 -41
  3. package/dist/hls-demo.js.map +1 -1
  4. package/dist/hls.js +2903 -4542
  5. package/dist/hls.js.d.ts +112 -186
  6. package/dist/hls.js.map +1 -1
  7. package/dist/hls.light.js +2284 -3295
  8. package/dist/hls.light.js.map +1 -1
  9. package/dist/hls.light.min.js +1 -1
  10. package/dist/hls.light.min.js.map +1 -1
  11. package/dist/hls.light.mjs +1804 -2817
  12. package/dist/hls.light.mjs.map +1 -1
  13. package/dist/hls.min.js +1 -1
  14. package/dist/hls.min.js.map +1 -1
  15. package/dist/hls.mjs +4652 -6293
  16. package/dist/hls.mjs.map +1 -1
  17. package/dist/hls.worker.js +1 -1
  18. package/dist/hls.worker.js.map +1 -1
  19. package/package.json +38 -38
  20. package/src/config.ts +2 -5
  21. package/src/controller/abr-controller.ts +25 -39
  22. package/src/controller/audio-stream-controller.ts +136 -156
  23. package/src/controller/audio-track-controller.ts +1 -1
  24. package/src/controller/base-playlist-controller.ts +10 -27
  25. package/src/controller/base-stream-controller.ts +107 -263
  26. package/src/controller/buffer-controller.ts +98 -252
  27. package/src/controller/buffer-operation-queue.ts +19 -16
  28. package/src/controller/cap-level-controller.ts +2 -3
  29. package/src/controller/cmcd-controller.ts +14 -51
  30. package/src/controller/content-steering-controller.ts +15 -29
  31. package/src/controller/eme-controller.ts +23 -10
  32. package/src/controller/error-controller.ts +22 -28
  33. package/src/controller/fps-controller.ts +3 -8
  34. package/src/controller/fragment-finders.ts +16 -44
  35. package/src/controller/fragment-tracker.ts +25 -58
  36. package/src/controller/gap-controller.ts +16 -43
  37. package/src/controller/id3-track-controller.ts +35 -45
  38. package/src/controller/latency-controller.ts +13 -18
  39. package/src/controller/level-controller.ts +19 -37
  40. package/src/controller/stream-controller.ts +83 -100
  41. package/src/controller/subtitle-stream-controller.ts +47 -35
  42. package/src/controller/subtitle-track-controller.ts +3 -5
  43. package/src/controller/timeline-controller.ts +22 -20
  44. package/src/crypt/aes-crypto.ts +2 -21
  45. package/src/crypt/decrypter.ts +16 -32
  46. package/src/crypt/fast-aes-key.ts +5 -28
  47. package/src/demux/audio/aacdemuxer.ts +5 -5
  48. package/src/demux/audio/ac3-demuxer.ts +4 -5
  49. package/src/demux/audio/adts.ts +4 -9
  50. package/src/demux/audio/base-audio-demuxer.ts +14 -16
  51. package/src/demux/audio/mp3demuxer.ts +3 -4
  52. package/src/demux/audio/mpegaudio.ts +1 -1
  53. package/src/demux/id3.ts +411 -0
  54. package/src/demux/inject-worker.ts +4 -38
  55. package/src/demux/mp4demuxer.ts +7 -7
  56. package/src/demux/sample-aes.ts +0 -2
  57. package/src/demux/transmuxer-interface.ts +83 -106
  58. package/src/demux/transmuxer-worker.ts +77 -111
  59. package/src/demux/transmuxer.ts +22 -46
  60. package/src/demux/tsdemuxer.ts +62 -122
  61. package/src/demux/video/avc-video-parser.ts +121 -210
  62. package/src/demux/video/base-video-parser.ts +2 -135
  63. package/src/demux/video/exp-golomb.ts +208 -0
  64. package/src/errors.ts +0 -2
  65. package/src/events.ts +1 -8
  66. package/src/exports-named.ts +1 -1
  67. package/src/hls.ts +48 -97
  68. package/src/loader/date-range.ts +5 -71
  69. package/src/loader/fragment-loader.ts +21 -23
  70. package/src/loader/fragment.ts +4 -8
  71. package/src/loader/key-loader.ts +1 -3
  72. package/src/loader/level-details.ts +6 -6
  73. package/src/loader/level-key.ts +9 -10
  74. package/src/loader/m3u8-parser.ts +144 -138
  75. package/src/loader/playlist-loader.ts +7 -5
  76. package/src/remux/mp4-generator.ts +1 -196
  77. package/src/remux/mp4-remuxer.ts +84 -55
  78. package/src/remux/passthrough-remuxer.ts +8 -23
  79. package/src/task-loop.ts +2 -5
  80. package/src/types/component-api.ts +1 -3
  81. package/src/types/demuxer.ts +0 -3
  82. package/src/types/events.ts +6 -19
  83. package/src/types/fragment-tracker.ts +2 -2
  84. package/src/types/general.ts +6 -0
  85. package/src/types/media-playlist.ts +1 -9
  86. package/src/types/remuxer.ts +1 -1
  87. package/src/utils/attr-list.ts +9 -96
  88. package/src/utils/buffer-helper.ts +31 -12
  89. package/src/utils/cea-608-parser.ts +3 -1
  90. package/src/utils/codecs.ts +5 -34
  91. package/src/utils/discontinuities.ts +47 -21
  92. package/src/utils/fetch-loader.ts +1 -1
  93. package/src/utils/hdr.ts +7 -4
  94. package/src/utils/imsc1-ttml-parser.ts +1 -1
  95. package/src/utils/keysystem-util.ts +6 -1
  96. package/src/utils/level-helper.ts +44 -71
  97. package/src/utils/logger.ts +23 -58
  98. package/src/utils/mp4-tools.ts +3 -5
  99. package/src/utils/rendition-helper.ts +74 -100
  100. package/src/utils/variable-substitution.ts +19 -0
  101. package/src/utils/webvtt-parser.ts +12 -2
  102. package/src/crypt/decrypter-aes-mode.ts +0 -4
  103. package/src/demux/video/hevc-video-parser.ts +0 -749
  104. package/src/utils/encryption-methods-util.ts +0 -21
  105. package/src/utils/hash.ts +0 -10
  106. package/src/utils/utf8-utils.ts +0 -18
  107. package/src/version.ts +0 -1
@@ -1,5 +1,5 @@
1
1
  import { Events } from '../events';
2
- import { Fragment, MediaFragment, Part } from '../loader/fragment';
2
+ import { Fragment, Part } from '../loader/fragment';
3
3
  import { PlaylistLevelType } from '../types/loader';
4
4
  import type { SourceBufferName } from '../types/buffer';
5
5
  import type {
@@ -47,7 +47,6 @@ export class FragmentTracker implements ComponentAPI {
47
47
 
48
48
  private _registerListeners() {
49
49
  const { hls } = this;
50
- hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
51
50
  hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this);
52
51
  hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
53
52
  hls.on(Events.FRAG_LOADED, this.onFragLoaded, this);
@@ -55,7 +54,6 @@ export class FragmentTracker implements ComponentAPI {
55
54
 
56
55
  private _unregisterListeners() {
57
56
  const { hls } = this;
58
- hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
59
57
  hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this);
60
58
  hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
61
59
  hls.off(Events.FRAG_LOADED, this.onFragLoaded, this);
@@ -109,23 +107,12 @@ export class FragmentTracker implements ComponentAPI {
109
107
  public getBufferedFrag(
110
108
  position: number,
111
109
  levelType: PlaylistLevelType,
112
- ): MediaFragment | null {
113
- return this.getFragAtPos(position, levelType, true);
114
- }
115
-
116
- public getFragAtPos(
117
- position: number,
118
- levelType: PlaylistLevelType,
119
- buffered?: boolean,
120
- ): MediaFragment | null {
110
+ ): Fragment | null {
121
111
  const { fragments } = this;
122
112
  const keys = Object.keys(fragments);
123
113
  for (let i = keys.length; i--; ) {
124
114
  const fragmentEntity = fragments[keys[i]];
125
- if (
126
- fragmentEntity?.body.type === levelType &&
127
- (!buffered || fragmentEntity.buffered)
128
- ) {
115
+ if (fragmentEntity?.body.type === levelType && fragmentEntity.buffered) {
129
116
  const frag = fragmentEntity.body;
130
117
  if (frag.start <= position && position <= frag.end) {
131
118
  return frag;
@@ -145,26 +132,22 @@ export class FragmentTracker implements ComponentAPI {
145
132
  timeRange: TimeRanges,
146
133
  playlistType: PlaylistLevelType,
147
134
  appendedPart?: Part | null,
148
- removeAppending?: boolean,
149
135
  ) {
150
136
  if (this.timeRanges) {
151
137
  this.timeRanges[elementaryStream] = timeRange;
152
138
  }
153
139
  // Check if any flagged fragments have been unloaded
154
140
  // excluding anything newer than appendedPartSn
155
- const appendedPartSn = appendedPart?.fragment.sn || -1;
141
+ const appendedPartSn = (appendedPart?.fragment.sn || -1) as number;
156
142
  Object.keys(this.fragments).forEach((key) => {
157
143
  const fragmentEntity = this.fragments[key];
158
144
  if (!fragmentEntity) {
159
145
  return;
160
146
  }
161
- if (appendedPartSn >= fragmentEntity.body.sn) {
147
+ if (appendedPartSn >= (fragmentEntity.body.sn as number)) {
162
148
  return;
163
149
  }
164
- if (
165
- !fragmentEntity.buffered &&
166
- (!fragmentEntity.loaded || removeAppending)
167
- ) {
150
+ if (!fragmentEntity.buffered && !fragmentEntity.loaded) {
168
151
  if (fragmentEntity.body.type === playlistType) {
169
152
  this.removeFragment(fragmentEntity.body);
170
153
  }
@@ -174,10 +157,6 @@ export class FragmentTracker implements ComponentAPI {
174
157
  if (!esData) {
175
158
  return;
176
159
  }
177
- if (esData.time.length === 0) {
178
- this.removeFragment(fragmentEntity.body);
179
- return;
180
- }
181
160
  esData.time.some((time: FragmentTimeRange) => {
182
161
  const isNotBuffered = !this.isTimeBuffered(
183
162
  time.startPTS,
@@ -199,11 +178,11 @@ export class FragmentTracker implements ComponentAPI {
199
178
  */
200
179
  public detectPartialFragments(data: FragBufferedData) {
201
180
  const timeRanges = this.timeRanges;
202
- if (!timeRanges || data.frag.sn === 'initSegment') {
181
+ const { frag, part } = data;
182
+ if (!timeRanges || frag.sn === 'initSegment') {
203
183
  return;
204
184
  }
205
185
 
206
- const frag = data.frag as MediaFragment;
207
186
  const fragKey = getFragmentKey(frag);
208
187
  const fragmentEntity = this.fragments[fragKey];
209
188
  if (!fragmentEntity || (fragmentEntity.buffered && frag.gap)) {
@@ -219,7 +198,7 @@ export class FragmentTracker implements ComponentAPI {
219
198
  const partial = isFragHint || streamInfo.partial === true;
220
199
  fragmentEntity.range[elementaryStream] = this.getBufferedTimes(
221
200
  frag,
222
- data.part,
201
+ part,
223
202
  partial,
224
203
  timeRange,
225
204
  );
@@ -234,7 +213,7 @@ export class FragmentTracker implements ComponentAPI {
234
213
  }
235
214
  if (!isPartial(fragmentEntity)) {
236
215
  // Remove older fragment parts from lookup after frag is tracked as buffered
237
- this.removeParts(frag.sn - 1, frag.type);
216
+ this.removeParts((frag.sn as number) - 1, frag.type);
238
217
  }
239
218
  } else {
240
219
  // remove fragment if nothing was appended
@@ -248,11 +227,11 @@ export class FragmentTracker implements ComponentAPI {
248
227
  return;
249
228
  }
250
229
  this.activePartLists[levelType] = activeParts.filter(
251
- (part) => part.fragment.sn >= snToKeep,
230
+ (part) => (part.fragment.sn as number) >= snToKeep,
252
231
  );
253
232
  }
254
233
 
255
- public fragBuffered(frag: MediaFragment, force?: true) {
234
+ public fragBuffered(frag: Fragment, force?: true) {
256
235
  const fragKey = getFragmentKey(frag);
257
236
  let fragmentEntity = this.fragments[fragKey];
258
237
  if (!fragmentEntity && force) {
@@ -397,20 +376,16 @@ export class FragmentTracker implements ComponentAPI {
397
376
  return false;
398
377
  }
399
378
 
400
- private onManifestLoading() {
401
- this.removeAllFragments();
402
- }
403
-
404
379
  private onFragLoaded(event: Events.FRAG_LOADED, data: FragLoadedData) {
380
+ const { frag, part } = data;
405
381
  // don't track initsegment (for which sn is not a number)
406
382
  // don't track frags used for bitrateTest, they're irrelevant.
407
- if (data.frag.sn === 'initSegment' || data.frag.bitrateTest) {
383
+ if (frag.sn === 'initSegment' || frag.bitrateTest) {
408
384
  return;
409
385
  }
410
386
 
411
- const frag = data.frag as MediaFragment;
412
387
  // Fragment entity `loaded` FragLoadedData is null when loading parts
413
- const loaded = data.part ? null : data;
388
+ const loaded = part ? null : data;
414
389
 
415
390
  const fragKey = getFragmentKey(frag);
416
391
  this.fragments[fragKey] = {
@@ -426,7 +401,7 @@ export class FragmentTracker implements ComponentAPI {
426
401
  event: Events.BUFFER_APPENDED,
427
402
  data: BufferAppendedData,
428
403
  ) {
429
- const { frag, part, timeRanges, type } = data;
404
+ const { frag, part, timeRanges } = data;
430
405
  if (frag.sn === 'initSegment') {
431
406
  return;
432
407
  }
@@ -440,8 +415,15 @@ export class FragmentTracker implements ComponentAPI {
440
415
  }
441
416
  // Store the latest timeRanges loaded in the buffer
442
417
  this.timeRanges = timeRanges;
443
- const timeRange = timeRanges[type] as TimeRanges;
444
- this.detectEvictedFragments(type, timeRange, playlistType, part);
418
+ Object.keys(timeRanges).forEach((elementaryStream: SourceBufferName) => {
419
+ const timeRange = timeRanges[elementaryStream] as TimeRanges;
420
+ this.detectEvictedFragments(
421
+ elementaryStream,
422
+ timeRange,
423
+ playlistType,
424
+ part,
425
+ );
426
+ });
445
427
  }
446
428
 
447
429
  private onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
@@ -453,21 +435,6 @@ export class FragmentTracker implements ComponentAPI {
453
435
  return !!this.fragments[fragKey];
454
436
  }
455
437
 
456
- public hasFragments(type?: PlaylistLevelType): boolean {
457
- const { fragments } = this;
458
- const keys = Object.keys(fragments);
459
- if (!type) {
460
- return keys.length > 0;
461
- }
462
- for (let i = keys.length; i--; ) {
463
- const fragmentEntity = fragments[keys[i]];
464
- if (fragmentEntity?.body.type === type) {
465
- return true;
466
- }
467
- }
468
- return false;
469
- }
470
-
471
438
  public hasParts(type: PlaylistLevelType): boolean {
472
439
  return !!this.activePartLists[type]?.length;
473
440
  }
@@ -1,22 +1,20 @@
1
- import { State } from './base-stream-controller';
1
+ import type { BufferInfo } from '../utils/buffer-helper';
2
2
  import { BufferHelper } from '../utils/buffer-helper';
3
3
  import { ErrorTypes, ErrorDetails } from '../errors';
4
4
  import { PlaylistLevelType } from '../types/loader';
5
5
  import { Events } from '../events';
6
- import { Logger } from '../utils/logger';
6
+ import { logger } from '../utils/logger';
7
7
  import type Hls from '../hls';
8
- import type { BufferInfo } from '../utils/buffer-helper';
9
8
  import type { HlsConfig } from '../config';
10
9
  import type { Fragment } from '../loader/fragment';
11
10
  import type { FragmentTracker } from './fragment-tracker';
12
- import type { LevelDetails } from '../loader/level-details';
13
11
 
14
12
  export const STALL_MINIMUM_DURATION_MS = 250;
15
13
  export const MAX_START_GAP_JUMP = 2.0;
16
14
  export const SKIP_BUFFER_HOLE_STEP_SECONDS = 0.1;
17
15
  export const SKIP_BUFFER_RANGE_START = 0.05;
18
16
 
19
- export default class GapController extends Logger {
17
+ export default class GapController {
20
18
  private config: HlsConfig;
21
19
  private media: HTMLMediaElement | null = null;
22
20
  private fragmentTracker: FragmentTracker;
@@ -26,15 +24,8 @@ export default class GapController extends Logger {
26
24
  private stalled: number | null = null;
27
25
  private moved: boolean = false;
28
26
  private seeking: boolean = false;
29
- private ended: number = 0;
30
27
 
31
- constructor(
32
- config: HlsConfig,
33
- media: HTMLMediaElement,
34
- fragmentTracker: FragmentTracker,
35
- hls: Hls,
36
- ) {
37
- super('gap-controller', hls.logger);
28
+ constructor(config, media, fragmentTracker, hls) {
38
29
  this.config = config;
39
30
  this.media = media;
40
31
  this.fragmentTracker = fragmentTracker;
@@ -53,12 +44,7 @@ export default class GapController extends Logger {
53
44
  *
54
45
  * @param lastCurrentTime - Previously read playhead position
55
46
  */
56
- public poll(
57
- lastCurrentTime: number,
58
- activeFrag: Fragment | null,
59
- levelDetails: LevelDetails | undefined,
60
- state: string,
61
- ) {
47
+ public poll(lastCurrentTime: number, activeFrag: Fragment | null) {
62
48
  const { config, media, stalled } = this;
63
49
  if (media === null) {
64
50
  return;
@@ -71,7 +57,6 @@ export default class GapController extends Logger {
71
57
 
72
58
  // The playhead is moving, no-op
73
59
  if (currentTime !== lastCurrentTime) {
74
- this.ended = 0;
75
60
  this.moved = true;
76
61
  if (!seeking) {
77
62
  this.nudgeRetry = 0;
@@ -80,7 +65,7 @@ export default class GapController extends Logger {
80
65
  // The playhead is now moving, but was previously stalled
81
66
  if (this.stallReported) {
82
67
  const stalledDuration = self.performance.now() - stalled;
83
- this.warn(
68
+ logger.warn(
84
69
  `playback not stuck anymore @${currentTime}, after ${Math.round(
85
70
  stalledDuration,
86
71
  )}ms`,
@@ -143,9 +128,12 @@ export default class GapController extends Logger {
143
128
  // When joining a live stream with audio tracks, account for live playlist window sliding by allowing
144
129
  // a larger jump over start gaps caused by the audio-stream-controller buffering a start fragment
145
130
  // that begins over 1 target duration after the video start position.
146
- const isLive = !!levelDetails?.live;
131
+ const level = this.hls.levels
132
+ ? this.hls.levels[this.hls.currentLevel]
133
+ : null;
134
+ const isLive = level?.details?.live;
147
135
  const maxStartGapJump = isLive
148
- ? levelDetails!.targetduration * 2
136
+ ? level!.details!.targetduration * 2
149
137
  : MAX_START_GAP_JUMP;
150
138
  const partialOrGap = this.fragmentTracker.getPartialFragment(currentTime);
151
139
  if (startJump > 0 && (startJump <= maxStartGapJump || partialOrGap)) {
@@ -165,21 +153,6 @@ export default class GapController extends Logger {
165
153
 
166
154
  const stalledDuration = tnow - stalled;
167
155
  if (!seeking && stalledDuration >= STALL_MINIMUM_DURATION_MS) {
168
- // Dispatch MEDIA_ENDED when media.ended/ended event is not signalled at end of stream
169
- if (
170
- state === State.ENDED &&
171
- !levelDetails?.live &&
172
- Math.abs(currentTime - (levelDetails?.edge || 0)) < 1
173
- ) {
174
- if (stalledDuration < 1000 || this.ended) {
175
- return;
176
- }
177
- this.ended = currentTime;
178
- this.hls.trigger(Events.MEDIA_ENDED, {
179
- stalled: true,
180
- });
181
- return;
182
- }
183
156
  // Report stalling after trying to fix
184
157
  this._reportStall(bufferInfo);
185
158
  if (!this.media) {
@@ -233,7 +206,7 @@ export default class GapController extends Logger {
233
206
  bufferInfo.nextStart - currentTime < config.maxBufferHole)) &&
234
207
  stalledDurationMs > config.highBufferWatchdogPeriod * 1000
235
208
  ) {
236
- this.warn('Trying to nudge playhead over buffer-hole');
209
+ logger.warn('Trying to nudge playhead over buffer-hole');
237
210
  // Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds
238
211
  // We only try to jump the hole if it's under the configured size
239
212
  // Reset stalled so to rearm watchdog timer
@@ -257,7 +230,7 @@ export default class GapController extends Logger {
257
230
  media.currentTime
258
231
  } due to low buffer (${JSON.stringify(bufferInfo)})`,
259
232
  );
260
- this.warn(error.message);
233
+ logger.warn(error.message);
261
234
  hls.trigger(Events.ERROR, {
262
235
  type: ErrorTypes.MEDIA_ERROR,
263
236
  details: ErrorDetails.BUFFER_STALLED_ERROR,
@@ -332,7 +305,7 @@ export default class GapController extends Logger {
332
305
  startTime + SKIP_BUFFER_RANGE_START,
333
306
  currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS,
334
307
  );
335
- this.warn(
308
+ logger.warn(
336
309
  `skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`,
337
310
  );
338
311
  this.moved = true;
@@ -375,7 +348,7 @@ export default class GapController extends Logger {
375
348
  const error = new Error(
376
349
  `Nudging 'currentTime' from ${currentTime} to ${targetTime}`,
377
350
  );
378
- this.warn(error.message);
351
+ logger.warn(error.message);
379
352
  media.currentTime = targetTime;
380
353
  hls.trigger(Events.ERROR, {
381
354
  type: ErrorTypes.MEDIA_ERROR,
@@ -387,7 +360,7 @@ export default class GapController extends Logger {
387
360
  const error = new Error(
388
361
  `Playhead still not moving while enough data buffered @${currentTime} after ${config.nudgeMaxRetry} nudges`,
389
362
  );
390
- this.error(error.message);
363
+ logger.error(error.message);
391
364
  hls.trigger(Events.ERROR, {
392
365
  type: ErrorTypes.MEDIA_ERROR,
393
366
  details: ErrorDetails.BUFFER_STALLED_ERROR,
@@ -4,24 +4,21 @@ import {
4
4
  clearCurrentCues,
5
5
  removeCuesInRange,
6
6
  } from '../utils/texttrack-utils';
7
+ import * as ID3 from '../demux/id3';
7
8
  import {
8
9
  DateRange,
9
10
  isDateRangeCueAttribute,
10
11
  isSCTE35Attribute,
11
12
  } from '../loader/date-range';
12
- import { LevelDetails } from '../loader/level-details';
13
13
  import { MetadataSchema } from '../types/demuxer';
14
14
  import type {
15
15
  BufferFlushingData,
16
16
  FragParsingMetadataData,
17
- LevelPTSUpdatedData,
18
17
  LevelUpdatedData,
19
18
  MediaAttachedData,
20
19
  } from '../types/events';
21
20
  import type { ComponentAPI } from '../types/component-api';
22
21
  import type Hls from '../hls';
23
- import { getId3Frames } from '@svta/common-media-library/id3/getId3Frames';
24
- import { isId3TimestampFrame } from '@svta/common-media-library/id3/isId3TimestampFrame';
25
22
 
26
23
  declare global {
27
24
  interface Window {
@@ -71,6 +68,10 @@ const MAX_CUE_ENDTIME = (() => {
71
68
  return Number.POSITIVE_INFINITY;
72
69
  })();
73
70
 
71
+ function dateRangeDateToTimelineSeconds(date: Date, offset: number): number {
72
+ return date.getTime() / 1000 - offset;
73
+ }
74
+
74
75
  function hexToArrayBuffer(str): ArrayBuffer {
75
76
  return Uint8Array.from(
76
77
  str
@@ -98,7 +99,7 @@ class ID3TrackController implements ComponentAPI {
98
99
  this._registerListeners();
99
100
  }
100
101
 
101
- public destroy() {
102
+ destroy() {
102
103
  this._unregisterListeners();
103
104
  this.id3Track = null;
104
105
  this.media = null;
@@ -115,7 +116,6 @@ class ID3TrackController implements ComponentAPI {
115
116
  hls.on(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this);
116
117
  hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
117
118
  hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
118
- hls.on(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this);
119
119
  }
120
120
 
121
121
  private _unregisterListeners() {
@@ -126,22 +126,22 @@ class ID3TrackController implements ComponentAPI {
126
126
  hls.off(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this);
127
127
  hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
128
128
  hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
129
- hls.off(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this);
130
129
  }
131
130
 
132
131
  // Add ID3 metatadata text track.
133
- private onMediaAttached(
132
+ protected onMediaAttached(
134
133
  event: Events.MEDIA_ATTACHED,
135
134
  data: MediaAttachedData,
136
135
  ): void {
137
136
  this.media = data.media;
138
137
  }
139
138
 
140
- private onMediaDetaching(): void {
141
- if (this.id3Track) {
142
- clearCurrentCues(this.id3Track);
143
- this.id3Track = null;
139
+ protected onMediaDetaching(): void {
140
+ if (!this.id3Track) {
141
+ return;
144
142
  }
143
+ clearCurrentCues(this.id3Track);
144
+ this.id3Track = null;
145
145
  this.media = null;
146
146
  this.dateRangeCuesAppended = {};
147
147
  }
@@ -150,13 +150,13 @@ class ID3TrackController implements ComponentAPI {
150
150
  this.dateRangeCuesAppended = {};
151
151
  }
152
152
 
153
- private createTrack(media: HTMLMediaElement): TextTrack {
153
+ createTrack(media: HTMLMediaElement): TextTrack {
154
154
  const track = this.getID3Track(media.textTracks) as TextTrack;
155
155
  track.mode = 'hidden';
156
156
  return track;
157
157
  }
158
158
 
159
- private getID3Track(textTracks: TextTrackList): TextTrack | void {
159
+ getID3Track(textTracks: TextTrackList): TextTrack | void {
160
160
  if (!this.media) {
161
161
  return;
162
162
  }
@@ -173,7 +173,7 @@ class ID3TrackController implements ComponentAPI {
173
173
  return this.media.addTextTrack('metadata', 'id3');
174
174
  }
175
175
 
176
- private onFragParsingMetadata(
176
+ onFragParsingMetadata(
177
177
  event: Events.FRAG_PARSING_METADATA,
178
178
  data: FragParsingMetadataData,
179
179
  ) {
@@ -211,7 +211,7 @@ class ID3TrackController implements ComponentAPI {
211
211
  continue;
212
212
  }
213
213
 
214
- const frames = getId3Frames(samples[i].data);
214
+ const frames = ID3.getID3Frames(samples[i].data);
215
215
  if (frames) {
216
216
  const startTime = samples[i].pts;
217
217
  let endTime: number = startTime + samples[i].duration;
@@ -228,7 +228,7 @@ class ID3TrackController implements ComponentAPI {
228
228
  for (let j = 0; j < frames.length; j++) {
229
229
  const frame = frames[j];
230
230
  // Safari doesn't put the timestamp frame in the TextTrack
231
- if (!isId3TimestampFrame(frame)) {
231
+ if (!ID3.isTimeStampFrame(frame)) {
232
232
  // add a bounds to any unbounded cues
233
233
  this.updateId3CueEnds(startTime, type);
234
234
  const cue = createCueWithDataFields(
@@ -247,7 +247,7 @@ class ID3TrackController implements ComponentAPI {
247
247
  }
248
248
  }
249
249
 
250
- private updateId3CueEnds(startTime: number, type: MetadataSchema) {
250
+ updateId3CueEnds(startTime: number, type: MetadataSchema) {
251
251
  const cues = this.id3Track?.cues;
252
252
  if (cues) {
253
253
  for (let i = cues.length; i--; ) {
@@ -263,7 +263,7 @@ class ID3TrackController implements ComponentAPI {
263
263
  }
264
264
  }
265
265
 
266
- private onBufferFlushing(
266
+ onBufferFlushing(
267
267
  event: Events.BUFFER_FLUSHING,
268
268
  { startOffset, endOffset, type }: BufferFlushingData,
269
269
  ) {
@@ -295,23 +295,7 @@ class ID3TrackController implements ComponentAPI {
295
295
  }
296
296
  }
297
297
 
298
- private onLevelUpdated(
299
- event: Events.LEVEL_UPDATED,
300
- { details }: LevelUpdatedData,
301
- ) {
302
- this.updateDateRangeCues(details, true);
303
- }
304
-
305
- private onLevelPtsUpdated(
306
- event: Events.LEVEL_PTS_UPDATED,
307
- data: LevelPTSUpdatedData,
308
- ) {
309
- if (Math.abs(data.drift) > 0.01) {
310
- this.updateDateRangeCues(data.details);
311
- }
312
- }
313
-
314
- private updateDateRangeCues(details: LevelDetails, removeOldCues?: true) {
298
+ onLevelUpdated(event: Events.LEVEL_UPDATED, { details }: LevelUpdatedData) {
315
299
  if (
316
300
  !this.media ||
317
301
  !details.hasProgramDateTime ||
@@ -323,7 +307,7 @@ class ID3TrackController implements ComponentAPI {
323
307
  const { dateRanges } = details;
324
308
  const ids = Object.keys(dateRanges);
325
309
  // Remove cues from track not found in details.dateRanges
326
- if (id3Track && removeOldCues) {
310
+ if (id3Track) {
327
311
  const idsToRemove = Object.keys(dateRangeCuesAppended).filter(
328
312
  (id) => !ids.includes(id),
329
313
  );
@@ -345,20 +329,26 @@ class ID3TrackController implements ComponentAPI {
345
329
  this.id3Track = this.createTrack(this.media);
346
330
  }
347
331
 
332
+ const dateTimeOffset =
333
+ (lastFragment.programDateTime as number) / 1000 - lastFragment.start;
348
334
  const Cue = getCueClass();
335
+
349
336
  for (let i = 0; i < ids.length; i++) {
350
337
  const id = ids[i];
351
338
  const dateRange = dateRanges[id];
352
- const startTime = dateRange.startTime;
339
+ const startTime = dateRangeDateToTimelineSeconds(
340
+ dateRange.startDate,
341
+ dateTimeOffset,
342
+ );
353
343
 
354
344
  // Process DateRanges to determine end-time (known DURATION, END-DATE, or END-ON-NEXT)
355
345
  const appendedDateRangeCues = dateRangeCuesAppended[id];
356
346
  const cues = appendedDateRangeCues?.cues || {};
357
347
  let durationKnown = appendedDateRangeCues?.durationKnown || false;
358
348
  let endTime = MAX_CUE_ENDTIME;
359
- const { duration, endDate } = dateRange;
360
- if (endDate && duration !== null) {
361
- endTime = startTime + duration;
349
+ const endDate = dateRange.endDate;
350
+ if (endDate) {
351
+ endTime = dateRangeDateToTimelineSeconds(endDate, dateTimeOffset);
362
352
  durationKnown = true;
363
353
  } else if (dateRange.endOnNext && !durationKnown) {
364
354
  const nextDateRangeWithSameClass = ids.reduce(
@@ -379,7 +369,10 @@ class ID3TrackController implements ComponentAPI {
379
369
  null,
380
370
  );
381
371
  if (nextDateRangeWithSameClass) {
382
- endTime = nextDateRangeWithSameClass.startTime;
372
+ endTime = dateRangeDateToTimelineSeconds(
373
+ nextDateRangeWithSameClass.startDate,
374
+ dateTimeOffset,
375
+ );
383
376
  durationKnown = true;
384
377
  }
385
378
  }
@@ -396,9 +389,6 @@ class ID3TrackController implements ComponentAPI {
396
389
  if (cue) {
397
390
  if (durationKnown && !appendedDateRangeCues.durationKnown) {
398
391
  cue.endTime = endTime;
399
- } else if (Math.abs(cue.startTime - startTime) > 0.01) {
400
- cue.startTime = startTime;
401
- cue.endTime = endTime;
402
392
  }
403
393
  } else if (Cue) {
404
394
  let data = dateRange.attr[key];
@@ -6,6 +6,7 @@ import type {
6
6
  LevelUpdatedData,
7
7
  MediaAttachingData,
8
8
  } from '../types/events';
9
+ import { logger } from '../utils/logger';
9
10
  import type { ComponentAPI } from '../types/component-api';
10
11
  import type Hls from '../hls';
11
12
  import type { HlsConfig } from '../config';
@@ -18,7 +19,7 @@ export default class LatencyController implements ComponentAPI {
18
19
  private currentTime: number = 0;
19
20
  private stallCount: number = 0;
20
21
  private _latency: number | null = null;
21
- private _targetLatencyUpdated = false;
22
+ private timeupdateHandler = () => this.timeupdate();
22
23
 
23
24
  constructor(hls: Hls) {
24
25
  this.hls = hls;
@@ -51,7 +52,6 @@ export default class LatencyController implements ComponentAPI {
51
52
  const userConfig = this.hls.userConfig;
52
53
  let targetLatency = lowLatencyMode ? partHoldBack || holdBack : holdBack;
53
54
  if (
54
- this._targetLatencyUpdated ||
55
55
  userConfig.liveSyncDuration ||
56
56
  userConfig.liveSyncDurationCount ||
57
57
  targetLatency === 0
@@ -62,21 +62,16 @@ export default class LatencyController implements ComponentAPI {
62
62
  : liveSyncDurationCount * targetduration;
63
63
  }
64
64
  const maxLiveSyncOnStallIncrease = targetduration;
65
+ const liveSyncOnStallIncrease = 1.0;
65
66
  return (
66
67
  targetLatency +
67
68
  Math.min(
68
- this.stallCount * this.config.liveSyncOnStallIncrease,
69
+ this.stallCount * liveSyncOnStallIncrease,
69
70
  maxLiveSyncOnStallIncrease,
70
71
  )
71
72
  );
72
73
  }
73
74
 
74
- set targetLatency(latency: number) {
75
- this.stallCount = 0;
76
- this.config.liveSyncDuration = latency;
77
- this._targetLatencyUpdated = true;
78
- }
79
-
80
75
  get liveSyncPosition(): number | null {
81
76
  const liveEdge = this.estimateLiveEdge();
82
77
  const targetLatency = this.targetLatency;
@@ -131,7 +126,7 @@ export default class LatencyController implements ComponentAPI {
131
126
  this.onMediaDetaching();
132
127
  this.levelDetails = null;
133
128
  // @ts-ignore
134
- this.hls = null;
129
+ this.hls = this.timeupdateHandler = null;
135
130
  }
136
131
 
137
132
  private registerListeners() {
@@ -155,12 +150,12 @@ export default class LatencyController implements ComponentAPI {
155
150
  data: MediaAttachingData,
156
151
  ) {
157
152
  this.media = data.media;
158
- this.media.addEventListener('timeupdate', this.onTimeupdate);
153
+ this.media.addEventListener('timeupdate', this.timeupdateHandler);
159
154
  }
160
155
 
161
156
  private onMediaDetaching() {
162
157
  if (this.media) {
163
- this.media.removeEventListener('timeupdate', this.onTimeupdate);
158
+ this.media.removeEventListener('timeupdate', this.timeupdateHandler);
164
159
  this.media = null;
165
160
  }
166
161
  }
@@ -177,10 +172,10 @@ export default class LatencyController implements ComponentAPI {
177
172
  ) {
178
173
  this.levelDetails = details;
179
174
  if (details.advanced) {
180
- this.onTimeupdate();
175
+ this.timeupdate();
181
176
  }
182
177
  if (!details.live && this.media) {
183
- this.media.removeEventListener('timeupdate', this.onTimeupdate);
178
+ this.media.removeEventListener('timeupdate', this.timeupdateHandler);
184
179
  }
185
180
  }
186
181
 
@@ -190,13 +185,13 @@ export default class LatencyController implements ComponentAPI {
190
185
  }
191
186
  this.stallCount++;
192
187
  if (this.levelDetails?.live) {
193
- this.hls.logger.warn(
194
- '[latency-controller]: Stall detected, adjusting target latency',
188
+ logger.warn(
189
+ '[playback-rate-controller]: Stall detected, adjusting target latency',
195
190
  );
196
191
  }
197
192
  }
198
193
 
199
- private onTimeupdate = () => {
194
+ private timeupdate() {
200
195
  const { media, levelDetails } = this;
201
196
  if (!media || !levelDetails) {
202
197
  return;
@@ -247,7 +242,7 @@ export default class LatencyController implements ComponentAPI {
247
242
  } else if (media.playbackRate !== 1 && media.playbackRate !== 0) {
248
243
  media.playbackRate = 1;
249
244
  }
250
- };
245
+ }
251
246
 
252
247
  private estimateLiveEdge(): number | null {
253
248
  const { levelDetails } = this;