hls.js 1.5.9 → 1.5.10-0.canary.10321

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 (90) 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 +3479 -2197
  5. package/dist/hls.js.d.ts +108 -85
  6. package/dist/hls.js.map +1 -1
  7. package/dist/hls.light.js +2407 -1761
  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 +1994 -1321
  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 +2868 -1563
  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 +35 -35
  20. package/src/config.ts +3 -2
  21. package/src/controller/abr-controller.ts +24 -20
  22. package/src/controller/audio-stream-controller.ts +68 -74
  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 +160 -38
  26. package/src/controller/buffer-controller.ts +230 -92
  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 +6 -8
  33. package/src/controller/fps-controller.ts +8 -3
  34. package/src/controller/fragment-tracker.ts +15 -11
  35. package/src/controller/gap-controller.ts +43 -16
  36. package/src/controller/id3-track-controller.ts +7 -7
  37. package/src/controller/latency-controller.ts +9 -11
  38. package/src/controller/level-controller.ts +37 -19
  39. package/src/controller/stream-controller.ts +37 -32
  40. package/src/controller/subtitle-stream-controller.ts +28 -40
  41. package/src/controller/subtitle-track-controller.ts +5 -3
  42. package/src/controller/timeline-controller.ts +19 -21
  43. package/src/crypt/aes-crypto.ts +21 -2
  44. package/src/crypt/decrypter-aes-mode.ts +4 -0
  45. package/src/crypt/decrypter.ts +32 -16
  46. package/src/crypt/fast-aes-key.ts +28 -5
  47. package/src/demux/audio/aacdemuxer.ts +2 -2
  48. package/src/demux/audio/ac3-demuxer.ts +4 -3
  49. package/src/demux/audio/adts.ts +9 -4
  50. package/src/demux/audio/base-audio-demuxer.ts +16 -14
  51. package/src/demux/audio/mp3demuxer.ts +4 -3
  52. package/src/demux/audio/mpegaudio.ts +1 -1
  53. package/src/demux/mp4demuxer.ts +7 -7
  54. package/src/demux/sample-aes.ts +2 -0
  55. package/src/demux/transmuxer-interface.ts +4 -12
  56. package/src/demux/transmuxer-worker.ts +4 -4
  57. package/src/demux/transmuxer.ts +16 -3
  58. package/src/demux/tsdemuxer.ts +71 -37
  59. package/src/demux/video/avc-video-parser.ts +208 -119
  60. package/src/demux/video/base-video-parser.ts +147 -18
  61. package/src/demux/video/exp-golomb.ts +0 -208
  62. package/src/demux/video/hevc-video-parser.ts +749 -0
  63. package/src/empty-es.js +5 -0
  64. package/src/events.ts +8 -1
  65. package/src/exports-named.ts +1 -1
  66. package/src/hls.ts +61 -38
  67. package/src/loader/fragment-loader.ts +10 -3
  68. package/src/loader/key-loader.ts +3 -1
  69. package/src/loader/level-key.ts +10 -9
  70. package/src/loader/playlist-loader.ts +4 -5
  71. package/src/remux/mp4-generator.ts +196 -1
  72. package/src/remux/mp4-remuxer.ts +24 -8
  73. package/src/task-loop.ts +5 -2
  74. package/src/types/component-api.ts +3 -1
  75. package/src/types/demuxer.ts +4 -0
  76. package/src/types/events.ts +4 -0
  77. package/src/types/remuxer.ts +1 -1
  78. package/src/utils/buffer-helper.ts +12 -31
  79. package/src/utils/cea-608-parser.ts +1 -3
  80. package/src/utils/codecs.ts +34 -5
  81. package/src/utils/encryption-methods-util.ts +21 -0
  82. package/src/utils/fetch-loader.ts +1 -1
  83. package/src/utils/imsc1-ttml-parser.ts +1 -1
  84. package/src/utils/keysystem-util.ts +1 -6
  85. package/src/utils/logger.ts +58 -23
  86. package/src/utils/mp4-tools.ts +5 -3
  87. package/src/utils/utf8-utils.ts +18 -0
  88. package/src/utils/webvtt-parser.ts +1 -1
  89. package/src/utils/xhr-loader.ts +5 -5
  90. package/src/demux/id3.ts +0 -411
@@ -1,15 +1,15 @@
1
1
  import { Events } from '../events';
