hls.js 1.5.13 → 1.5.14-0.canary.10417

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