rx-player 4.5.0-dev.2026060400 → 4.5.0

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 (38) hide show
  1. package/CHANGELOG.md +15 -14
  2. package/VERSION +1 -1
  3. package/dist/commonjs/__GENERATED_CODE/embedded_dash_wasm.d.ts.map +1 -1
  4. package/dist/commonjs/__GENERATED_CODE/embedded_dash_wasm.js +1 -1
  5. package/dist/commonjs/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
  6. package/dist/commonjs/__GENERATED_CODE/embedded_worker.js +1 -1
  7. package/dist/commonjs/core/fetchers/utils/schedule_request.d.ts.map +1 -1
  8. package/dist/commonjs/core/fetchers/utils/schedule_request.js +5 -2
  9. package/dist/commonjs/main_thread/api/public_api.js +2 -2
  10. package/dist/commonjs/main_thread/core_interface/monothread.d.ts.map +1 -1
  11. package/dist/commonjs/main_thread/core_interface/monothread.js +3 -2
  12. package/dist/commonjs/main_thread/render_thumbnail.d.ts.map +1 -1
  13. package/dist/commonjs/main_thread/render_thumbnail.js +7 -4
  14. package/dist/commonjs/main_thread/tracks_store/media_element_tracks_store.js +28 -27
  15. package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.d.ts.map +1 -1
  16. package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.js +1 -1
  17. package/dist/es2017/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
  18. package/dist/es2017/__GENERATED_CODE/embedded_worker.js +1 -1
  19. package/dist/es2017/core/fetchers/utils/schedule_request.d.ts.map +1 -1
  20. package/dist/es2017/core/fetchers/utils/schedule_request.js +5 -2
  21. package/dist/es2017/main_thread/api/public_api.js +2 -2
  22. package/dist/es2017/main_thread/core_interface/monothread.d.ts.map +1 -1
  23. package/dist/es2017/main_thread/core_interface/monothread.js +3 -2
  24. package/dist/es2017/main_thread/render_thumbnail.d.ts.map +1 -1
  25. package/dist/es2017/main_thread/render_thumbnail.js +7 -4
  26. package/dist/es2017/main_thread/tracks_store/media_element_tracks_store.js +28 -27
  27. package/dist/mpd-parser.wasm +0 -0
  28. package/dist/worker.js +1 -1
  29. package/package.json +2 -2
  30. package/src/__GENERATED_CODE/embedded_dash_wasm.ts +1 -1
  31. package/src/__GENERATED_CODE/embedded_worker.ts +1 -1
  32. package/src/core/fetchers/utils/schedule_request.ts +4 -2
  33. package/src/main_thread/__tests__/render_thumbnail.test.ts +193 -0
  34. package/src/main_thread/api/public_api.ts +2 -2
  35. package/src/main_thread/core_interface/monothread.ts +3 -2
  36. package/src/main_thread/render_thumbnail.ts +7 -4
  37. package/src/main_thread/tracks_store/media_element_tracks_store.ts +18 -18
  38. package/src/parsers/manifest/dash/wasm-parser/rs/lib.rs +1 -0
@@ -46,7 +46,8 @@ function shouldRetry(error: unknown): boolean {
46
46
  error.status === 415 || // some CDN seems to use that code when
47
47
  // requesting low-latency segments too much
48
48
  // in advance
49
- error.status === 412
49
+ error.status === 412 ||
50
+ error.status === 429 // TODO: Handle optional Retry-After header
50
51
  );
51
52
  }
52
53
  return (
@@ -64,7 +65,8 @@ function shouldRetry(error: unknown): boolean {
64
65
  error.xhr.status === 415 || // some CDN seems to use that code when
65
66
  // requesting low-latency segments too much
66
67
  // in advance
67
- error.xhr.status === 412
68
+ error.xhr.status === 412 ||
69
+ error.xhr.status === 429
68
70
  );
69
71
  }
70
72
  return false;
