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
@@ -190,7 +190,6 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
190
190
  manifest: null,
191
191
  mediaSourceInfo: null,
192
192
  rebufferingController: null,
193
- streamEventsEmitter: null,
194
193
  initialTime: undefined,
195
194
  autoPlay: undefined,
196
195
  initialPlayPerformed: null,
@@ -353,21 +352,30 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
353
352
  }
354
353
 
355
354
  let textDisplayer: ITextDisplayer | null = null;
356
- if (
357
- this._settings.textTrackOptions.textTrackMode === "html" &&
358
- features.htmlTextDisplayer !== null
359
- ) {
360
- assert(this._hasTextBufferFeature());
361
- textDisplayer = new features.htmlTextDisplayer(
362
- mediaElement,
363
- this._settings.textTrackOptions.textTrackElement,
355
+ try {
356
+ if (
357
+ this._settings.textTrackOptions.textTrackMode === "html" &&
358
+ features.htmlTextDisplayer !== null
359
+ ) {
360
+ assert(this._hasTextBufferFeature());
361
+ textDisplayer = new features.htmlTextDisplayer(
362
+ mediaElement,
363
+ this._settings.textTrackOptions.textTrackElement,
364
+ );
365
+ } else if (features.nativeTextDisplayer !== null) {
366
+ assert(this._hasTextBufferFeature());
367
+ textDisplayer = new features.nativeTextDisplayer(mediaElement);
368
+ } else {
369
+ assert(!this._hasTextBufferFeature());
370
+ }
371
+ } catch (err) {
372
+ log.error(
373
+ "Init",
374
+ "failed to initialize text displayer",
375
+ err instanceof Error ? err : "Unknown Error",
364
376
  );
365
- } else if (features.nativeTextDisplayer !== null) {
366
- assert(this._hasTextBufferFeature());
367
- textDisplayer = new features.nativeTextDisplayer(mediaElement);
368
- } else {
369
- assert(!this._hasTextBufferFeature());
370
377
  }
378
+
371
379
  this._initCanceller.signal.register((err) => {
372
380
  textDisplayer?.stop(err.reason);
373
381
  });
@@ -402,12 +410,32 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
402
410
  contentInfo.contentDecryptor = contentDecryptor;
403
411
  }
404
412
 
413
+ const streamEventsEmitter = new StreamEventsEmitter(playbackObserver);
414
+ streamEventsEmitter.addEventListener(
415
+ "event",
416
+ (payload) => {
417
+ this.trigger("streamEvent", payload);
418
+ },
419
+ this._initCanceller.signal,
420
+ );
421
+ streamEventsEmitter.addEventListener(
422
+ "eventSkip",
423
+ (payload) => {
424
+ this.trigger("streamEventSkip", payload);
425
+ },
426
+ this._initCanceller.signal,
427
+ );
428
+ this._initCanceller.signal.register((err) => {
429
+ streamEventsEmitter.stop(err.reason);
430
+ });
431
+
405
432
  const playbackStartParams = {
406
433
  mediaElement,
407
434
  textDisplayer,
408
435
  playbackObserver,
409
436
  drmInitializationStatus,
410
437
  mediaSourceStatus,
438
+ streamEventsEmitter,
411
439
  };
412
440
  mediaSourceStatus.onUpdate(
413
441
  (msInitStatus, stopListeningMSStatus) => {
@@ -510,6 +538,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
510
538
  textDisplayer,
511
539
  playbackObserver,
512
540
  mediaSourceStatus,
541
+ streamEventsEmitter,
513
542
  position,
514
543
  !isPaused,
515
544
  );
@@ -921,6 +950,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
921
950
  return;
922
951
  }
923
952
  const manifest = msgData.value.manifest;
953
+ streamEventsEmitter.start(manifest);
924
954
  this._currentContentInfo.manifest = manifest;
925
955
  this._updateCodecSupport(manifest, mediaElement);
926
956
  this._startPlaybackIfReady(playbackStartParams);
@@ -948,8 +978,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
948
978
  msgData.value.updates,
949
979
  );
