rx-player 4.2.0-dev.2024092400 → 4.2.0-dev.2024101500

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 (105) hide show
  1. package/.vscode/settings.json +9 -0
  2. package/CHANGELOG.md +22 -3
  3. package/VERSION +1 -1
  4. package/dist/commonjs/__GENERATED_CODE/embedded_dash_wasm.d.ts.map +1 -1
  5. package/dist/commonjs/__GENERATED_CODE/embedded_dash_wasm.js +1 -1
  6. package/dist/commonjs/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
  7. package/dist/commonjs/__GENERATED_CODE/embedded_worker.js +1 -1
  8. package/dist/commonjs/core/main/worker/worker_main.d.ts.map +1 -1
  9. package/dist/commonjs/core/main/worker/worker_main.js +3 -0
  10. package/dist/commonjs/core/segment_sinks/segment_buffers_store.d.ts.map +1 -1
  11. package/dist/commonjs/core/stream/adaptation/adaptation_stream.d.ts.map +1 -1
  12. package/dist/commonjs/core/stream/adaptation/adaptation_stream.js +15 -0
  13. package/dist/commonjs/core/stream/representation/representation_stream.d.ts.map +1 -1
  14. package/dist/commonjs/core/stream/representation/representation_stream.js +2 -0
  15. package/dist/commonjs/experimental/tools/mediaCapabilitiesProber/probers/DRMInfos.d.ts.map +1 -1
  16. package/dist/commonjs/experimental/tools/mediaCapabilitiesProber/probers/DRMInfos.js +85 -8
  17. package/dist/commonjs/main_thread/api/debug/modules/general_info.js +1 -1
  18. package/dist/commonjs/main_thread/api/public_api.js +2 -2
  19. package/dist/commonjs/main_thread/decrypt/find_key_system.d.ts.map +1 -1
  20. package/dist/commonjs/main_thread/decrypt/find_key_system.js +3 -0
  21. package/dist/commonjs/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
  22. package/dist/commonjs/main_thread/init/directfile_content_initializer.js +14 -6
  23. package/dist/commonjs/main_thread/init/media_source_content_initializer.d.ts +11 -1
  24. package/dist/commonjs/main_thread/init/media_source_content_initializer.d.ts.map +1 -1
  25. package/dist/commonjs/main_thread/init/media_source_content_initializer.js +23 -11
  26. package/dist/commonjs/main_thread/init/multi_thread_content_initializer.d.ts +15 -1
  27. package/dist/commonjs/main_thread/init/multi_thread_content_initializer.d.ts.map +1 -1
  28. package/dist/commonjs/main_thread/init/multi_thread_content_initializer.js +103 -50
  29. package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.d.ts +1 -1
  30. package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
  31. package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.js +21 -5
  32. package/dist/commonjs/main_thread/text_displayer/native/native_text_displayer.d.ts +1 -0
  33. package/dist/commonjs/main_thread/text_displayer/native/native_text_displayer.d.ts.map +1 -1
  34. package/dist/commonjs/main_thread/text_displayer/native/native_text_displayer.js +19 -16
  35. package/dist/commonjs/mse/main_media_source_interface.d.ts.map +1 -1
  36. package/dist/commonjs/mse/main_media_source_interface.js +21 -2
  37. package/dist/commonjs/transports/smooth/pipelines.d.ts.map +1 -1
  38. package/dist/commonjs/transports/smooth/pipelines.js +1 -0
  39. package/dist/commonjs/transports/utils/parse_text_track.d.ts.map +1 -1
  40. package/dist/commonjs/transports/utils/parse_text_track.js +1 -0
  41. package/dist/commonjs/utils/sync_or_async.d.ts.map +1 -1
  42. package/dist/commonjs/utils/sync_or_async.js +3 -1
  43. package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.d.ts.map +1 -1
  44. package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.js +1 -1
  45. package/dist/es2017/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
  46. package/dist/es2017/__GENERATED_CODE/embedded_worker.js +1 -1
  47. package/dist/es2017/core/main/worker/worker_main.d.ts.map +1 -1
  48. package/dist/es2017/core/main/worker/worker_main.js +3 -0
  49. package/dist/es2017/core/segment_sinks/segment_buffers_store.d.ts.map +1 -1
  50. package/dist/es2017/core/stream/adaptation/adaptation_stream.d.ts.map +1 -1
  51. package/dist/es2017/core/stream/adaptation/adaptation_stream.js +15 -0
  52. package/dist/es2017/core/stream/representation/representation_stream.d.ts.map +1 -1
  53. package/dist/es2017/core/stream/representation/representation_stream.js +2 -0
  54. package/dist/es2017/experimental/tools/mediaCapabilitiesProber/probers/DRMInfos.d.ts.map +1 -1
  55. package/dist/es2017/experimental/tools/mediaCapabilitiesProber/probers/DRMInfos.js +36 -7
  56. package/dist/es2017/main_thread/api/debug/modules/general_info.js +1 -1
  57. package/dist/es2017/main_thread/api/public_api.js +2 -2
  58. package/dist/es2017/main_thread/decrypt/find_key_system.d.ts.map +1 -1
  59. package/dist/es2017/main_thread/decrypt/find_key_system.js +3 -0
  60. package/dist/es2017/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
  61. package/dist/es2017/main_thread/init/directfile_content_initializer.js +14 -6
  62. package/dist/es2017/main_thread/init/media_source_content_initializer.d.ts +11 -1
  63. package/dist/es2017/main_thread/init/media_source_content_initializer.d.ts.map +1 -1
  64. package/dist/es2017/main_thread/init/media_source_content_initializer.js +23 -11
  65. package/dist/es2017/main_thread/init/multi_thread_content_initializer.d.ts +15 -1
  66. package/dist/es2017/main_thread/init/multi_thread_content_initializer.d.ts.map +1 -1
  67. package/dist/es2017/main_thread/init/multi_thread_content_initializer.js +87 -46
  68. package/dist/es2017/main_thread/init/utils/initial_seek_and_play.d.ts +1 -1
  69. package/dist/es2017/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
  70. package/dist/es2017/main_thread/init/utils/initial_seek_and_play.js +19 -3
  71. package/dist/es2017/main_thread/text_displayer/native/native_text_displayer.d.ts +1 -0
  72. package/dist/es2017/main_thread/text_displayer/native/native_text_displayer.d.ts.map +1 -1
  73. package/dist/es2017/main_thread/text_displayer/native/native_text_displayer.js +19 -16
  74. package/dist/es2017/mse/main_media_source_interface.d.ts.map +1 -1
  75. package/dist/es2017/mse/main_media_source_interface.js +19 -0
  76. package/dist/es2017/transports/smooth/pipelines.d.ts.map +1 -1
  77. package/dist/es2017/transports/smooth/pipelines.js +1 -0
  78. package/dist/es2017/transports/utils/parse_text_track.d.ts.map +1 -1
  79. package/dist/es2017/transports/utils/parse_text_track.js +1 -0
  80. package/dist/es2017/utils/sync_or_async.d.ts.map +1 -1
  81. package/dist/es2017/utils/sync_or_async.js +3 -1
  82. package/dist/mpd-parser.wasm +0 -0
  83. package/dist/rx-player.js +104 -41
  84. package/dist/rx-player.min.js +17 -17
  85. package/dist/worker.js +5 -5
  86. package/package.json +19 -7
  87. package/src/__GENERATED_CODE/embedded_dash_wasm.ts +1 -1
  88. package/src/__GENERATED_CODE/embedded_worker.ts +1 -1
  89. package/src/core/main/worker/worker_main.ts +3 -0
  90. package/src/core/segment_sinks/segment_buffers_store.ts +6 -2
  91. package/src/core/stream/adaptation/adaptation_stream.ts +23 -1
  92. package/src/core/stream/representation/representation_stream.ts +11 -0
  93. package/src/experimental/tools/mediaCapabilitiesProber/probers/DRMInfos.ts +39 -7
  94. package/src/main_thread/api/debug/modules/general_info.ts +1 -1
  95. package/src/main_thread/api/public_api.ts +2 -2
  96. package/src/main_thread/decrypt/find_key_system.ts +3 -0
  97. package/src/main_thread/init/directfile_content_initializer.ts +20 -10
  98. package/src/main_thread/init/media_source_content_initializer.ts +50 -17
  99. package/src/main_thread/init/multi_thread_content_initializer.ts +109 -50
  100. package/src/main_thread/init/utils/initial_seek_and_play.ts +24 -5
  101. package/src/main_thread/text_displayer/native/native_text_displayer.ts +22 -18
  102. package/src/mse/main_media_source_interface.ts +20 -0
  103. package/src/transports/smooth/pipelines.ts +1 -0
  104. package/src/transports/utils/parse_text_track.ts +1 -0
  105. package/src/utils/sync_or_async.ts +5 -3