@@ -0,0 +1,193 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import TaskCanceller from "../../utils/task_canceller";
3
+ import type { IPublicApiContentInfos } from "../api/public_api";
4
+ import renderThumbnail from "../render_thumbnail";
5
+
6
+ const { mockFormatError, mockGetPeriodForTime } = vi.hoisted(() => {
7
+ return {
8
+ mockFormatError: vi.fn((err: unknown) => err),
9
+ mockGetPeriodForTime: vi.fn(() => ({
10
+ id: "period-1",
11
+ thumbnailTracks: [{ id: "thumb-track-1" }],
12
+ })),
13
+ };
14
+ });
15
+
16
+ vi.mock("../../errors", () => ({
17
+ formatError: mockFormatError,
18
+ }));
19
+
20
+ vi.mock("../../manifest", () => ({
21
+ getPeriodForTime: mockGetPeriodForTime,
22
+ }));
23
+
24
+ describe("renderThumbnail", () => {
25
+ let imageInstances: FakeImage[];
26
+
27
+ beforeEach(() => {
28
+ imageInstances = [];
29
+ vi.stubGlobal(
30
+ "Image",
31
+ class FakeImage {
32
+ public onload: null | (() => void) = null;
33
+ public onerror: null | (() => void) = null;
34
+
35
+ set src(_value: string) {
36
+ imageInstances.push(this);
37
+ }
38
+ },
39
+ );
40
+ vi.stubGlobal("URL", {
41
+ createObjectURL: vi.fn(() => "blob:thumbnail"),
42
+ revokeObjectURL: vi.fn(),
43
+ });
44
+ vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue({
45
+ drawImage: vi.fn(),
46
+ } as unknown as CanvasRenderingContext2D);
47
+ });
48
+
49
+ afterEach(() => {
50
+ vi.unstubAllGlobals();
51
+ vi.restoreAllMocks();
52
+ });
53
+
54
+ it("keeps the newer thumbnail when an aborted older request finishes later", async () => {
55
+ let resolveFirstFetch!: (value: IThumbnailResponseLike) => void;
56
+ const contentInfos = createContentInfos(
57
+ vi
58
+ .fn()
59
+ .mockImplementationOnce(
60
+ () =>
61
+ new Promise<IThumbnailResponseLike>((resolve) => {
62
+ resolveFirstFetch = resolve;
63
+ }),
64
+ )
65
+ .mockResolvedValueOnce(makeThumbnailResponse(10, 20)),
66
+ );
67
+ const container = document.createElement("div");
68
+
69
+ const firstPromise = renderThumbnail(contentInfos, { container, time: 0.5 });
70
+ const secondPromise = renderThumbnail(contentInfos, { container, time: 12 });
71
+
72
+ await Promise.resolve();
73
+ expect(imageInstances).toHaveLength(1);
74
+ imageInstances[0].onload?.();
75
+ await expect(secondPromise).resolves.toBeUndefined();
76
+ expect(container.childElementCount).toBe(1);
77
+
78
+ resolveFirstFetch(makeThumbnailResponse(0, 10));
79
+ await expect(firstPromise).rejects.toMatchObject({ code: "ABORTED" });
80
+ expect(container.childElementCount).toBe(1);
81
+ expect(container.firstElementChild?.className).toBe("__rx-thumbnail__");
82
+ });
83
+
84
+ it("does not remove the newer pending request when an older request aborts", async () => {
85
+ let resolveFirstFetch!: (value: IThumbnailResponseLike) => void;
86
+ const contentInfos = createContentInfos(
87
+ vi
88
+ .fn()
89
+ .mockImplementationOnce(
90
+ () =>
91
+ new Promise<IThumbnailResponseLike>((resolve) => {
92
+ resolveFirstFetch = resolve;
93
+ }),
94
+ )
95
+ .mockResolvedValueOnce(makeThumbnailResponse(10, 20)),
96
+ );
97
+ const container = document.createElement("div");
98
+
99
+ const firstPromise = renderThumbnail(contentInfos, { container, time: 0.5 });
100
+ const secondPromise = renderThumbnail(contentInfos, { container, time: 12 });
101
+ const pendingRequest =
102
+ contentInfos.thumbnailRequestsInfo.pendingRequests.get(container);
103
+
104
+ expect(pendingRequest).toBeDefined();
105
+ await Promise.resolve();
106
+ expect(imageInstances).toHaveLength(1);
107
+
108
+ resolveFirstFetch(makeThumbnailResponse(0, 10));
109
+ await expect(firstPromise).rejects.toMatchObject({ code: "ABORTED" });
110
+ expect(contentInfos.thumbnailRequestsInfo.pendingRequests.get(container)).toBe(
111
+ pendingRequest,
112
+ );
113
+
114
+ imageInstances[0].onload?.();
115
+ await expect(secondPromise).resolves.toBeUndefined();
116
+ expect(
117
+ contentInfos.thumbnailRequestsInfo.pendingRequests.get(container),
118
+ ).toBeUndefined();
119
+ });
120
+ });
121
+
122
+ function createContentInfos(
123
+ fetchThumbnailDataCallback: (
124
+ periodId: string,
125
+ thumbnailTrackId: string,
126
+ time: number,
127
+ ) => Promise<IThumbnailResponseLike>,
128
+ ): IPublicApiContentInfos {
129
+ return {
130
+ contentId: "content-1",
131
+ originalUrl: undefined,
132
+ manifest: {} as IPublicApiContentInfos["manifest"],
133
+ currentPeriod: null,
134
+ activeAdaptations: null,
135
+ activeRepresentations: null,
136
+ defaultAudioTrackSwitchingMode: "reload",
137
+ fetchThumbnailDataCallback,
138
+ handledTrackTypes: {
139
+ audio: true,
140
+ video: true,
141
+ text: true,
142
+ },
143
+ initializer: {} as IPublicApiContentInfos["initializer"],
144
+ isDirectFile: false,
145
+ mediaElementTracksStore: null,
146
+ onAudioTracksNotPlayable: "continue",
147
+ onVideoTracksNotPlayable: "continue",
148
+ playbackObserver: {} as IPublicApiContentInfos["playbackObserver"],
149
+ segmentSinkMetricsCallback: null,
150
+ thumbnailRequestsInfo: {
151
+ pendingRequests: new WeakMap(),
152
+ lastResponse: null,
153
+ },
154
+ tracksStore: null,
155
+ useWorker: false,
156
+ currentContentCanceller: new TaskCanceller("content"),
157
+ };
158
+ }
159
+
160
+ function makeThumbnailResponse(start: number, end: number): IThumbnailResponseLike {
161
+ return {
162
+ data: new Uint8Array([1, 2, 3]).buffer,
163
+ mimeType: "image/jpeg",
164
+ thumbnails: [
165
+ {
166
+ start,
167
+ end,
168
+ width: 320,
169
+ height: 180,
170
+ offsetX: 0,
171
+ offsetY: 0,
172
+ },
173
+ ],
174
+ };
175
+ }
176
+
177
+ interface IThumbnailResponseLike {
178
+ data: ArrayBuffer;
179
+ mimeType: string;
180
+ thumbnails: Array<{
181
+ start: number;
182
+ end: number;
183
+ width: number;
184
+ height: number;
185
+ offsetX: number;
186
+ offsetY: number;
187
+ }>;
188
+ }
189
+
190
+ interface FakeImage {
191
+ onload: null | (() => void);
192
+ onerror: null | (() => void);
193
+ }
@@ -439,7 +439,7 @@ class Player extends EventEmitter<IPublicAPIEvent> {
439
439
  // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1194624
440
440
  videoElement.preload = "auto";
441
441
 
442
- this.version = /* PLAYER_VERSION */ "4.5.0-dev.2026060400";
442
+ this.version = /* PLAYER_VERSION */ "4.5.0";
443
443
  this.log = log;
444
444
  this.state = "STOPPED";
445
445
  this.videoElement = videoElement;
@@ -3845,7 +3845,7 @@ class Player extends EventEmitter<IPublicAPIEvent> {
3845
3845
  return true;
3846
3846
  }
3847
3847
  }