950
980
  }
951
- this._currentContentInfo?.streamEventsEmitter?.onManifestUpdate(manifest);
952
-
981
+ streamEventsEmitter.onManifestUpdate(manifest);
953
982
  this._updateCodecSupport(manifest, mediaElement);
954
983
  this.trigger("manifestUpdate", msgData.value.updates);
955
984
  break;
@@ -1558,6 +1587,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
1558
1587
  textDisplayer: ITextDisplayer | null,
1559
1588
  playbackObserver: IMediaElementPlaybackObserver,
1560
1589
  mediaSourceStatus: SharedReference<MediaSourceInitializationStatus>,
1590
+ streamEventsEmitter: StreamEventsEmitter,
1561
1591
  position: number,
1562
1592
  autoPlay: boolean,
1563
1593
  ) {
@@ -1565,6 +1595,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
1565
1595
  this._currentMediaSourceCanceller = new TaskCanceller("Init MediaSource");
1566
1596
  this._currentMediaSourceCanceller.linkToSignal(this._initCanceller.signal);
1567
1597
  mediaSourceStatus.setValue(MediaSourceInitializationStatus.AttachNow);
1598
+ streamEventsEmitter.pause();
1568
1599
  this.trigger("reloadingMediaSource", { position, autoPlay });
1569
1600
 
1570
1601
  mediaSourceStatus.onUpdate(
@@ -1580,6 +1611,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
1580
1611
  mediaElement,
1581
1612
  textDisplayer,
1582
1613
  playbackObserver,
1614
+ streamEventsEmitter,
1583
1615
  },
1584
1616
  this._currentMediaSourceCanceller.signal,
1585
1617
  );
@@ -1635,6 +1667,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
1635
1667
  mediaElement: IMediaElement;
1636
1668
  textDisplayer: ITextDisplayer | null;
1637
1669
  playbackObserver: IMediaElementPlaybackObserver;
1670
+ streamEventsEmitter: StreamEventsEmitter;
1638
1671
  },
1639
1672
  cancelSignal: CancellationSignal,