@@ -80,6 +80,9 @@ export default function initializeWorkerMain() {
80
80
  */
81
81
  let playbackObservationRef: SharedReference<IWorkerPlaybackObservation> | null = null;
82
82
 
83
+ onmessageerror = (_msg: MessageEvent) => {
84
+ log.error("MTCI: Error when receiving message from main thread.");
85
+ };
83
86
  onmessage = function (e: MessageEvent<IMainThreadMessage>) {
84
87
  log.debug("Worker: received message", e.data.type);
85
88
 
@@ -103,7 +103,11 @@ export default class SegmentSinksStore {
103
103
  * disabled. This means that the corresponding type (e.g. audio, video etc.)
104
104
  * won't be needed when playing the current content.
105
105
  */
106
- private _initializedSegmentSinks: Partial<Record<IBufferType, SegmentSink | null>>;
106
+ private _initializedSegmentSinks: {
107
+ audio?: AudioVideoSegmentSink | undefined | null;
108
+ video?: AudioVideoSegmentSink | undefined | null;
109
+ text?: TextSegmentSink | null;
110
+ };
107
111
 
108
112
  /**
109
113
  * Callbacks called after a SourceBuffer is either created or disabled.
@@ -308,7 +312,7 @@ export default class SegmentSinksStore {
308
312
  return memorizedSegmentSink;
309
313
  }
310
314
 
311
- let segmentSink: SegmentSink;
315
+ let segmentSink: TextSegmentSink;
312
316
  if (bufferType === "text") {
313
317
  log.info("SB: Creating a new text SegmentSink");
314
318
  if (this._textInterface === null) {
@@ -376,8 +376,13 @@ export default function AdaptationStream(
376
376
  representationStreamCallbacks: IRepresentationStreamCallbacks,
377
377
  fnCancelSignal: CancellationSignal,
378
378
  ): void {
379
+ /** Set to `true` if we've encountered an error with this `RepresentationStream` */
380
+ let hasEncounteredError = false;
381
+
379
382
  const bufferGoalCanceller = new TaskCanceller();
380
383
  bufferGoalCanceller.linkToSignal(fnCancelSignal);
384
+
385
+ /** Actually built buffer size, in seconds. */
381
386
  const bufferGoal = createMappedReference(
382
387
  wantedBufferAhead,
383
388
  (prev) => {
@@ -385,6 +390,7 @@ export default function AdaptationStream(
385
390
  },
386
391
  bufferGoalCanceller.signal,
387
392
  );
393
+
388
394
  const maxBufferSize =
389
395
  adaptation.type === "video" ? maxVideoBufferSize : new SharedReference(Infinity);
390
396
  log.info(
@@ -394,7 +400,18 @@ export default function AdaptationStream(
394
400
  representation.bitrate,
395
401
  );
396
402
  const updatedCallbacks = objectAssign({}, representationStreamCallbacks, {
397
- error(err: unknown) {
403
+ error(err: Error) {
404
+ if (hasEncounteredError) {
405
+ // A RepresentationStream might trigger multiple Errors (for example
406
+ // multiple segments it tried to push at once led to errors).
407
+ // In that case, we'll only consider the first Error.
408
+ //
409
+ // That could mean that we're hiding legitimate issues but handling
410
+ // multiple of those errors at once is too hard a task for now.
411
+ log.warn("Stream: Ignoring RepresentationStream error", err);
412
+ return;
413
+ }
414
+ hasEncounteredError = true;
398
415
  const formattedError = formatError(err, {
399
416
  defaultCode: "NONE",
400
417
  defaultReason: "Unknown `RepresentationStream` error",
@@ -402,6 +419,11 @@ export default function AdaptationStream(
402
419
  if (formattedError.code !== "BUFFER_FULL_ERROR") {
403
420
  representationStreamCallbacks.error(err);
404
421
  } else {
422
+ log.warn(
423
+ "Stream: received BUFFER_FULL_ERROR",
424
+ adaptation.type,
425
+ representation.bitrate,
426
+ );
405
427
  const wba = wantedBufferAhead.getValue();
406
428
  const lastBufferGoalRatio = bufferGoalRatioMap.get(representation.id) ?? 1;
407
429
  // 70%, 49%, 34.3%, 24%, 16.81%, 11.76%, 8.24% and 5.76%
@@ -90,6 +90,11 @@ export default function RepresentationStream<TSegmentDataType>(
90
90
  callbacks: IRepresentationStreamCallbacks,
91
91
  parentCancelSignal: CancellationSignal,
92
92
  ): void {
93
+ log.debug(
94
+ "Stream: Creating RepresentationStream",
95
+ content.adaptation.type,
96
+ content.representation.bitrate,
97
+ );
93
98
  const { period, adaptation, representation } = content;
94
99
  const { bufferGoal, maxBufferSize, drmSystemId, fastSwitchThreshold } = options;
95
100
  const bufferType = adaptation.type;
@@ -488,6 +493,12 @@ export default function RepresentationStream<TSegmentDataType>(
488
493
  // We can thus ignore it, it is very unlikely to lead to true buffer issues.
489
494
  return;
490
495
  }
496
+ log.warn(
497
+ "Stream: Received fatal buffer error",
498
+ adaptation.type,
499
+ representation.bitrate,
500
+ err instanceof Error ? err : null,
501
+ );
491
502
  globalCanceller.cancel();
492
503
  callbacks.error(err);
493
504
  }
@@ -14,7 +14,12 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
+ import { canRelyOnRequestMediaKeySystemAccess } from "../../../../compat/can_rely_on_request_media_key_system_access";
17
18
  import eme from "../../../../compat/eme";
19
+ import {
20
+ DUMMY_PLAY_READY_HEADER,
21
+ generatePlayReadyInitData,
22
+ } from "../../../../compat/generate_init_data";
18
23
  import isNullOrUndefined from "../../../../utils/is_null_or_undefined";
19
24
  import log from "../log";
20
25
  import type { ICompatibleKeySystem, IMediaConfiguration } from "../types";
@@ -57,13 +62,40 @@ export default function probeDRMInfos(
57
62
 
58
63
  return eme
59
64
  .requestMediaKeySystemAccess(type, [configuration])
60
- .then((keySystemAccess) => {
61
- result.compatibleConfiguration = keySystemAccess.getConfiguration();
62
- const status: [ProberStatus, ICompatibleKeySystem?] = [
63
- ProberStatus.Supported,
64
- result,
65
- ];
66
- return status;
65
+ .then(async (keySystemAccess) => {
66
+ if (!canRelyOnRequestMediaKeySystemAccess(type)) {
67
+ try {
68
+ const mediaKeys = await keySystemAccess.createMediaKeys();
69
+ const session = mediaKeys.createSession();
70
+ const initData = generatePlayReadyInitData(DUMMY_PLAY_READY_HEADER);
71
+ await session.generateRequest("cenc", initData);
72
+ session.close().catch(() => {
73
+ log.warn("DRM: Failed to close the dummy session");
74
+ });
75
+ result.compatibleConfiguration = keySystemAccess.getConfiguration();
76
+ const status: [ProberStatus, ICompatibleKeySystem?] = [
77
+ ProberStatus.Supported,
78
+ result,
79
+ ];
80
+ return status;
81
+ } catch (err) {
82
+ log.debug("DRM: KeySystemAccess was granted but it is not usable");
83
+
84
+ const statusError: [ProberStatus, ICompatibleKeySystem] = [
85
+ ProberStatus.NotSupported,
86
+ result,
87
+ ];
88
+ return statusError;
89
+ }
90
+ } else {
91
+ result.compatibleConfiguration = keySystemAccess.getConfiguration();
92
+
93
+ const status: [ProberStatus, ICompatibleKeySystem?] = [
94
+ ProberStatus.Supported,
95
+ result,
96
+ ];
97
+ return status;
98
+ }
67
99
  })
68
100
  .catch(() => {
69
101
  return [ProberStatus.NotSupported, result];
@@ -65,7 +65,7 @@ export default function constructDebugGeneralInfo(
65
65
  valuesLine1.push(["wo", "0"]);
66
66
  }
67
67
 
68
- const valuesLine2: Array<[string, string]> = [];
68
+ const valuesLine2: Array<[string, string]> = [["v", instance.version]];
69
69
  const ks = instance.getKeySystemConfiguration();
70
70
  if (ks !== null) {
71
71
  valuesLine2.push(["ks", ks.keySystem]);
@@ -411,7 +411,7 @@ class Player extends EventEmitter<IPublicAPIEvent> {
411
411
  // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1194624
412
412
  videoElement.preload = "auto";
413
413
 
414
- this.version = /* PLAYER_VERSION */ "4.2.0-dev.2024092400";
414
+ this.version = /* PLAYER_VERSION */ "4.2.0-dev.2024101500";
415
415
  this.log = log;
416
416
  this.state = "STOPPED";
417
417
  this.videoElement = videoElement;
@@ -3330,7 +3330,7 @@ class Player extends EventEmitter<IPublicAPIEvent> {
3330
3330
  }
3331
3331
  }
3332
3332
  }
3333
- Player.version = /* PLAYER_VERSION */ "4.2.0-dev.2024092400";
3333
+ Player.version = /* PLAYER_VERSION */ "4.2.0-dev.2024101500";
3334
3334
 
3335
3335
  /** Every events sent by the RxPlayer's public API. */
3336
3336
  interface IPublicAPIEvent {
@@ -539,6 +539,9 @@ export async function testKeySystem(
539
539
  const session = mediaKeys.createSession();
540
540
  const initData = generatePlayReadyInitData(DUMMY_PLAY_READY_HEADER);
541
541
  await session.generateRequest("cenc", initData);
542
+ session.close().catch(() => {
543
+ log.warn("DRM: Failed to close the dummy session");
544
+ });
542
545
  } catch (err) {
543
546
  log.debug("DRM: KeySystemAccess was granted but it is not usable");
544
547
  throw err;
@@ -254,7 +254,7 @@ export default class DirectFileContentInitializer extends ContentInitializer {
254
254
  function getDirectFileInitialTime(
255
255
  mediaElement: IMediaElement,
256
256
  startAt?: IInitialTimeOptions,
257
- ): number {
257
+ ): number | undefined {
258
258
  if (isNullOrUndefined(startAt)) {
259
259
  return 0;
260
260
  }
@@ -268,28 +268,38 @@ function getDirectFileInitialTime(
268
268
  }
269
269
 
270
270
  const duration = mediaElement.duration;
271
-
272
271
  if (typeof startAt.fromLastPosition === "number") {
273
- if (isNullOrUndefined(duration) || !isFinite(duration)) {
274
- log.warn(
275
- "startAt.fromLastPosition set but no known duration, " + "beginning at 0.",
276
- );
277
- return 0;
272
+ if (!isNullOrUndefined(duration) && isFinite(duration)) {
273
+ return Math.max(0, duration + startAt.fromLastPosition);
278
274
  }
279
- return Math.max(0, duration + startAt.fromLastPosition);
275
+
276
+ if (mediaElement.seekable.length > 0) {
277
+ const lastSegmentEnd = mediaElement.seekable.end(mediaElement.seekable.length - 1);
278
+ if (isFinite(lastSegmentEnd)) {
279
+ return Math.max(0, lastSegmentEnd + startAt.fromLastPosition);
280
+ }
281
+ }
282
+ log.warn(
283
+ "Init: startAt.fromLastPosition set but no known duration, " +
284
+ "it may be too soon to seek",
285
+ );
286
+ return undefined;
280
287
  } else if (typeof startAt.fromLivePosition === "number") {
281
288
  const livePosition =
282
289
  mediaElement.seekable.length > 0 ? mediaElement.seekable.end(0) : duration;
283
290
  if (isNullOrUndefined(livePosition)) {
284
291
  log.warn(
285
- "startAt.fromLivePosition set but no known live position, " + "beginning at 0.",
292
+ "Init: startAt.fromLivePosition set but no known live position, " +
293
+ "beginning at 0.",
286
294
  );
287
295
  return 0;
288
296
  }
289
297
  return Math.max(0, livePosition + startAt.fromLivePosition);
290
298
  } else if (!isNullOrUndefined(startAt.percentage)) {
291
299
  if (isNullOrUndefined(duration) || !isFinite(duration)) {
292
- log.warn("startAt.percentage set but no known duration, " + "beginning at 0.");
300
+ log.warn(
301
+ "Init: startAt.percentage set but no known duration, " + "beginning at 0.",
302
+ );
293
303
  return 0;
294
304
  }
295
305
  const { percentage } = startAt;
@@ -92,7 +92,7 @@ import listenToMediaError from "./utils/throw_on_media_error";
92
92
  */
93
93
  export default class MediaSourceContentInitializer extends ContentInitializer {
94
94
  /** Constructor settings associated to this `MediaSourceContentInitializer`. */
95
- private _settings: IInitializeArguments;
95
+ private _initSettings: IInitializeArguments;
96
96
  /**
97
97
  * `TaskCanceller` allowing to abort everything that the
98
98
  * `MediaSourceContentInitializer` is doing.
@@ -145,7 +145,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
145
145
  */
146
146
  constructor(settings: IInitializeArguments) {
147
147
  super();
148
- this._settings = settings;
148
+ this._initSettings = settings;
149
149
  this._initCanceller = new TaskCanceller();
150
150
  this._manifest = null;
151
151
  this._decryptionCapabilities = { status: "uninitialized", value: null };
@@ -234,6 +234,10 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
234
234
  this._initCanceller.cancel();
235
235
  }
236
236
 
237
+ /**
238
+ * Callback called when an error interrupting playback arised.
239
+ * @param {*} err
240
+ */
237
241
  private _onFatalError(err: unknown) {
238
242
  if (this._initCanceller.isUsed()) {
239
243
  return;
@@ -242,6 +246,12 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
242
246
  this.trigger("error", err);
243
247
  }
244
248
 
249
+ /**
250
+ * Initialize decryption mechanisms if needed and begin creating and relying
251
+ * on the initial `MediaSourceInterface` for this content.
252
+ * @param {HTMLMediaElement|null} mediaElement
253
+ * @returns {Promise.<Object>}
254
+ */
245
255
  private _initializeMediaSourceAndDecryption(mediaElement: IMediaElement): Promise<{
246
256
  mediaSource: MainMediaSourceInterface;
247
257
  drmSystemId: string | undefined;
@@ -249,7 +259,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
249
259
  }> {
250
260
  const initCanceller = this._initCanceller;
251
261
  return createCancellablePromise(initCanceller.signal, (resolve) => {
252
- const { keySystems } = this._settings;
262
+ const { keySystems } = this._initSettings;
253
263
 
254
264
  /** Initialize decryption capabilities. */
255
265
  const { statusRef: drmInitRef, contentDecryptor } = initializeContentDecryption(
@@ -383,7 +393,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
383
393
  startAt,
384
394
  textTrackOptions,
385
395
  transport,
386
- } = this._settings;
396
+ } = this._initSettings;
387
397
  const initCanceller = this._initCanceller;
388
398
  assert(this._manifest !== null);
389
399
  let manifest: IManifest;
@@ -526,7 +536,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
526
536
  */
527
537
  private _startBufferingOnMediaSource(
528
538
  args: IBufferingMediaSettings,
529
- onReloadOrder: (reloadOrder: { position: number; autoPlay: boolean }) => void,
539
+ onReloadOrder: IReloadMediaSourceCallback,
530
540
  cancelSignal: CancellationSignal,
531
541
  ): void {
532
542
  const {
@@ -553,18 +563,10 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
553
563
  }
554
564
 
555
565
  let textDisplayerInterface: ITextDisplayerInterface | null = null;
556
- let textDisplayer: ITextDisplayer | null = null;
557
- if (
558
- this._settings.textTrackOptions.textTrackMode === "html" &&
559
- features.htmlTextDisplayer !== null
560
- ) {
561
- textDisplayer = new features.htmlTextDisplayer(
562
- mediaElement,
563
- this._settings.textTrackOptions.textTrackElement,
564
- );
565
- } else if (features.nativeTextDisplayer !== null) {
566
- textDisplayer = new features.nativeTextDisplayer(mediaElement);
567
- }
566
+ const textDisplayer = createTextDisplayer(
567
+ mediaElement,
568
+ this._initSettings.textTrackOptions,
569
+ );
568
570
  if (textDisplayer !== null) {
569
571
  const sender = new MainThreadTextDisplayerInterface(textDisplayer);
570
572
  textDisplayerInterface = sender;
@@ -1123,6 +1125,21 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
1123
1125
  }
1124
1126
  }
1125
1127
 
1128
+ function createTextDisplayer(
1129
+ mediaElement: IMediaElement,
1130
+ textTrackOptions: ITextDisplayerOptions,
1131
+ ): ITextDisplayer | null {
1132
+ if (textTrackOptions.textTrackMode === "html" && features.htmlTextDisplayer !== null) {
1133
+ return new features.htmlTextDisplayer(
1134
+ mediaElement,
1135
+ textTrackOptions.textTrackElement,
1136
+ );
1137
+ } else if (features.nativeTextDisplayer !== null) {
1138
+ return new features.nativeTextDisplayer(mediaElement);
1139
+ }
1140
+ return null;
1141
+ }
1142
+
1126
1143
  /** Arguments to give to the `InitializeOnMediaSource` function. */
1127
1144
  export interface IInitializeArguments {
1128
1145
  /** Options concerning the ABR logic. */
@@ -1324,3 +1341,19 @@ function blackListProtectionDataOnManifest(
1324
1341
  return rep.decipherable;
1325
1342
  });
1326
1343
  }
1344
+
1345
+ /**
1346
+ * Function to call when you want to "reload" the MediaSource: basically
1347
+ * restarting playback on a new MediaSource for the same content (it may
1348
+ * be for varied reasons, such as ensuring data buffers are empty, or
1349
+ * restarting after some kind of fatal error).
1350
+ * @param {Object} reloadOrder
1351
+ * @param {number} reloadOrder.position - Position in seconds at which we
1352
+ * should restart from when playback restarts.
1353
+ * @param {boolean} reloadOrder.autoPlay - If `true` we will directly play
1354
+ * once enough data is re-loaded.
1355
+ */
1356
+ type IReloadMediaSourceCallback = (reloadOrder: {
1357
+ position: number;
1358
+ autoPlay: boolean;
1359
+ }) => void;
@@ -78,6 +78,20 @@ export default class MultiThreadContentInitializer extends ContentInitializer {
78
78
  /** Constructor settings associated to this `MultiThreadContentInitializer`. */
79
79
  private _settings: IInitializeArguments;
80
80
 
81
+ /**
82
+ * The WebWorker may be sending messages as soon as we're preparing the
83
+ * content but the `MultiThreadContentInitializer` is only able to handle all of
84
+ * them only once `start`ed.
85
+ *
86
+ * As such `_queuedWorkerMessages` is set to an Array when `prepare` has been
87
+ * called but not `start` yet, and contains all worker messages that have to
88
+ * be processed when `start` is called.
89
+ *
90
+ * It is set to `null` when there's no need to rely on that queue (either not
91
+ * yet `prepare`d or already `start`ed).
92
+ */
93
+ private _queuedWorkerMessages: MessageEvent[] | null;
94
+
81
95
  /**
82
96
  * Information relative to the current loaded content.
83
97
  *
@@ -99,12 +113,13 @@ export default class MultiThreadContentInitializer extends ContentInitializer {
99
113
  private _currentMediaSourceCanceller: TaskCanceller;
100
114
 
101
115
  /**
102
- * Stores the resolvers and the current messageId that is sent to the web worker to receive segment sink metrics.
116
+ * Stores the resolvers and the current messageId that is sent to the web worker to
117
+ * receive segment sink metrics.
103
118
  * The purpose of collecting metrics is for monitoring and debugging.
104
119
  */
105
120
  private _segmentMetrics: {
106
121
  lastMessageId: number;
107
- resolvers: Record<number, (value: ISegmentSinkMetrics | undefined) => void>;
122
+ resolvers: Map<number, (value: ISegmentSinkMetrics | undefined) => void>;
108
123
  };
109
124
 
110
125
  /**
@@ -121,8 +136,9 @@ export default class MultiThreadContentInitializer extends ContentInitializer {
121
136
  this._currentContentInfo = null;
122
137
  this._segmentMetrics = {
123
138
  lastMessageId: 0,
124
- resolvers: {},
139
+ resolvers: new Map(),
125
140
  };
141
+ this._queuedWorkerMessages = null;
126
142
  }
127
143
 
128
144
  /**
@@ -176,6 +192,66 @@ export default class MultiThreadContentInitializer extends ContentInitializer {
176
192
  if (this._initCanceller.isUsed()) {
177
193
  return;
178
194
  }
195
+ this._queuedWorkerMessages = [];
196
+ log.debug("MTCI: addEventListener prepare buffering worker messages");
197
+ const onmessage = (evt: MessageEvent): void => {
198
+ const msgData = evt.data as unknown as IWorkerMessage;
199
+ const type = msgData.type;
200
+ switch (type) {
201
+ case WorkerMessageType.LogMessage: {
202
+ const formatted = msgData.value.logs.map((l) => {
203
+ switch (typeof l) {
204
+ case "string":
205
+ case "number":
206
+ case "boolean":
207
+ case "undefined":
208
+ return l;
209
+ case "object":
210
+ if (l === null) {
211
+ return null;
212
+ }
213
+ return formatWorkerError(l);
214
+ default:
215
+ assertUnreachable(l);
216
+ }
217
+ });
218
+ switch (msgData.value.logLevel) {
219
+ case "NONE":
220
+ break;
221
+ case "ERROR":
222
+ log.error(...formatted);
223
+ break;
224
+ case "WARNING":
225
+ log.warn(...formatted);
226
+ break;
227
+ case "INFO":
228
+ log.info(...formatted);
229
+ break;
230
+ case "DEBUG":
231
+ log.debug(...formatted);
232
+ break;
233
+ default:
234
+ assertUnreachable(msgData.value.logLevel);
235
+ }
236
+ break;
237
+ }
238
+ default:
239
+ if (this._queuedWorkerMessages !== null) {
240
+ this._queuedWorkerMessages.push(evt);
241
+ }
242
+ break;
243
+ }
244
+ };
245
+ this._settings.worker.addEventListener("message", onmessage);
246
+ const onmessageerror = (_msg: MessageEvent) => {
247
+ log.error("MTCI: Error when receiving message from worker.");
248
+ };
249
+ this._settings.worker.addEventListener("messageerror", onmessageerror);
250
+ this._initCanceller.signal.register(() => {
251
+ log.debug("MTCI: removeEventListener prepare for worker message");
252
+ this._settings.worker.removeEventListener("message", onmessage);
253
+ this._settings.worker.removeEventListener("messageerror", onmessageerror);
254
+ });
179
255
 
180
256
  // Also bind all `SharedReference` objects:
181
257
 
@@ -1042,69 +1118,44 @@ export default class MultiThreadContentInitializer extends ContentInitializer {
1042
1118
  }
1043
1119
  break;
1044
1120
 
1045
- case WorkerMessageType.LogMessage: {
1046
- const formatted = msgData.value.logs.map((l) => {
1047
- switch (typeof l) {
1048
- case "string":
1049
- case "number":
1050
- case "boolean":
1051
- case "undefined":
1052
- return l;
1053
- case "object":
1054
- if (l === null) {
1055
- return null;
1056
- }
1057
- return formatWorkerError(l);
1058
- default:
1059
- assertUnreachable(l);
1060
- }
1061
- });
1062
- switch (msgData.value.logLevel) {
1063
- case "NONE":
1064
- break;
1065
- case "ERROR":
1066
- log.error(...formatted);
1067
- break;
1068
- case "WARNING":
1069
- log.warn(...formatted);
1070
- break;
1071
- case "INFO":
1072
- log.info(...formatted);
1073
- break;
1074
- case "DEBUG":
1075
- log.debug(...formatted);
1076
- break;
1077
- default:
1078
- assertUnreachable(msgData.value.logLevel);
1079
- }
1080
- break;
1081
- }
1082
-
1083
- case WorkerMessageType.InitSuccess:
1084
- case WorkerMessageType.InitError:
1085
- // Should already be handled by the API
1086
- break;
1087
-
1088
1121
  case WorkerMessageType.SegmentSinkStoreUpdate: {
1089
1122
  if (this._currentContentInfo?.contentId !== msgData.contentId) {
1090
1123
  return;
1091
1124
  }
1092
- const resolveFn = this._segmentMetrics.resolvers[msgData.value.messageId];
1125
+ const resolveFn = this._segmentMetrics.resolvers.get(msgData.value.messageId);
1093
1126
  if (resolveFn !== undefined) {
1094
1127
  resolveFn(msgData.value.segmentSinkMetrics);
1095
- delete this._segmentMetrics.resolvers[msgData.value.messageId];
1096
1128
  } else {
1097
1129
  log.error("MTCI: Failed to send segment sink store update");
1098
1130
  }
1099
1131
  break;
1100
1132
  }
1133
+
1134
+ case WorkerMessageType.InitSuccess:
1135
+ case WorkerMessageType.InitError:
1136
+ // Should already be handled by the API
1137
+ break;
1138
+
1139
+ case WorkerMessageType.LogMessage:
1140
+ // Already handled by prepare's handler
1141
+ break;
1101
1142
  default:
1102
1143
  assertUnreachable(msgData);
1103
1144
  }
1104
1145
  };
1105
1146
 
1147
+ log.debug("MTCI: addEventListener for worker message");
1148
+ if (this._queuedWorkerMessages !== null) {
1149
+ const bufferedMessages = this._queuedWorkerMessages.slice();
1150
+ log.debug("MTCI: Processing buffered messages", bufferedMessages.length);
1151
+ for (const message of bufferedMessages) {
1152
+ onmessage(message);
1153
+ }
1154
+ this._queuedWorkerMessages = null;
1155
+ }
1106
1156
  this._settings.worker.addEventListener("message", onmessage);
1107
1157
  this._initCanceller.signal.register(() => {
1158
+ log.debug("MTCI: removeEventListener for worker message");
1108
1159
  this._settings.worker.removeEventListener("message", onmessage);
1109
1160
  });
1110
1161
  }
@@ -1538,11 +1589,19 @@ export default class MultiThreadContentInitializer extends ContentInitializer {
1538
1589
  value: { messageId },
1539
1590
  });
1540
1591
  return new Promise((resolve, reject) => {
1541
- this._segmentMetrics.resolvers[messageId] = resolve;
1542
1592
  const rejectFn = (err: CancellationError) => {
1543
- delete this._segmentMetrics.resolvers[messageId];
1593
+ cancelSignal.deregister(rejectFn);
1594
+ this._segmentMetrics.resolvers.delete(messageId);
1544
1595
  return reject(err);
1545
1596
  };
1597
+ this._segmentMetrics.resolvers.set(
1598
+ messageId,
1599
+ (value: ISegmentSinkMetrics | undefined) => {
1600
+ cancelSignal.deregister(rejectFn);
1601
+ this._segmentMetrics.resolvers.delete(messageId);
1602
+ resolve(value);
1603
+ },
1604
+ );
1546
1605
  cancelSignal.register(rejectFn);
1547
1606
  });
1548
1607
  };
@@ -71,7 +71,7 @@ export default function performInitialSeekAndPlay(
71
71
  }: {
72
72
  mediaElement: IMediaElement;
73
73
  playbackObserver: IMediaElementPlaybackObserver;
74
- startTime: number | (() => number);
74
+ startTime: number | (() => number | undefined);
75
75
  mustAutoPlay: boolean;
76
76
  isDirectfile: boolean;
77
77
  onWarning: (err: IPlayerError) => void;
@@ -107,18 +107,37 @@ export default function performInitialSeekAndPlay(
107
107
  if (!isDirectfile || typeof startTime === "number") {
108
108
  const initiallySeekedTime =
109
109
  typeof startTime === "number" ? startTime : startTime();
110
- if (initiallySeekedTime !== 0) {
110
+ if (initiallySeekedTime !== 0 && initiallySeekedTime !== undefined) {
111
111
  performInitialSeek(initiallySeekedTime);
112
112
  }
113
113
  waitForSeekable();
114
114
  } else {
115
115
  playbackObserver.listen(
116
116
  (obs, stopListening) => {
117
+ const initiallySeekedTime =
118
+ typeof startTime === "number" ? startTime : startTime();
119
+ if (
120
+ initiallySeekedTime === undefined &&
121
+ obs.readyState < HTMLMediaElement.HAVE_CURRENT_DATA
122
+ ) {
123
+ /**
124
+ * On browser, such as Safari, the HTMLMediaElement.duration
125
+ * and HTMLMediaElement.buffered may not be initialized at readyState 1, leading
126
+ * to cases where it can be equal to `Infinity`.
127
+ * If so, the range in which it is possible to seek is not yet known.
128
+ * To solve this, the seek should be done after readyState HAVE_CURRENT_DATA (2),
129
+ * at that time the previously mentioned attributes are correctly initialized and
130
+ * the range in which it is possible to seek is correctly known.
131
+ * If the initiallySeekedTime is still `undefined` when the readyState is >= 2,
132
+ * let assume that the initiallySeekedTime will never be known and continue
133
+ * the logic without seeking.
134
+ */
135
+ return;
136
+ }
117
137
  if (obs.readyState >= 1) {
118
138
  stopListening();
119
- const initiallySeekedTime =
120
- typeof startTime === "number" ? startTime : startTime();
121
- if (initiallySeekedTime !== 0) {
139
+
140
+ if (initiallySeekedTime !== 0 && initiallySeekedTime !== undefined) {
122
141
  if (canSeekDirectlyAfterLoadedMetadata) {
123
142
  performInitialSeek(initiallySeekedTime);
124
143
  } else {