2
- import Hls from '../hls';
2
+ import type Hls from '../hls';
3
3
  import { Cmcd } from '@svta/common-media-library/cmcd/Cmcd';
4
4
  import { CmcdObjectType } from '@svta/common-media-library/cmcd/CmcdObjectType';
5
5
  import { CmcdStreamingFormat } from '@svta/common-media-library/cmcd/CmcdStreamingFormat';
6
6
  import { appendCmcdHeaders } from '@svta/common-media-library/cmcd/appendCmcdHeaders';
7
7
  import { appendCmcdQuery } from '@svta/common-media-library/cmcd/appendCmcdQuery';
8
+ import type { CmcdEncodeOptions } from '@svta/common-media-library/cmcd/CmcdEncodeOptions';
8
9
  import { uuid } from '@svta/common-media-library/utils/uuid';
9
10
  import { BufferHelper } from '../utils/buffer-helper';
10
- import { logger } from '../utils/logger';
11
11
  import type { ComponentAPI } from '../types/component-api';
12
- import type { Fragment } from '../loader/fragment';
12
+ import type { Fragment, Part } from '../loader/fragment';
13
13
  import type { BufferCreatedData, MediaAttachedData } from '../types/events';
14
14
  import type {
15
15
  FragmentLoaderContext,
@@ -81,7 +81,7 @@ export default class CMCDController implements ComponentAPI {
81
81
  // @ts-ignore
82
82
  this.hls = this.config = this.audioBuffer = this.videoBuffer = null;
83
83
  // @ts-ignore
84
- this.onWaiting = this.onPlaying = null;
84
+ this.onWaiting = this.onPlaying = this.media = null;
85
85
  }
86
86
 
87
87
  private onMediaAttached(
@@ -165,7 +165,7 @@ export default class CMCDController implements ComponentAPI {
165
165
  data.su = this.buffering;
166
166
  }
167
167
 
168
- // TODO: Implement rtp, nrr, nor, dl
168
+ // TODO: Implement rtp, nrr, dl
169
169
 
170
170
  const { includeKeys } = this;
171
171
  if (includeKeys) {
@@ -175,14 +175,18 @@ export default class CMCDController implements ComponentAPI {
175
175
  }, {});
176
176
  }
177
177
 
178
+ const options: CmcdEncodeOptions = {
179
+ baseUrl: context.url,
180
+ };
181
+
178
182
  if (this.useHeaders) {
179
183
  if (!context.headers) {
180
184
  context.headers = {};
181
185
  }
182
186
 
183
- appendCmcdHeaders(context.headers, data);
187
+ appendCmcdHeaders(context.headers, data, options);
184
188
  } else {
185
- context.url = appendCmcdQuery(context.url, data);
189
+ context.url = appendCmcdQuery(context.url, data, options);
186
190
  }
187
191
  }
188
192
 
@@ -196,7 +200,7 @@ export default class CMCDController implements ComponentAPI {
196
200
  su: !this.initialized,
197
201
  });
198
202
  } catch (error) {
199
- logger.warn('Could not generate manifest CMCD data.', error);
203
+ this.hls.logger.warn('Could not generate manifest CMCD data.', error);
200
204
  }
201
205
  };
202
206
 
