hls.js 1.5.10-0.canary.10320 → 1.5.10-0.canary.10326

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -130,5 +130,5 @@
130
130
  "url-toolkit": "2.2.5",
131
131
  "wrangler": "3.57.1"
132
132
  },
133
- "version": "1.5.10-0.canary.10320"
133
+ "version": "1.5.10-0.canary.10326"
134
134
  }
@@ -438,7 +438,7 @@ export default class BaseStreamController
438
438
  if (this.state === State.STOPPED || this.state === State.ERROR) {
439
439
  return;
440
440
  }
441
- this.warn(reason);
441
+ this.warn(`Frag error: ${reason?.message || reason}`);
442
442
  this.resetFragmentLoading(frag);
443
443
  });
444
444
  }
@@ -1398,7 +1398,7 @@ export default class BaseStreamController
1398
1398
  let { fragPrevious } = this;
1399
1399
  let { fragments, endSN } = levelDetails;
1400
1400
  const { fragmentHint } = levelDetails;
1401
- const tolerance = config.maxFragLookUpTolerance;
1401
+ const { maxFragLookUpTolerance } = config;
1402
1402
  const partList = levelDetails.partList;
1403
1403
 
1404
1404
  const loadingParts = !!(
@@ -1414,7 +1414,8 @@ export default class BaseStreamController
1414
1414
 
1415
1415
  let frag;
1416
1416
  if (bufferEnd < end) {
1417
- const lookupTolerance = bufferEnd > end - tolerance ? 0 : tolerance;
1417
+ const lookupTolerance =
1418
+ bufferEnd > end - maxFragLookUpTolerance ? 0 : maxFragLookUpTolerance;
1418
1419
  // Remove the tolerance if it would put the bufferEnd past the actual end of stream
1419
1420
  // Uses buffer and sequence number to calculate switch segment (required if using EXT-X-DISCONTINUITY-SEQUENCE)
1420
1421
  frag = findFragmentByPTS(
@@ -58,6 +58,7 @@ export function findFragmentByPTS(
58
58
  fragments: Array<Fragment>,
59
59
  bufferEnd: number = 0,
60
60
  maxFragLookUpTolerance: number = 0,
61
+ nextFragLookupTolerance: number = 0.005,
61
62
  ): Fragment | null {
62
63
  let fragNext: Fragment | null = null;
63
64
  if (fragPrevious) {
@@ -76,9 +77,17 @@ export function findFragmentByPTS(
76
77
  // Prefer the next fragment if it's within tolerance
77
78
  if (
78
79
  fragNext &&
79
- (!fragPrevious || fragPrevious.level === fragNext.level) &&
80
- fragmentWithinToleranceTest(bufferEnd, maxFragLookUpTolerance, fragNext) ===
81
- 0
80
+ (((!fragPrevious || fragPrevious.level === fragNext.level) &&
81
+ fragmentWithinToleranceTest(
82
+ bufferEnd,
83
+ maxFragLookUpTolerance,
84
+ fragNext,
85
+ ) === 0) ||
86
+ fragmentWithinFastStartSwitch(
87
+ fragNext,
88
+ fragPrevious,
89
+ Math.min(nextFragLookupTolerance, maxFragLookUpTolerance),
90
+ ))
82
91
  ) {
83
92
  return fragNext;
84
93
  }
@@ -94,6 +103,28 @@ export function findFragmentByPTS(
94
103
  return fragNext;
95
104
  }
96
105
 
106
+ function fragmentWithinFastStartSwitch(
107
+ fragNext: Fragment,
108
+ fragPrevious: Fragment | null,
109
+ nextFragLookupTolerance: number,
110
+ ): boolean {
111
+ if (
112
+ fragPrevious &&
113
+ fragPrevious.start === 0 &&
114
+ fragPrevious.level < fragNext.level &&
115
+ (fragPrevious.endPTS || 0) > 0
116
+ ) {
117
+ const firstDuration = fragPrevious.tagList.reduce((duration, tag) => {
118
+ if (tag[0] === 'INF') {
119
+ duration += parseFloat(tag[1]);
120
+ }
121
+ return duration;
122
+ }, nextFragLookupTolerance);
123
+ return fragNext.start <= firstDuration;
124
+ }
125
+ return false;
126
+ }
127
+
97
128
  /**
98
129
  * The test function used by the findFragmentBySn's BinarySearch to look for the best match to the current buffer conditions.
99
130
  * @param candidate - The fragment to test
@@ -1268,7 +1268,13 @@ export default class StreamController
1268
1268
  // In the case that AAC and HE-AAC audio codecs are signalled in manifest,
1269
1269
  // force HE-AAC, as it seems that most browsers prefers it.
1270
1270
  // don't force HE-AAC if mono stream, or in Firefox
1271
- if (audio.metadata.channelCount !== 1 && ua.indexOf('firefox') === -1) {
1271
+ const audioMetadata = audio.metadata;
1272
+ if (
1273
+ audioMetadata &&
1274
+ 'channelCount' in audioMetadata &&
1275
+ (audioMetadata.channelCount || 1) !== 1 &&
1276
+ ua.indexOf('firefox') === -1
1277
+ ) {
1272
1278
  audioCodec = 'mp4a.40.5';
1273
1279
  }
1274
1280
  }
@@ -17,7 +17,7 @@ import { Fragment, Part } from '../loader/fragment';
17
17
  import { getM2TSSupportedAudioTypes } from '../utils/codecs';
18
18
  import type { ChunkMetadata, TransmuxerResult } from '../types/transmuxer';
19
19
  import type Hls from '../hls';
20
- import type { HlsEventEmitter } from '../events';
20
+ import type { HlsEventEmitter, HlsListeners } from '../events';
21
21
  import type { PlaylistLevelType } from '../types/loader';
22
22
  import type { RationalTimestamp } from '../utils/timescale-conversion';
23
23
 
@@ -30,7 +30,9 @@ export default class TransmuxerInterface {
30
30
  private part: Part | null = null;
31
31
  private useWorker: boolean;
32
32
  private workerContext: WorkerContext | null = null;
33
- private onwmsg?: Function;
33
+ private onwmsg?: (
34
+ event: MessageEvent<{ event: string; data?: any } | null>,
35
+ ) => void;
34
36
  private transmuxer: Transmuxer | null = null;
35
37
  private onTransmuxComplete: (transmuxResult: TransmuxerResult) => void;
36
38
  private onFlush: (chunkMeta: ChunkMetadata) => void;
@@ -67,9 +69,6 @@ export default class TransmuxerInterface {
67
69
  config.preferManagedMediaSource,
68
70
  );
69
71
 
70
- // navigator.vendor is not always available in Web Worker
71
- // refer to https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/navigator
72
- const vendor = navigator.vendor;
73
72
  if (this.useWorker && typeof Worker !== 'undefined') {
74
73
  const canCreateWorker = config.workerPath || hasUMDWorker();
75
74
  if (canCreateWorker) {
@@ -81,9 +80,9 @@ export default class TransmuxerInterface {
81
80
  logger.log(`injecting Web Worker for "${id}"`);
82
81
  this.workerContext = injectWorker();
83
82
  }
84
- this.onwmsg = (ev: any) => this.onWorkerMessage(ev);
83
+ this.onwmsg = (event) => this.onWorkerMessage(event);
85
84
  const { worker } = this.workerContext;
86
- worker.addEventListener('message', this.onwmsg as any);
85
+ worker.addEventListener('message', this.onwmsg);
87
86
  worker.onerror = (event) => {
88
87
  const error = new Error(
89
88
  `${event.message} (${event.filename}:${event.lineno})`,
@@ -101,7 +100,7 @@ export default class TransmuxerInterface {
101
100
  worker.postMessage({
102
101
  cmd: 'init',
103
102
  typeSupported: m2tsTypeSupported,
104
- vendor: vendor,
103
+ vendor: '',
105
104
  id: id,
106
105
  config: JSON.stringify(config),
107
106
  });
@@ -116,7 +115,7 @@ export default class TransmuxerInterface {
116
115
  this.observer,
117
116
  m2tsTypeSupported,
118
117
  config,
119
- vendor,
118
+ '',
120
119
  id,
121
120
  );
122
121
  }
@@ -128,12 +127,12 @@ export default class TransmuxerInterface {
128
127
  this.observer,
129
128
  m2tsTypeSupported,
130
129
  config,
131
- vendor,
130
+ '',
132
131
  id,
133
132
  );
134
133
  }
135
134
 
136
- resetWorker(): void {
135
+ resetWorker() {
137
136
  if (this.workerContext) {
138
137
  const { worker, objectURL } = this.workerContext;
139
138
  if (objectURL) {
@@ -147,7 +146,7 @@ export default class TransmuxerInterface {
147
146
  }
148
147
  }
149
148
 
150
- destroy(): void {
149
+ destroy() {
151
150
  if (this.workerContext) {
152
151
  this.resetWorker();
153
152
  this.onwmsg = undefined;
@@ -180,7 +179,7 @@ export default class TransmuxerInterface {
180
179
  accurateTimeOffset: boolean,
181
180
  chunkMeta: ChunkMetadata,
182
181
  defaultInitPTS?: RationalTimestamp,
183
- ): void {
182
+ ) {
184
183
  chunkMeta.transmuxing.start = self.performance.now();
185
184
  const { transmuxer } = this;
186
185
  const timeOffset = part ? part.start : frag.start;
@@ -347,9 +346,20 @@ export default class TransmuxerInterface {
347
346
  this.onFlush(chunkMeta);
348
347
  }
349
348
 
350
- private onWorkerMessage(ev: any): void {
351
- const data = ev.data;
349
+ private onWorkerMessage(
350
+ event: MessageEvent<{ event: string; data?: any } | null>,
351
+ ) {
352
+ const data = event.data;
353
+ if (!data?.event) {
354
+ logger.warn(
355
+ `worker message received with no ${data ? 'event name' : 'data'}`,
356
+ );
357
+ return;
358
+ }
352
359
  const hls = this.hls;
360
+ if (!this.hls) {
361
+ return;
362
+ }
353
363
  switch (data.event) {
354
364
  case 'init': {
355
365
  const objectURL = this.workerContext?.objectURL;
@@ -381,7 +391,7 @@ export default class TransmuxerInterface {
381
391
  data.data = data.data || {};
382
392
  data.data.frag = this.frag;
383
393
  data.data.id = this.id;
384
- hls.trigger(data.event, data.data);
394
+ hls.trigger(data.event as keyof HlsListeners, data.data);
385
395
  break;
386
396
  }
387
397
  }
@@ -43,7 +43,7 @@ function startWorker(self) {
43
43
  observer,
44
44
  data.typeSupported,
45
45
  config,
46
- data.vendor,
46
+ '',
47
47
  data.id,
48
48
  );
49
49
  const logger = enableLogs(config.debug, data.id);
@@ -374,6 +374,7 @@ class TSDemuxer implements Demuxer {
374
374
  offset,
375
375
  this.typeSupported,
376
376
  isSampleAes,
377
+ this.observer,
377
378
  );
378
379
 
379
380
  // only update track id if track PID found while parsing PMT
@@ -422,16 +423,12 @@ class TSDemuxer implements Demuxer {
422
423
  }
423
424
 
424
425
  if (tsPacketErrors > 0) {
425
- const error = new Error(
426
- `Found ${tsPacketErrors} TS packet/s that do not start with 0x47`,
426
+ emitParsingError(
427
+ this.observer,
428
+ new Error(
429
+ `Found ${tsPacketErrors} TS packet/s that do not start with 0x47`,
430
+ ),
427
431
  );
428
- this.observer.emit(Events.ERROR, Events.ERROR, {
429
- type: ErrorTypes.MEDIA_ERROR,
430
- details: ErrorDetails.FRAG_PARSING_ERROR,
431
- fatal: false,
432
- error,
433
- reason: error.message,
434
- });
435
432
  }
436
433
 
437
434
  videoTrack.pesData = videoData;
@@ -628,16 +625,7 @@ class TSDemuxer implements Demuxer {
628
625
  } else {
629
626
  reason = 'No ADTS header found in AAC PES';
630
627
  }
631
- const error = new Error(reason);
632
- logger.warn(`parsing error: ${reason}`);
633
- this.observer.emit(Events.ERROR, Events.ERROR, {
634
- type: ErrorTypes.MEDIA_ERROR,
635
- details: ErrorDetails.FRAG_PARSING_ERROR,
636
- fatal: false,
637
- levelRetry: recoverable,
638
- error,
639
- reason,
640
- });
628
+ emitParsingError(this.observer, new Error(reason), recoverable);
641
629
  if (!recoverable) {
642
630
  return;
643
631
  }
@@ -768,6 +756,7 @@ function parsePMT(
768
756
  offset: number,
769
757
  typeSupported: TypeSupported,
770
758
  isSampleAes: boolean,
759
+ observer: HlsEventEmitter,
771
760
  ) {
772
761
  const result = {
773
762
  audioPid: -1,
@@ -897,7 +886,8 @@ function parsePMT(
897
886
  case 0xc2: // SAMPLE-AES EC3
898
887
  /* falls through */
899
888
  case 0x87:
900
- throw new Error('Unsupported EC-3 in M2TS found');
889
+ emitParsingError(observer, new Error('Unsupported EC-3 in M2TS found'));
890
+ return result;
901
891
 
902
892
  case 0x24: // ITU-T Rec. H.265 and ISO/IEC 23008-2 (HEVC)
903
893
  if (__USE_M2TS_ADVANCED_CODECS__) {
@@ -907,7 +897,11 @@ function parsePMT(
907
897
  logger.log('HEVC in M2TS found');
908
898
  }
909
899
  } else {
910
- throw new Error('Unsupported HEVC in M2TS found');
900
+ emitParsingError(
901
+ observer,
902
+ new Error('Unsupported HEVC in M2TS found'),
903
+ );
904
+ return result;
911
905
  }
912
906
  break;
913
907
 
@@ -922,6 +916,22 @@ function parsePMT(
922
916
  return result;
923
917
  }
924
918
 
919
+ function emitParsingError(
920
+ observer: HlsEventEmitter,
921
+ error: Error,
922
+ levelRetry?: boolean,
923
+ ) {
924
+ logger.warn(`parsing error: ${error.message}`);
925
+ observer.emit(Events.ERROR, Events.ERROR, {
926
+ type: ErrorTypes.MEDIA_ERROR,
927
+ details: ErrorDetails.FRAG_PARSING_ERROR,
928
+ fatal: false,
929
+ levelRetry,
930
+ error,
931
+ reason: error.message,
932
+ });
933
+ }
934
+
925
935
  function logEncryptedSamplesFoundInUnencryptedStream(type: string) {
926
936
  logger.log(`${type} with AES-128-CBC encryption found in unencrypted stream`);
927
937
  }
@@ -40,8 +40,6 @@ class XhrLoader implements Loader<LoaderContext> {
40
40
  this.config = null;
41
41
  this.context = null;
42
42
  this.xhrSetup = null;
43
- // @ts-ignore
44
- this.stats = null;
45
43
  }
46
44
 
47
45
  abortInternal() {
@@ -100,15 +98,16 @@ class XhrLoader implements Loader<LoaderContext> {
100
98
  if (xhrSetup) {
101
99
  Promise.resolve()
102
100
  .then(() => {
103
- if (this.stats.aborted) return;
101
+ if (this.loader !== xhr || this.stats.aborted) return;
104
102
  return xhrSetup(xhr, context.url);
105
103
  })
106
104
  .catch((error: Error) => {
105
+ if (this.loader !== xhr || this.stats.aborted) return;
107
106
  xhr.open('GET', context.url, true);
108
107
  return xhrSetup(xhr, context.url);
109
108
  })
110
109
  .then(() => {
111
- if (this.stats.aborted) return;
110
+ if (this.loader !== xhr || this.stats.aborted) return;
112
111
  this.openAndSendXhr(xhr, context, config);
113
112
  })
114
113
  .catch((error: Error) => {
@@ -263,7 +262,8 @@ class XhrLoader implements Loader<LoaderContext> {
263
262
  }
264
263
 
265
264
  loadtimeout() {
266
- const retryConfig = this.config?.loadPolicy.timeoutRetry;
265
+ if (!this.config) return;
266
+ const retryConfig = this.config.loadPolicy.timeoutRetry;
267
267
  const retryCount = this.stats.retry;
268
268
  if (shouldRetry(retryConfig, retryCount, true)) {
269
269
  this.retry(retryConfig);