1640
1673
  ): IReadOnlyPlaybackObserver<ICorePlaybackObservation> | null {
@@ -1652,10 +1685,17 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
1652
1685
 
1653
1686
  const { manifest, mediaSourceInfo } = this._currentContentInfo;
1654
1687
  const { speed } = this._settings;
1655
- const { initialTime, autoPlay, mediaElement, textDisplayer, playbackObserver } =
1656
- parameters;
1688
+ const {
1689
+ initialTime,
1690
+ autoPlay,
1691
+ mediaElement,
1692
+ textDisplayer,
1693
+ playbackObserver,
1694
+ streamEventsEmitter,
1695
+ } = parameters;
1657
1696
  this._currentContentInfo.initialTime = initialTime;
1658
1697
  this._currentContentInfo.autoPlay = autoPlay;
1698
+ streamEventsEmitter.pause(); // Only start polling events once ready to play
1659
1699
 
1660
1700
  const { autoPlayResult, initialPlayPerformed } = performInitialSeekAndPlay(
1661
1701
  {
@@ -1669,6 +1709,15 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
1669
1709
  cancelSignal,
1670
1710
  );
1671
1711
  this._currentContentInfo.initialPlayPerformed = initialPlayPerformed;
1712
+ initialPlayPerformed.onUpdate(
1713
+ (isPerformed, stopListening) => {
1714
+ if (isPerformed) {
1715
+ stopListening();
1716
+ streamEventsEmitter.resume(); // We can now start polling events
1717
+ }
1718
+ },
1719
+ { clearSignal: cancelSignal, emitCurrentValue: true },
1720
+ );
1672
1721
  const corePlaybackObserver = createCorePlaybackObserver(
1673
1722
  playbackObserver,
1674
1723
  {
@@ -1711,36 +1760,6 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
1711
1760
  rebufferingController.start();
1712
1761
  this._currentContentInfo.rebufferingController = rebufferingController;
1713
1762
 
1714
- const currentContentInfo = this._currentContentInfo;
1715
- initialPlayPerformed.onUpdate(
1716
- (isPerformed, stopListening) => {
1717
- if (isPerformed) {
1718
- stopListening();
1719
- const streamEventsEmitter = new StreamEventsEmitter(manifest, playbackObserver);
1720
- currentContentInfo.streamEventsEmitter = streamEventsEmitter;
1721
- streamEventsEmitter.addEventListener(
1722
- "event",
1723
- (payload) => {
1724
- this.trigger("streamEvent", payload);
1725
- },
1726
- cancelSignal,
1727
- );
1728
- streamEventsEmitter.addEventListener(
1729
- "eventSkip",
1730
- (payload) => {
1731
- this.trigger("streamEventSkip", payload);
1732
- },
1733
- cancelSignal,
1734
- );
1735
- streamEventsEmitter.start();
1736
- cancelSignal.register((err) => {
1737
- streamEventsEmitter.stop(err.reason);
1738
- });
1739
- }
1740
- },
1741
- { clearSignal: cancelSignal, emitCurrentValue: true },
1742
- );
1743
-
1744
1763
  const _getSegmentSinkMetrics = async (): Promise<ISegmentSinkMetrics | undefined> => {
1745
1764
  this._awaitingRequests.nextRequestId++;
1746
1765
  const requestId = this._awaitingRequests.nextRequestId;
@@ -1855,6 +1874,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
1855
1874
  playbackObserver: IMediaElementPlaybackObserver;
1856
1875
  drmInitializationStatus: IReadOnlySharedReference<IDrmInitializationStatus>;
1857
1876
  mediaSourceStatus: IReadOnlySharedReference<MediaSourceInitializationStatus>;
1877
+ streamEventsEmitter: StreamEventsEmitter;
1858
1878
  }): boolean {
1859
1879
  if (this._currentContentInfo === null || this._currentContentInfo.manifest === null) {
1860
1880
  return false;
@@ -1884,6 +1904,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
1884
1904
  mediaElement: parameters.mediaElement,
1885
1905
  textDisplayer: parameters.textDisplayer,
1886
1906
  playbackObserver: parameters.playbackObserver,
1907
+ streamEventsEmitter: parameters.streamEventsEmitter,
1887
1908
  },
1888
1909
  this._currentMediaSourceCanceller.signal,
1889
1910
  );
@@ -2055,13 +2076,6 @@ export interface IMediaSourceContentInitializerContentInfos {
2055
2076
  * `null` if none is currently created for the content.
2056
2077
  */
2057
2078
  rebufferingController: RebufferingController | null;
2058
- /**
2059
- * Current `StreamEventsEmitter` linked to the content, allowing to
2060
- * send events found in the Manifest.
2061
- *
2062
- * `null` if none is currently created for the content.
2063
- */
2064
- streamEventsEmitter: StreamEventsEmitter | null;
2065
2079
  /**
2066
2080
  * The initial position to seek to in seconds once the content is loadeed.
2067
2081
  * `undefined` if unknown yet.
@@ -0,0 +1,175 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import type { IManifestMetadata } from "../../../../manifest";
3
+ import type {
4
+ IPlaybackObservation,
5
+ IReadOnlyPlaybackObserver,
6
+ } from "../../../../playback_observer";
7
+ import { SeekingState } from "../../../../playback_observer";
8
+ import SharedReference from "../../../../utils/reference";
9
+ import type { CancellationSignal } from "../../../../utils/task_canceller";
10
+ import TaskCanceller from "../../../../utils/task_canceller";
11
+ import StreamEventsEmitter from "../stream_events_emitter/stream_events_emitter";
12
+
13
+ describe("init - StreamEventsEmitter", () => {
14
+ afterEach(() => {
15
+ vi.useRealTimers();
16
+ });
17
+
18
+ it("should not send skipped events for positions crossed while the media is not ready", () => {
19
+ vi.useFakeTimers();
20
+
21
+ const observationRef = new SharedReference(generateObservation(4));
22
+ const playbackObserver = createPlaybackObserver(observationRef);
23
+ const streamEventsEmitter = new StreamEventsEmitter(playbackObserver);
24
+ const skippedEvents: unknown[] = [];
25
+ const regularEvents: unknown[] = [];
26
+ const stopCanceller = new TaskCanceller("test");
27
+
28
+ streamEventsEmitter.addEventListener(
29
+ "event",
30
+ (evt) => {
31
+ regularEvents.push(evt);
32
+ },
33
+ stopCanceller.signal,
34
+ );
35
+ streamEventsEmitter.addEventListener(
36
+ "eventSkip",
37
+ (evt) => {
38
+ skippedEvents.push(evt);
39
+ },
40
+ stopCanceller.signal,
41
+ );
42
+
43
+ streamEventsEmitter.start(generateManifest());
44
+ streamEventsEmitter.pause();
45
+
46
+ observationRef.setValue(generateObservation(9));
47
+ vi.advanceTimersByTime(200);
48
+
49
+ streamEventsEmitter.resume();
50
+ observationRef.setValue(generateObservation(9.1));
51
+ vi.advanceTimersByTime(200);
52
+
53
+ expect(regularEvents).toHaveLength(0);
54
+ expect(skippedEvents).toHaveLength(0);
55
+
56
+ streamEventsEmitter.stop("test end");
57
+ stopCanceller.cancel("test end");
58
+ });
59
+
60
+ it("should compare from the current position immediately after resuming", () => {
61
+ vi.useFakeTimers();
62
+
63
+ const observationRef = new SharedReference(generateObservation(4));
64
+ const playbackObserver = createPlaybackObserver(observationRef);
65
+ const streamEventsEmitter = new StreamEventsEmitter(playbackObserver);
66
+ const regularEvents: unknown[] = [];
67
+ const stopCanceller = new TaskCanceller("test");
68
+
69
+ streamEventsEmitter.addEventListener(
70
+ "event",
71
+ (evt) => {
72
+ regularEvents.push(evt);
73
+ },
74
+ stopCanceller.signal,
75
+ );
76
+
77
+ streamEventsEmitter.start(generateManifest());
78
+ streamEventsEmitter.pause();
79
+
80
+ observationRef.setValue(generateObservation(4.5));
81
+ streamEventsEmitter.resume();
82
+ observationRef.setValue(generateObservation(8.5));
83
+ vi.advanceTimersByTime(200);
84
+
85
+ expect(regularEvents).toHaveLength(1);
86
+
87
+ streamEventsEmitter.stop("test end");
88
+ stopCanceller.cancel("test end");
89
+ });
90
+ });
91
+
92
+ function createPlaybackObserver(
93
+ observationRef: SharedReference<IPlaybackObservation>,
94
+ ): IReadOnlyPlaybackObserver<IPlaybackObservation> {
95
+ return {
96
+ getCurrentTime() {
97
+ return observationRef.getValue().position.getPolled();
98
+ },
99
+ getPlaybackRate() {
100
+ return 1;
101
+ },
102
+ getReadyState() {
103
+ return 4;
104
+ },
105
+ getIsPaused() {
106
+ return false;
107
+ },
108
+ getReference() {
109
+ return observationRef;
110
+ },
111
+ listen(
112
+ cb: (observation: IPlaybackObservation, stopListening: () => void) => void,
113
+ options: {
114
+ includeLastObservation?: boolean | undefined;
115
+ clearSignal: CancellationSignal;
116
+ },
117
+ ) {
118
+ observationRef.onUpdate(cb, {
119
+ clearSignal: options.clearSignal,
120
+ emitCurrentValue: options.includeLastObservation,
121
+ });
122
+ },
123
+ deriveReadOnlyObserver() {
124
+ throw new Error("unused in this test");
125
+ },
126
+ } as unknown as IReadOnlyPlaybackObserver<IPlaybackObservation>;
127
+ }
128
+
129
+ function generateManifest(): IManifestMetadata {
130
+ return {
131
+ periods: [
132
+ {
133
+ start: 0,
134
+ streamEvents: [
135
+ {
136
+ start: 5,
137
+ end: 8,
138
+ id: "evt",
139
+ data: {
140
+ type: "dash-event-stream",
141
+ value: {
142
+ schemeIdUri: "urn:test",
143
+ timescale: 1,
144
+ xmlData: { data: "<Event />", namespaces: [] },
145
+ },
146
+ },
147
+ },
148
+ ],
149
+ },
150
+ ],
151
+ } as unknown as IManifestMetadata;
152
+ }
153
+
154
+ function generateObservation(currentTime: number): IPlaybackObservation {
155
+ return {
156
+ event: "timeupdate",
157
+ seeking: SeekingState.None,
158
+ rebuffering: null,
159
+ freezing: null,
160
+ duration: 100,
161
+ ended: false,
162
+ paused: false,
163
+ playbackRate: 1,
164
+ readyState: 4,
165
+ bufferGap: 10,
166
+ fullyLoaded: false,
167
+ currentRange: { start: 0, end: 20 },
168
+ buffered: {} as TimeRanges,
169
+ position: {
170
+ getPolled() {
171
+ return currentTime;
172
+ },
173
+ } as IPlaybackObservation["position"],
174
+ };
175
+ }
@@ -16,11 +16,11 @@
16
16
 
17
17
  import config from "../../../../config";
18
18
  import type { IManifestMetadata } from "../../../../manifest";
19
- import { SeekingState } from "../../../../playback_observer";
20
19
  import type {
21
20
  IPlaybackObservation,
22
21
  IReadOnlyPlaybackObserver,
23
22
  } from "../../../../playback_observer";
23
+ import { SeekingState } from "../../../../playback_observer";
24
24
  import EventEmitter from "../../../../utils/event_emitter";
25
25
  import SharedReference from "../../../../utils/reference";
26
26
  import type { CancellationSignal } from "../../../../utils/task_canceller";
@@ -42,27 +42,34 @@ interface IStreamEventsEmitterEvent {
42
42
  * Get events from manifest and emit each time an event has to be emitted
43
43
  */
44
44
  export default class StreamEventsEmitter extends EventEmitter<IStreamEventsEmitterEvent> {
45
- private _manifest: IManifestMetadata;
45
+ /** Regularly emit playback metrics such as the position. */
46
46
  private _playbackObserver: IReadOnlyPlaybackObserver<IPlaybackObservation>;
47
+ /** Current stream events tracked for the whole content. */
47
48
  private _scheduledEventsRef: SharedReference<
48
49
  Array<IStreamEventPayload | INonFiniteStreamEventPayload>
49
50
  >;
51
+ /** Events currently considered active, to only emit their enter/exit once. */
50
52
  private _eventsBeingPlayed: WeakMap<
51
53
  IStreamEventPayload | INonFiniteStreamEventPayload,
52
54
  true
53
55
  >;
56
+ /** Whether event evaluation is temporarily suspended, e.g. during a reload. */
57
+ private _isPaused: boolean;
58
+ /** Last observation used as comparison point for the next event check. */
59
+ private _previousObservation: {
60
+ /** Media position that is being played. */
61
+ currentTime: number;
62
+ /** If `true`, we're currently seeking in the media. */
63
+ isSeeking: boolean;
64
+ } | null;
65
+ /** Cancels the current emitter lifecycle. */
54
66
  private _canceller: TaskCanceller | null;
55
67
 
56
68
  /**
57
- * @param {Object} manifest
58
69
  * @param {Object} playbackObserver
59
70
  */
60
- constructor(
61
- manifest: IManifestMetadata,
62
- playbackObserver: IReadOnlyPlaybackObserver<IPlaybackObservation>,
63
- ) {
71
+ constructor(playbackObserver: IReadOnlyPlaybackObserver<IPlaybackObservation>) {
64
72
  super();
65
- this._manifest = manifest;
66
73
  this._playbackObserver = playbackObserver;
67
74
  this._canceller = null;
68
75
  this._scheduledEventsRef = new SharedReference<
@@ -72,22 +79,36 @@ export default class StreamEventsEmitter extends EventEmitter<IStreamEventsEmitt
72
79
  IStreamEventPayload | INonFiniteStreamEventPayload,
73
80
  true
74
81
  >();
82
+ this._isPaused = true;
83
+ this._previousObservation = null;
75
84
  }
76
85
 
77
- public start(): void {
86
+ /**
87
+ * Initialize the emitter for the given content manifest.
88
+ *
89
+ * The emitter starts in a paused state so the caller can resume it only once
90
+ * the corresponding media instance is actually ready.
91
+ * @param {Object} manifest
92
+ */
93
+ public start(manifest: IManifestMetadata): void {
78
94
  if (this._canceller !== null) {
79
95
  return;
80
96
  }
81
97
  this._canceller = new TaskCanceller("StreamEventsEmitter");
82
98
 
83
99
  const cancelSignal = this._canceller.signal;
84
- const playbackObserver = this._playbackObserver;
100
+ this._isPaused = true;
101
+ this._previousObservation = null;
102
+ this._eventsBeingPlayed = new WeakMap<
103
+ IStreamEventPayload | INonFiniteStreamEventPayload,
104
+ true
105
+ >();
85
106
 
86
107
  let isPollingEvents = false;
87
108
  let cancelCurrentPolling = new TaskCanceller("StreamEventsEmitter Polling");
88
109
  cancelCurrentPolling.linkToSignal(cancelSignal);
89
110
 
90
- this._scheduledEventsRef.setValue(refreshScheduledEventsList([], this._manifest));
111
+ this._scheduledEventsRef.setValue(refreshScheduledEventsList([], manifest));
91
112
 
92
113
  this._scheduledEventsRef.onUpdate(
93
114
  ({ length: scheduledEventsLength }) => {
@@ -103,16 +124,22 @@ export default class StreamEventsEmitter extends EventEmitter<IStreamEventsEmitt
103
124
  return;
104
125
  }
105
126
  isPollingEvents = true;
106
- let oldObservation = constructObservation();
107
127
  const checkStreamEvents = () => {
108
- const newObservation = constructObservation();
128
+ if (this._isPaused) {
129
+ return;
130
+ }
131
+ const newObservation = this._constructObservation();
132
+ if (this._previousObservation === null) {
133
+ this._previousObservation = newObservation;
134
+ return;
135
+ }
109
136
  this._emitStreamEvents(
110
137
  this._scheduledEventsRef.getValue(),
111
- oldObservation,
138
+ this._previousObservation,
112
139
  newObservation,
113
140
  cancelCurrentPolling.signal,
114
141
  );
115
- oldObservation = newObservation;
142
+ this._previousObservation = newObservation;
116
143
  };
117
144
 
118
145
  const { STREAM_EVENT_EMITTER_POLL_INTERVAL } = config.getCurrent();
@@ -120,7 +147,7 @@ export default class StreamEventsEmitter extends EventEmitter<IStreamEventsEmitt
120
147
  checkStreamEvents,
121
148
  STREAM_EVENT_EMITTER_POLL_INTERVAL,
122
149
  );
123
- playbackObserver.listen(checkStreamEvents, {
150
+ this._playbackObserver.listen(checkStreamEvents, {
124
151
  includeLastObservation: false,
125
152
  clearSignal: cancelCurrentPolling.signal,
126
153
  });
@@ -128,20 +155,34 @@ export default class StreamEventsEmitter extends EventEmitter<IStreamEventsEmitt
128
155
  cancelCurrentPolling.signal.register(() => {
129
156
  clearInterval(intervalId);
130
157
  });
131
-
132
- function constructObservation() {
133
- const lastObservation = playbackObserver.getReference().getValue();
134
- const currentTime =
135
- playbackObserver.getCurrentTime() ??
136
- playbackObserver.getReference().getValue().position.getPolled();
137
- const isSeeking = lastObservation.seeking !== SeekingState.None;
138
- return { currentTime, isSeeking };
139
- }
140
158
  },
141
159
  { emitCurrentValue: true, clearSignal: cancelSignal },
142
160
  );
143
161
  }
144
162
 
163
+ /**
164
+ * Suspend event evaluation until `resume` has been called.
165
+ *
166
+ * To use when playback metrics should be temporarily ignored, for example
167
+ * while reloading a content.
168
+ */
169
+ public pause(): void {
170
+ this._isPaused = true;
171
+ this._previousObservation = null;
172
+ }
173
+
174
+ /**
175
+ * Resume event evaluation from the current playback state.
176
+ */
177
+ public resume(): void {
178
+ this._isPaused = false;
179
+
180
+ // Take a snapshot right now to avoid comparing a post-reload position
181
+ // with a stale pre-reload observation.
182
+ this._previousObservation = this._constructObservation();
183
+ }
184
+
185
+ /** Refresh the tracked stream-event list after a manifest update. */
145
186
  public onManifestUpdate(man: IManifestMetadata) {
146
187
  const prev = this._scheduledEventsRef.getValue();
147
188
  this._scheduledEventsRef.setValue(refreshScheduledEventsList(prev, man));
@@ -156,12 +197,35 @@ export default class StreamEventsEmitter extends EventEmitter<IStreamEventsEmitt
156
197
  if (this._canceller !== null) {
157
198
  this._canceller.cancel(reason ?? "StreamEventsEmitter stop");
158
199
  this._canceller = null;
200
+ this._previousObservation = null;
201
+ this._eventsBeingPlayed = new WeakMap<
202
+ IStreamEventPayload | INonFiniteStreamEventPayload,
203
+ true
204
+ >();
159
205
  }
160
206
  }
161
207
 
208
+ /**
209
+ * Construct observation object relied on by the `StreamEventsEmitted` by
210
+ * generating it from this instance's `PlaybackObserver`.
211
+ */
212
+ private _constructObservation(): {
213
+ /** Current media position that is being played. */
214
+ currentTime: number;
215
+ /** If `true`, we're currently seeking in the media. */
216
+ isSeeking: boolean;
217
+ } {
218
+ const lastObservation = this._playbackObserver.getReference().getValue();
219
+ const currentTime =
220
+ this._playbackObserver.getCurrentTime() ??
221
+ this._playbackObserver.getReference().getValue().position.getPolled();
222
+ const isSeeking = lastObservation.seeking !== SeekingState.None;
223
+ return { currentTime, isSeeking };
224
+ }
225
+
162
226
  /**
163
227
  * Examine playback situation from playback observations to emit stream events and
164
- * prepare set onExit callbacks if needed.
228
+ * prepare `onExit` callbacks if needed.
165
229
  * @param {Array.<Object>} scheduledEvents
166
230
  * @param {Object} oldObservation
167
231
  * @param {Object} newObservation
@@ -218,6 +218,10 @@ export default async function renderThumbnail(
218
218
  );
219
219
  throw error;
220
220
  }
221
+ if (srcError instanceof ThumbnailRenderingError) {
222
+ onFinished();
223
+ throw srcError;
224
+ }
221
225
  const formattedErr = formatError(srcError, {
222
226
  defaultCode: "NONE",
223
227
  defaultReason: "Unknown error",
@@ -16,6 +16,18 @@ curtains.
16
16
 
17
17
  ## `TracksStore`
18
18
 
19
+ `TracksStore` is the main implementation used for Manifest-based contents. It centralizes
20
+ the currently chosen tracks and qualities and exposes simple operations for the API to
21
+ switch them.
22
+
19
23
  ## `TrackDispatcher`
20
24
 
25
+ `TrackDispatcher` links track decisions to the underlying streaming logic for a given
26
+ type. It is responsible for propagating updated choices and constraints when the selected
27
+ track or representations change.
28
+
21
29
  ## `MediaElementTracksStore`
30
+
31
+ `MediaElementTracksStore` is the directfile-oriented counterpart. It relies on the media
32
+ element's native track APIs (`audioTracks`, `videoTracks`, `textTracks`) instead of the
33
+ Manifest-based track model used by `TracksStore`.