@@ -205,11 +209,11 @@ export default class CMCDController implements ComponentAPI {
205
209
  */
206
210
  private applyFragmentData = (context: FragmentLoaderContext) => {
207
211
  try {
208
- const fragment = context.frag;
209
- const level = this.hls.levels[fragment.level];
210
- const ot = this.getObjectType(fragment);
212
+ const { frag, part } = context;
213
+ const level = this.hls.levels[frag.level];
214
+ const ot = this.getObjectType(frag);
211
215
  const data: Cmcd = {
212
- d: fragment.duration * 1000,
216
+ d: (part || frag).duration * 1000,
213
217
  ot,
214
218
  };
215
219
 
@@ -223,12 +227,45 @@ export default class CMCDController implements ComponentAPI {
223
227
  data.bl = this.getBufferLength(ot);
224
228
  }
225
229
 
230
+ const next = part ? this.getNextPart(part) : this.getNextFrag(frag);
231
+
232
+ if (next?.url && next.url !== frag.url) {
233
+ data.nor = next.url;
234
+ }
235
+
226
236
  this.apply(context, data);
227
237
  } catch (error) {
228
- logger.warn('Could not generate segment CMCD data.', error);
238
+ this.hls.logger.warn('Could not generate segment CMCD data.', error);
229
239
  }
230
240
  };
231
241
 
242
+ private getNextFrag(fragment: Fragment): Fragment | undefined {
243
+ const levelDetails = this.hls.levels[fragment.level]?.details;
244
+ if (levelDetails) {
245
+ const index = (fragment.sn as number) - levelDetails.startSN;
246
+ return levelDetails.fragments[index + 1];
247
+ }
248
+
249
+ return undefined;
250
+ }
251
+
252
+ private getNextPart(part: Part): Part | undefined {
253
+ const { index, fragment } = part;
254
+ const partList = this.hls.levels[fragment.level]?.details?.partList;
255
+
256
+ if (partList) {
257
+ const { sn } = fragment;
258
+ for (let i = partList.length - 1; i >= 0; i--) {
259
+ const p = partList[i];
260
+ if (p.index === index && p.fragment.sn === sn) {
261
+ return partList[i + 1];
262
+ }
263
+ }
264
+ }
265
+
266
+ return undefined;
267
+ }
268
+
232
269
  /**
233
270
  * The CMCD object type.
234
271
  */
@@ -287,7 +324,7 @@ export default class CMCDController implements ComponentAPI {
287
324
  * Get the buffer length for a media type in milliseconds
288
325
  */
289
326
  private getBufferLength(type: CmcdObjectType) {
290
- const media = this.hls.media;
327
+ const media = this.media;
291
328
  const buffer =
292
329
  type === CmcdObjectType.AUDIO ? this.audioBuffer : this.videoBuffer;
293
330
 
@@ -3,7 +3,7 @@ import { Level } from '../types/level';
3
3
  import { reassignFragmentLevelIndexes } from '../utils/level-helper';
4
4
  import { AttrList } from '../utils/attr-list';
5
5
  import { ErrorActionFlags, NetworkErrorAction } from './error-controller';
6
- import { logger } from '../utils/logger';
6
+ import { Logger } from '../utils/logger';
7
7
  import {
8
8
  PlaylistContextType,
9
9
  type Loader,
@@ -48,13 +48,15 @@ export type UriReplacement = {
48
48
 
49
49
  const PATHWAY_PENALTY_DURATION_MS = 300000;
50
50
 
51
- export default class ContentSteeringController implements NetworkComponentAPI {
51
+ export default class ContentSteeringController
52
+ extends Logger
53
+ implements NetworkComponentAPI
54
+ {
52
55
  private readonly hls: Hls;
53
- private log: (msg: any) => void;
54
56
  private loader: Loader<LoaderContext> | null = null;
55
57
  private uri: string | null = null;
56
58
  private pathwayId: string = '.';
57
- private pathwayPriority: string[] | null = null;
59
+ private _pathwayPriority: string[] | null = null;
58
60
  private timeToLoad: number = 300;
59
61
  private reloadTimer: number = -1;
60
62
  private updated: number = 0;
@@ -66,8 +68,8 @@ export default class ContentSteeringController implements NetworkComponentAPI {
66
68
  private penalizedPathways: { [pathwayId: string]: number } = {};
67
69
 
68
70
  constructor(hls: Hls) {
71
+ super('content-steering', hls.logger);
69
72
  this.hls = hls;
70
- this.log = logger.log.bind(logger, `[content-steering]:`);
71
73
  this.registerListeners();
72
74
  }
73
75
 
@@ -90,6 +92,23 @@ export default class ContentSteeringController implements NetworkComponentAPI {
90
92
  hls.off(Events.ERROR, this.onError, this);
91
93
  }
92
94
 
95
+ pathways() {
96
+ return (this.levels || []).reduce((pathways, level) => {
97
+ if (pathways.indexOf(level.pathwayId) === -1) {
98
+ pathways.push(level.pathwayId);
99
+ }
100
+ return pathways;
101
+ }, [] as string[]);
102
+ }
103
+
104
+ get pathwayPriority(): string[] | null {
105
+ return this._pathwayPriority;
106
+ }
107
+
108
+ set pathwayPriority(pathwayPriority: string[]) {
109
+ this.updatePathwayPriority(pathwayPriority);
110
+ }
111
+
93
112
  startLoad() {
94
113
  this.started = true;
95
114
  this.clearTimeout();
@@ -176,7 +195,7 @@ export default class ContentSteeringController implements NetworkComponentAPI {
176
195
  errorAction.flags === ErrorActionFlags.MoveAllAlternatesMatchingHost
177
196
  ) {
178
197
  const levels = this.levels;
179
- let pathwayPriority = this.pathwayPriority;
198
+ let pathwayPriority = this._pathwayPriority;
180
199
  let errorPathway = this.pathwayId;
181
200
  if (data.context) {
182
201
  const { groupId, pathwayId, type } = data.context;
@@ -191,19 +210,14 @@ export default class ContentSteeringController implements NetworkComponentAPI {
191
210
  }
192
211
  if (!pathwayPriority && levels) {
193
212
  // If PATHWAY-PRIORITY was not provided, list pathways for error handling
194
- pathwayPriority = levels.reduce((pathways, level) => {
195
- if (pathways.indexOf(level.pathwayId) === -1) {
196
- pathways.push(level.pathwayId);
197
- }
198
- return pathways;
199
- }, [] as string[]);
213
+ pathwayPriority = this.pathways();
200
214
  }
201
215
  if (pathwayPriority && pathwayPriority.length > 1) {
202
216
  this.updatePathwayPriority(pathwayPriority);
203
217
  errorAction.resolved = this.pathwayId !== errorPathway;
204
218
  }
205
219
  if (!errorAction.resolved) {
206
- logger.warn(
220
+ this.warn(
207
221
  `Could not resolve ${data.details} ("${
208
222
  data.error.message
209
223
  }") with content-steering for Pathway: ${errorPathway} levels: ${
@@ -245,7 +259,7 @@ export default class ContentSteeringController implements NetworkComponentAPI {
245
259
  }
246
260
 
247
261
  private updatePathwayPriority(pathwayPriority: string[]) {
248
- this.pathwayPriority = pathwayPriority;
262
+ this._pathwayPriority = pathwayPriority;
249
263
  let levels: Level[] | undefined;
250
264
 
251
265
  // Evaluate if we should remove the pathway from the penalized list
@@ -442,7 +456,7 @@ export default class ContentSteeringController implements NetworkComponentAPI {
442
456
  ) => {
443
457
  this.log(`Loaded steering manifest: "${url}"`);
444
458
  const steeringData = response.data as SteeringManifest;
445
- if (steeringData.VERSION !== 1) {
459
+ if (steeringData?.VERSION !== 1) {
446
460
  this.log(`Steering VERSION ${steeringData.VERSION} not supported!`);
447
461
  return;
448
462
  }
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { Events } from '../events';
7
7
  import { ErrorTypes, ErrorDetails } from '../errors';
8
- import { logger } from '../utils/logger';
8
+ import { Logger } from '../utils/logger';
9
9
  import {
10
10
  getKeySystemsForConfig,
11
11
  getSupportedMediaKeySystemConfigurations,
@@ -19,7 +19,7 @@ import {
19
19
  KeySystems,
20
20
  requestMediaKeySystemAccess,
21
21
  } from '../utils/mediakeys-helper';
22
- import { strToUtf8array } from '../utils/keysystem-util';
22
+ import { strToUtf8array } from '../utils/utf8-utils';
23
23
  import { base64Decode } from '../utils/numeric-encoding-utils';
24
24
  import { DecryptData, LevelKey } from '../loader/level-key';
25
25
  import Hex from '../utils/hex';
@@ -41,9 +41,6 @@ import type {
41
41
  LoaderConfiguration,
42
42
  LoaderContext,
43
43
  } from '../types/loader';
44
-
45
- const LOGGER_PREFIX = '[eme]';
46
-
47
44
  interface KeySystemAccessPromises {
48
45
  keySystemAccess: Promise<MediaKeySystemAccess>;
49
46
  mediaKeys?: Promise<MediaKeys>;
@@ -68,7 +65,7 @@ export interface MediaKeySessionContext {
68
65
  * @class
69
66
  * @constructor
70
67
  */
71
- class EMEController implements ComponentAPI {
68
+ class EMEController extends Logger implements ComponentAPI {
72
69
  public static CDMCleanupPromise: Promise<void> | void;
73
70
 
74
71
  private readonly hls: Hls;
@@ -90,15 +87,9 @@ class EMEController implements ComponentAPI {
90
87
  private setMediaKeysQueue: Promise<void>[] = EMEController.CDMCleanupPromise
91
88
  ? [EMEController.CDMCleanupPromise]
92
89
  : [];
93
- private onMediaEncrypted = this._onMediaEncrypted.bind(this);
94
- private onWaitingForKey = this._onWaitingForKey.bind(this);
95
-
96
- private debug: (msg: any) => void = logger.debug.bind(logger, LOGGER_PREFIX);
97
- private log: (msg: any) => void = logger.log.bind(logger, LOGGER_PREFIX);
98
- private warn: (msg: any) => void = logger.warn.bind(logger, LOGGER_PREFIX);
99
- private error: (msg: any) => void = logger.error.bind(logger, LOGGER_PREFIX);
100
90
 
101
91
  constructor(hls: Hls) {
92
+ super('eme', hls.logger);
102
93
  this.hls = hls;
103
94
  this.config = hls.config;
104
95
  this.registerListeners();
@@ -113,13 +104,9 @@ class EMEController implements ComponentAPI {
113
104
  config.licenseXhrSetup = config.licenseResponseCallback = undefined;
114
105
  config.drmSystems = config.drmSystemOptions = {};
115
106
  // @ts-ignore
116
- this.hls =
117
- this.onMediaEncrypted =
118
- this.onWaitingForKey =
119
- this.keyIdToKeySessionPromise =
120
- null as any;
107
+ this.hls = this.config = this.keyIdToKeySessionPromise = null;
121
108
  // @ts-ignore
122
- this.config = null;
109
+ this.onMediaEncrypted = this.onWaitingForKey = null;
123
110
  }
124
111
 
125
112
  private registerListeners() {
@@ -523,7 +510,7 @@ class EMEController implements ComponentAPI {
523
510
  return this.attemptKeySystemAccess(keySystemsToAttempt);
524
511
  }
525
512
 
526
- private _onMediaEncrypted(event: MediaEncryptedEvent) {
513
+ private onMediaEncrypted = (event: MediaEncryptedEvent) => {
527
514
  const { initDataType, initData } = event;
528
515
  this.debug(`"${event.type}" event: init data type: "${initDataType}"`);
529
516
 
@@ -639,11 +626,11 @@ class EMEController implements ComponentAPI {
639
626
  );
640
627
  }
641
628
  keySessionContextPromise.catch((error) => this.handleError(error));
642
- }
629
+ };
643
630
 
644
- private _onWaitingForKey(event: Event) {
631
+ private onWaitingForKey = (event: Event) => {
645
632
  this.log(`"${event.type}" event`);
646
- }
633
+ };
647
634
 
648
635
  private attemptSetMediaKeys(
649
636
  keySystem: KeySystems,
@@ -8,7 +8,7 @@ import {
8
8
  } from '../utils/error-helper';
9
9
  import { findFragmentByPTS } from './fragment-finders';
10
10
  import { HdcpLevel, HdcpLevels } from '../types/level';
11
- import { logger } from '../utils/logger';
11
+ import { Logger } from '../utils/logger';
12
12
  import type Hls from '../hls';
13
13
  import type { RetryConfig } from '../config';
14
14
  import type { NetworkComponentAPI } from '../types/component-api';
@@ -50,19 +50,17 @@ type PenalizedRendition = {
50
50
 
51
51
  type PenalizedRenditions = { [key: number]: PenalizedRendition };
52
52
 
53
- export default class ErrorController implements NetworkComponentAPI {
53
+ export default class ErrorController
54
+ extends Logger
55
+ implements NetworkComponentAPI
56
+ {
54
57
  private readonly hls: Hls;
55
58
  private playlistError: number = 0;
56
59
  private penalizedRenditions: PenalizedRenditions = {};
57
- private log: (msg: any) => void;
58
- private warn: (msg: any) => void;
59
- private error: (msg: any) => void;
60
60
 
61
61
  constructor(hls: Hls) {
62
+ super('error-controller', hls.logger);
62
63
  this.hls = hls;
63
- this.log = logger.log.bind(logger, `[info]:`);
64
- this.warn = logger.warn.bind(logger, `[warning]:`);
65
- this.error = logger.error.bind(logger, `[error]:`);
66
64
  this.registerListeners();
67
65
  }
68
66
 
@@ -1,5 +1,4 @@
1
1
  import { Events } from '../events';
2
- import { logger } from '../utils/logger';
3
2
  import type { ComponentAPI } from '../types/component-api';
4
3
  import type Hls from '../hls';
5
4
  import type { MediaAttachingData } from '../types/events';
@@ -28,10 +27,12 @@ class FPSController implements ComponentAPI {
28
27
 
29
28
  protected registerListeners() {
30
29
  this.hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
30
+ this.hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
31
31
  }
32
32
 
33
33
  protected unregisterListeners() {
34
34
  this.hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
35
+ this.hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
35
36
  }
36
37
 
37
38
  destroy() {
@@ -65,6 +66,10 @@ class FPSController implements ComponentAPI {
65
66
  }
66
67
  }
67
68
 
69
+ private onMediaDetaching() {
70
+ this.media = null;
71
+ }
72
+
68
73
  checkFPS(
69
74
  video: HTMLVideoElement,
70
75
  decodedFrames: number,
@@ -84,13 +89,13 @@ class FPSController implements ComponentAPI {
84
89
  totalDroppedFrames: droppedFrames,
85
90
  });
86
91
  if (droppedFPS > 0) {
87
- // logger.log('checkFPS : droppedFPS/decodedFPS:' + droppedFPS/(1000 * currentDecoded / currentPeriod));
92
+ // hls.logger.log('checkFPS : droppedFPS/decodedFPS:' + droppedFPS/(1000 * currentDecoded / currentPeriod));
88
93
  if (
89
94
  currentDropped >
90
95
  hls.config.fpsDroppedMonitoringThreshold * currentDecoded
91
96
  ) {
92
97
  let currentLevel = hls.currentLevel;
93
- logger.warn(
98
+ hls.logger.warn(
94
99
  'drop FPS ratio greater than max allowed value for currentLevel: ' +
95
100
  currentLevel,
96
101
  );
@@ -107,12 +107,23 @@ export class FragmentTracker implements ComponentAPI {
107
107
  public getBufferedFrag(
108
108
  position: number,
109
109
  levelType: PlaylistLevelType,
110
+ ): Fragment | null {
111
+ return this.getFragAtPos(position, levelType, true);
112
+ }
113
+
114
+ public getFragAtPos(
115
+ position: number,
116
+ levelType: PlaylistLevelType,
117
+ buffered?: boolean,
110
118
  ): Fragment | null {
111
119
  const { fragments } = this;
112
120
  const keys = Object.keys(fragments);
113
121
  for (let i = keys.length; i--; ) {
114
122
  const fragmentEntity = fragments[keys[i]];
115
- if (fragmentEntity?.body.type === levelType && fragmentEntity.buffered) {
123
+ if (
124
+ fragmentEntity?.body.type === levelType &&
125
+ (!buffered || fragmentEntity.buffered)
126
+ ) {
116
127
  const frag = fragmentEntity.body;
117
128
  if (frag.start <= position && position <= frag.end) {
118
129
  return frag;
@@ -401,7 +412,7 @@ export class FragmentTracker implements ComponentAPI {
401
412
  event: Events.BUFFER_APPENDED,
402
413
  data: BufferAppendedData,
403
414
  ) {
404
- const { frag, part, timeRanges } = data;
415
+ const { frag, part, timeRanges, type } = data;
405
416
  if (frag.sn === 'initSegment') {
406
417
  return;
407
418
  }
@@ -415,15 +426,8 @@ export class FragmentTracker implements ComponentAPI {
415
426
  }
416
427
  // Store the latest timeRanges loaded in the buffer
417
428
  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
- });
429
+ const timeRange = timeRanges[type] as TimeRanges;
430
+ this.detectEvictedFragments(type, timeRange, playlistType, part);
427
431
  }
428
432
 
429
433
  private onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
@@ -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,7 +4,6 @@ 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,
@@ -19,6 +18,8 @@ import type {
19
18
  } from '../types/events';
20
19
  import type { ComponentAPI } from '../types/component-api';
21
20
  import type Hls from '../hls';
21
+ import { getId3Frames } from '@svta/common-media-library/id3/getId3Frames';
22
+ import { isId3TimestampFrame } from '@svta/common-media-library/id3/isId3TimestampFrame';
22
23
 
23
24
  declare global {
24
25
  interface Window {
@@ -137,11 +138,10 @@ class ID3TrackController implements ComponentAPI {
137
138
  }
138
139
 
139
140
  protected onMediaDetaching(): void {
140
- if (!this.id3Track) {
141
- return;
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
  }
@@ -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(