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

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 (63) hide show
  1. package/CHANGELOG.md +10 -3
  2. package/VERSION +1 -1
  3. package/dist/commonjs/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
  4. package/dist/commonjs/__GENERATED_CODE/embedded_worker.js +1 -1
  5. package/dist/commonjs/core/main/worker/worker_main.d.ts.map +1 -1
  6. package/dist/commonjs/core/main/worker/worker_main.js +3 -0
  7. package/dist/commonjs/core/stream/adaptation/adaptation_stream.d.ts.map +1 -1
  8. package/dist/commonjs/core/stream/adaptation/adaptation_stream.js +15 -0
  9. package/dist/commonjs/core/stream/representation/representation_stream.d.ts.map +1 -1
  10. package/dist/commonjs/core/stream/representation/representation_stream.js +2 -0
  11. package/dist/commonjs/main_thread/api/public_api.js +2 -2
  12. package/dist/commonjs/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
  13. package/dist/commonjs/main_thread/init/directfile_content_initializer.js +14 -6
  14. package/dist/commonjs/main_thread/init/multi_thread_content_initializer.d.ts +13 -0
  15. package/dist/commonjs/main_thread/init/multi_thread_content_initializer.d.ts.map +1 -1
  16. package/dist/commonjs/main_thread/init/multi_thread_content_initializer.js +94 -45
  17. package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.d.ts +1 -1
  18. package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
  19. package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.js +21 -5
  20. package/dist/commonjs/mse/main_media_source_interface.d.ts.map +1 -1
  21. package/dist/commonjs/mse/main_media_source_interface.js +21 -2
  22. package/dist/commonjs/transports/smooth/pipelines.d.ts.map +1 -1
  23. package/dist/commonjs/transports/smooth/pipelines.js +1 -0
  24. package/dist/commonjs/transports/utils/parse_text_track.d.ts.map +1 -1
  25. package/dist/commonjs/transports/utils/parse_text_track.js +1 -0
  26. package/dist/es2017/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
  27. package/dist/es2017/__GENERATED_CODE/embedded_worker.js +1 -1
  28. package/dist/es2017/core/main/worker/worker_main.d.ts.map +1 -1
  29. package/dist/es2017/core/main/worker/worker_main.js +3 -0
  30. package/dist/es2017/core/stream/adaptation/adaptation_stream.d.ts.map +1 -1
  31. package/dist/es2017/core/stream/adaptation/adaptation_stream.js +15 -0
  32. package/dist/es2017/core/stream/representation/representation_stream.d.ts.map +1 -1
  33. package/dist/es2017/core/stream/representation/representation_stream.js +2 -0
  34. package/dist/es2017/main_thread/api/public_api.js +2 -2
  35. package/dist/es2017/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
  36. package/dist/es2017/main_thread/init/directfile_content_initializer.js +14 -6
  37. package/dist/es2017/main_thread/init/multi_thread_content_initializer.d.ts +13 -0
  38. package/dist/es2017/main_thread/init/multi_thread_content_initializer.d.ts.map +1 -1
  39. package/dist/es2017/main_thread/init/multi_thread_content_initializer.js +78 -41
  40. package/dist/es2017/main_thread/init/utils/initial_seek_and_play.d.ts +1 -1
  41. package/dist/es2017/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
  42. package/dist/es2017/main_thread/init/utils/initial_seek_and_play.js +19 -3
  43. package/dist/es2017/mse/main_media_source_interface.d.ts.map +1 -1
  44. package/dist/es2017/mse/main_media_source_interface.js +19 -0
  45. package/dist/es2017/transports/smooth/pipelines.d.ts.map +1 -1
  46. package/dist/es2017/transports/smooth/pipelines.js +1 -0
  47. package/dist/es2017/transports/utils/parse_text_track.d.ts.map +1 -1
  48. package/dist/es2017/transports/utils/parse_text_track.js +1 -0
  49. package/dist/rx-player.js +53 -14
  50. package/dist/rx-player.min.js +8 -8
  51. package/dist/worker.js +5 -5
  52. package/package.json +19 -7
  53. package/src/__GENERATED_CODE/embedded_worker.ts +1 -1
  54. package/src/core/main/worker/worker_main.ts +3 -0
  55. package/src/core/stream/adaptation/adaptation_stream.ts +23 -1
  56. package/src/core/stream/representation/representation_stream.ts +11 -0
  57. package/src/main_thread/api/public_api.ts +2 -2
  58. package/src/main_thread/init/directfile_content_initializer.ts +20 -10
  59. package/src/main_thread/init/multi_thread_content_initializer.ts +94 -43
  60. package/src/main_thread/init/utils/initial_seek_and_play.ts +24 -5
  61. package/src/mse/main_media_source_interface.ts +20 -0
  62. package/src/transports/smooth/pipelines.ts +1 -0
  63. package/src/transports/utils/parse_text_track.ts +1 -0
@@ -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
 
@@ -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
  }
@@ -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.2024100200";
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.2024100200";
3334
3334
 
3335
3335
  /** Every events sent by the RxPlayer's public API. */
3336
3336
  interface IPublicAPIEvent {
@@ -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;
@@ -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
  *
@@ -123,6 +137,7 @@ export default class MultiThreadContentInitializer extends ContentInitializer {
123
137
  lastMessageId: 0,
124
138
  resolvers: {},
125
139
  };
140
+ this._queuedWorkerMessages = null;
126
141
  }