3848
- Player.version = /* PLAYER_VERSION */ "4.5.0-dev.2026060400";
3848
+ Player.version = /* PLAYER_VERSION */ "4.5.0";
3849
3849
 
3850
3850
  /** Every events sent by the RxPlayer's public API. */
3851
3851
  interface IPublicAPIEvent {
@@ -5,6 +5,7 @@ import {
5
5
  } from "../../core/types";
6
6
  import log from "../../log";
7
7
  import noop from "../../utils/noop";
8
+ import queueMicrotaskUtil from "../../utils/queue_microtask";
8
9
  import type { IMainThreadMessage } from "../types";
9
10
  import CoreInterface from "./base";
10
11
 
@@ -18,7 +19,7 @@ export class MonoThreadCoreInterface extends CoreInterface {
18
19
 
19
20
  public sendMessage(msg: IMainThreadMessage) {
20
21
  log.debug("M-->C", "Sending message", { name: msg.type });
21
- queueMicrotask(() => {
22
+ queueMicrotaskUtil(() => {
22
23
  // NOTE: We don't clone for performance reasons
23
24
  this._currentCoreListener({ data: msg });
24
25
  });
@@ -32,7 +33,7 @@ export class MonoThreadCoreInterface extends CoreInterface {
32
33
  this._currentCoreListener = handler;
33
34
  };
34
35
  const sendCoreMessage = (msg: ICoreMessage, _transferables?: Transferable[]) => {
35
- queueMicrotask(() => {
36
+ queueMicrotaskUtil(() => {
36
37
  if (msg.type !== CoreMessageType.LogMessage) {
37
38
  log.debug("M<--C", "Sending message", { name: msg.type });
38
39
  }
@@ -63,7 +63,9 @@ export default async function renderThumbnail(
63
63
  const onFinished = () => {
64
64
  unlinkCanceller();
65
65
  canceller.cancel("thumbnail request finished");
66
- thumbnailRequestsInfo.pendingRequests.delete(container);
66
+ if (thumbnailRequestsInfo.pendingRequests.get(container) === canceller) {
67
+ thumbnailRequestsInfo.pendingRequests.delete(container);
68
+ }
67
69
 
68
70
  // Let's revoke the URL after a round-trip to the event loop just in case
69
71
  // to prevent revoking before the browser use it.
@@ -208,16 +210,17 @@ export default async function renderThumbnail(
208
210
  };
209
211
  });
210
212
  } catch (srcError) {
211
- if (options.keepPreviousThumbnailOnError !== true) {
212
- clearPreviousThumbnails();
213
- }
214
213
  if (srcError !== null && srcError === canceller.signal.cancellationError) {
214
+ onFinished();
215
215
  const error = new ThumbnailRenderingError(
216
216
  "ABORTED",
217
217
  "Thumbnail rendering has been aborted",
218
218
  );
219
219
  throw error;
220
220
  }
221
+ if (options.keepPreviousThumbnailOnError !== true) {
222
+ clearPreviousThumbnails();
223
+ }
221
224
  if (srcError instanceof ThumbnailRenderingError) {
222
225
  onFinished();
223
226
  throw srcError;
@@ -612,9 +612,9 @@ export default class MediaElementTracksStore extends EventEmitter<IMediaElementT
612
612
  }
613
613
  }
614
614
  };
615
- this._nativeAudioTracks.addEventListener("addtrack", onAddAudioTrack);
615
+ this._nativeAudioTracks.addEventListener?.("addtrack", onAddAudioTrack);
616
616
  this.taskCanceller.signal.register(() => {
617
- this._nativeAudioTracks?.removeEventListener("addtrack", onAddAudioTrack);
617
+ this._nativeAudioTracks?.removeEventListener?.("addtrack", onAddAudioTrack);
618
618
  });
619
619
 
620
620
  const onRemoveAudioTrack = () => {
@@ -633,9 +633,9 @@ export default class MediaElementTracksStore extends EventEmitter<IMediaElementT
633
633
  }
634
634
  };
635
635
 
636
- this._nativeAudioTracks.addEventListener("removetrack", onRemoveAudioTrack);
636
+ this._nativeAudioTracks.addEventListener?.("removetrack", onRemoveAudioTrack);
637
637
  this.taskCanceller.signal.register(() => {
638
- this._nativeAudioTracks?.removeEventListener("removetrack", onRemoveAudioTrack);
638
+ this._nativeAudioTracks?.removeEventListener?.("removetrack", onRemoveAudioTrack);
639
639
  });
640
640
 
641
641
  const onAudioChange = () => {
@@ -658,9 +658,9 @@ export default class MediaElementTracksStore extends EventEmitter<IMediaElementT
658
658
  return;
659
659
  };
660
660
 
661
- this._nativeAudioTracks.addEventListener("change", onAudioChange);
661
+ this._nativeAudioTracks.addEventListener?.("change", onAudioChange);
662
662
  this.taskCanceller.signal.register(() => {
663
- this._nativeAudioTracks?.removeEventListener("change", onAudioChange);
663
+ this._nativeAudioTracks?.removeEventListener?.("change", onAudioChange);
664
664
  });
665
665
  }
666
666
 
@@ -681,9 +681,9 @@ export default class MediaElementTracksStore extends EventEmitter<IMediaElementT
681
681
  }
682
682
  };
683
683
 
684
- this._nativeTextTracks.addEventListener("addtrack", onAddTextTrack);
684
+ this._nativeTextTracks.addEventListener?.("addtrack", onAddTextTrack);
685
685
  this.taskCanceller.signal.register(() => {
686
- this._nativeTextTracks?.removeEventListener("addtrack", onAddTextTrack);
686
+ this._nativeTextTracks?.removeEventListener?.("addtrack", onAddTextTrack);
687
687
  });
688
688
 
689
689
  const onRemoveTextTrack = () => {
@@ -701,9 +701,9 @@ export default class MediaElementTracksStore extends EventEmitter<IMediaElementT
701
701
  }
702
702
  }
703
703
  };
704
- this._nativeTextTracks.addEventListener("removetrack", onRemoveTextTrack);
704
+ this._nativeTextTracks.addEventListener?.("removetrack", onRemoveTextTrack);
705
705
  this.taskCanceller.signal.register(() => {
706
- this._nativeTextTracks?.removeEventListener("removetrack", onRemoveTextTrack);
706
+ this._nativeTextTracks?.removeEventListener?.("removetrack", onRemoveTextTrack);
707
707
  });
708
708
 
709
709
  const onTextChange = () => {
@@ -726,9 +726,9 @@ export default class MediaElementTracksStore extends EventEmitter<IMediaElementT
726
726
  return;
727
727
  };
728
728
 
729
- this._nativeTextTracks.addEventListener("change", onTextChange);
729
+ this._nativeTextTracks.addEventListener?.("change", onTextChange);
730
730
  this.taskCanceller.signal.register(() => {
731
- this._nativeTextTracks?.removeEventListener("change", onTextChange);
731
+ this._nativeTextTracks?.removeEventListener?.("change", onTextChange);
732
732
  });
733
733
  }
734
734
 
@@ -749,9 +749,9 @@ export default class MediaElementTracksStore extends EventEmitter<IMediaElementT
749
749
  }
