rx-player 4.5.0-dev.2026033100 → 4.5.0-dev.2026041501

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 (251) hide show
  1. package/CHANGELOG.md +22 -5
  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/can_patch_out_pssh.d.ts +42 -0
  8. package/dist/commonjs/compat/can_patch_out_pssh.d.ts.map +1 -0
  9. package/dist/commonjs/compat/can_patch_out_pssh.js +53 -0
  10. package/dist/commonjs/compat/env_detector.d.ts +2 -0
  11. package/dist/commonjs/compat/env_detector.d.ts.map +1 -1
  12. package/dist/commonjs/compat/env_detector.js +5 -0
  13. package/dist/commonjs/core/adaptive/network_analyzer.d.ts +1 -2
  14. package/dist/commonjs/core/adaptive/network_analyzer.d.ts.map +1 -1
  15. package/dist/commonjs/core/adaptive/network_analyzer.js +3 -3
  16. package/dist/commonjs/core/adaptive/utils/representation_score_calculator.js +2 -2
  17. package/dist/commonjs/core/cmcd/cmcd_data_builder.d.ts.map +1 -1
  18. package/dist/commonjs/core/cmcd/cmcd_data_builder.js +9 -3
  19. package/dist/commonjs/core/entry/content_preparer.d.ts.map +1 -1
  20. package/dist/commonjs/core/entry/content_preparer.js +5 -7
  21. package/dist/commonjs/core/entry/core_entry.d.ts.map +1 -1
  22. package/dist/commonjs/core/entry/core_entry.js +6 -2
  23. package/dist/commonjs/core/entry/core_text_displayer_interface.js +3 -3
  24. package/dist/commonjs/core/fetchers/manifest/manifest_fetcher.d.ts.map +1 -1
  25. package/dist/commonjs/core/fetchers/manifest/manifest_fetcher.js +12 -0
  26. package/dist/commonjs/core/fetchers/thumbnails/thumbnail_fetcher.js +1 -1
  27. package/dist/commonjs/core/fetchers/utils/schedule_request.d.ts.map +1 -1
  28. package/dist/commonjs/core/fetchers/utils/schedule_request.js +4 -3
  29. package/dist/commonjs/core/segment_sinks/garbage_collector.d.ts +0 -2
  30. package/dist/commonjs/core/segment_sinks/garbage_collector.d.ts.map +1 -1
  31. package/dist/commonjs/core/segment_sinks/garbage_collector.js +0 -3
  32. package/dist/commonjs/core/segment_sinks/implementations/text/text_segment_sink.d.ts +1 -1
  33. package/dist/commonjs/core/segment_sinks/implementations/text/text_segment_sink.d.ts.map +1 -1
  34. package/dist/commonjs/core/segment_sinks/implementations/text/text_segment_sink.js +2 -2
  35. package/dist/commonjs/core/stream/adaptation/adaptation_stream.d.ts.map +1 -1
  36. package/dist/commonjs/core/stream/adaptation/adaptation_stream.js +6 -6
  37. package/dist/commonjs/core/stream/orchestrator/stream_orchestrator.d.ts.map +1 -1
  38. package/dist/commonjs/core/stream/orchestrator/stream_orchestrator.js +37 -40
  39. package/dist/commonjs/default_config.d.ts +5 -0
  40. package/dist/commonjs/default_config.d.ts.map +1 -1
  41. package/dist/commonjs/default_config.js +5 -0
  42. package/dist/commonjs/main_thread/api/public_api.d.ts.map +1 -1
  43. package/dist/commonjs/main_thread/api/public_api.js +23 -16
  44. package/dist/commonjs/main_thread/decrypt/content_decryptor.d.ts.map +1 -1
  45. package/dist/commonjs/main_thread/decrypt/content_decryptor.js +4 -1
  46. package/dist/commonjs/main_thread/decrypt/session_events_listener.js +1 -1
  47. package/dist/commonjs/main_thread/decrypt/set_server_certificate.d.ts +2 -0
  48. package/dist/commonjs/main_thread/decrypt/set_server_certificate.d.ts.map +1 -1
  49. package/dist/commonjs/main_thread/decrypt/set_server_certificate.js +4 -0
  50. package/dist/commonjs/main_thread/init/media_source_content_initializer.d.ts +0 -8
  51. package/dist/commonjs/main_thread/init/media_source_content_initializer.d.ts.map +1 -1
  52. package/dist/commonjs/main_thread/init/media_source_content_initializer.js +58 -50
  53. package/dist/commonjs/main_thread/init/utils/stream_events_emitter/stream_events_emitter.d.ts +35 -5
  54. package/dist/commonjs/main_thread/init/utils/stream_events_emitter/stream_events_emitter.d.ts.map +1 -1
  55. package/dist/commonjs/main_thread/init/utils/stream_events_emitter/stream_events_emitter.js +60 -19
  56. package/dist/commonjs/main_thread/render_thumbnail.d.ts.map +1 -1
  57. package/dist/commonjs/main_thread/render_thumbnail.js +4 -0
  58. package/dist/commonjs/main_thread/tracks_store/media_element_tracks_store.d.ts.map +1 -1
  59. package/dist/commonjs/main_thread/tracks_store/media_element_tracks_store.js +1 -0
  60. package/dist/commonjs/main_thread/tracks_store/tracks_store.d.ts +1 -1
  61. package/dist/commonjs/main_thread/tracks_store/tracks_store.js +1 -1
  62. package/dist/{es2017/parsers/containers/isobmff/take_pssh_out.d.ts → commonjs/parsers/containers/isobmff/extract_pssh.d.ts} +6 -4
  63. package/dist/commonjs/parsers/containers/isobmff/extract_pssh.d.ts.map +1 -0
  64. package/dist/commonjs/parsers/containers/isobmff/{take_pssh_out.js → extract_pssh.js} +22 -17
  65. package/dist/commonjs/parsers/containers/isobmff/index.d.ts +2 -2
  66. package/dist/commonjs/parsers/containers/isobmff/index.d.ts.map +1 -1
  67. package/dist/commonjs/parsers/containers/isobmff/index.js +4 -4
  68. package/dist/commonjs/playback_observer/core_playback_observer.d.ts +4 -4
  69. package/dist/commonjs/playback_observer/core_playback_observer.d.ts.map +1 -1
  70. package/dist/commonjs/playback_observer/media_element_playback_observer.d.ts +1 -2
  71. package/dist/commonjs/playback_observer/media_element_playback_observer.d.ts.map +1 -1
  72. package/dist/commonjs/transports/dash/segment_parser.js +1 -1
  73. package/dist/commonjs/transports/local/segment_parser.js +1 -1
  74. package/dist/commonjs/utils/test-utils.d.ts +30 -0
  75. package/dist/commonjs/utils/test-utils.d.ts.map +1 -0
  76. package/dist/commonjs/utils/test-utils.js +79 -0
  77. package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.d.ts.map +1 -1
  78. package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.js +1 -1
  79. package/dist/es2017/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
  80. package/dist/es2017/__GENERATED_CODE/embedded_worker.js +1 -1
  81. package/dist/es2017/compat/can_patch_out_pssh.d.ts +42 -0
  82. package/dist/es2017/compat/can_patch_out_pssh.d.ts.map +1 -0
  83. package/dist/es2017/compat/can_patch_out_pssh.js +50 -0
  84. package/dist/es2017/compat/env_detector.d.ts +2 -0
  85. package/dist/es2017/compat/env_detector.d.ts.map +1 -1
  86. package/dist/es2017/compat/env_detector.js +5 -0
  87. package/dist/es2017/core/adaptive/network_analyzer.d.ts +1 -2
  88. package/dist/es2017/core/adaptive/network_analyzer.d.ts.map +1 -1
  89. package/dist/es2017/core/adaptive/network_analyzer.js +3 -3
  90. package/dist/es2017/core/adaptive/utils/representation_score_calculator.js +2 -2
  91. package/dist/es2017/core/cmcd/cmcd_data_builder.d.ts.map +1 -1
  92. package/dist/es2017/core/cmcd/cmcd_data_builder.js +9 -3
  93. package/dist/es2017/core/entry/content_preparer.d.ts.map +1 -1
  94. package/dist/es2017/core/entry/content_preparer.js +3 -5
  95. package/dist/es2017/core/entry/core_entry.d.ts.map +1 -1
  96. package/dist/es2017/core/entry/core_entry.js +6 -2
  97. package/dist/es2017/core/entry/core_text_displayer_interface.js +3 -3
  98. package/dist/es2017/core/fetchers/manifest/manifest_fetcher.d.ts.map +1 -1
  99. package/dist/es2017/core/fetchers/manifest/manifest_fetcher.js +12 -0
  100. package/dist/es2017/core/fetchers/thumbnails/thumbnail_fetcher.js +1 -1
  101. package/dist/es2017/core/fetchers/utils/schedule_request.d.ts.map +1 -1
  102. package/dist/es2017/core/fetchers/utils/schedule_request.js +2 -3
  103. package/dist/es2017/core/segment_sinks/garbage_collector.d.ts +0 -2
  104. package/dist/es2017/core/segment_sinks/garbage_collector.d.ts.map +1 -1
  105. package/dist/es2017/core/segment_sinks/garbage_collector.js +0 -3
  106. package/dist/es2017/core/segment_sinks/implementations/text/text_segment_sink.d.ts +1 -1
  107. package/dist/es2017/core/segment_sinks/implementations/text/text_segment_sink.d.ts.map +1 -1
  108. package/dist/es2017/core/segment_sinks/implementations/text/text_segment_sink.js +2 -2
  109. package/dist/es2017/core/stream/adaptation/adaptation_stream.d.ts.map +1 -1
  110. package/dist/es2017/core/stream/adaptation/adaptation_stream.js +6 -6
  111. package/dist/es2017/core/stream/orchestrator/stream_orchestrator.d.ts.map +1 -1
  112. package/dist/es2017/core/stream/orchestrator/stream_orchestrator.js +40 -39
  113. package/dist/es2017/default_config.d.ts +5 -0
  114. package/dist/es2017/default_config.d.ts.map +1 -1
  115. package/dist/es2017/default_config.js +5 -0
  116. package/dist/es2017/main_thread/api/public_api.d.ts.map +1 -1
  117. package/dist/es2017/main_thread/api/public_api.js +20 -13
  118. package/dist/es2017/main_thread/decrypt/content_decryptor.d.ts.map +1 -1
  119. package/dist/es2017/main_thread/decrypt/content_decryptor.js +4 -1
  120. package/dist/es2017/main_thread/decrypt/session_events_listener.js +1 -1
  121. package/dist/es2017/main_thread/decrypt/set_server_certificate.d.ts +2 -0
  122. package/dist/es2017/main_thread/decrypt/set_server_certificate.d.ts.map +1 -1
  123. package/dist/es2017/main_thread/decrypt/set_server_certificate.js +4 -0
  124. package/dist/es2017/main_thread/init/media_source_content_initializer.d.ts +0 -8
  125. package/dist/es2017/main_thread/init/media_source_content_initializer.d.ts.map +1 -1
  126. package/dist/es2017/main_thread/init/media_source_content_initializer.js +58 -50
  127. package/dist/es2017/main_thread/init/utils/stream_events_emitter/stream_events_emitter.d.ts +35 -5
  128. package/dist/es2017/main_thread/init/utils/stream_events_emitter/stream_events_emitter.d.ts.map +1 -1
  129. package/dist/es2017/main_thread/init/utils/stream_events_emitter/stream_events_emitter.js +60 -19
  130. package/dist/es2017/main_thread/render_thumbnail.d.ts.map +1 -1
  131. package/dist/es2017/main_thread/render_thumbnail.js +4 -0
  132. package/dist/es2017/main_thread/tracks_store/media_element_tracks_store.d.ts.map +1 -1
  133. package/dist/es2017/main_thread/tracks_store/media_element_tracks_store.js +1 -0
  134. package/dist/es2017/main_thread/tracks_store/tracks_store.d.ts +1 -1
  135. package/dist/es2017/main_thread/tracks_store/tracks_store.js +1 -1
  136. package/dist/{commonjs/parsers/containers/isobmff/take_pssh_out.d.ts → es2017/parsers/containers/isobmff/extract_pssh.d.ts} +6 -4
  137. package/dist/es2017/parsers/containers/isobmff/extract_pssh.d.ts.map +1 -0
  138. package/dist/es2017/parsers/containers/isobmff/{take_pssh_out.js → extract_pssh.js} +21 -16
  139. package/dist/es2017/parsers/containers/isobmff/index.d.ts +2 -2
  140. package/dist/es2017/parsers/containers/isobmff/index.d.ts.map +1 -1
  141. package/dist/es2017/parsers/containers/isobmff/index.js +2 -2
  142. package/dist/es2017/playback_observer/core_playback_observer.d.ts +4 -4
  143. package/dist/es2017/playback_observer/core_playback_observer.d.ts.map +1 -1
  144. package/dist/es2017/playback_observer/media_element_playback_observer.d.ts +1 -2
  145. package/dist/es2017/playback_observer/media_element_playback_observer.d.ts.map +1 -1
  146. package/dist/es2017/transports/dash/segment_parser.js +2 -2
  147. package/dist/es2017/transports/local/segment_parser.js +2 -2
  148. package/dist/es2017/utils/test-utils.d.ts +30 -0
  149. package/dist/es2017/utils/test-utils.d.ts.map +1 -0
  150. package/dist/es2017/utils/test-utils.js +36 -0
  151. package/dist/mpd-parser.wasm +0 -0
  152. package/dist/worker.js +6 -6
  153. package/package.json +4 -2
  154. package/src/README.md +7 -7
  155. package/src/__GENERATED_CODE/embedded_dash_wasm.ts +1 -1
  156. package/src/__GENERATED_CODE/embedded_worker.ts +1 -1
  157. package/src/compat/__tests__/can_patch_out_pssh.test.ts +40 -0
  158. package/src/compat/can_patch_out_pssh.ts +53 -0
  159. package/src/compat/env_detector.ts +4 -0
  160. package/src/core/README.md +1 -1
  161. package/src/core/adaptive/README.md +3 -3
  162. package/src/core/adaptive/__tests__/adaptive_representation_selector.test.ts +181 -110
  163. package/src/core/adaptive/__tests__/guess_based_chooser.test.ts +229 -123
  164. package/src/core/adaptive/__tests__/mocks.ts +100 -0
  165. package/src/core/adaptive/__tests__/network_analyzer.test.ts +152 -59
  166. package/src/core/adaptive/network_analyzer.ts +4 -4
  167. package/src/core/adaptive/utils/__tests__/filter_by_bitrate.test.ts +11 -19
  168. package/src/core/adaptive/utils/__tests__/filter_by_resolution.test.ts +12 -12
  169. package/src/core/adaptive/utils/__tests__/last_estimate_storage.test.ts +25 -12
  170. package/src/core/adaptive/utils/__tests__/pending_requests_store.test.ts +13 -9
  171. package/src/core/adaptive/utils/__tests__/representation_score_calculator.test.ts +11 -11
  172. package/src/core/adaptive/utils/__tests__/select_optimal_representation.test.ts +13 -23
  173. package/src/core/adaptive/utils/representation_score_calculator.ts +2 -2
  174. package/src/core/cmcd/__tests__/cmcd_data_builder.test.ts +60 -15
  175. package/src/core/cmcd/cmcd_data_builder.ts +9 -3
  176. package/src/core/entry/README.md +2 -2
  177. package/src/core/entry/__tests__/core_text_displayer_interface.test.ts +20 -0
  178. package/src/core/entry/content_preparer.ts +2 -5
  179. package/src/core/entry/core_entry.ts +6 -2
  180. package/src/core/entry/core_text_displayer_interface.ts +3 -3
  181. package/src/core/fetchers/manifest/__tests__/manifest_fetcher.test.ts +52 -3
  182. package/src/core/fetchers/manifest/manifest_fetcher.ts +12 -0
  183. package/src/core/fetchers/thumbnails/__tests__/thumbnail_fetcher.test.ts +70 -0
  184. package/src/core/fetchers/thumbnails/thumbnail_fetcher.ts +1 -1
  185. package/src/core/fetchers/utils/schedule_request.ts +5 -3
  186. package/src/core/segment_sinks/__tests__/garbage_collector.test.ts +434 -0
  187. package/src/core/segment_sinks/__tests__/mocks.ts +49 -0
  188. package/src/core/segment_sinks/garbage_collector.ts +0 -3
  189. package/src/core/segment_sinks/implementations/text/__tests__/text_segment_sink.test.ts +177 -0
  190. package/src/core/segment_sinks/implementations/text/text_segment_sink.ts +2 -2
  191. package/src/core/segment_sinks/inventory/__tests__/buffered_history.test.ts +215 -0
  192. package/src/core/segment_sinks/inventory/__tests__/segment_inventory.test.ts +448 -0
  193. package/src/core/stream/adaptation/__tests__/adaptation_stream.test.ts +973 -0
  194. package/src/core/stream/adaptation/__tests__/get_representations_switch_strategy.test.ts +283 -374
  195. package/src/core/stream/adaptation/adaptation_stream.ts +6 -8
  196. package/src/core/stream/orchestrator/README.md +4 -4
  197. package/src/core/stream/orchestrator/__tests__/stream_orchestrator.test.ts +707 -0
  198. package/src/core/stream/orchestrator/stream_orchestrator.ts +41 -46
  199. package/src/core/stream/period/utils/__tests__/get_adaptation_switch_strategy.test.ts +290 -220
  200. package/src/core/stream/representation/__tests__/encryption_data_notifier.test.ts +93 -63
  201. package/src/core/stream/representation/utils/__tests__/append_segment_to_buffer.test.ts +106 -63
  202. package/src/core/stream/representation/utils/__tests__/check_for_discontinuity.test.ts +179 -204
  203. package/src/core/stream/representation/utils/__tests__/get_segment_priority.test.ts +7 -7
  204. package/src/core/stream/representation/utils/__tests__/push_init_segment.test.ts +103 -60
  205. package/src/core/stream/representation/utils/__tests__/push_media_segment.test.ts +231 -165
  206. package/src/default_config.ts +6 -0
  207. package/src/experimental/README.md +1 -1
  208. package/src/features/README.md +3 -3
  209. package/src/main_thread/api/README.md +6 -7
  210. package/src/main_thread/api/public_api.ts +16 -10
  211. package/src/main_thread/decrypt/README.md +4 -4
  212. package/src/main_thread/decrypt/__tests__/__global__/content_decryptor.test.ts +135 -0
  213. package/src/main_thread/decrypt/__tests__/__global__/get_license.test.ts +70 -0
  214. package/src/main_thread/decrypt/__tests__/__global__/server_certificate.test.ts +44 -0
  215. package/src/main_thread/decrypt/__tests__/__global__/utils.ts +2 -2
  216. package/src/main_thread/decrypt/content_decryptor.ts +6 -1
  217. package/src/main_thread/decrypt/session_events_listener.ts +1 -1
  218. package/src/main_thread/decrypt/set_server_certificate.ts +5 -0
  219. package/src/main_thread/init/media_source_content_initializer.ts +69 -55
  220. package/src/main_thread/init/utils/__tests__/stream_events_emitter.test.ts +175 -0
  221. package/src/main_thread/init/utils/stream_events_emitter/stream_events_emitter.ts +90 -26
  222. package/src/main_thread/render_thumbnail.ts +4 -0
  223. package/src/main_thread/tracks_store/README.md +12 -0
  224. package/src/main_thread/tracks_store/__tests__/media_element_tracks_store.test.ts +25 -18
  225. package/src/main_thread/tracks_store/media_element_tracks_store.ts +1 -0
  226. package/src/main_thread/tracks_store/tracks_store.ts +1 -1
  227. package/src/manifest/classes/__tests__/mocks.ts +202 -0
  228. package/src/parsers/containers/isobmff/__tests__/extract_pssh.test.ts +199 -0
  229. package/src/parsers/containers/isobmff/{take_pssh_out.ts → extract_pssh.ts} +21 -17
  230. package/src/parsers/containers/isobmff/index.ts +2 -2
  231. package/src/parsers/manifest/dash/wasm-parser/README.md +9 -9
  232. package/src/playback_observer/__tests__/mocks.ts +152 -0
  233. package/src/playback_observer/core_playback_observer.ts +4 -4
  234. package/src/playback_observer/media_element_playback_observer.ts +1 -1
  235. package/src/tools/README.md +2 -2
  236. package/src/transports/README.md +5 -5
  237. package/src/transports/dash/segment_parser.ts +2 -2
  238. package/src/transports/local/segment_parser.ts +2 -2
  239. package/src/transports/metaplaylist/README.md +4 -4
  240. package/src/utils/README.md +3 -3
  241. package/src/utils/test-utils.ts +50 -0
  242. package/dist/commonjs/core/entry/synchronize_sinks_on_observation.d.ts +0 -11
  243. package/dist/commonjs/core/entry/synchronize_sinks_on_observation.d.ts.map +0 -1
  244. package/dist/commonjs/core/entry/synchronize_sinks_on_observation.js +0 -20
  245. package/dist/commonjs/parsers/containers/isobmff/take_pssh_out.d.ts.map +0 -1
  246. package/dist/es2017/core/entry/synchronize_sinks_on_observation.d.ts +0 -11
  247. package/dist/es2017/core/entry/synchronize_sinks_on_observation.d.ts.map +0 -1
  248. package/dist/es2017/core/entry/synchronize_sinks_on_observation.js +0 -17
  249. package/dist/es2017/parsers/containers/isobmff/take_pssh_out.d.ts.map +0 -1
  250. package/src/core/adaptive/utils/__tests__/bandwith_estimator.test.ts +0 -117
  251. package/src/core/entry/synchronize_sinks_on_observation.ts +0 -22
