rx-player 4.5.0-dev.2026012302 → 4.5.0-dev.2026031700

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 (180) hide show
  1. package/CHANGELOG.md +21 -3
  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/compat/should_seek_at_zero.d.ts +12 -0
  8. package/dist/commonjs/compat/should_seek_at_zero.d.ts.map +1 -0
  9. package/dist/commonjs/compat/should_seek_at_zero.js +23 -0
  10. package/dist/commonjs/core/cmcd/cmcd_data_builder.js +2 -2
  11. package/dist/commonjs/core/entry/FreezeResolver.js +2 -2
  12. package/dist/commonjs/core/entry/content_preparer.d.ts +1 -6
  13. package/dist/commonjs/core/entry/content_preparer.d.ts.map +1 -1
  14. package/dist/commonjs/core/entry/content_preparer.js +2 -125
  15. package/dist/commonjs/core/entry/content_time_boundaries_observer.d.ts.map +1 -1
  16. package/dist/commonjs/core/entry/content_time_boundaries_observer.js +8 -0
  17. package/dist/commonjs/core/entry/core_entry.d.ts +1 -1
  18. package/dist/commonjs/core/entry/core_entry.d.ts.map +1 -1
  19. package/dist/commonjs/core/entry/core_entry.js +2 -7
  20. package/dist/commonjs/core/entry/core_text_displayer_interface.d.ts.map +1 -1
  21. package/dist/commonjs/core/entry/core_text_displayer_interface.js +3 -1
  22. package/dist/commonjs/core/entry/index.d.ts +1 -1
  23. package/dist/commonjs/core/entry/index.d.ts.map +1 -1
  24. package/dist/commonjs/core/entry/utils.d.ts +64 -0
  25. package/dist/commonjs/core/entry/utils.d.ts.map +1 -1
  26. package/dist/commonjs/core/entry/utils.js +165 -0
  27. package/dist/commonjs/core/fetchers/segment/segment_queue.d.ts +5 -6
  28. package/dist/commonjs/core/fetchers/segment/segment_queue.d.ts.map +1 -1
  29. package/dist/commonjs/core/fetchers/thumbnails/thumbnail_fetcher.d.ts.map +1 -1
  30. package/dist/commonjs/core/fetchers/thumbnails/thumbnail_fetcher.js +34 -30
  31. package/dist/commonjs/core/stream/representation/encryption_data_notifier.d.ts +70 -0
  32. package/dist/commonjs/core/stream/representation/encryption_data_notifier.d.ts.map +1 -0
  33. package/dist/commonjs/core/stream/representation/encryption_data_notifier.js +107 -0
  34. package/dist/commonjs/core/stream/representation/representation_stream.d.ts.map +1 -1
  35. package/dist/commonjs/core/stream/representation/representation_stream.js +12 -60
  36. package/dist/commonjs/core/stream/representation/utils/get_needed_segments.js +1 -1
  37. package/dist/commonjs/default_config.d.ts +5 -0
  38. package/dist/commonjs/default_config.d.ts.map +1 -1
  39. package/dist/commonjs/default_config.js +5 -0
  40. package/dist/commonjs/experimental/features/worker/index.d.ts +2 -1
  41. package/dist/commonjs/experimental/features/worker/index.d.ts.map +1 -1
  42. package/dist/commonjs/experimental/features/worker/index.js +4 -3
  43. package/dist/commonjs/main_thread/api/public_api.d.ts.map +1 -1
  44. package/dist/commonjs/main_thread/api/public_api.js +28 -4
  45. package/dist/commonjs/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
  46. package/dist/commonjs/main_thread/init/directfile_content_initializer.js +23 -1
  47. package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
  48. package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.js +3 -1
  49. package/dist/commonjs/manifest/classes/index.d.ts +2 -1
  50. package/dist/commonjs/manifest/classes/index.d.ts.map +1 -1
  51. package/dist/commonjs/manifest/classes/representation.d.ts +1 -1
  52. package/dist/commonjs/manifest/classes/representation.d.ts.map +1 -1
  53. package/dist/commonjs/manifest/classes/representation.js +2 -2
  54. package/dist/commonjs/manifest/classes/representation_index/types.d.ts +2 -2
  55. package/dist/commonjs/parsers/texttracks/ttml/html/apply_line_height.d.ts.map +1 -1
  56. package/dist/commonjs/parsers/texttracks/ttml/html/apply_line_height.js +16 -1
  57. package/dist/commonjs/utils/base64.d.ts +15 -1
  58. package/dist/commonjs/utils/base64.d.ts.map +1 -1
  59. package/dist/commonjs/utils/base64.js +6 -0
  60. package/dist/commonjs/utils/ranges.d.ts +1 -1
  61. package/dist/commonjs/utils/ranges.js +1 -1
  62. package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.d.ts.map +1 -1
  63. package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.js +1 -1
  64. package/dist/es2017/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
  65. package/dist/es2017/__GENERATED_CODE/embedded_worker.js +1 -1
  66. package/dist/es2017/compat/should_seek_at_zero.d.ts +12 -0
  67. package/dist/es2017/compat/should_seek_at_zero.d.ts.map +1 -0
  68. package/dist/es2017/compat/should_seek_at_zero.js +20 -0
  69. package/dist/es2017/core/cmcd/cmcd_data_builder.js +2 -2
  70. package/dist/es2017/core/entry/FreezeResolver.js +1 -1
  71. package/dist/es2017/core/entry/content_preparer.d.ts +1 -6
  72. package/dist/es2017/core/entry/content_preparer.d.ts.map +1 -1
  73. package/dist/es2017/core/entry/content_preparer.js +1 -76
  74. package/dist/es2017/core/entry/content_time_boundaries_observer.d.ts.map +1 -1
  75. package/dist/es2017/core/entry/content_time_boundaries_observer.js +8 -0
  76. package/dist/es2017/core/entry/core_entry.d.ts +1 -1
  77. package/dist/es2017/core/entry/core_entry.d.ts.map +1 -1
  78. package/dist/es2017/core/entry/core_entry.js +2 -7
  79. package/dist/es2017/core/entry/core_text_displayer_interface.d.ts.map +1 -1
  80. package/dist/es2017/core/entry/core_text_displayer_interface.js +3 -1
  81. package/dist/es2017/core/entry/index.d.ts +1 -1
  82. package/dist/es2017/core/entry/index.d.ts.map +1 -1
  83. package/dist/es2017/core/entry/utils.d.ts +64 -0
  84. package/dist/es2017/core/entry/utils.d.ts.map +1 -1
  85. package/dist/es2017/core/entry/utils.js +98 -0
  86. package/dist/es2017/core/fetchers/segment/segment_queue.d.ts +5 -6
  87. package/dist/es2017/core/fetchers/segment/segment_queue.d.ts.map +1 -1
  88. package/dist/es2017/core/fetchers/thumbnails/thumbnail_fetcher.d.ts.map +1 -1
  89. package/dist/es2017/core/fetchers/thumbnails/thumbnail_fetcher.js +21 -16
  90. package/dist/es2017/core/stream/representation/encryption_data_notifier.d.ts +70 -0
  91. package/dist/es2017/core/stream/representation/encryption_data_notifier.d.ts.map +1 -0
  92. package/dist/es2017/core/stream/representation/encryption_data_notifier.js +80 -0
  93. package/dist/es2017/core/stream/representation/representation_stream.d.ts.map +1 -1
  94. package/dist/es2017/core/stream/representation/representation_stream.js +10 -38
  95. package/dist/es2017/core/stream/representation/utils/get_needed_segments.js +1 -1
  96. package/dist/es2017/default_config.d.ts +5 -0
  97. package/dist/es2017/default_config.d.ts.map +1 -1
  98. package/dist/es2017/default_config.js +5 -0
  99. package/dist/es2017/experimental/features/worker/index.d.ts +2 -1
  100. package/dist/es2017/experimental/features/worker/index.d.ts.map +1 -1
  101. package/dist/es2017/experimental/features/worker/index.js +2 -3
  102. package/dist/es2017/main_thread/api/public_api.d.ts.map +1 -1
  103. package/dist/es2017/main_thread/api/public_api.js +28 -4
  104. package/dist/es2017/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
  105. package/dist/es2017/main_thread/init/directfile_content_initializer.js +23 -1
  106. package/dist/es2017/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
  107. package/dist/es2017/main_thread/init/utils/initial_seek_and_play.js +3 -1
  108. package/dist/es2017/manifest/classes/index.d.ts +2 -1
  109. package/dist/es2017/manifest/classes/index.d.ts.map +1 -1
  110. package/dist/es2017/manifest/classes/representation.d.ts +1 -1
  111. package/dist/es2017/manifest/classes/representation.d.ts.map +1 -1
  112. package/dist/es2017/manifest/classes/representation.js +1 -1
  113. package/dist/es2017/manifest/classes/representation_index/types.d.ts +2 -2
  114. package/dist/es2017/parsers/texttracks/ttml/html/apply_line_height.d.ts.map +1 -1
  115. package/dist/es2017/parsers/texttracks/ttml/html/apply_line_height.js +16 -1
  116. package/dist/es2017/utils/base64.d.ts +15 -1
  117. package/dist/es2017/utils/base64.d.ts.map +1 -1
  118. package/dist/es2017/utils/base64.js +6 -0
  119. package/dist/es2017/utils/ranges.d.ts +1 -1
  120. package/dist/es2017/utils/ranges.js +1 -1
  121. package/dist/mpd-parser.wasm +0 -0
  122. package/dist/worker.js +8 -8
  123. package/package.json +26 -23
  124. package/src/__GENERATED_CODE/embedded_dash_wasm.ts +1 -1
  125. package/src/__GENERATED_CODE/embedded_worker.ts +1 -1
  126. package/src/compat/should_seek_at_zero.ts +23 -0
  127. package/src/core/adaptive/__tests__/adaptive_representation_selector.test.ts +734 -0
  128. package/src/core/adaptive/__tests__/guess_based_chooser.test.ts +793 -0
  129. package/src/core/adaptive/__tests__/network_analyzer.test.ts +514 -0
  130. package/src/core/adaptive/utils/__tests__/bandwidth_estimator.test.ts +117 -0
  131. package/src/core/adaptive/utils/__tests__/last_estimate_storage.test.ts +76 -0
  132. package/src/core/adaptive/utils/__tests__/pending_requests_store.test.ts +288 -0
  133. package/src/core/adaptive/utils/__tests__/representation_score_calculator.test.ts +249 -0
  134. package/src/core/cmcd/__tests__/cmcd_data_builder.test.ts +464 -0
  135. package/src/core/cmcd/cmcd_data_builder.ts +2 -2
  136. package/src/core/entry/FreezeResolver.ts +1 -1
  137. package/src/core/entry/__tests__/FreezeResolver.test.ts +542 -0
  138. package/src/core/entry/__tests__/content_time_boundaries_observer.test.ts +867 -0
  139. package/src/core/entry/__tests__/core_text_displayer_interface.test.ts +205 -0
  140. package/src/core/entry/__tests__/get_buffered_data_per_media_buffer.test.ts +95 -0
  141. package/src/core/entry/__tests__/track_choice_setter.test.ts +288 -0
  142. package/src/core/entry/__tests__/utils.test.ts +309 -0
  143. package/src/core/entry/content_preparer.ts +7 -118
  144. package/src/core/entry/content_time_boundaries_observer.ts +10 -0
  145. package/src/core/entry/core_entry.ts +7 -14
  146. package/src/core/entry/core_text_displayer_interface.ts +3 -1
  147. package/src/core/entry/index.ts +1 -1
  148. package/src/core/entry/utils.ts +151 -0
  149. package/src/core/fetchers/__tests__/cdn_prioritizer.test.ts +166 -0
  150. package/src/core/fetchers/manifest/__tests__/manifest_fetcher.test.ts +803 -0
  151. package/src/core/fetchers/segment/__tests__/segment_fetcher.test.ts +872 -0
  152. package/src/core/fetchers/segment/__tests__/segment_queue.test.ts +557 -0
  153. package/src/core/fetchers/segment/segment_queue.ts +5 -6
  154. package/src/core/fetchers/thumbnails/__tests__/thumbnail_fetcher.test.ts +435 -0
  155. package/src/core/fetchers/thumbnails/thumbnail_fetcher.ts +28 -20
  156. package/src/core/fetchers/utils/__tests__/schedule_request.test.ts +331 -0
  157. package/src/core/stream/adaptation/__tests__/get_representations_switch_strategy.test.ts +593 -0
  158. package/src/core/stream/period/utils/__tests__/get_adaptation_switch_strategy.test.ts +646 -0
  159. package/src/core/stream/representation/__tests__/encryption_data_notifier.test.ts +531 -0
  160. package/src/core/stream/representation/encryption_data_notifier.ts +122 -0
  161. package/src/core/stream/representation/representation_stream.ts +11 -49
  162. package/src/core/stream/representation/utils/__tests__/append_segment_to_buffer.test.ts +321 -0
  163. package/src/core/stream/representation/utils/__tests__/check_for_discontinuity.test.ts +513 -0
  164. package/src/core/stream/representation/utils/__tests__/get_segment_priority.test.ts +132 -0
  165. package/src/core/stream/representation/utils/__tests__/push_init_segment.test.ts +209 -0
  166. package/src/core/stream/representation/utils/__tests__/push_media_segment.test.ts +343 -0
  167. package/src/core/stream/representation/utils/get_needed_segments.ts +1 -1
  168. package/src/default_config.ts +6 -0
  169. package/src/experimental/features/worker/index.ts +2 -4
  170. package/src/main_thread/api/public_api.ts +33 -4
  171. package/src/main_thread/init/directfile_content_initializer.ts +26 -1
  172. package/src/main_thread/init/utils/initial_seek_and_play.ts +5 -1
  173. package/src/manifest/classes/index.ts +2 -0
  174. package/src/manifest/classes/representation.ts +2 -1
  175. package/src/manifest/classes/representation_index/types.ts +2 -2
  176. package/src/mse/__tests__/main_media_source_interface.test.ts +846 -0
  177. package/src/parsers/texttracks/ttml/html/apply_line_height.ts +16 -1
  178. package/src/utils/__tests__/base64.test.ts +159 -191
  179. package/src/utils/base64.ts +30 -1
  180. package/src/utils/ranges.ts +1 -1