750
750
  };
751
751
 
752
- this._nativeVideoTracks.addEventListener("addtrack", onAddVideoTrack);
752
+ this._nativeVideoTracks.addEventListener?.("addtrack", onAddVideoTrack);
753
753
  this.taskCanceller.signal.register(() => {
754
- this._nativeVideoTracks?.removeEventListener("addtrack", onAddVideoTrack);
754
+ this._nativeVideoTracks?.removeEventListener?.("addtrack", onAddVideoTrack);
755
755
  });
756
756
 
757
757
  const onRemoveVideoTrack = () => {
@@ -770,9 +770,9 @@ export default class MediaElementTracksStore extends EventEmitter<IMediaElementT
770
770
  }
771
771
  };
772
772
 
773
- this._nativeVideoTracks.addEventListener("removetrack", onRemoveVideoTrack);
773
+ this._nativeVideoTracks.addEventListener?.("removetrack", onRemoveVideoTrack);
774
774
  this.taskCanceller.signal.register(() => {
775
- this._nativeVideoTracks?.removeEventListener("removetrack", onRemoveVideoTrack);
775
+ this._nativeVideoTracks?.removeEventListener?.("removetrack", onRemoveVideoTrack);
776
776
  });
777
777
 
778
778
  const onVideoChange = () => {
@@ -795,9 +795,9 @@ export default class MediaElementTracksStore extends EventEmitter<IMediaElementT
795
795
  return;
796
796
  };
797
797
 
798
- this._nativeVideoTracks.addEventListener("change", onVideoChange);
798
+ this._nativeVideoTracks.addEventListener?.("change", onVideoChange);
799
799
  this.taskCanceller.signal.register(() => {
800
- this._nativeVideoTracks?.removeEventListener("change", onVideoChange);
800
+ this._nativeVideoTracks?.removeEventListener?.("change", onVideoChange);
801
801
  });
802
802
  }
803
803
  }
@@ -15,6 +15,7 @@ use processor::MPDProcessor;
15
15
  use reader::MPDReader;
16
16
  use std::io::BufReader;
17
17
 
18
+ #[cfg_attr(target_family = "wasm", link(wasm_import_module = "env"))]
18
19
  extern "C" {
19
20
  /// JS callback called each time a new known tag is encountered in the MPD.
20
21
  ///