@@ -1,14 +1,17 @@
1
1
  import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2
+ import {
3
+ DummyManifest,
4
+ DummyPeriod,
5
+ DummyAdaptation,
6
+ DummyRepresentation,
7
+ createSegment,
8
+ } from "../../../../manifest/classes/__tests__/mocks";
2
9
  import PendingRequestsStore from "../pending_requests_store";
3
10
  import type {
4
11
  IPendingRequestStoreBegin,
5
12
  IPendingRequestStoreProgress,
6
13
  } from "../pending_requests_store";
7
14
 
8
- /* eslint-disable @typescript-eslint/no-explicit-any */
9
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
10
- /* eslint-disable @typescript-eslint/no-unsafe-member-access */
11
-
12
15
  const mocks = vi.hoisted(() => ({
13
16
  warn: vi.fn(),
14
17
  }));
@@ -24,11 +27,11 @@ describe("PendingRequestsStore", () => {
24
27
  let store: PendingRequestsStore;
25
28
 
26
29
  const createMockContent = (segmentTime: number = 0) => ({
27
- manifest: {} as any,
28
- period: {} as any,
29
- adaptation: {} as any,
30
- representation: {} as any,
31
- segment: { time: segmentTime } as any,
30
+ manifest: new DummyManifest(),
31
+ period: new DummyPeriod(),
32
+ adaptation: new DummyAdaptation(),
33
+ representation: new DummyRepresentation(),
34
+ segment: createSegment({ time: segmentTime }),
32
35
  });
33
36
 
34
37
  const createMockRequest = (
@@ -231,6 +234,7 @@ describe("PendingRequestsStore", () => {
231
234
 
232
235
  store.add(request);
233
236
  // Manually corrupt the internal state to test filtering
237
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
234
238
  (store as any)._currentRequests.corrupted = null;
235
239
 
236
240
  const requests = store.getRequests();
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
2
  import type { IRepresentation } from "../../../../manifest";
3
+ import { DummyRepresentation } from "../../../../manifest/classes/__tests__/mocks";
3
4
  import RepresentationScoreCalculator, {
4
5
  ScoreConfidenceLevel,
5
6
  } from "../representation_score_calculator";
@@ -40,15 +41,15 @@ describe("RepresentationScoreCalculator", () => {
40
41
  beforeEach(() => {
41
42
  calculator = new RepresentationScoreCalculator();
42
43
 
43
- mockRepresentation1 = {
44
+ mockRepresentation1 = new DummyRepresentation({
44
45
  id: "rep1",
45
46
  bitrate: 1000000,
46
- } as IRepresentation;
47
+ });
47
48
 
48
- mockRepresentation2 = {
49
+ mockRepresentation2 = new DummyRepresentation({
49
50
  id: "rep2",
50
51
  bitrate: 2000000,
51
- } as IRepresentation;
52
+ });
52
53
  });
53
54
  afterEach(() => {
54
55
  vi.resetAllMocks();
@@ -118,7 +119,7 @@ describe("RepresentationScoreCalculator", () => {
118
119
  expect(estimate).toBeUndefined();
119
120
  });
120
121
 
121
- it("should return LOW confidence when less than 5 segments loaded", () => {
122
+ it("should return LOW confidence when less than 6 segments loaded", () => {
122
123
  calculator.addSample(mockRepresentation1, 2, 4);
123
124
  calculator.addSample(mockRepresentation1, 2, 4);
124
125
 
@@ -127,8 +128,8 @@ describe("RepresentationScoreCalculator", () => {
127
128
  });
128
129
 
129
130
  it("should return LOW confidence when loaded duration is less than 10 seconds", () => {
130
- // Add 5 segments but with short duration
131
- for (let i = 0; i < 5; i++) {
131
+ // Add 6 segments but with short duration
132
+ for (let i = 0; i < 6; i++) {
132
133
  calculator.addSample(mockRepresentation1, 1, 1);
133
134
  }
134
135
 
@@ -136,8 +137,7 @@ describe("RepresentationScoreCalculator", () => {
136
137
  expect(estimate?.confidenceLevel).toBe(ScoreConfidenceLevel.LOW);
137
138
  });
138
139
 
139
- it("should return HIGH confidence when > 5 segments and >= 10 seconds loaded", () => {
140
- // Add 5 segments with 2 seconds each (total 10 seconds)
140
+ it("should return HIGH confidence when 6 segments and >= 10 seconds loaded", () => {
141
141
  for (let i = 0; i < 6; i++) {
142
142
  calculator.addSample(mockRepresentation1, 1, 2);
143
143
  }
@@ -146,8 +146,8 @@ describe("RepresentationScoreCalculator", () => {
146
146
  expect(estimate?.confidenceLevel).toBe(ScoreConfidenceLevel.HIGH);
147
147
  });
148
148
 
149
- it("should return HIGH confidence when > 5 segments and > 10 seconds loaded", () => {
150
- // Add 5 segments with 3 seconds each (total 15 seconds)
149
+ it("should return HIGH confidence when > 6 segments and > 10 seconds loaded", () => {
150
+ // Add 6 segments with 3 seconds each (total 15 seconds)
151
151
  for (let i = 0; i < 6; i++) {
152
152
  calculator.addSample(mockRepresentation1, 1, 3);
153
153
  }
@@ -1,48 +1,38 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import type { IRepresentation } from "../../../../manifest";
2
+ import { DummyRepresentation } from "../../../../manifest/classes/__tests__/mocks";
3
3
  import selectOptimalRepresentation from "../select_optimal_representation";
4
4
 
5
5
  describe("ABR - selectOptimalRepresentation", () => {
6
6
  const fakeReps = [
7
- { bitrate: 100 },
8
- { bitrate: 1000 },
9
- { bitrate: 10000 },
10
- { bitrate: 100000 },
7
+ new DummyRepresentation({ bitrate: 100 }),
8
+ new DummyRepresentation({ bitrate: 1000 }),
9
+ new DummyRepresentation({ bitrate: 10000 }),
10
+ new DummyRepresentation({ bitrate: 100000 }),
11
11
  ];
12
12
 
13
13
  it("should return the best representation when the optimal bitrate given is Infinity", () => {
14
- expect(selectOptimalRepresentation(fakeReps as IRepresentation[], Infinity)).toBe(
14
+ expect(selectOptimalRepresentation(fakeReps, Infinity)).toBe(
15
15
  fakeReps[fakeReps.length - 1],
16
16
  );
17
17
  });
18
18
 
19
19
  it("should return the best representation when both the optimal bitrate and the higher limit given are higher or equal than the highest Representation", () => {
20
- expect(selectOptimalRepresentation(fakeReps as IRepresentation[], 100000)).toBe(
20
+ expect(selectOptimalRepresentation(fakeReps, 100000)).toBe(
21
21
  fakeReps[fakeReps.length - 1],
22
22
  );
23
- expect(selectOptimalRepresentation(fakeReps as IRepresentation[], 900000)).toBe(
23
+ expect(selectOptimalRepresentation(fakeReps, 900000)).toBe(
24
24
  fakeReps[fakeReps.length - 1],
25
25
  );
26
26
  });
27
27
 
28
28
  it("should return the worst representation when the optimal bitrate given is 0", () => {
29
- expect(selectOptimalRepresentation(fakeReps as IRepresentation[], 0)).toBe(
30
- fakeReps[0],
31
- );
29
+ expect(selectOptimalRepresentation(fakeReps, 0)).toBe(fakeReps[0]);
32
30
  });
33
31
 
34
32
  it("should return the worst representation when the optimal bitrate is lower or equal than the lowest Representation", () => {
35
- expect(selectOptimalRepresentation(fakeReps as IRepresentation[], 4)).toBe(
36
- fakeReps[0],
37
- );
38
- expect(selectOptimalRepresentation(fakeReps as IRepresentation[], 100)).toBe(
39
- fakeReps[0],
40
- );
41
- expect(selectOptimalRepresentation(fakeReps as IRepresentation[], 0)).toBe(
42
- fakeReps[0],
43
- );
44
- expect(selectOptimalRepresentation(fakeReps as IRepresentation[], 100)).toBe(
45
- fakeReps[0],
46
- );
33
+ expect(selectOptimalRepresentation(fakeReps, 4)).toBe(fakeReps[0]);
34
+ expect(selectOptimalRepresentation(fakeReps, 100)).toBe(fakeReps[0]);
35
+ expect(selectOptimalRepresentation(fakeReps, 0)).toBe(fakeReps[0]);
36
+ expect(selectOptimalRepresentation(fakeReps, 100)).toBe(fakeReps[0]);
47
37
  });
48
38
  });
@@ -110,7 +110,7 @@ export default class RepresentationScoreCalculator {
110
110
  representation,
111
111
  ewma: currentEWMA,
112
112
  loadedDuration: segmentDuration,
113
- loadedSegments: 0,
113
+ loadedSegments: 1,
114
114
  };
115
115
  }
116
116
 
@@ -143,7 +143,7 @@ export default class RepresentationScoreCalculator {
143
143
  const { ewma, loadedSegments, loadedDuration } = this._currentRepresentationData;
144
144
  const estimate = ewma.getEstimate();
145
145
  const confidenceLevel =
146
- loadedSegments >= 5 && loadedDuration >= 10
146
+ loadedSegments >= 6 && loadedDuration >= 10
147
147
  ? ScoreConfidenceLevel.HIGH
148
148
  : ScoreConfidenceLevel.LOW;
149
149
 
@@ -358,9 +358,7 @@ describe("CmcdDataBuilder", () => {
358
358
  speed: 1,
359
359
  rebuffering: null,
360
360
  });
361
- builder.startMonitoringPlayback(
362
- observer as Parameters<typeof builder.startMonitoringPlayback>[0],
363
- );
361
+ builder.startMonitoringPlayback(observer);
364
362
 
365
363
  const payload: any = builder.getCmcdDataForSegmentRequest(makeSegmentInfo());
366
364
  const requestHeader = payload.value["CMCD-Request"];
@@ -371,9 +369,7 @@ describe("CmcdDataBuilder", () => {
371
369
  it("includes pr when playback speed is not 1", () => {
372
370
  const builder = new CmcdDataBuilder({ communicationType: "headers" });
373
371
  const observer = makePlaybackObserver({ speed: 2, rebuffering: null });
374
- builder.startMonitoringPlayback(
375
- observer as Parameters<typeof builder.startMonitoringPlayback>[0],
376
- );
372
+ builder.startMonitoringPlayback(observer);
377
373
 
378
374
  const payload: any = builder.getCmcdDataForManifest("dash");
379
375
  const sessionHeader = payload.value["CMCD-Session"];
@@ -383,9 +379,17 @@ describe("CmcdDataBuilder", () => {
383
379
  it("does not include pr when speed is 1", () => {
384
380
  const builder = new CmcdDataBuilder({ communicationType: "headers" });
385
381
  const observer = makePlaybackObserver({ speed: 1, rebuffering: null });
386
- builder.startMonitoringPlayback(
387
- observer as Parameters<typeof builder.startMonitoringPlayback>[0],
388
- );
382
+ builder.startMonitoringPlayback(observer);
383
+
384
+ const payload: any = builder.getCmcdDataForManifest("dash");
385
+ const sessionHeader = payload.value["CMCD-Session"];
386
+ expect(sessionHeader).not.toContain("pr=");
387
+ });
388
+
389
+ it("does not include pr when playback speed is negative", () => {
390
+ const builder = new CmcdDataBuilder({ communicationType: "headers" });
391
+ const observer = makePlaybackObserver({ speed: -1, rebuffering: null });
392
+ builder.startMonitoringPlayback(observer);
389
393
 
390
394
  const payload: any = builder.getCmcdDataForManifest("dash");
391
395
  const sessionHeader = payload.value["CMCD-Session"];
@@ -395,23 +399,40 @@ describe("CmcdDataBuilder", () => {
395
399
  it("includes su when rebuffering is active", () => {
396
400
  const builder = new CmcdDataBuilder({ communicationType: "headers" });
397
401
  const observer = makePlaybackObserver({ rebuffering: { timestamp: 0 }, speed: 1 });
398
- builder.startMonitoringPlayback(
399
- observer as Parameters<typeof builder.startMonitoringPlayback>[0],
400
- );
402
+ builder.startMonitoringPlayback(observer);
401
403
 
402
404
  const payload: any = builder.getCmcdDataForManifest("dash");
403
405
  const requestHeader = payload.value["CMCD-Request"];
404
406
  expect(requestHeader).toContain("su");
405
407
  });
408
+
409
+ it("does not include dl or rtp when playback speed is 0", () => {
410
+ const builder = new CmcdDataBuilder({ communicationType: "headers" });
411
+ const observer = makePlaybackObserver({
412
+ buffered: {
413
+ video: [{ start: 5, end: 20 }],
414
+ audio: null,
415
+ text: null,
416
+ },
417
+ speed: 0,
418
+ rebuffering: null,
419
+ });
420
+ builder.startMonitoringPlayback(observer);
421
+
422
+ const payload: any = builder.getCmcdDataForSegmentRequest(makeSegmentInfo());
423
+ const requestHeader = payload.value["CMCD-Request"];
424
+ const statusHeader = payload.value["CMCD-Status"];
425
+ expect(requestHeader).toContain("bl=10000");
426
+ expect(requestHeader).not.toContain("dl=");
427
+ expect(statusHeader).not.toContain("rtp=");
428
+ });
406
429
  });
407
430
 
408
431
  describe("stopMonitoringPlayback", () => {
409
432
  it("clears the playback observer so subsequent calls work without observation data", () => {
410
433
  const builder = new CmcdDataBuilder({ communicationType: "headers" });
411
434
  const observer = makePlaybackObserver({ speed: 2, rebuffering: null });
412
- builder.startMonitoringPlayback(
413
- observer as Parameters<typeof builder.startMonitoringPlayback>[0],
414
- );
435
+ builder.startMonitoringPlayback(observer);
415
436
  builder.stopMonitoringPlayback();
416
437
 
417
438
  const payload: any = builder.getCmcdDataForManifest("dash");
@@ -460,5 +481,29 @@ describe("CmcdDataBuilder", () => {
460
481
  }
461
482
  }
462
483
  });
484
+
485
+ it("escapes all backslashes and quotes in header strings", () => {
486
+ const builder = new CmcdDataBuilder({
487
+ sessionId: 's\\"id\\tail',
488
+ contentId: 'c\\"id\\tail',
489
+ communicationType: "headers",
490
+ });
491
+ const payload: any = builder.getCmcdDataForManifest("dash");
492
+ const sessionHeader = payload.value["CMCD-Session"];
493
+ expect(sessionHeader).toContain('cid="c\\\\\\"id\\\\tail"');
494
+ expect(sessionHeader).toContain('sid="s\\\\\\"id\\\\tail"');
495
+ });
496
+
497
+ it("preserves all backslashes and quotes after query decoding", () => {
498
+ const builder = new CmcdDataBuilder({
499
+ sessionId: 's\\"id\\tail',
500
+ contentId: 'c\\"id\\tail',
501
+ communicationType: "query",
502
+ });
503
+ const payload: any = builder.getCmcdDataForManifest("dash");
504
+ const qs = decodeURIComponent(payload.value[0][1]);
505
+ expect(qs).toContain('cid="c\\\\\\"id\\\\tail"');
506
+ expect(qs).toContain('sid="s\\\\\\"id\\\\tail"');
507
+ });
463
508
  });
464
509
  });
@@ -186,7 +186,10 @@ export default class CmcdDataBuilder {
186
186
 
187
187
  const lastObservation = this._playbackObserver?.getReference().getValue();
188
188
  props.pr =
189
- lastObservation === undefined || lastObservation.speed === 1
189
+ lastObservation === undefined ||
190
+ !isFinite(lastObservation.speed) ||
191
+ lastObservation.speed < 0 ||
192
+ lastObservation.speed === 1
190
193
  ? undefined
191
194
  : lastObservation.speed;
192
195
  if (lastObservation !== undefined) {
@@ -300,7 +303,10 @@ export default class CmcdDataBuilder {
300
303
  }
301
304
 
302
305
  const precizeDeadlineMs =
303
- precizeBufferLengthMs === undefined || lastObservation === undefined
306
+ precizeBufferLengthMs === undefined ||
307
+ lastObservation === undefined ||
308
+ !isFinite(lastObservation.speed) ||
309
+ lastObservation.speed <= 0
304
310
  ? undefined
305
311
  : precizeBufferLengthMs / lastObservation.speed;
306
312
 
@@ -396,7 +402,7 @@ export default class CmcdDataBuilder {
396
402
  ): void => {
397
403
  const val = props[prop];
398
404
  if (val !== undefined) {
399
- const formatted = `"${val.replace("\\", "\\\\").replace('"', '\\"')}"`;
405
+ const formatted = `"${val.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
400
406
  const toAdd = `${prop}=${formatted},`;
401
407
  addPayload(toAdd, headerName);
402
408
  }
@@ -1,4 +1,4 @@
1
- # Core Main
1
+ # Core Entry
2
2
 
3
3
  | Consideration | Status |
4
4
  | ----------------------- | ------------------------------------------ |
@@ -11,7 +11,7 @@ any file may be directly imported.
11
11
 
12
12
  ## Overview
13
13
 
14
- The Core Main serves as the entry point of the `core` code, which may optionally run in a
14
+ The Core Entry serves as the entry point of the `core` code, which may optionally run in a
15
15
  WebWorker.
16
16
 
17
17
  When in monothreading mode, files declared here may be imported directly to make the link
@@ -78,6 +78,16 @@ describe("CoreTextDisplayerInterface", () => {
78
78
  cdi.onPushedTrackError(error);
79
79
  await expect(promise).rejects.toThrow("push failed");
80
80
  });
81
+
82
+ it("should resolve even if the sender synchronously responds", async () => {
83
+ const ranges = [{ start: 0, end: 10 }];
84
+ messageSender = vi.fn(() => {
85
+ cdi.onPushedTrackSuccess(ranges);
86
+ });
87
+ cdi = new CoreTextDisplayerInterface(CONTENT_ID, messageSender);
88
+
89
+ await expect(cdi.pushTextData({} as any)).resolves.toEqual(ranges);
90
+ });
81
91
  });
82
92
 
83
93
  describe("remove", () => {
@@ -109,6 +119,16 @@ describe("CoreTextDisplayerInterface", () => {
109
119
  cdi.onRemoveError(error);
110
120
  await expect(promise).rejects.toThrow("remove failed");
111
121
  });
122
+
123
+ it("should resolve even if the sender synchronously responds", async () => {
124
+ const ranges = [{ start: 5, end: 20 }];
125
+ messageSender = vi.fn(() => {
126
+ cdi.onRemoveSuccess(ranges);
127
+ });
128
+ cdi = new CoreTextDisplayerInterface(CONTENT_ID, messageSender);
129
+
130
+ await expect(cdi.remove(5, 20)).resolves.toEqual(ranges);
131
+ });
112
132
  });
113
133
 
114
134
  describe("reset", () => {
@@ -257,11 +257,6 @@ export default class ContentPreparer {
257
257
  manifestFetcher.addEventListener(
258
258
  "error",
259
259
  (err: unknown) => {
260
- sendMessage({
261
- type: CoreMessageType.Error,
262
- contentId,
263
- value: formatErrorForSender(err),
264
- });
265
260
  rej(err);
266
261
  },
267
262
  contentCanceller.signal,
@@ -336,7 +331,9 @@ export default class ContentPreparer {
336
331
  return Promise.reject(new Error("CP: No content anymore"));
337
332
  }
338
333
  this._currentContent.trackChoiceSetter.reset();
334
+ this._currentContent.coreTextSender?.stop("ContentPreparer MediaSource reload");
339
335
  this._currentMediaSourceCanceller = new TaskCanceller("ContentPreparer MediaSource");
336
+ this._currentMediaSourceCanceller.linkToSignal(this._contentCanceller.signal);
340
337
 
341
338
  const [mediaSourceInterface, segmentSinksStore, coreTextSender] =
342
339
  createMediaSourceInterfaceAndSegmentSinksStore(
@@ -1221,6 +1221,10 @@ function sendThumbnailData(
1221
1221
  msg.value.time,
1222
1222
  ).then(
1223
1223
  (result) => {
1224
+ // Multiple thumbnail requests can share the same fetched payload.
1225
+ // Transfer a copy here so replying to one request does not detach the
1226
+ // ArrayBuffer that still has to be sent to another requester.
1227
+ const data = result.data.slice(0);
1224
1228
  sendMessage(
1225
1229
  {
1226
1230
  type: CoreMessageType.ThumbnailDataResponse,
@@ -1228,10 +1232,10 @@ function sendThumbnailData(
1228
1232
  value: {
1229
1233
  status: "success",
1230
1234
  requestId: msg.value.requestId,
1231
- data: result,
1235
+ data: { ...result, data },
1232
1236
  },
1233
1237
  },
1234
- [result.data],
1238
+ [data],
1235
1239
  );
1236
1240
  },
1237
1241
  (err) => {
@@ -49,12 +49,12 @@ export default class CoreTextDisplayerInterface implements ITextDisplayerInterfa
49
49
  */
50
50
  public pushTextData(infos: ITextDisplayerData): Promise<IRange[]> {
51
51
  return new Promise((resolve, reject) => {
52
+ this._queues.pushTextData.push({ resolve, reject });
52
53
  this._messageSender({
53
54
  type: CoreMessageType.PushTextData,
54
55
  contentId: this._contentId,
55
56
  value: infos,
56
57
  });
57
- this._queues.pushTextData.push({ resolve, reject });
58
58
  });
59
59
  }
60
60
 
@@ -63,12 +63,12 @@ export default class CoreTextDisplayerInterface implements ITextDisplayerInterfa
63
63
  */
64
64
  public remove(start: number, end: number): Promise<IRange[]> {
65
65
  return new Promise((resolve, reject) => {
66
+ this._queues.remove.push({ resolve, reject });
66
67
  this._messageSender({
67
68
  type: CoreMessageType.RemoveTextData,
68
69
  contentId: this._contentId,
69
70
  value: { start, end },
70
71
  });
71
- this._queues.remove.push({ resolve, reject });
72
72
  });
73
73
  }
74
74
 
@@ -153,7 +153,7 @@ export default class CoreTextDisplayerInterface implements ITextDisplayerInterfa
153
153
  public onRemoveError(err: Error): void {
154
154
  const element = this._queues.remove.shift();
155
155
  if (element === undefined) {
156
- log.error("text", "pushTextData error for inexistant operation");
156
+ log.error("text", "remove error for inexistant operation");
157
157
  return;
158
158
  }
159
159
  element.reject(err);
@@ -96,7 +96,7 @@ function makeLoadResponse(overrides: Record<string, unknown> = {}) {
96
96
  };
97
97
  }
98
98
 
99
- describe.skip("ManifestFetcher", () => {
99
+ describe("ManifestFetcher", () => {
100
100
  beforeEach(() => {
101
101
  mockConfigGetCurrent.mockReturnValue(DEFAULT_CONFIG);
102
102
  mockFormatError.mockImplementation((e: unknown) => e);
@@ -488,6 +488,51 @@ describe.skip("ManifestFetcher", () => {
488
488
  expect.anything(),
489
489
  );
490
490
  });
491
+
492
+ it("updates the stored manifest URLs", async () => {
493
+ const manifest = new MockManifest();
494
+ const fetcher = new ManifestFetcher(
495
+ ["http://example.com/manifest"],
496
+ makePipelines(),
497
+ makeSettings({ initialManifest: manifest }),
498
+ );
499
+ fetcher.start();
500
+ await flushPromises();
501
+
502
+ fetcher.updateContentUrls(["http://new.com/manifest"], false);
503
+
504
+ expect(fetcher).toHaveProperty("_manifestUrls", ["http://new.com/manifest"]);
505
+ });
506
+
507
+ it("uses the new URL once for the next full refresh even when refreshNow is false", async () => {
508
+ const manifest = new MockManifest();
509
+ mockLoadManifest.mockResolvedValue(makeLoadResponse());
510
+ mockParseManifest.mockReturnValue({ manifest: new MockManifest() });
511
+
512
+ const fetcher = new ManifestFetcher(
513
+ ["http://example.com/manifest"],
514
+ makePipelines(),
515
+ makeSettings({ initialManifest: manifest }),
516
+ );
517
+ fetcher.start();
518
+ await flushPromises();
519
+
520
+ fetcher.updateContentUrls(["http://new.com/manifest"], false);
521
+ fetcher.scheduleManualRefresh({
522
+ enablePartialRefresh: false,
523
+ delay: 0,
524
+ canUseUnsafeMode: false,
525
+ });
526
+ await sleep(0);
527
+ await flushPromises();
528
+
529
+ expect(mockLoadManifest).toHaveBeenCalledTimes(1);
530
+ expect(mockLoadManifest).toHaveBeenLastCalledWith(
531
+ "http://new.com/manifest",
532
+ expect.anything(),
533
+ expect.anything(),
534
+ );
535
+ });
491
536
  });
492
537
 
493
538
  describe("automatic refresh scheduling", () => {
@@ -694,7 +739,9 @@ describe.skip("ManifestFetcher", () => {
694
739
  });
695
740
 
696
741
  const refreshManifest = new MockManifest();
697
- mockLoadManifest.mockResolvedValue(makeLoadResponse());
742
+ mockLoadManifest.mockResolvedValue(
743
+ makeLoadResponse({ sendingTime: undefined, receivedTime: undefined }),
744
+ );
698
745
  mockParseManifest.mockReturnValue({ manifest: refreshManifest });
699
746
 
700
747
  const fetcher = new ManifestFetcher(
@@ -715,7 +762,9 @@ describe.skip("ManifestFetcher", () => {
715
762
  await flushPromises();
716
763
 
717
764
  // Fallback full refresh fires after FAILED_PARTIAL_UPDATE_MANIFEST_REFRESH_DELAY
718
- vi.advanceTimersByTime(DEFAULT_CONFIG.FAILED_PARTIAL_UPDATE_MANIFEST_REFRESH_DELAY);
765
+ vi.advanceTimersByTime(
766
+ DEFAULT_CONFIG.FAILED_PARTIAL_UPDATE_MANIFEST_REFRESH_DELAY + 1,
767
+ );
719
768
  await vi.waitFor(noop, { timeout: 0 }); // real delay
720
769
  await flushPromises();
721
770
 
@@ -177,6 +177,12 @@ export default class ManifestFetcher extends EventEmitter<IManifestFetcherEvent>
177
177
  * will be triggered immediately.
178
178
  */
179
179
  public updateContentUrls(urls: string[] | undefined, refreshNow: boolean): void {
180
+ // Keep track of the application-provided content URLs.
181
+ this._manifestUrls = urls;
182
+ // Indicate that we want to prioritize for the next refresh.
183
+ // Once a refreshed Manifest has been parsed we still let the Manifest's own
184
+ // URL list drive subsequent refreshes (e.g. if the transport/parser updated
185
+ // it on purpose).
180
186
  this._prioritizedContentUrl = urls?.[0] ?? undefined;
181
187
  if (refreshNow) {
182
188
  this.scheduleManualRefresh({
@@ -608,10 +614,16 @@ export default class ManifestFetcher extends EventEmitter<IManifestFetcherEvent>
608
614
  let fullRefresh: boolean;
609
615
  let refreshURL: string | undefined;
610
616
  if (this._prioritizedContentUrl !== null) {
617
+ // `updateContentUrls` explicitly requested that the next refresh uses that
618
+ // URL. This override is one-shot on purpose.
611
619
  fullRefresh = true;
612
620
  refreshURL = this._prioritizedContentUrl;
613
621
  this._prioritizedContentUrl = null;
614
622
  } else {
623
+ // Outside of that explicit one-shot override, prefer the URLs currently
624
+ // exposed by the Manifest itself. This allows parser/content-level URL
625
+ // updates (e.g. redirects or manifest-provided alternatives) to stay the
626
+ // source of truth after the refresh completed.
615
627
  fullRefresh = !enablePartialRefresh || manifestUpdateUrl === undefined;
616
628
  refreshURL = fullRefresh ? manifest.getUrls()[0] : manifestUpdateUrl;
617
629
  }
@@ -277,6 +277,76 @@ describe("createThumbnailFetcher", () => {
277
277
  // p1 should reject
278
278
  await expect(p1).rejects.toBeInstanceOf(CancellationError);
279
279
  });
280
+
281
+ it("does not pass the first caller's cancellation signal to the shared loader", async () => {
282
+ let loadSignal: CancellationSignal | undefined;
283
+ let resolveRequest!: (v: IRequestedData<ArrayBuffer>) => void;
284
+ const pipeline = makePipeline(
285
+ (
286
+ _cdnMetadata,
287
+ _segment,
288
+ _options,
289
+ cancellationSignal: CancellationSignal,
290
+ ): Promise<IRequestedData<ArrayBuffer>> => {
291
+ loadSignal = cancellationSignal;
292
+ return new Promise((res) => {
293
+ resolveRequest = res;
294
+ });
295
+ },
296
+ );
297
+
298
+ mockScheduleRequestWithCdns.mockImplementation(
299
+ (
300
+ cdns: ICdnMetadata[] | null,
301
+ _cdnPrioritizer: CdnPrioritizer | null,
302
+ performRequest: (
303
+ cdn: ICdnMetadata | null,
304
+ cancellationSignal: CancellationSignal,
305
+ ) => Promise<IRequestedData<ArrayBuffer>>,
306
+ _options: IBackoffSettings,
307
+ globalCancellationSignal: CancellationSignal,
308
+ ): Promise<IRequestedData<ArrayBuffer>> => {
309
+ return performRequest(cdns?.[0] ?? null, globalCancellationSignal);
310
+ },
311
+ );
312
+
313
+ const fetcher = createThumbnailFetcher(pipeline, null);
314
+ const canceller1 = new TaskCanceller("test1");
315
+ const canceller2 = new TaskCanceller("test2");
316
+ const ctx = makeThumbnailContext();
317
+
318
+ const p1 = fetcher(ctx, canceller1.signal);
319
+ const p2 = fetcher(ctx, canceller2.signal);
320
+
321
+ expect(mockScheduleRequestWithCdns).toHaveBeenCalledTimes(1);
322
+ expect(loadSignal).not.toBe(canceller1.signal);
323
+ expect(loadSignal).not.toBe(canceller2.signal);
324
+
325
+ canceller1.cancel("only first cancels");
326
+ expect(loadSignal?.isCancelled()).toBe(false);
327
+
328
+ resolveRequest({
329
+ responseData: new Uint8Array([4, 7, 7]).buffer,
330
+ size: 3,
331
+ requestDuration: 500,
332
+ });
333
+
334
+ await expect(p1).rejects.toBeInstanceOf(CancellationError);
335
+ await expect(p2).resolves.toEqual({
336
+ data: new Uint8Array([4, 7, 7]).buffer,
337
+ mimeType: "image/png",
338
+ thumbnails: [
339
+ {
340
+ height: 720,
341
+ width: 1280,
342
+ offsetX: 0,
343
+ offsetY: 0,
344
+ start: 0,
345
+ end: Number.MAX_SAFE_INTEGER,
346
+ },
347
+ ],
348
+ });
349
+ });
280
350
  });
281
351
 
282
352
  describe("error handling", () => {