@@ -1,6 +1,36 @@
1
+ import { MediaSource_ } from "../../compat/browser_compatibility_types";
1
2
  import { formatError } from "../../errors";
3
+ import type { ICorePlaybackObservation } from "../../main_thread/init/utils/create_core_playback_observer";
4
+ import { createRepresentationFilterFromFnString } from "../../manifest";
5
+ import type Manifest from "../../manifest/classes";
6
+ import type {
7
+ IManifestLoader,
8
+ IRepresentationFilter,
9
+ ISegmentLoader,
10
+ } from "../../public_types";
11
+ import isNullOrUndefined from "../../utils/is_null_or_undefined";
12
+ import type SegmentSinksStore from "../segment_sinks";
2
13
  import type { ISentError } from "../types";
3
14
 
15
+ /**
16
+ * "Plugins" are a specific kind of API where an application can define complex
17
+ * functions that the RxPlayer will call in some normally-core aspects of media
18
+ * playback: e.g. loading segments.
19
+ *
20
+ * This interface lists them.
21
+ */
22
+ export interface ICorePlugins {
23
+ representationFilters: Map<string, IRepresentationFilter>;
24
+ segmentLoaders: Map<string, ISegmentLoader>;
25
+ manifestLoaders: Map<string, IManifestLoader>;
26
+ }
27
+
28
+ /**
29
+ * Format an error, which may be of any form, into an Object than can easily be
30
+ * serialized - e.g. to be communicated through a postMessage-like API.
31
+ * @param {*} error - The error to serialized
32
+ * @returns {Object} - A serialization-safe error format
33
+ */
4
34
  export function formatErrorForSender(error: unknown): ISentError {
5
35
  const formattedError = formatError(error, {
6
36
  defaultCode: "NONE",
@@ -9,3 +39,124 @@ export function formatErrorForSender(error: unknown): ISentError {
9
39
 
10
40
  return formattedError.serialize();
11
41
  }
42
+
43
+ /**
44
+ * Synchronize SegmentSinks with what has been buffered.
45
+ * @param {Object} observation - The just-received playback observation,
46
+ * including what has been buffered on lower-level buffers
47
+ * @param {Object} segmentSinksStore - Interface allowing to interact
48
+ * with `SegmentSink`s, so their inventory can be updated accordingly.
49
+ */
50
+ export function synchronizeSegmentSinksOnObservation(
51
+ observation: ICorePlaybackObservation,
52
+ segmentSinksStore: SegmentSinksStore,
53
+ ): void {
54
+ // Synchronize SegmentSinks with what has been buffered.
55
+ ["video" as const, "audio" as const, "text" as const].forEach((tType) => {
56
+ const segmentSinkStatus = segmentSinksStore.getStatus(tType);
57
+ if (segmentSinkStatus.type === "initialized") {
58
+ segmentSinkStatus.value.synchronizeInventory(observation.buffered[tType] ?? []);
59
+ }
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Set Representation.isCodecSupportedInWebWorker to true or false
65
+ * If the codec is supported in the current context.
66
+ * If MSE in worker is not available, the attribute is not set.
67
+ */
68
+ export function updateCodecSupportInWorkerMode(manifestToUpdate: Manifest) {
69
+ if (isNullOrUndefined(MediaSource_)) {
70
+ return;
71
+ }
72
+
73
+ const codecsMap = new Map<string, boolean>();
74
+ for (const period of manifestToUpdate.periods) {
75
+ const checkedAdaptations = [
76
+ ...(period.adaptations.video ?? []),
77
+ ...(period.adaptations.audio ?? []),
78
+ ];
79
+ for (const adaptation of checkedAdaptations) {
80
+ for (const representation of adaptation.representations) {
81
+ const codec = `${representation.mimeType};codecs="${representation.codecs[0]}"`;
82
+ if (codecsMap.has(codec)) {
83
+ representation.isCodecSupportedInWebWorker = codecsMap.get(codec);
84
+ } else {
85
+ const supported = MediaSource_.isTypeSupported(codec);
86
+ representation.isCodecSupportedInWebWorker = supported;
87
+ codecsMap.set(codec, supported);
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Some functions may be defined by the API, we call those "plugins".
96
+ * This function parses and extract the actual function from the different
97
+ * ways an application can provide it to us.
98
+ * @param {Object} input - The API input
99
+ * @param {Object} corePlugins - Context on what identified functions are
100
+ * defined right now.
101
+ * @returns {Function}
102
+ */
103
+ export function extractExternalPlugins(
104
+ input: {
105
+ manifestLoader:
106
+ | {
107
+ fn?: IManifestLoader | undefined;
108
+ workerId?: string | undefined;
109
+ }
110
+ | undefined;
111
+ segmentLoader:
112
+ | {
113
+ fn?: ISegmentLoader | undefined;
114
+ workerId?: string | undefined;
115
+ }
116
+ | undefined;
117
+ representationFilter:
118
+ | undefined
119
+ | {
120
+ fn?: IRepresentationFilter | undefined;
121
+ eval?: string | undefined;
122
+ workerId?: string | undefined;
123
+ };
124
+ },
125
+ corePlugins: ICorePlugins,
126
+ ): {
127
+ manifestLoader: IManifestLoader | undefined;
128
+ segmentLoader: ISegmentLoader | undefined;
129
+ representationFilter: IRepresentationFilter | undefined;
130
+ } {
131
+ let manifestLoader;
132
+ let segmentLoader;
133
+ let representationFilter;
134
+ if (typeof input.representationFilter?.fn === "function") {
135
+ representationFilter = input.representationFilter.fn;
136
+ } else if (typeof input.representationFilter?.eval === "string") {
137
+ representationFilter = createRepresentationFilterFromFnString(
138
+ input.representationFilter.eval,
139
+ );
140
+ } else if (typeof input.representationFilter?.workerId === "string") {
141
+ representationFilter = corePlugins.representationFilters.get(
142
+ input.representationFilter.workerId,
143
+ );
144
+ }
145
+
146
+ if (typeof input.manifestLoader?.fn === "function") {
147
+ manifestLoader = input.manifestLoader.fn;
148
+ } else if (typeof input.manifestLoader?.workerId === "string") {
149
+ manifestLoader = corePlugins.manifestLoaders.get(input.manifestLoader.workerId);
150
+ }
151
+
152
+ if (typeof input.segmentLoader?.fn === "function") {
153
+ segmentLoader = input.segmentLoader.fn;
154
+ } else if (typeof input.segmentLoader?.workerId === "string") {
155
+ segmentLoader = corePlugins.segmentLoaders.get(input.segmentLoader.workerId);
156
+ }
157
+ return {
158
+ manifestLoader,
159
+ segmentLoader,
160
+ representationFilter,
161
+ };
162
+ }
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import CdnPrioritizer from "../cdn_prioritizer";
3
+
4
+ /* eslint-disable @typescript-eslint/no-explicit-any */
5
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
6
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
7
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
8
+
9
+ const mockGetCurrent = vi.hoisted(() => {
10
+ return vi.fn(() => ({ DEFAULT_CDN_DOWNGRADE_TIME: 60000 }));
11
+ });
12
+
13
+ vi.mock("../../../config", () => ({
14
+ default: { getCurrent: mockGetCurrent },
15
+ }));
16
+
17
+ vi.mock("../../../utils/array_find_index", () => ({
18
+ // eslint-disable-next-line no-restricted-properties
19
+ default: (arr: unknown[], fn: (elt: unknown) => boolean) => arr.findIndex(fn),
20
+ }));
21
+
22
+ vi.mock("../../../utils/event_emitter", () => {
23
+ class MockEventEmitter {
24
+ trigger = vi.fn();
25
+ addEventListener = vi.fn();
26
+ removeEventListener = vi.fn();
27
+ }
28
+ return { default: MockEventEmitter };
29
+ });
30
+
31
+ function makeCancellationSignal(): any {
32
+ let _cb: (() => void) | null = null;
33
+ return {
34
+ signal: {
35
+ register: (cb: () => void) => {
36
+ _cb = cb;
37
+ },
38
+ },
39
+ cancel: () => _cb?.(),
40
+ };
41
+ }
42
+
43
+ describe("CdnPrioritizer", () => {
44
+ beforeEach(() => {
45
+ vi.useFakeTimers();
46
+ });
47
+
48
+ afterEach(() => {
49
+ vi.useRealTimers();
50
+ });
51
+
52
+ it("returns single CDN as-is without prioritization", () => {
53
+ const { signal } = makeCancellationSignal();
54
+ const prioritizer = new CdnPrioritizer(signal);
55
+ const cdns = [{ id: "cdn1", baseUrl: "https://cdn1.example.com" }];
56
+ expect(prioritizer.getCdnPreferenceForResource(cdns)).toEqual(cdns);
57
+ });
58
+
59
+ it("returns multiple CDNs in original order when none are downgraded", () => {
60
+ const { signal } = makeCancellationSignal();
61
+ const prioritizer = new CdnPrioritizer(signal);
62
+ const cdns = [
63
+ { id: "cdn1", baseUrl: "https://cdn1.example.com" },
64
+ { id: "cdn2", baseUrl: "https://cdn2.example.com" },
65
+ ];
66
+ expect(prioritizer.getCdnPreferenceForResource(cdns)).toEqual(cdns);
67
+ });
68
+
69
+ it("puts downgraded CDN last", () => {
70
+ const { signal } = makeCancellationSignal();
71
+ const prioritizer = new CdnPrioritizer(signal);
72
+ const cdn1 = { id: "cdn1", baseUrl: "https://cdn1.example.com" };
73
+ const cdn2 = { id: "cdn2", baseUrl: "https://cdn2.example.com" };
74
+ prioritizer.downgradeCdn(cdn1);
75
+ const result = prioritizer.getCdnPreferenceForResource([cdn1, cdn2]);
76
+ expect(result).toEqual([cdn2, cdn1]);
77
+ });
78
+
79
+ it("triggers priorityChange when downgrading a CDN", () => {
80
+ const { signal } = makeCancellationSignal();
81
+ const prioritizer = new CdnPrioritizer(signal);
82
+ const cdn = { id: "cdn1", baseUrl: "https://cdn1.example.com" };
83
+ prioritizer.downgradeCdn(cdn);
84
+ expect((prioritizer as any).trigger).toHaveBeenCalledWith("priorityChange", null);
85
+ });
86
+
87
+ it("triggers priorityChange when downgrade expires", () => {
88
+ mockGetCurrent.mockReturnValue({ DEFAULT_CDN_DOWNGRADE_TIME: 5000 });
89
+ const { signal } = makeCancellationSignal();
90
+ const prioritizer = new CdnPrioritizer(signal);
91
+ const cdn = { id: "cdn1", baseUrl: "https://cdn1.example.com" };
92
+ prioritizer.downgradeCdn(cdn);
93
+ const triggerMock = (prioritizer as any).trigger;
94
+ const callCountBefore = triggerMock.mock.calls.length;
95
+ vi.advanceTimersByTime(5000);
96
+ expect(triggerMock.mock.calls.length).toBeGreaterThan(callCountBefore);
97
+ });
98
+
99
+ it("CDN is no longer downgraded after timeout expires", () => {
100
+ mockGetCurrent.mockReturnValue({ DEFAULT_CDN_DOWNGRADE_TIME: 5000 });
101
+ const { signal } = makeCancellationSignal();
102
+ const prioritizer = new CdnPrioritizer(signal);
103
+ const cdn1 = { id: "cdn1", baseUrl: "https://cdn1.example.com" };
104
+ const cdn2 = { id: "cdn2", baseUrl: "https://cdn2.example.com" };
105
+ prioritizer.downgradeCdn(cdn1);
106
+ vi.advanceTimersByTime(5000);
107
+ const result = prioritizer.getCdnPreferenceForResource([cdn1, cdn2]);
108
+ expect(result[0]).toEqual(cdn1);
109
+ });
110
+
111
+ it("re-downgrading a CDN resets its downgrade timer", () => {
112
+ mockGetCurrent.mockReturnValue({ DEFAULT_CDN_DOWNGRADE_TIME: 5000 });
113
+ const { signal } = makeCancellationSignal();
114
+ const prioritizer = new CdnPrioritizer(signal);
115
+ const cdn1 = { id: "cdn1", baseUrl: "https://cdn1.example.com" };
116
+ const cdn2 = { id: "cdn2", baseUrl: "https://cdn2.example.com" };
117
+
118
+ prioritizer.downgradeCdn(cdn1);
119
+ vi.advanceTimersByTime(3000);
120
+ prioritizer.downgradeCdn(cdn1); // reset
121
+ vi.advanceTimersByTime(3000); // only 3s since reset, should still be downgraded
122
+ const result = prioritizer.getCdnPreferenceForResource([cdn1, cdn2]);
123
+ expect(result[0]).toEqual(cdn2);
124
+ });
125
+
126
+ it("matches CDN by baseUrl when id is undefined", () => {
127
+ const { signal } = makeCancellationSignal();
128
+ const prioritizer = new CdnPrioritizer(signal);
129
+ const cdn1 = { baseUrl: "https://cdn1.example.com" } as any;
130
+ const cdn2 = { baseUrl: "https://cdn2.example.com" } as any;
131
+ prioritizer.downgradeCdn(cdn1);
132
+ const result = prioritizer.getCdnPreferenceForResource([cdn1, cdn2]);
133
+ expect(result).toEqual([cdn2, cdn1]);
134
+ });
135
+
136
+ it("clears timeouts and resets on destroy signal", () => {
137
+ const { signal, cancel } = makeCancellationSignal();
138
+ const prioritizer = new CdnPrioritizer(signal);
139
+ const cdn = { id: "cdn1", baseUrl: "https://cdn1.example.com" };
140
+ prioritizer.downgradeCdn(cdn);
141
+ cancel();
142
+ // After destroy, downgraded list should be empty
143
+ expect((prioritizer as any)._downgradedCdnList.metadata).toHaveLength(0);
144
+ expect((prioritizer as any)._downgradedCdnList.timeouts).toHaveLength(0);
145
+ });
146
+
147
+ it("returns empty array when given empty array", () => {
148
+ const { signal } = makeCancellationSignal();
149
+ const prioritizer = new CdnPrioritizer(signal);
150
+ expect(prioritizer.getCdnPreferenceForResource([])).toEqual([]);
151
+ });
152
+
153
+ it("handles multiple downgraded CDNs correctly", () => {
154
+ const { signal } = makeCancellationSignal();
155
+ const prioritizer = new CdnPrioritizer(signal);
156
+ const cdn1 = { id: "cdn1", baseUrl: "https://cdn1.example.com" };
157
+ const cdn2 = { id: "cdn2", baseUrl: "https://cdn2.example.com" };
158
+ const cdn3 = { id: "cdn3", baseUrl: "https://cdn3.example.com" };
159
+ prioritizer.downgradeCdn(cdn1);
160
+ prioritizer.downgradeCdn(cdn2);
161
+ const result = prioritizer.getCdnPreferenceForResource([cdn1, cdn2, cdn3]);
162
+ expect(result[0]).toEqual(cdn3);
163
+ expect(result).toContain(cdn1);
164
+ expect(result).toContain(cdn2);
165
+ });
166
+ });