127
142
 
128
143
  /**
@@ -176,6 +191,66 @@ export default class MultiThreadContentInitializer extends ContentInitializer {
176
191
  if (this._initCanceller.isUsed()) {
177
192
  return;
178
193
  }
194
+ this._queuedWorkerMessages = [];
195
+ log.debug("MTCI: addEventListener prepare buffering worker messages");
196
+ const onmessage = (evt: MessageEvent): void => {
197
+ const msgData = evt.data as unknown as IWorkerMessage;
198
+ const type = msgData.type;
199
+ switch (type) {
200
+ case WorkerMessageType.LogMessage: {
201
+ const formatted = msgData.value.logs.map((l) => {
202
+ switch (typeof l) {
203
+ case "string":
204
+ case "number":
205
+ case "boolean":
206
+ case "undefined":
207
+ return l;
208
+ case "object":
209
+ if (l === null) {
210
+ return null;
211
+ }
212
+ return formatWorkerError(l);
213
+ default:
214
+ assertUnreachable(l);
215
+ }
216
+ });
217
+ switch (msgData.value.logLevel) {
218
+ case "NONE":
219
+ break;
220
+ case "ERROR":
221
+ log.error(...formatted);
222
+ break;
223
+ case "WARNING":
224
+ log.warn(...formatted);
225
+ break;
226
+ case "INFO":
227
+ log.info(...formatted);
228
+ break;
229
+ case "DEBUG":
230
+ log.debug(...formatted);
231
+ break;
232
+ default:
233
+ assertUnreachable(msgData.value.logLevel);
234
+ }
235
+ break;
236
+ }
237
+ default:
238
+ if (this._queuedWorkerMessages !== null) {
239
+ this._queuedWorkerMessages.push(evt);
240
+ }
241
+ break;
242
+ }
243
+ };
244
+ this._settings.worker.addEventListener("message", onmessage);
245
+ const onmessageerror = (_msg: MessageEvent) => {
246
+ log.error("MTCI: Error when receiving message from worker.");
247
+ };
248
+ this._settings.worker.addEventListener("messageerror", onmessageerror);
249
+ this._initCanceller.signal.register(() => {
250
+ log.debug("MTCI: removeEventListener prepare for worker message");
251
+ this._settings.worker.removeEventListener("message", onmessage);
252
+ this._settings.worker.removeEventListener("messageerror", onmessageerror);
253
+ });
179
254
 
180
255
  // Also bind all `SharedReference` objects:
181
256
 
@@ -1042,49 +1117,6 @@ export default class MultiThreadContentInitializer extends ContentInitializer {
1042
1117
  }
1043
1118
  break;
1044
1119
 
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
1120
  case WorkerMessageType.SegmentSinkStoreUpdate: {
1089
1121
  if (this._currentContentInfo?.contentId !== msgData.contentId) {
1090
1122
  return;
@@ -1098,13 +1130,32 @@ export default class MultiThreadContentInitializer extends ContentInitializer {
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
  }
@@ -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 {
@@ -464,6 +464,20 @@ export class MainSourceBufferInterface implements ISourceBufferInterface {
464
464
  op.reject(error);
465
465
  });
466
466
  this._currentOperations = [];
467
+
468
+ // A synchronous error probably will not lead to updateend event, so we need to
469
+ // go to next queue element manually
470
+ //
471
+ // FIXME: This here is needed to ensure that we're not left with a
472
+ // dangling queue of operations.
473
+ // However it can potentially be counter-productive if e.g. the `appendBuffer`
474
+ // error was due to a full buffer and if there are pushing operations awaiting in
475
+ // the queue.
476
+ //
477
+ // A better solution might just be to reject all push operations right away here?
478
+ // Only for a `QuotaExceededError` (to check MSE)?
479
+ // However this is too disruptive for what is now a hotfix
480
+ this._performNextOperation();
467
481
  }
468
482
  } else {
469
483
  // TODO merge contiguous removes?
@@ -482,7 +496,13 @@ export class MainSourceBufferInterface implements ISourceBufferInterface {
482
496
  false,
483
497
  );
484
498
  nextElem.reject(error);
499
+ this._currentOperations.forEach((op) => {
500
+ op.reject(error);
501
+ });
485
502
  this._currentOperations = [];
503
+ // A synchronous error probably will not lead to updateend event, so we need to
504
+ // go to next queue element manually
505
+ this._performNextOperation();
486
506
  }
487
507
  }
488
508
  }
@@ -350,6 +350,7 @@ export default function (transportOptions: ITransportOptions): ITransportPipelin
350
350
  if (
351
351
  mimeType === "application/ttml+xml+mp4" ||
352
352
  lcCodec === "stpp" ||
353
+ lcCodec === "stpp.ttml" ||
353
354
  lcCodec === "stpp.ttml.im1t"
354
355
  ) {
355
356
  _sdType = "ttml";
@@ -42,6 +42,7 @@ export function getISOBMFFTextTrackFormat(codecs: string | undefined): "ttml" |
42
42
  }
43
43
  switch (codecs.toLowerCase()) {
44
44
  case "stpp": // stpp === TTML in MP4
45
+ case "stpp.ttml":
45
46
  case "stpp.ttml.im1t":
46
47
  return "ttml";
47
48
  case "wvtt": // wvtt === WebVTT in MP4