@livepeer-frameworks/player-core 0.1.0 → 0.1.2

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 (294) hide show
  1. package/README.md +11 -9
  2. package/dist/cjs/core/ABRController.js +456 -0
  3. package/dist/cjs/core/ABRController.js.map +1 -0
  4. package/dist/cjs/core/CodecUtils.js +195 -0
  5. package/dist/cjs/core/CodecUtils.js.map +1 -0
  6. package/dist/cjs/core/ErrorClassifier.js +410 -0
  7. package/dist/cjs/core/ErrorClassifier.js.map +1 -0
  8. package/dist/cjs/core/EventEmitter.js +108 -0
  9. package/dist/cjs/core/EventEmitter.js.map +1 -0
  10. package/dist/cjs/core/GatewayClient.js +342 -0
  11. package/dist/cjs/core/GatewayClient.js.map +1 -0
  12. package/dist/cjs/core/InteractionController.js +606 -0
  13. package/dist/cjs/core/InteractionController.js.map +1 -0
  14. package/dist/cjs/core/LiveDurationProxy.js +186 -0
  15. package/dist/cjs/core/LiveDurationProxy.js.map +1 -0
  16. package/dist/cjs/core/MetaTrackManager.js +624 -0
  17. package/dist/cjs/core/MetaTrackManager.js.map +1 -0
  18. package/dist/cjs/core/MistReporter.js +449 -0
  19. package/dist/cjs/core/MistReporter.js.map +1 -0
  20. package/dist/cjs/core/MistSignaling.js +264 -0
  21. package/dist/cjs/core/MistSignaling.js.map +1 -0
  22. package/dist/cjs/core/PlayerController.js +2658 -0
  23. package/dist/cjs/core/PlayerController.js.map +1 -0
  24. package/dist/cjs/core/PlayerInterface.js +269 -0
  25. package/dist/cjs/core/PlayerInterface.js.map +1 -0
  26. package/dist/cjs/core/PlayerManager.js +806 -0
  27. package/dist/cjs/core/PlayerManager.js.map +1 -0
  28. package/dist/cjs/core/PlayerRegistry.js +270 -0
  29. package/dist/cjs/core/PlayerRegistry.js.map +1 -0
  30. package/dist/cjs/core/QualityMonitor.js +474 -0
  31. package/dist/cjs/core/QualityMonitor.js.map +1 -0
  32. package/dist/cjs/core/SeekingUtils.js +292 -0
  33. package/dist/cjs/core/SeekingUtils.js.map +1 -0
  34. package/dist/cjs/core/StreamStateClient.js +381 -0
  35. package/dist/cjs/core/StreamStateClient.js.map +1 -0
  36. package/dist/cjs/core/SubtitleManager.js +227 -0
  37. package/dist/cjs/core/SubtitleManager.js.map +1 -0
  38. package/dist/cjs/core/TelemetryReporter.js +258 -0
  39. package/dist/cjs/core/TelemetryReporter.js.map +1 -0
  40. package/dist/cjs/core/TimeFormat.js +176 -0
  41. package/dist/cjs/core/TimeFormat.js.map +1 -0
  42. package/dist/cjs/core/TimerManager.js +176 -0
  43. package/dist/cjs/core/TimerManager.js.map +1 -0
  44. package/dist/cjs/core/UrlUtils.js +160 -0
  45. package/dist/cjs/core/UrlUtils.js.map +1 -0
  46. package/dist/cjs/core/detector.js +293 -0
  47. package/dist/cjs/core/detector.js.map +1 -0
  48. package/dist/cjs/core/scorer.js +443 -0
  49. package/dist/cjs/core/scorer.js.map +1 -0
  50. package/dist/cjs/index.js +121 -20134
  51. package/dist/cjs/index.js.map +1 -1
  52. package/dist/cjs/lib/utils.js +11 -0
  53. package/dist/cjs/lib/utils.js.map +1 -0
  54. package/dist/cjs/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js +6 -0
  55. package/dist/cjs/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js.map +1 -0
  56. package/dist/cjs/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js +3042 -0
  57. package/dist/cjs/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
  58. package/dist/cjs/players/DashJsPlayer.js +638 -0
  59. package/dist/cjs/players/DashJsPlayer.js.map +1 -0
  60. package/dist/cjs/players/HlsJsPlayer.js +482 -0
  61. package/dist/cjs/players/HlsJsPlayer.js.map +1 -0
  62. package/dist/cjs/players/MewsWsPlayer/SourceBufferManager.js +522 -0
  63. package/dist/cjs/players/MewsWsPlayer/SourceBufferManager.js.map +1 -0
  64. package/dist/cjs/players/MewsWsPlayer/WebSocketManager.js +215 -0
  65. package/dist/cjs/players/MewsWsPlayer/WebSocketManager.js.map +1 -0
  66. package/dist/cjs/players/MewsWsPlayer/index.js +987 -0
  67. package/dist/cjs/players/MewsWsPlayer/index.js.map +1 -0
  68. package/dist/cjs/players/MistPlayer.js +185 -0
  69. package/dist/cjs/players/MistPlayer.js.map +1 -0
  70. package/dist/cjs/players/MistWebRTCPlayer/index.js +635 -0
  71. package/dist/cjs/players/MistWebRTCPlayer/index.js.map +1 -0
  72. package/dist/cjs/players/NativePlayer.js +762 -0
  73. package/dist/cjs/players/NativePlayer.js.map +1 -0
  74. package/dist/cjs/players/VideoJsPlayer.js +585 -0
  75. package/dist/cjs/players/VideoJsPlayer.js.map +1 -0
  76. package/dist/cjs/players/WebCodecsPlayer/JitterBuffer.js +236 -0
  77. package/dist/cjs/players/WebCodecsPlayer/JitterBuffer.js.map +1 -0
  78. package/dist/cjs/players/WebCodecsPlayer/LatencyProfiles.js +143 -0
  79. package/dist/cjs/players/WebCodecsPlayer/LatencyProfiles.js.map +1 -0
  80. package/dist/cjs/players/WebCodecsPlayer/RawChunkParser.js +96 -0
  81. package/dist/cjs/players/WebCodecsPlayer/RawChunkParser.js.map +1 -0
  82. package/dist/cjs/players/WebCodecsPlayer/SyncController.js +359 -0
  83. package/dist/cjs/players/WebCodecsPlayer/SyncController.js.map +1 -0
  84. package/dist/cjs/players/WebCodecsPlayer/WebSocketController.js +460 -0
  85. package/dist/cjs/players/WebCodecsPlayer/WebSocketController.js.map +1 -0
  86. package/dist/cjs/players/WebCodecsPlayer/index.js +1467 -0
  87. package/dist/cjs/players/WebCodecsPlayer/index.js.map +1 -0
  88. package/dist/cjs/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js +320 -0
  89. package/dist/cjs/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js.map +1 -0
  90. package/dist/cjs/styles/index.js +57 -0
  91. package/dist/cjs/styles/index.js.map +1 -0
  92. package/dist/cjs/vanilla/FrameWorksPlayer.js +269 -0
  93. package/dist/cjs/vanilla/FrameWorksPlayer.js.map +1 -0
  94. package/dist/cjs/vanilla.js +11 -0
  95. package/dist/cjs/vanilla.js.map +1 -0
  96. package/dist/esm/core/ABRController.js +454 -0
  97. package/dist/esm/core/ABRController.js.map +1 -0
  98. package/dist/esm/core/CodecUtils.js +193 -0
  99. package/dist/esm/core/CodecUtils.js.map +1 -0
  100. package/dist/esm/core/ErrorClassifier.js +408 -0
  101. package/dist/esm/core/ErrorClassifier.js.map +1 -0
  102. package/dist/esm/core/EventEmitter.js +106 -0
  103. package/dist/esm/core/EventEmitter.js.map +1 -0
  104. package/dist/esm/core/GatewayClient.js +340 -0
  105. package/dist/esm/core/GatewayClient.js.map +1 -0
  106. package/dist/esm/core/InteractionController.js +604 -0
  107. package/dist/esm/core/InteractionController.js.map +1 -0
  108. package/dist/esm/core/LiveDurationProxy.js +184 -0
  109. package/dist/esm/core/LiveDurationProxy.js.map +1 -0
  110. package/dist/esm/core/MetaTrackManager.js +622 -0
  111. package/dist/esm/core/MetaTrackManager.js.map +1 -0
  112. package/dist/esm/core/MistReporter.js +447 -0
  113. package/dist/esm/core/MistReporter.js.map +1 -0
  114. package/dist/esm/core/MistSignaling.js +262 -0
  115. package/dist/esm/core/MistSignaling.js.map +1 -0
  116. package/dist/esm/core/PlayerController.js +2651 -0
  117. package/dist/esm/core/PlayerController.js.map +1 -0
  118. package/dist/esm/core/PlayerInterface.js +267 -0
  119. package/dist/esm/core/PlayerInterface.js.map +1 -0
  120. package/dist/esm/core/PlayerManager.js +804 -0
  121. package/dist/esm/core/PlayerManager.js.map +1 -0
  122. package/dist/esm/core/PlayerRegistry.js +264 -0
  123. package/dist/esm/core/PlayerRegistry.js.map +1 -0
  124. package/dist/esm/core/QualityMonitor.js +471 -0
  125. package/dist/esm/core/QualityMonitor.js.map +1 -0
  126. package/dist/esm/core/SeekingUtils.js +280 -0
  127. package/dist/esm/core/SeekingUtils.js.map +1 -0
  128. package/dist/esm/core/StreamStateClient.js +379 -0
  129. package/dist/esm/core/StreamStateClient.js.map +1 -0
  130. package/dist/esm/core/SubtitleManager.js +225 -0
  131. package/dist/esm/core/SubtitleManager.js.map +1 -0
  132. package/dist/esm/core/TelemetryReporter.js +256 -0
  133. package/dist/esm/core/TelemetryReporter.js.map +1 -0
  134. package/dist/esm/core/TimeFormat.js +169 -0
  135. package/dist/esm/core/TimeFormat.js.map +1 -0
  136. package/dist/esm/core/TimerManager.js +174 -0
  137. package/dist/esm/core/TimerManager.js.map +1 -0
  138. package/dist/esm/core/UrlUtils.js +151 -0
  139. package/dist/esm/core/UrlUtils.js.map +1 -0
  140. package/dist/esm/core/detector.js +279 -0
  141. package/dist/esm/core/detector.js.map +1 -0
  142. package/dist/esm/core/scorer.js +422 -0
  143. package/dist/esm/core/scorer.js.map +1 -0
  144. package/dist/esm/index.js +26 -20043
  145. package/dist/esm/index.js.map +1 -1
  146. package/dist/esm/lib/utils.js +9 -0
  147. package/dist/esm/lib/utils.js.map +1 -0
  148. package/dist/esm/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js +4 -0
  149. package/dist/esm/node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.js.map +1 -0
  150. package/dist/esm/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js +3036 -0
  151. package/dist/esm/node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
  152. package/dist/esm/players/DashJsPlayer.js +636 -0
  153. package/dist/esm/players/DashJsPlayer.js.map +1 -0
  154. package/dist/esm/players/HlsJsPlayer.js +480 -0
  155. package/dist/esm/players/HlsJsPlayer.js.map +1 -0
  156. package/dist/esm/players/MewsWsPlayer/SourceBufferManager.js +520 -0
  157. package/dist/esm/players/MewsWsPlayer/SourceBufferManager.js.map +1 -0
  158. package/dist/esm/players/MewsWsPlayer/WebSocketManager.js +213 -0
  159. package/dist/esm/players/MewsWsPlayer/WebSocketManager.js.map +1 -0
  160. package/dist/esm/players/MewsWsPlayer/index.js +985 -0
  161. package/dist/esm/players/MewsWsPlayer/index.js.map +1 -0
  162. package/dist/esm/players/MistPlayer.js +183 -0
  163. package/dist/esm/players/MistPlayer.js.map +1 -0
  164. package/dist/esm/players/MistWebRTCPlayer/index.js +633 -0
  165. package/dist/esm/players/MistWebRTCPlayer/index.js.map +1 -0
  166. package/dist/esm/players/NativePlayer.js +759 -0
  167. package/dist/esm/players/NativePlayer.js.map +1 -0
  168. package/dist/esm/players/VideoJsPlayer.js +583 -0
  169. package/dist/esm/players/VideoJsPlayer.js.map +1 -0
  170. package/dist/esm/players/WebCodecsPlayer/JitterBuffer.js +233 -0
  171. package/dist/esm/players/WebCodecsPlayer/JitterBuffer.js.map +1 -0
  172. package/dist/esm/players/WebCodecsPlayer/LatencyProfiles.js +134 -0
  173. package/dist/esm/players/WebCodecsPlayer/LatencyProfiles.js.map +1 -0
  174. package/dist/esm/players/WebCodecsPlayer/RawChunkParser.js +91 -0
  175. package/dist/esm/players/WebCodecsPlayer/RawChunkParser.js.map +1 -0
  176. package/dist/esm/players/WebCodecsPlayer/SyncController.js +357 -0
  177. package/dist/esm/players/WebCodecsPlayer/SyncController.js.map +1 -0
  178. package/dist/esm/players/WebCodecsPlayer/WebSocketController.js +458 -0
  179. package/dist/esm/players/WebCodecsPlayer/WebSocketController.js.map +1 -0
  180. package/dist/esm/players/WebCodecsPlayer/index.js +1458 -0
  181. package/dist/esm/players/WebCodecsPlayer/index.js.map +1 -0
  182. package/dist/esm/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js +315 -0
  183. package/dist/esm/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.js.map +1 -0
  184. package/dist/esm/styles/index.js +54 -0
  185. package/dist/esm/styles/index.js.map +1 -0
  186. package/dist/esm/vanilla/FrameWorksPlayer.js +264 -0
  187. package/dist/esm/vanilla/FrameWorksPlayer.js.map +1 -0
  188. package/dist/esm/vanilla.js +2 -0
  189. package/dist/esm/vanilla.js.map +1 -0
  190. package/dist/player.css +185 -42
  191. package/dist/types/core/ABRController.d.ts +4 -4
  192. package/dist/types/core/CodecUtils.d.ts +1 -1
  193. package/dist/types/core/ErrorClassifier.d.ts +77 -0
  194. package/dist/types/core/GatewayClient.d.ts +4 -4
  195. package/dist/types/core/MetaTrackManager.d.ts +2 -2
  196. package/dist/types/core/MistReporter.d.ts +3 -3
  197. package/dist/types/core/MistSignaling.d.ts +12 -12
  198. package/dist/types/core/PlayerController.d.ts +19 -14
  199. package/dist/types/core/PlayerInterface.d.ts +100 -2
  200. package/dist/types/core/PlayerManager.d.ts +36 -9
  201. package/dist/types/core/PlayerRegistry.d.ts +11 -11
  202. package/dist/types/core/QualityMonitor.d.ts +2 -2
  203. package/dist/types/core/SeekingUtils.d.ts +2 -2
  204. package/dist/types/core/StreamStateClient.d.ts +2 -2
  205. package/dist/types/core/TelemetryReporter.d.ts +1 -1
  206. package/dist/types/core/TimerManager.d.ts +1 -1
  207. package/dist/types/core/detector.d.ts +1 -1
  208. package/dist/types/core/index.d.ts +44 -44
  209. package/dist/types/core/scorer.d.ts +1 -1
  210. package/dist/types/core/selector.d.ts +2 -2
  211. package/dist/types/index.d.ts +35 -34
  212. package/dist/types/players/DashJsPlayer.d.ts +3 -3
  213. package/dist/types/players/HlsJsPlayer.d.ts +3 -3
  214. package/dist/types/players/MewsWsPlayer/SourceBufferManager.d.ts +1 -1
  215. package/dist/types/players/MewsWsPlayer/WebSocketManager.d.ts +1 -1
  216. package/dist/types/players/MewsWsPlayer/index.d.ts +2 -2
  217. package/dist/types/players/MewsWsPlayer/types.d.ts +15 -15
  218. package/dist/types/players/MistPlayer.d.ts +2 -2
  219. package/dist/types/players/MistWebRTCPlayer/index.d.ts +3 -3
  220. package/dist/types/players/NativePlayer.d.ts +3 -3
  221. package/dist/types/players/VideoJsPlayer.d.ts +3 -3
  222. package/dist/types/players/WebCodecsPlayer/JitterBuffer.d.ts +3 -3
  223. package/dist/types/players/WebCodecsPlayer/LatencyProfiles.d.ts +1 -1
  224. package/dist/types/players/WebCodecsPlayer/RawChunkParser.d.ts +2 -2
  225. package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +2 -2
  226. package/dist/types/players/WebCodecsPlayer/WebSocketController.d.ts +3 -3
  227. package/dist/types/players/WebCodecsPlayer/index.d.ts +9 -9
  228. package/dist/types/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.d.ts +1 -1
  229. package/dist/types/players/WebCodecsPlayer/types.d.ts +49 -49
  230. package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +31 -31
  231. package/dist/types/players/index.d.ts +5 -8
  232. package/dist/types/types.d.ts +15 -15
  233. package/dist/types/vanilla/FrameWorksPlayer.d.ts +2 -2
  234. package/dist/types/vanilla/index.d.ts +4 -4
  235. package/dist/workers/decoder.worker.js +129 -122
  236. package/dist/workers/decoder.worker.js.map +1 -1
  237. package/package.json +31 -15
  238. package/src/core/ABRController.ts +38 -36
  239. package/src/core/CodecUtils.ts +49 -46
  240. package/src/core/Disposable.ts +4 -4
  241. package/src/core/ErrorClassifier.ts +499 -0
  242. package/src/core/EventEmitter.ts +1 -1
  243. package/src/core/GatewayClient.ts +41 -39
  244. package/src/core/InteractionController.ts +89 -82
  245. package/src/core/LiveDurationProxy.ts +14 -15
  246. package/src/core/MetaTrackManager.ts +73 -65
  247. package/src/core/MistReporter.ts +72 -45
  248. package/src/core/MistSignaling.ts +59 -56
  249. package/src/core/PlayerController.ts +542 -384
  250. package/src/core/PlayerInterface.ts +192 -59
  251. package/src/core/PlayerManager.ts +354 -164
  252. package/src/core/PlayerRegistry.ts +238 -87
  253. package/src/core/QualityMonitor.ts +38 -31
  254. package/src/core/ScreenWakeLockManager.ts +8 -9
  255. package/src/core/SeekingUtils.ts +31 -22
  256. package/src/core/StreamStateClient.ts +74 -68
  257. package/src/core/SubtitleManager.ts +24 -22
  258. package/src/core/TelemetryReporter.ts +38 -32
  259. package/src/core/TimeFormat.ts +13 -17
  260. package/src/core/TimerManager.ts +24 -8
  261. package/src/core/UrlUtils.ts +20 -17
  262. package/src/core/detector.ts +44 -44
  263. package/src/core/index.ts +57 -48
  264. package/src/core/scorer.ts +136 -141
  265. package/src/core/selector.ts +2 -6
  266. package/src/global.d.ts +1 -1
  267. package/src/index.ts +56 -36
  268. package/src/players/DashJsPlayer.ts +164 -115
  269. package/src/players/HlsJsPlayer.ts +132 -78
  270. package/src/players/MewsWsPlayer/SourceBufferManager.ts +41 -36
  271. package/src/players/MewsWsPlayer/WebSocketManager.ts +9 -9
  272. package/src/players/MewsWsPlayer/index.ts +192 -152
  273. package/src/players/MewsWsPlayer/types.ts +21 -21
  274. package/src/players/MistPlayer.ts +45 -26
  275. package/src/players/MistWebRTCPlayer/index.ts +175 -129
  276. package/src/players/NativePlayer.ts +203 -143
  277. package/src/players/VideoJsPlayer.ts +170 -118
  278. package/src/players/WebCodecsPlayer/JitterBuffer.ts +6 -7
  279. package/src/players/WebCodecsPlayer/LatencyProfiles.ts +43 -43
  280. package/src/players/WebCodecsPlayer/RawChunkParser.ts +10 -10
  281. package/src/players/WebCodecsPlayer/SyncController.ts +45 -53
  282. package/src/players/WebCodecsPlayer/WebSocketController.ts +66 -68
  283. package/src/players/WebCodecsPlayer/index.ts +265 -223
  284. package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +12 -17
  285. package/src/players/WebCodecsPlayer/types.ts +56 -56
  286. package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +238 -182
  287. package/src/players/WebCodecsPlayer/worker/types.ts +31 -31
  288. package/src/players/index.ts +5 -16
  289. package/src/styles/animations.css +2 -1
  290. package/src/styles/player.css +185 -42
  291. package/src/styles/tailwind.css +473 -159
  292. package/src/types.ts +43 -43
  293. package/src/vanilla/FrameWorksPlayer.ts +26 -14
  294. package/src/vanilla/index.ts +4 -4
@@ -0,0 +1,1467 @@
1
+ 'use strict';
2
+
3
+ var PlayerInterface = require('../../core/PlayerInterface.js');
4
+ var WebSocketController = require('./WebSocketController.js');
5
+ var SyncController = require('./SyncController.js');
6
+ var RawChunkParser = require('./RawChunkParser.js');
7
+ var LatencyProfiles = require('./LatencyProfiles.js');
8
+ var MediaStreamTrackGenerator$1 = require('./polyfills/MediaStreamTrackGenerator.js');
9
+ var JitterBuffer = require('./JitterBuffer.js');
10
+
11
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
12
+ /**
13
+ * WebCodecs Player Implementation
14
+ *
15
+ * Low-latency WebSocket streaming using WebCodecs API for video/audio decoding.
16
+ * Decoding runs in a Web Worker for optimal performance.
17
+ *
18
+ * Features:
19
+ * - Ultra-low latency streaming (configurable via profiles)
20
+ * - Worker-based VideoDecoder/AudioDecoder
21
+ * - Adaptive playback speed for live catchup/slowdown
22
+ * - Jitter compensation
23
+ * - Firefox polyfill for MediaStreamTrackGenerator
24
+ *
25
+ * Protocol: MistServer raw WebSocket frames (12-byte header + data)
26
+ */
27
+ /**
28
+ * Detect if running on Safari (which has VideoTrackGenerator in worker but not MediaStreamTrackGenerator on main thread)
29
+ */
30
+ function isSafari() {
31
+ if (typeof navigator === "undefined")
32
+ return false;
33
+ const ua = navigator.userAgent;
34
+ return /^((?!chrome|android).)*safari/i.test(ua);
35
+ }
36
+ // Import inline worker (bundled via rollup-plugin-web-worker-loader)
37
+ /**
38
+ * Convert string (ASCII with escaped chars) to Uint8Array
39
+ * Reference: rawws.js:76-84 - init data is raw ASCII from stream info JSON
40
+ */
41
+ function str2bin(str) {
42
+ const out = new Uint8Array(str.length);
43
+ for (let i = 0; i < str.length; i++) {
44
+ out[i] = str.charCodeAt(i);
45
+ }
46
+ return out;
47
+ }
48
+ /**
49
+ * Create a TimeRanges-like object from an array of [start, end] pairs
50
+ */
51
+ function createTimeRanges(ranges) {
52
+ return {
53
+ length: ranges.length,
54
+ start(index) {
55
+ if (index < 0 || index >= ranges.length)
56
+ throw new DOMException("Index out of bounds");
57
+ return ranges[index][0];
58
+ },
59
+ end(index) {
60
+ if (index < 0 || index >= ranges.length)
61
+ throw new DOMException("Index out of bounds");
62
+ return ranges[index][1];
63
+ },
64
+ };
65
+ }
66
+ /**
67
+ * WebCodecsPlayerImpl - WebCodecs-based low-latency player
68
+ */
69
+ class WebCodecsPlayerImpl extends PlayerInterface.BasePlayer {
70
+ constructor() {
71
+ super(...arguments);
72
+ this.capability = {
73
+ name: "WebCodecs Player",
74
+ shortname: "webcodecs",
75
+ priority: 0, // Highest priority - lowest latency option
76
+ // Raw WebSocket (12-byte header + codec frames) - NOT MP4-muxed
77
+ // MistServer's output_wsraw.cpp provides full codec negotiation (audio + video)
78
+ // MistServer's output_h264.cpp uses same 12-byte header but Annex B payload (video-only)
79
+ // NOTE: ws/video/mp4 is MP4-fragmented which needs MEWS player (uses MSE)
80
+ mimes: [
81
+ "ws/video/raw",
82
+ "wss/video/raw", // Raw codec frames - AVCC format (audio + video)
83
+ "ws/video/h264",
84
+ "wss/video/h264", // Annex B H264/HEVC (video-only, same 12-byte header)
85
+ ],
86
+ };
87
+ this.wsController = null;
88
+ this.syncController = null;
89
+ this.worker = null;
90
+ this.mediaStream = null;
91
+ this.container = null;
92
+ this.pipelines = new Map();
93
+ this.tracks = [];
94
+ this.tracksByIndex = new Map(); // Track metadata indexed by track idx
95
+ this.queuedInitData = new Map(); // Queued INIT data waiting for track info
96
+ this.queuedChunks = new Map(); // Queued chunks waiting for decoder config
97
+ this.isDestroyed = false;
98
+ this.debugging = false;
99
+ this.verboseDebugging = false;
100
+ this.streamType = "live";
101
+ /** Payload format: 'avcc' for ws/video/raw, 'annexb' for ws/video/h264 */
102
+ this.payloadFormat = "avcc";
103
+ this.workerUidCounter = 0;
104
+ this.workerListeners = new Map();
105
+ // Playback state
106
+ this._duration = Infinity;
107
+ this._currentTime = 0;
108
+ this._bufferMs = 0;
109
+ this._avDrift = 0;
110
+ this._frameCallbackId = null;
111
+ this._statsInterval = null;
112
+ this._framesDropped = 0;
113
+ this._framesDecoded = 0;
114
+ this._bytesReceived = 0;
115
+ this._messagesReceived = 0;
116
+ this._isPaused = true;
117
+ this._suppressPlayPauseSync = false;
118
+ this._pendingStepPause = false;
119
+ this._stepPauseTimeout = null;
120
+ }
121
+ /**
122
+ * Get cache key for a track's codec configuration
123
+ */
124
+ static getCodecCacheKey(track) {
125
+ const codecStr = track.codecstring ?? track.codec?.toLowerCase() ?? "";
126
+ // Simple hash of init data for cache key (just first/last bytes + length)
127
+ const init = track.init ?? "";
128
+ const initHash = init.length > 0
129
+ ? `${init.length}_${init.charCodeAt(0)}_${init.charCodeAt(init.length - 1)}`
130
+ : "";
131
+ return `${codecStr}|${initHash}`;
132
+ }
133
+ /**
134
+ * Test if a track's codec is supported by WebCodecs
135
+ * Reference: rawws.js:75-137 - isTrackSupported()
136
+ */
137
+ static async isTrackSupported(track) {
138
+ const cacheKey = WebCodecsPlayerImpl.getCodecCacheKey(track);
139
+ // Check cache first
140
+ if (WebCodecsPlayerImpl.codecCache.has(cacheKey)) {
141
+ const cached = WebCodecsPlayerImpl.codecCache.get(cacheKey);
142
+ return { supported: cached, config: { codec: track.codecstring ?? track.codec } };
143
+ }
144
+ // Build codec config
145
+ const codecStr = track.codecstring ?? (track.codec ?? "").toLowerCase();
146
+ const config = { codec: codecStr };
147
+ // Add description (init data) if present
148
+ if (track.init && track.init !== "") {
149
+ config.description = str2bin(track.init);
150
+ }
151
+ let result;
152
+ try {
153
+ switch (track.type) {
154
+ case "video": {
155
+ // Special handling for JPEG - uses ImageDecoder
156
+ if (track.codec === "JPEG") {
157
+ if (!("ImageDecoder" in window)) {
158
+ result = { supported: false, config: { codec: "image/jpeg" } };
159
+ }
160
+ else {
161
+ // @ts-ignore - ImageDecoder may not have types
162
+ const isSupported = await window.ImageDecoder.isTypeSupported("image/jpeg");
163
+ result = { supported: isSupported, config: { codec: "image/jpeg" } };
164
+ }
165
+ }
166
+ else {
167
+ // Use VideoDecoder.isConfigSupported()
168
+ const videoResult = await VideoDecoder.isConfigSupported(config);
169
+ result = { supported: videoResult.supported === true, config: videoResult.config };
170
+ }
171
+ break;
172
+ }
173
+ case "audio": {
174
+ // Audio requires numberOfChannels and sampleRate
175
+ config.numberOfChannels = track.channels ?? 2;
176
+ config.sampleRate = track.rate ?? 48000;
177
+ const audioResult = await AudioDecoder.isConfigSupported(config);
178
+ result = { supported: audioResult.supported === true, config: audioResult.config };
179
+ break;
180
+ }
181
+ default:
182
+ result = { supported: false, config };
183
+ }
184
+ }
185
+ catch (err) {
186
+ console.warn(`[WebCodecs] isConfigSupported failed for ${track.codec}:`, err);
187
+ result = { supported: false, config };
188
+ }
189
+ // Cache the result
190
+ WebCodecsPlayerImpl.codecCache.set(cacheKey, result.supported);
191
+ return result;
192
+ }
193
+ /**
194
+ * Validate all tracks and return which are supported
195
+ * Returns array of supported track types ('video', 'audio')
196
+ */
197
+ static async validateTracks(tracks) {
198
+ const supportedTypes = new Set();
199
+ const validationPromises = tracks
200
+ .filter((t) => t.type === "video" || t.type === "audio")
201
+ .map(async (track) => {
202
+ const result = await WebCodecsPlayerImpl.isTrackSupported(track);
203
+ if (result.supported) {
204
+ supportedTypes.add(track.type);
205
+ }
206
+ return { track, supported: result.supported };
207
+ });
208
+ const results = await Promise.all(validationPromises);
209
+ // Log validation results for debugging
210
+ for (const { track, supported } of results) {
211
+ console.debug(`[WebCodecs] Track ${track.idx} (${track.type} ${track.codec}): ${supported ? "supported" : "UNSUPPORTED"}`);
212
+ }
213
+ return Array.from(supportedTypes);
214
+ }
215
+ isMimeSupported(mimetype) {
216
+ return this.capability.mimes.includes(mimetype);
217
+ }
218
+ isBrowserSupported(mimetype, source, streamInfo) {
219
+ // Basic requirements
220
+ if (!("WebSocket" in window)) {
221
+ return false;
222
+ }
223
+ if (!("Worker" in window)) {
224
+ return false;
225
+ }
226
+ if (!("VideoDecoder" in window) || !("AudioDecoder" in window)) {
227
+ // WebCodecs not available (requires HTTPS)
228
+ return false;
229
+ }
230
+ // Check for HTTP/HTTPS mismatch
231
+ const sourceUrl = new URL(source.url.replace(/^ws/, "http"), location.href);
232
+ if (location.protocol === "https:" && sourceUrl.protocol === "http:") {
233
+ return false;
234
+ }
235
+ // Check track codec support using cache when available
236
+ // Reference: rawws.js tests codecs via isConfigSupported() before selection
237
+ const playableTracks = {};
238
+ for (const track of streamInfo.meta.tracks) {
239
+ if (track.type === "video" || track.type === "audio") {
240
+ // Check cache for this track's codec
241
+ const cacheKey = WebCodecsPlayerImpl.getCodecCacheKey(track);
242
+ if (WebCodecsPlayerImpl.codecCache.has(cacheKey)) {
243
+ // Use cached result
244
+ if (WebCodecsPlayerImpl.codecCache.get(cacheKey)) {
245
+ playableTracks[track.type] = true;
246
+ }
247
+ }
248
+ else {
249
+ // Not in cache - assume supported for now, validate in initialize()
250
+ // This is necessary because isBrowserSupported is synchronous
251
+ playableTracks[track.type] = true;
252
+ }
253
+ }
254
+ else if (track.type === "meta" && track.codec === "subtitle") {
255
+ // Subtitles supported via text track
256
+ playableTracks["subtitle"] = true;
257
+ }
258
+ }
259
+ // Annex B H264 WebSocket is video-only (no audio payloads)
260
+ if (mimetype.includes("video/h264")) {
261
+ delete playableTracks.audio;
262
+ }
263
+ if (Object.keys(playableTracks).length === 0) {
264
+ return false;
265
+ }
266
+ return Object.keys(playableTracks);
267
+ }
268
+ async initialize(container, source, options, streamInfo) {
269
+ // Clear any leftover state from previous initialization FIRST
270
+ // This fixes race condition where async destroy() clears state after new initialize()
271
+ this.tracksByIndex.clear();
272
+ this.pipelines.clear();
273
+ this.tracks = [];
274
+ this.queuedInitData.clear();
275
+ this.queuedChunks.clear();
276
+ this.isDestroyed = false;
277
+ this._duration = Infinity;
278
+ this._currentTime = 0;
279
+ this._bufferMs = 0;
280
+ this._avDrift = 0;
281
+ this._framesDropped = 0;
282
+ this._framesDecoded = 0;
283
+ this._bytesReceived = 0;
284
+ this._messagesReceived = 0;
285
+ // Detect payload format from source MIME type
286
+ // ws/video/h264 uses Annex B (start code delimited NALs), ws/video/raw uses AVCC (length-prefixed)
287
+ this.payloadFormat = source.type?.includes("h264") ? "annexb" : "avcc";
288
+ if (this.payloadFormat === "annexb") {
289
+ this.log("Using Annex B payload format (ws/video/h264)");
290
+ }
291
+ this.container = container;
292
+ container.classList.add("fw-player-container");
293
+ // Pre-populate track metadata from streamInfo (fetched via HTTP before WebSocket)
294
+ // This is how the reference player (rawws.js) gets track info - from MistVideo.info.meta.tracks
295
+ if (streamInfo?.meta?.tracks) {
296
+ this.log(`Pre-populating ${streamInfo.meta.tracks.length} tracks from streamInfo`);
297
+ for (const track of streamInfo.meta.tracks) {
298
+ if (track.idx !== undefined) {
299
+ // Convert StreamTrack to TrackInfo (WebCodecs format)
300
+ const trackInfo = {
301
+ idx: track.idx,
302
+ type: track.type,
303
+ codec: track.codec,
304
+ codecstring: track.codecstring,
305
+ init: track.init,
306
+ width: track.width,
307
+ height: track.height,
308
+ fpks: track.fpks,
309
+ channels: track.channels,
310
+ rate: track.rate,
311
+ size: track.size,
312
+ };
313
+ this.tracksByIndex.set(track.idx, trackInfo);
314
+ this.log(`Pre-registered track ${track.idx}: ${track.type} ${track.codec}`);
315
+ }
316
+ }
317
+ }
318
+ // Parse WebCodecs-specific options
319
+ const wcOptions = options;
320
+ this.debugging = wcOptions.debug ?? wcOptions.devMode ?? false;
321
+ this.verboseDebugging = wcOptions.verboseDebug ?? false;
322
+ // Determine stream type
323
+ this.streamType = source.type === "live" ? "live" : "vod";
324
+ // Select latency profile
325
+ const profileName = wcOptions.latencyProfile ?? LatencyProfiles.selectDefaultProfile(this.streamType === "live");
326
+ const profile = LatencyProfiles.mergeLatencyProfile(profileName, wcOptions.customLatencyProfile);
327
+ this.log(`Initializing WebCodecs player with ${profile.name} profile`);
328
+ // Create video element
329
+ const video = document.createElement("video");
330
+ video.classList.add("fw-player-video");
331
+ video.setAttribute("playsinline", "");
332
+ video.setAttribute("crossorigin", "anonymous");
333
+ if (options.autoplay)
334
+ video.autoplay = true;
335
+ if (options.muted)
336
+ video.muted = true;
337
+ video.controls = options.controls === true;
338
+ if (options.loop && this.streamType !== "live")
339
+ video.loop = true;
340
+ if (options.poster)
341
+ video.poster = options.poster;
342
+ this.videoElement = video;
343
+ container.appendChild(video);
344
+ // Keep paused state in sync with actual element state
345
+ this._onVideoPlay = () => {
346
+ if (this._suppressPlayPauseSync)
347
+ return;
348
+ this._isPaused = false;
349
+ this.sendToWorker({
350
+ type: "frametiming",
351
+ action: "setPaused",
352
+ paused: false,
353
+ uid: this.workerUidCounter++,
354
+ }).catch(() => { });
355
+ };
356
+ this._onVideoPause = () => {
357
+ if (this._suppressPlayPauseSync)
358
+ return;
359
+ this._isPaused = true;
360
+ this.sendToWorker({
361
+ type: "frametiming",
362
+ action: "setPaused",
363
+ paused: true,
364
+ uid: this.workerUidCounter++,
365
+ }).catch(() => { });
366
+ };
367
+ video.addEventListener("play", this._onVideoPlay);
368
+ video.addEventListener("pause", this._onVideoPause);
369
+ // Create MediaStream for output
370
+ this.mediaStream = new MediaStream();
371
+ video.srcObject = this.mediaStream;
372
+ // Initialize worker
373
+ await this.initializeWorker();
374
+ // Initialize sync controller
375
+ this.syncController = new SyncController.SyncController({
376
+ profile,
377
+ isLive: this.streamType === "live",
378
+ onSpeedChange: (main, tweak) => {
379
+ this.sendToWorker({
380
+ type: "frametiming",
381
+ action: "setSpeed",
382
+ speed: main,
383
+ tweak,
384
+ uid: this.workerUidCounter++,
385
+ });
386
+ if (this.videoElement) {
387
+ this.videoElement.playbackRate = main * tweak;
388
+ }
389
+ },
390
+ onFastForwardRequest: (ms) => {
391
+ this.wsController?.fastForward(ms);
392
+ },
393
+ });
394
+ // Initialize WebSocket - URL should already be .raw from source selection
395
+ this.wsController = new WebSocketController.WebSocketController(source.url, {
396
+ debug: this.debugging,
397
+ });
398
+ this.setupWebSocketHandlers();
399
+ // Validate track codecs using isConfigSupported() BEFORE connecting
400
+ // Reference: rawws.js:75-137 tests each track's codec support
401
+ // This fixes "codec unsupported" errors by only sending verified codecs
402
+ const supportedAudioCodecs = new Set();
403
+ const supportedVideoCodecs = new Set();
404
+ if (streamInfo?.meta?.tracks) {
405
+ this.log("Validating track codecs with isConfigSupported()...");
406
+ for (const track of streamInfo.meta.tracks) {
407
+ if (track.type === "video" || track.type === "audio") {
408
+ const trackInfo = {
409
+ idx: track.idx ?? 0,
410
+ type: track.type,
411
+ codec: track.codec,
412
+ codecstring: track.codecstring,
413
+ init: track.init,
414
+ width: track.width,
415
+ height: track.height,
416
+ channels: track.channels,
417
+ rate: track.rate,
418
+ };
419
+ const result = await WebCodecsPlayerImpl.isTrackSupported(trackInfo);
420
+ if (result.supported) {
421
+ if (track.type === "audio") {
422
+ supportedAudioCodecs.add(track.codec);
423
+ }
424
+ else {
425
+ supportedVideoCodecs.add(track.codec);
426
+ }
427
+ this.log(`Track ${track.idx} (${track.type} ${track.codec}): SUPPORTED`);
428
+ }
429
+ else {
430
+ this.log(`Track ${track.idx} (${track.type} ${track.codec}): NOT SUPPORTED`, "warn");
431
+ }
432
+ }
433
+ }
434
+ }
435
+ // If no codecs validated, check if we have any tracks at all
436
+ if (supportedAudioCodecs.size === 0 && supportedVideoCodecs.size === 0) {
437
+ // Fallback: Use default codec list if no tracks provided or all failed
438
+ // This handles streams where track info isn't available until WebSocket connects
439
+ this.log("No validated codecs, using default codec list");
440
+ ["AAC", "MP3", "opus", "FLAC", "AC3"].forEach((c) => supportedAudioCodecs.add(c));
441
+ ["H264", "HEVC", "VP8", "VP9", "AV1", "JPEG"].forEach((c) => supportedVideoCodecs.add(c));
442
+ }
443
+ // Connect and request codec data
444
+ // Per MistServer rawws.js line 1544, we need to tell the server what codecs we support
445
+ // Format: [[ [audio codecs], [video codecs] ]] - audio FIRST per Object.values({audio:[], video:[]}) order
446
+ const supportedCombinations = [
447
+ [
448
+ Array.from(supportedAudioCodecs), // Audio codecs (position 0)
449
+ Array.from(supportedVideoCodecs), // Video codecs (position 1)
450
+ ],
451
+ ];
452
+ this.log(`Requesting codecs: audio=[${supportedCombinations[0][0].join(", ")}], video=[${supportedCombinations[0][1].join(", ")}]`);
453
+ try {
454
+ await this.wsController.connect();
455
+ this.wsController.requestCodecData(supportedCombinations);
456
+ }
457
+ catch (err) {
458
+ this.log(`Failed to connect: ${err}`, "error");
459
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
460
+ throw err;
461
+ }
462
+ // Proactively create pipelines for pre-populated tracks
463
+ // This ensures pipelines exist when first chunks arrive, they just need init data
464
+ for (const [idx, track] of this.tracksByIndex) {
465
+ if (track.type === "video" || track.type === "audio") {
466
+ this.log(`Creating pipeline proactively for track ${idx} (${track.type} ${track.codec})`);
467
+ await this.createPipeline(track);
468
+ }
469
+ }
470
+ // Set up video event listeners
471
+ this.setupVideoEventListeners(video, options);
472
+ // Set up requestVideoFrameCallback for accurate frame timing
473
+ this.setupFrameCallback();
474
+ this.isDestroyed = false;
475
+ return video;
476
+ }
477
+ async destroy() {
478
+ if (this.isDestroyed)
479
+ return;
480
+ this.isDestroyed = true;
481
+ this.log("Destroying WebCodecs player");
482
+ // Cancel frame callback
483
+ this.cancelFrameCallback();
484
+ // Stop stats interval
485
+ if (this._statsInterval) {
486
+ clearInterval(this._statsInterval);
487
+ this._statsInterval = null;
488
+ }
489
+ // Stop WebSocket
490
+ this.wsController?.disconnect();
491
+ this.wsController = null;
492
+ // Close all pipelines
493
+ for (const pipeline of this.pipelines.values()) {
494
+ await this.closePipeline(pipeline.idx, false);
495
+ }
496
+ this.pipelines.clear();
497
+ // Terminate worker
498
+ this.worker?.terminate();
499
+ this.worker = null;
500
+ this.workerListeners.clear();
501
+ // Clean up MediaStream
502
+ if (this.mediaStream) {
503
+ for (const track of this.mediaStream.getTracks()) {
504
+ track.stop();
505
+ this.mediaStream.removeTrack(track);
506
+ }
507
+ this.mediaStream = null;
508
+ }
509
+ // Clean up video element
510
+ if (this.videoElement) {
511
+ if (this._onVideoPlay) {
512
+ this.videoElement.removeEventListener("play", this._onVideoPlay);
513
+ this._onVideoPlay = undefined;
514
+ }
515
+ if (this._onVideoPause) {
516
+ this.videoElement.removeEventListener("pause", this._onVideoPause);
517
+ this._onVideoPause = undefined;
518
+ }
519
+ if (this._stepPauseTimeout) {
520
+ clearTimeout(this._stepPauseTimeout);
521
+ this._stepPauseTimeout = null;
522
+ }
523
+ this._pendingStepPause = false;
524
+ this.videoElement.srcObject = null;
525
+ this.videoElement.remove();
526
+ this.videoElement = null;
527
+ }
528
+ this.syncController = null;
529
+ // NOTE: Don't clear tracks/tracksByIndex/queues here!
530
+ // Since PlayerManager reuses instances, a concurrent initialize() may have
531
+ // already pre-populated these. Clearing happens at the START of initialize().
532
+ }
533
+ // ============================================================================
534
+ // Worker Management
535
+ // ============================================================================
536
+ /**
537
+ * Try to load a worker from a URL with proper async error detection.
538
+ * new Worker() doesn't throw on invalid URLs - it fires error events async.
539
+ */
540
+ tryLoadWorker(url) {
541
+ return new Promise((resolve, reject) => {
542
+ let worker;
543
+ try {
544
+ worker = new Worker(url, { type: "module" });
545
+ }
546
+ catch (e) {
547
+ reject(e);
548
+ return;
549
+ }
550
+ const cleanup = () => {
551
+ clearTimeout(timeout);
552
+ worker.removeEventListener("error", onError);
553
+ worker.removeEventListener("message", onMessage);
554
+ };
555
+ const onError = (e) => {
556
+ cleanup();
557
+ worker.terminate();
558
+ reject(new Error(e.message || "Worker failed to load"));
559
+ };
560
+ const onMessage = () => {
561
+ cleanup();
562
+ resolve(worker);
563
+ };
564
+ // Timeout: if no error after 500ms, assume loaded (worker may not send immediate message)
565
+ const timeout = setTimeout(() => {
566
+ cleanup();
567
+ resolve(worker);
568
+ }, 500);
569
+ worker.addEventListener("error", onError);
570
+ worker.addEventListener("message", onMessage);
571
+ });
572
+ }
573
+ async initializeWorker() {
574
+ // Worker paths to try in order:
575
+ // 1. Dev server path (Vite plugin serves /workers/* from source)
576
+ // 2. Production npm package path (relative to built module)
577
+ const paths = ["/workers/decoder.worker.js"];
578
+ // Add production path (may fail in dev but that's ok)
579
+ try {
580
+ paths.push(new URL("../workers/decoder.worker.js", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('players/WebCodecsPlayer/index.js', document.baseURI).href))).href);
581
+ }
582
+ catch {
583
+ // import.meta.url may not work in all environments
584
+ }
585
+ let lastError = null;
586
+ for (const path of paths) {
587
+ try {
588
+ this.log(`Trying worker path: ${path}`);
589
+ this.worker = await this.tryLoadWorker(path);
590
+ this.log(`Worker loaded from: ${path}`);
591
+ break;
592
+ }
593
+ catch (e) {
594
+ lastError = e instanceof Error ? e : new Error(String(e));
595
+ this.log(`Worker path failed: ${path} - ${lastError.message}`, "warn");
596
+ }
597
+ }
598
+ if (!this.worker) {
599
+ throw new Error("Failed to initialize WebCodecs worker. " + `Last error: ${lastError?.message ?? "unknown"}`);
600
+ }
601
+ // Set up worker event handlers (replace the ones from tryLoadWorker)
602
+ this.worker.onmessage = (event) => {
603
+ this.handleWorkerMessage(event.data);
604
+ };
605
+ this.worker.onerror = (err) => {
606
+ this.log(`Worker error: ${err?.message ?? "unknown error"}`, "error");
607
+ this.emit("error", new Error(`Worker error: ${err?.message ?? "unknown"}`));
608
+ };
609
+ // Configure debugging mode in worker
610
+ this.sendToWorker({
611
+ type: "debugging",
612
+ value: this.verboseDebugging ? "verbose" : this.debugging,
613
+ uid: this.workerUidCounter++,
614
+ });
615
+ }
616
+ sendToWorker(msg, transfer) {
617
+ return new Promise((resolve, reject) => {
618
+ // Reject with proper error if destroyed or no worker
619
+ // This prevents silent failures and allows callers to handle errors appropriately
620
+ if (this.isDestroyed) {
621
+ reject(new Error("Player destroyed"));
622
+ return;
623
+ }
624
+ if (!this.worker) {
625
+ reject(new Error("Worker not initialized"));
626
+ return;
627
+ }
628
+ const uid = msg.uid;
629
+ // Register listener for response
630
+ this.workerListeners.set(uid, (response) => {
631
+ this.workerListeners.delete(uid);
632
+ if (response.type === "ack" && response.status === "error") {
633
+ reject(new Error(response.error));
634
+ }
635
+ else {
636
+ resolve(response);
637
+ }
638
+ });
639
+ if (transfer) {
640
+ this.worker.postMessage(msg, transfer);
641
+ }
642
+ else {
643
+ this.worker.postMessage(msg);
644
+ }
645
+ });
646
+ }
647
+ handleWorkerMessage(msg) {
648
+ // Check for specific listener
649
+ if (msg.uid !== undefined && this.workerListeners.has(msg.uid)) {
650
+ this.workerListeners.get(msg.uid)(msg);
651
+ }
652
+ // Handle message by type
653
+ switch (msg.type) {
654
+ case "addtrack": {
655
+ const pipeline = this.pipelines.get(msg.idx);
656
+ if (pipeline && this.mediaStream) {
657
+ // If track was created in worker (Safari), use it directly
658
+ if (msg.track) {
659
+ this.mediaStream.addTrack(msg.track);
660
+ }
661
+ else if (pipeline.generator) {
662
+ // Otherwise use generator's track
663
+ this.mediaStream.addTrack(pipeline.generator.getTrack());
664
+ }
665
+ }
666
+ break;
667
+ }
668
+ case "removetrack": {
669
+ const pipeline = this.pipelines.get(msg.idx);
670
+ if (pipeline?.generator && this.mediaStream) {
671
+ const track = pipeline.generator.getTrack();
672
+ this.mediaStream.removeTrack(track);
673
+ }
674
+ break;
675
+ }
676
+ case "setplaybackrate": {
677
+ if (this.videoElement) {
678
+ this.videoElement.playbackRate = msg.speed;
679
+ }
680
+ break;
681
+ }
682
+ case "sendevent": {
683
+ if (msg.kind === "timeupdate") {
684
+ if (this._pendingStepPause) {
685
+ this.finishStepPause();
686
+ }
687
+ if (typeof msg.time === "number" && Number.isFinite(msg.time)) {
688
+ this._currentTime = msg.time;
689
+ this.emit("timeupdate", this._currentTime);
690
+ }
691
+ else if (this.videoElement) {
692
+ this.emit("timeupdate", this.videoElement.currentTime);
693
+ }
694
+ }
695
+ else if (msg.kind === "error") {
696
+ this.emit("error", new Error(msg.message ?? "Unknown error"));
697
+ }
698
+ break;
699
+ }
700
+ case "writeframe": {
701
+ // Safari audio: worker sends frames via postMessage, we write them here
702
+ // Reference: rawws.js line 897-918
703
+ const pipeline = this.pipelines.get(msg.idx);
704
+ if (pipeline?.safariAudioWriter) {
705
+ const frame = msg.frame;
706
+ const frameUid = msg.uid;
707
+ pipeline.safariAudioWriter
708
+ .write(frame)
709
+ .then(() => {
710
+ this.worker?.postMessage({
711
+ type: "writeframe",
712
+ idx: msg.idx,
713
+ uid: frameUid,
714
+ status: "ok",
715
+ });
716
+ })
717
+ .catch((err) => {
718
+ this.worker?.postMessage({
719
+ type: "writeframe",
720
+ idx: msg.idx,
721
+ uid: frameUid,
722
+ status: "error",
723
+ error: err.message,
724
+ });
725
+ });
726
+ }
727
+ else {
728
+ this.worker?.postMessage({
729
+ type: "writeframe",
730
+ idx: msg.idx,
731
+ uid: msg.uid,
732
+ status: "error",
733
+ error: "Pipeline not active or no audio writer",
734
+ });
735
+ }
736
+ break;
737
+ }
738
+ case "log": {
739
+ if (this.debugging) {
740
+ const level = msg.level ?? "info";
741
+ const logFn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
742
+ logFn(`[WebCodecs Worker] ${msg.msg}`);
743
+ }
744
+ break;
745
+ }
746
+ case "stats": {
747
+ // Could emit stats for monitoring
748
+ break;
749
+ }
750
+ case "closed": {
751
+ this.pipelines.delete(msg.idx);
752
+ break;
753
+ }
754
+ }
755
+ }
756
+ // ============================================================================
757
+ // WebSocket Handlers
758
+ // ============================================================================
759
+ setupWebSocketHandlers() {
760
+ if (!this.wsController)
761
+ return;
762
+ this.wsController.on("codecdata", (msg) => this.handleCodecData(msg));
763
+ this.wsController.on("info", (msg) => this.handleInfo(msg));
764
+ this.wsController.on("ontime", (msg) => this.handleOnTime(msg));
765
+ this.wsController.on("tracks", (tracks) => this.handleTracksChange(tracks));
766
+ this.wsController.on("chunk", (chunk) => this.handleChunk(chunk));
767
+ this.wsController.on("stop", () => this.handleStop());
768
+ this.wsController.on("error", (err) => this.handleError(err));
769
+ this.wsController.on("statechange", (state) => {
770
+ this.log(`Connection state: ${state}`);
771
+ if (state === "error") {
772
+ this.emit("error", new Error("WebSocket connection failed"));
773
+ }
774
+ });
775
+ }
776
+ async handleCodecData(msg) {
777
+ const codecs = msg.codecs ?? [];
778
+ const trackIndices = msg.tracks ?? []; // Array of track indices (numbers), NOT TrackInfo
779
+ this.log(`Received codec data: codecs=[${codecs.join(", ") || "none"}], tracks=[${trackIndices.join(", ") || "none"}]`);
780
+ if (codecs.length === 0 || trackIndices.length === 0) {
781
+ this.log("No playable codecs/tracks selected by server", "warn");
782
+ // Still start playback - info message may populate tracks later
783
+ this.wsController?.play();
784
+ return;
785
+ }
786
+ // Store codec strings by track index for later lookup
787
+ // Per rawws.js: codecs[i] corresponds to tracks[i]
788
+ for (let i = 0; i < trackIndices.length; i++) {
789
+ const trackIdx = trackIndices[i];
790
+ const codec = codecs[i];
791
+ if (codec) {
792
+ // If we have track metadata from info message, update it with codec
793
+ const existingTrack = this.tracksByIndex.get(trackIdx);
794
+ if (existingTrack) {
795
+ existingTrack.codec = codec;
796
+ }
797
+ else {
798
+ // Create minimal track info - will be filled in by info message
799
+ this.tracksByIndex.set(trackIdx, {
800
+ idx: trackIdx,
801
+ type: codec.match(/^(H264|HEVC|VP[89]|AV1|JPEG)/i)
802
+ ? "video"
803
+ : codec.match(/^(AAC|MP3|opus|FLAC|AC3|pcm)/i)
804
+ ? "audio"
805
+ : "meta",
806
+ codec,
807
+ });
808
+ }
809
+ this.log(`Track ${trackIdx}: codec=${codec}`);
810
+ }
811
+ }
812
+ // Create pipelines for selected tracks that have metadata
813
+ for (const trackIdx of trackIndices) {
814
+ const track = this.tracksByIndex.get(trackIdx);
815
+ if (track && (track.type === "video" || track.type === "audio")) {
816
+ await this.createPipeline(track);
817
+ }
818
+ }
819
+ // Start playback
820
+ this.wsController?.play();
821
+ }
822
+ /**
823
+ * Handle stream info message containing track metadata
824
+ * This is sent by MistServer with full track information
825
+ */
826
+ async handleInfo(msg) {
827
+ this.log("Received stream info");
828
+ // Extract tracks from meta.tracks object
829
+ if (msg.meta?.tracks) {
830
+ const tracksObj = msg.meta.tracks;
831
+ this.log(`Info contains ${Object.keys(tracksObj).length} tracks`);
832
+ for (const [_name, track] of Object.entries(tracksObj)) {
833
+ // Store track by its index for lookup when chunks arrive
834
+ if (track.idx !== undefined) {
835
+ this.tracksByIndex.set(track.idx, track);
836
+ this.log(`Registered track ${track.idx}: ${track.type} ${track.codec}`);
837
+ // Process any queued init data for this track
838
+ if (this.queuedInitData.has(track.idx)) {
839
+ if (track.type === "video" || track.type === "audio") {
840
+ this.log(`Processing queued INIT data for track ${track.idx}`);
841
+ await this.createPipeline(track);
842
+ const initData = this.queuedInitData.get(track.idx);
843
+ this.configurePipeline(track.idx, initData);
844
+ this.queuedInitData.delete(track.idx);
845
+ }
846
+ }
847
+ }
848
+ }
849
+ // Also update tracks array
850
+ this.tracks = Object.values(tracksObj);
851
+ }
852
+ }
853
+ handleOnTime(msg) {
854
+ // Update sync controller with server time
855
+ this.syncController?.updateServerTime(msg.current);
856
+ // Update current time if no frame callback available
857
+ if (this._frameCallbackId === null) {
858
+ this._currentTime = msg.current;
859
+ }
860
+ // Record server delay
861
+ const delay = this.wsController?.getServerDelay() ?? 0;
862
+ if (delay > 0) {
863
+ this.syncController?.recordServerDelay(delay);
864
+ }
865
+ // Update duration from server (VOD streams have finite duration)
866
+ if (msg.total !== undefined && isFinite(msg.total) && msg.total > 0) {
867
+ this._duration = msg.total;
868
+ }
869
+ // Update buffer level
870
+ const syncState = this.syncController?.getState();
871
+ if (syncState) {
872
+ this._bufferMs = syncState.buffer.current;
873
+ }
874
+ // Create pipelines for tracks mentioned in on_time.tracks (like reference player)
875
+ if (msg.tracks && msg.tracks.length > 0) {
876
+ for (const trackIdx of msg.tracks) {
877
+ if (!this.pipelines.has(trackIdx)) {
878
+ const track = this.tracksByIndex.get(trackIdx);
879
+ if (track && (track.type === "video" || track.type === "audio")) {
880
+ this.log(`Creating pipeline from on_time for track ${track.idx} (${track.type} ${track.codec})`);
881
+ this.createPipeline(track).then(() => {
882
+ // Process any queued init data
883
+ const queuedInit = this.queuedInitData.get(track.idx);
884
+ if (queuedInit) {
885
+ this.configurePipeline(track.idx, queuedInit);
886
+ this.queuedInitData.delete(track.idx);
887
+ }
888
+ });
889
+ }
890
+ }
891
+ }
892
+ }
893
+ }
894
+ async handleTracksChange(tracks) {
895
+ this.log(`Tracks changed: ${tracks.map((t) => `${t.idx}:${t.type}`).join(", ")}`);
896
+ // Check if codecs changed
897
+ const newTrackIds = new Set(tracks.map((t) => t.idx));
898
+ const oldTrackIds = new Set(this.pipelines.keys());
899
+ // Remove old pipelines
900
+ for (const idx of oldTrackIds) {
901
+ if (!newTrackIds.has(idx)) {
902
+ await this.closePipeline(idx, true);
903
+ }
904
+ }
905
+ // Update tracksByIndex and create new pipelines
906
+ for (const track of tracks) {
907
+ this.tracksByIndex.set(track.idx, track);
908
+ if (track.type === "video" || track.type === "audio") {
909
+ if (!this.pipelines.has(track.idx)) {
910
+ await this.createPipeline(track);
911
+ }
912
+ }
913
+ }
914
+ this.tracks = tracks;
915
+ }
916
+ handleChunk(chunk) {
917
+ if (this.isDestroyed)
918
+ return;
919
+ const pipeline = this.pipelines.get(chunk.trackIndex);
920
+ // Create pipeline if missing - look up track from tracksByIndex (populated by info message)
921
+ if (!pipeline) {
922
+ const track = this.tracksByIndex.get(chunk.trackIndex);
923
+ // If track info not available, try to infer from chunk type
924
+ // MistServer track indices: video typically 1, audio typically 2, meta typically 9
925
+ if (!track) {
926
+ // INIT data for an unknown track - we need to infer the track type
927
+ // For now, create a placeholder track entry based on common MistServer patterns
928
+ if (RawChunkParser.isInitData(chunk)) {
929
+ this.log(`Received INIT for unknown track ${chunk.trackIndex}, queuing for later`);
930
+ // Queue the init data - it will be processed when track info becomes available
931
+ this.queuedInitData.set(chunk.trackIndex, chunk.data);
932
+ return;
933
+ }
934
+ // For regular chunks without track info, we can't decode without codec config
935
+ this.log(`Received chunk for unknown track ${chunk.trackIndex} without track info`, "warn");
936
+ return;
937
+ }
938
+ if (track.type === "video" || track.type === "audio") {
939
+ this.log(`Creating pipeline for discovered track ${track.idx} (${track.type} ${track.codec})`);
940
+ this.createPipeline(track).then(() => {
941
+ if (this.isDestroyed)
942
+ return; // Guard against async completion after destroy
943
+ // Process any queued init data for this track
944
+ const queuedInit = this.queuedInitData.get(track.idx);
945
+ if (queuedInit) {
946
+ this.configurePipeline(track.idx, queuedInit);
947
+ this.queuedInitData.delete(track.idx);
948
+ }
949
+ // Re-process this chunk now that pipeline exists
950
+ this.handleChunk(chunk);
951
+ });
952
+ }
953
+ return;
954
+ }
955
+ // Handle init data
956
+ if (RawChunkParser.isInitData(chunk)) {
957
+ this.configurePipeline(pipeline.idx, chunk.data);
958
+ return;
959
+ }
960
+ // Queue chunks until pipeline is configured (decoder needs init data first)
961
+ // Per rawws.js: frames are queued when decoder is "unconfigured" (line 1408-1410)
962
+ if (!pipeline.configured) {
963
+ // For AUDIO tracks: configure on FIRST frame (audio doesn't have key/delta distinction)
964
+ // Audio chunks are sent as type 0 (delta) by the server even though they're independent
965
+ // Reference: rawws.js line 768-769 forces audio type to 'key'
966
+ const isAudioTrack = pipeline.track.type === "audio";
967
+ // For VIDEO tracks: wait for KEY frame before configuring
968
+ // This handles Annex B streams where SPS/PPS is inline with keyframes
969
+ const shouldConfigure = isAudioTrack || chunk.type === "key";
970
+ if (shouldConfigure) {
971
+ this.log(`Received ${chunk.type.toUpperCase()} frame for unconfigured ${pipeline.track.type} track ${chunk.trackIndex}, configuring`);
972
+ // Queue this frame at the FRONT so it's sent before any DELTAs
973
+ if (!this.queuedChunks.has(chunk.trackIndex)) {
974
+ this.queuedChunks.set(chunk.trackIndex, []);
975
+ }
976
+ this.queuedChunks.get(chunk.trackIndex).unshift(chunk);
977
+ // Configure without description (or with description from track.init if available)
978
+ // For audio codecs like opus/mp3 that don't need init data, this works fine
979
+ // For AAC, the description should come from track.init or the server will send INIT
980
+ const initData = pipeline.track.init ? str2bin(pipeline.track.init) : new Uint8Array(0);
981
+ this.configurePipeline(chunk.trackIndex, initData).catch((err) => {
982
+ this.log(`Failed to configure track ${chunk.trackIndex}: ${err}`, "error");
983
+ });
984
+ return;
985
+ }
986
+ // Otherwise queue the chunk (video delta before first keyframe)
987
+ if (!this.queuedChunks.has(chunk.trackIndex)) {
988
+ this.queuedChunks.set(chunk.trackIndex, []);
989
+ }
990
+ this.queuedChunks.get(chunk.trackIndex).push(chunk);
991
+ if (this.verboseDebugging) {
992
+ this.log(`Queued chunk for track ${chunk.trackIndex} (waiting for decoder config)`);
993
+ }
994
+ return;
995
+ }
996
+ // Track jitter
997
+ this.syncController?.recordChunkArrival(chunk.trackIndex, chunk.timestamp);
998
+ // Send to worker for decoding
999
+ this.sendChunkToWorker(chunk);
1000
+ }
1001
+ sendChunkToWorker(chunk) {
1002
+ const msg = {
1003
+ type: "receive",
1004
+ idx: chunk.trackIndex,
1005
+ chunk: {
1006
+ type: chunk.type === "key" ? "key" : "delta",
1007
+ timestamp: RawChunkParser.getPresentationTimestamp(chunk),
1008
+ data: chunk.data,
1009
+ },
1010
+ uid: this.workerUidCounter++,
1011
+ };
1012
+ this.worker?.postMessage(msg, [chunk.data.buffer]);
1013
+ }
1014
+ handleStop() {
1015
+ this.log("Stream stopped");
1016
+ this.emit("ended", undefined);
1017
+ }
1018
+ handleError(err) {
1019
+ this.log(`WebSocket error: ${err.message}`, "error");
1020
+ this.emit("error", err);
1021
+ }
1022
+ // ============================================================================
1023
+ // Pipeline Management
1024
+ // ============================================================================
1025
+ async createPipeline(track) {
1026
+ if (this.pipelines.has(track.idx))
1027
+ return;
1028
+ this.log(`Creating pipeline for track ${track.idx} (${track.type} ${track.codec})`);
1029
+ const pipeline = {
1030
+ idx: track.idx,
1031
+ track,
1032
+ generator: null,
1033
+ configured: false,
1034
+ };
1035
+ this.pipelines.set(track.idx, pipeline);
1036
+ this.syncController?.addTrack(track.idx, track);
1037
+ // Create worker pipeline
1038
+ await this.sendToWorker({
1039
+ type: "create",
1040
+ idx: track.idx,
1041
+ track,
1042
+ opts: {
1043
+ optimizeForLatency: this.streamType === "live",
1044
+ payloadFormat: this.payloadFormat, // 'avcc' for ws/video/raw, 'annexb' for ws/video/h264
1045
+ },
1046
+ uid: this.workerUidCounter++,
1047
+ });
1048
+ // Create track generator - three paths:
1049
+ // 1. Chrome/Edge: MediaStreamTrackGenerator on main thread, transfer writable to worker
1050
+ // 2. Safari: VideoTrackGenerator in worker (video) or frame relay (audio)
1051
+ // 3. Firefox: Use canvas/AudioWorklet polyfill
1052
+ if (MediaStreamTrackGenerator$1.hasNativeMediaStreamTrackGenerator()) {
1053
+ // Chrome/Edge: Create generator and transfer writable to worker
1054
+ // @ts-ignore
1055
+ const generator = new MediaStreamTrackGenerator({ kind: track.type });
1056
+ pipeline.generator = {
1057
+ writable: generator.writable,
1058
+ getTrack: () => generator,
1059
+ close: () => generator.stop?.(),
1060
+ };
1061
+ await this.sendToWorker({
1062
+ type: "setwritable",
1063
+ idx: track.idx,
1064
+ writable: generator.writable,
1065
+ uid: this.workerUidCounter++,
1066
+ }, [generator.writable]);
1067
+ }
1068
+ else if (isSafari()) {
1069
+ // Safari: Worker uses VideoTrackGenerator (video) or frame relay (audio)
1070
+ // Reference: rawws.js line 1012-1037
1071
+ this.log(`Safari detected - using worker-based track generator for ${track.type}`);
1072
+ if (track.type === "audio") {
1073
+ // Safari audio: create generator on main thread, frames relayed from worker
1074
+ // @ts-ignore - Safari has MediaStreamTrackGenerator for audio
1075
+ if (typeof MediaStreamTrackGenerator !== "undefined") {
1076
+ // @ts-ignore
1077
+ const audioGen = new MediaStreamTrackGenerator({ kind: "audio" });
1078
+ pipeline.safariAudioGenerator = audioGen;
1079
+ pipeline.safariAudioWriter = audioGen.writable.getWriter();
1080
+ // Add track to stream
1081
+ if (this.mediaStream) {
1082
+ this.mediaStream.addTrack(audioGen);
1083
+ }
1084
+ }
1085
+ }
1086
+ // Ask worker to create generator (video uses VideoTrackGenerator, audio sets up relay)
1087
+ await this.sendToWorker({
1088
+ type: "creategenerator",
1089
+ idx: track.idx,
1090
+ uid: this.workerUidCounter++,
1091
+ });
1092
+ }
1093
+ else {
1094
+ // Firefox/other: Use canvas/AudioWorklet polyfill
1095
+ pipeline.generator = MediaStreamTrackGenerator$1.createTrackGenerator(track.type);
1096
+ if (pipeline.generator.waitForInit) {
1097
+ await pipeline.generator.waitForInit();
1098
+ }
1099
+ // For polyfill, writable stays on main thread
1100
+ // Worker would need different architecture - for now, fall back to main thread decode
1101
+ this.log("Using MediaStreamTrackGenerator polyfill - main thread decode");
1102
+ // Add track to stream directly
1103
+ if (this.mediaStream && pipeline.generator) {
1104
+ this.mediaStream.addTrack(pipeline.generator.getTrack());
1105
+ }
1106
+ }
1107
+ // Per rawws.js: Do NOT configure from HTTP info automatically.
1108
+ // Wait for WebSocket binary INIT frames to configure decoders.
1109
+ // This ensures we use the exact init data the server sends for this session.
1110
+ //
1111
+ // However, if track.init is empty/undefined, the codec doesn't need init data
1112
+ // and we can configure immediately (per rawws.js line 1239-1241).
1113
+ // This applies to codecs like opus, mp3, vp8, vp9 that don't need init data.
1114
+ if (!track.init || track.init === "") {
1115
+ this.log(`Track ${track.idx} (${track.codec}) doesn't need init data, configuring immediately`);
1116
+ await this.configurePipeline(track.idx, new Uint8Array(0));
1117
+ }
1118
+ else {
1119
+ // For codecs that need init data (H264, HEVC, AAC), we have two paths:
1120
+ // 1. WebSocket sends INIT frame -> handleChunk triggers configurePipeline
1121
+ // 2. First frame arrives without prior INIT -> handleChunk uses track.init
1122
+ this.log(`Track ${track.idx} (${track.codec}) has init data (${track.init.length} bytes), waiting for first frame`);
1123
+ }
1124
+ }
1125
+ async configurePipeline(idx, header) {
1126
+ const pipeline = this.pipelines.get(idx);
1127
+ if (!pipeline || pipeline.configured)
1128
+ return;
1129
+ this.log(`Configuring decoder for track ${idx}`);
1130
+ // Copy the header to avoid transfer issues (neutered buffers)
1131
+ // The structured clone will copy this automatically
1132
+ const headerCopy = new Uint8Array(header);
1133
+ await this.sendToWorker({
1134
+ type: "configure",
1135
+ idx,
1136
+ header: headerCopy,
1137
+ uid: this.workerUidCounter++,
1138
+ });
1139
+ pipeline.configured = true;
1140
+ // Flush any queued chunks now that decoder is configured
1141
+ const queued = this.queuedChunks.get(idx);
1142
+ if (queued && queued.length > 0) {
1143
+ this.log(`Flushing ${queued.length} queued chunks for track ${idx}`);
1144
+ // Find first keyframe to start from (can't decode deltas without reference)
1145
+ let startIdx = 0;
1146
+ for (let i = 0; i < queued.length; i++) {
1147
+ if (queued[i].type === "key") {
1148
+ startIdx = i;
1149
+ break;
1150
+ }
1151
+ }
1152
+ if (startIdx > 0) {
1153
+ this.log(`Skipping ${startIdx} delta frames, starting from keyframe`);
1154
+ }
1155
+ for (let i = startIdx; i < queued.length; i++) {
1156
+ this.sendChunkToWorker(queued[i]);
1157
+ }
1158
+ this.queuedChunks.delete(idx);
1159
+ }
1160
+ }
1161
+ async closePipeline(idx, waitEmpty) {
1162
+ const pipeline = this.pipelines.get(idx);
1163
+ if (!pipeline)
1164
+ return;
1165
+ this.log(`Closing pipeline ${idx}`);
1166
+ // Close worker pipeline
1167
+ await this.sendToWorker({
1168
+ type: "close",
1169
+ idx,
1170
+ waitEmpty,
1171
+ uid: this.workerUidCounter++,
1172
+ });
1173
+ // Close generator
1174
+ pipeline.generator?.close();
1175
+ // Remove from sync controller
1176
+ this.syncController?.removeTrack(idx);
1177
+ this.pipelines.delete(idx);
1178
+ }
1179
+ // ============================================================================
1180
+ // Playback Control
1181
+ // ============================================================================
1182
+ async play() {
1183
+ this._isPaused = false;
1184
+ this.wsController?.play();
1185
+ this.sendToWorker({
1186
+ type: "frametiming",
1187
+ action: "setPaused",
1188
+ paused: false,
1189
+ uid: this.workerUidCounter++,
1190
+ });
1191
+ await this.videoElement?.play();
1192
+ }
1193
+ pause() {
1194
+ this._isPaused = true;
1195
+ this.wsController?.hold();
1196
+ this.sendToWorker({
1197
+ type: "frametiming",
1198
+ action: "setPaused",
1199
+ paused: true,
1200
+ uid: this.workerUidCounter++,
1201
+ });
1202
+ this.videoElement?.pause();
1203
+ }
1204
+ finishStepPause() {
1205
+ if (!this.videoElement) {
1206
+ this._pendingStepPause = false;
1207
+ this._suppressPlayPauseSync = false;
1208
+ if (this._stepPauseTimeout) {
1209
+ clearTimeout(this._stepPauseTimeout);
1210
+ this._stepPauseTimeout = null;
1211
+ }
1212
+ return;
1213
+ }
1214
+ if (this._stepPauseTimeout) {
1215
+ clearTimeout(this._stepPauseTimeout);
1216
+ this._stepPauseTimeout = null;
1217
+ }
1218
+ this._pendingStepPause = false;
1219
+ this._suppressPlayPauseSync = false;
1220
+ try {
1221
+ this.videoElement.pause();
1222
+ }
1223
+ catch { }
1224
+ }
1225
+ frameStep(direction, _seconds) {
1226
+ if (!this._isPaused)
1227
+ return;
1228
+ if (!this.videoElement)
1229
+ return;
1230
+ this.log(`Frame step requested dir=${direction} paused=${this._isPaused} videoPaused=${this.videoElement.paused}`);
1231
+ // Ensure worker is paused (in case pause didn't flow through)
1232
+ this.sendToWorker({
1233
+ type: "frametiming",
1234
+ action: "setPaused",
1235
+ paused: true,
1236
+ uid: this.workerUidCounter++,
1237
+ }).catch(() => { });
1238
+ // MediaStream-backed video elements don't present new frames while paused.
1239
+ // Pulse playback briefly so the stepped frame can render, then pause again.
1240
+ if (this.videoElement.paused) {
1241
+ const video = this.videoElement;
1242
+ this._suppressPlayPauseSync = true;
1243
+ this._pendingStepPause = true;
1244
+ try {
1245
+ const maybePromise = video.play();
1246
+ if (maybePromise && typeof maybePromise.catch === "function") {
1247
+ maybePromise.catch(() => { });
1248
+ }
1249
+ }
1250
+ catch { }
1251
+ if ("requestVideoFrameCallback" in video) {
1252
+ video.requestVideoFrameCallback(() => this.finishStepPause());
1253
+ }
1254
+ // Failsafe: avoid staying in suppressed state if no frame is delivered
1255
+ this._stepPauseTimeout = setTimeout(() => this.finishStepPause(), 200);
1256
+ }
1257
+ this.sendToWorker({
1258
+ type: "framestep",
1259
+ direction,
1260
+ uid: this.workerUidCounter++,
1261
+ });
1262
+ }
1263
+ seek(time) {
1264
+ if (!this.wsController || !this.syncController)
1265
+ return;
1266
+ const timeMs = time * 1000;
1267
+ const seekId = this.syncController.startSeek(timeMs);
1268
+ // Optimistically update current time for immediate UI feedback
1269
+ this._currentTime = time;
1270
+ this.emit("timeupdate", this._currentTime);
1271
+ // Flush worker queues
1272
+ this.sendToWorker({
1273
+ type: "seek",
1274
+ seekTime: timeMs,
1275
+ uid: this.workerUidCounter++,
1276
+ });
1277
+ // Send seek to server
1278
+ const desiredBuffer = this.syncController.getDesiredBuffer();
1279
+ this.wsController.seek(timeMs, desiredBuffer);
1280
+ // Mark seek complete after first frame (handled by worker)
1281
+ // In practice, we'd wait for first frame callback
1282
+ setTimeout(() => {
1283
+ if (this.syncController?.isSeekActive(seekId)) {
1284
+ this.syncController.completeSeek(seekId);
1285
+ this.sendToWorker({
1286
+ type: "frametiming",
1287
+ action: "reset",
1288
+ uid: this.workerUidCounter++,
1289
+ });
1290
+ }
1291
+ }, 100);
1292
+ }
1293
+ setPlaybackRate(rate) {
1294
+ this.syncController?.setMainSpeed(rate);
1295
+ }
1296
+ isPaused() {
1297
+ return this._isPaused;
1298
+ }
1299
+ isLive() {
1300
+ return this.streamType === "live";
1301
+ }
1302
+ jumpToLive() {
1303
+ if (this.streamType === "live" && this.wsController) {
1304
+ // For WebCodecs live, request fresh data from live edge
1305
+ // Send fast_forward to request 5 seconds of new data
1306
+ // Reference: rawws.js live catchup sends fast_forward
1307
+ const desiredBuffer = this.syncController?.getDesiredBuffer() ?? 2000;
1308
+ this.wsController.send({
1309
+ type: "fast_forward",
1310
+ ff_add: 5000, // Request 5 seconds ahead
1311
+ });
1312
+ // Also request buffer from current time to rebuild
1313
+ const serverTime = this.syncController?.getEstimatedServerTime() ?? 0;
1314
+ if (serverTime > 0) {
1315
+ this.wsController.seek(serverTime * 1000, desiredBuffer);
1316
+ }
1317
+ this.log("Jump to live: requested fresh data from server");
1318
+ }
1319
+ }
1320
+ /**
1321
+ * Check if seeking is supported.
1322
+ * WebCodecs can seek via server commands when connected.
1323
+ * Reference: rawws.js line 1294-1304 implements seeking via control channel
1324
+ */
1325
+ canSeek() {
1326
+ // WebCodecs CAN seek via server commands when WebSocket is connected
1327
+ // This overrides the default MediaStream check in SeekingUtils
1328
+ return this.wsController !== null && !this.isDestroyed;
1329
+ }
1330
+ // ============================================================================
1331
+ // Media Properties (Phase 2A)
1332
+ // ============================================================================
1333
+ /**
1334
+ * Get stream duration (Infinity for live streams)
1335
+ */
1336
+ get duration() {
1337
+ return this._duration;
1338
+ }
1339
+ getDuration() {
1340
+ return this._duration;
1341
+ }
1342
+ /**
1343
+ * Get current playback time (seconds)
1344
+ * Uses requestVideoFrameCallback for accurate timing when available
1345
+ */
1346
+ get currentTime() {
1347
+ return this._currentTime;
1348
+ }
1349
+ getCurrentTime() {
1350
+ return this._currentTime;
1351
+ }
1352
+ /**
1353
+ * Get buffered time ranges
1354
+ * Returns single range from current time to current + buffer
1355
+ */
1356
+ get buffered() {
1357
+ if (this._bufferMs <= 0) {
1358
+ return createTimeRanges([]);
1359
+ }
1360
+ const start = this._currentTime;
1361
+ const end = start + this._bufferMs / 1000;
1362
+ return createTimeRanges([[start, end]]);
1363
+ }
1364
+ /**
1365
+ * Get comprehensive player statistics
1366
+ */
1367
+ async getStats() {
1368
+ const syncState = this.syncController?.getState();
1369
+ return {
1370
+ latency: {
1371
+ buffer: syncState?.buffer.current ?? 0,
1372
+ target: syncState?.buffer.desired ?? 0,
1373
+ jitter: syncState?.jitter.weighted ?? 0,
1374
+ },
1375
+ sync: {
1376
+ avDrift: this._avDrift,
1377
+ playbackSpeed: syncState?.playbackSpeed ?? 1,
1378
+ },
1379
+ decoder: {
1380
+ videoQueueSize: 0, // Will be populated from worker stats
1381
+ audioQueueSize: 0,
1382
+ framesDropped: this._framesDropped,
1383
+ framesDecoded: this._framesDecoded,
1384
+ },
1385
+ network: {
1386
+ bytesReceived: this._bytesReceived,
1387
+ messagesReceived: this._messagesReceived,
1388
+ },
1389
+ };
1390
+ }
1391
+ // ============================================================================
1392
+ // Frame Timing (requestVideoFrameCallback)
1393
+ // ============================================================================
1394
+ /**
1395
+ * Set up requestVideoFrameCallback for accurate frame timing
1396
+ * This provides vsync-aligned frame metadata for A/V sync
1397
+ */
1398
+ setupFrameCallback() {
1399
+ if (!this.videoElement)
1400
+ return;
1401
+ // Check if requestVideoFrameCallback is available
1402
+ if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
1403
+ const callback = (_now, metadata) => {
1404
+ if (this.isDestroyed || !this.videoElement)
1405
+ return;
1406
+ this.onVideoFrame(metadata);
1407
+ // Schedule next callback
1408
+ this._frameCallbackId = this.videoElement.requestVideoFrameCallback(callback);
1409
+ };
1410
+ this._frameCallbackId = this.videoElement.requestVideoFrameCallback(callback);
1411
+ this.log("requestVideoFrameCallback enabled for accurate frame timing");
1412
+ }
1413
+ else {
1414
+ // Fallback: Use video element's currentTime directly
1415
+ this.log("requestVideoFrameCallback not available, using fallback timing");
1416
+ }
1417
+ }
1418
+ /**
1419
+ * Handle video frame presentation callback
1420
+ * Updates current time
1421
+ */
1422
+ onVideoFrame(metadata) {
1423
+ // Update current time from actual frame presentation
1424
+ this._currentTime = metadata.mediaTime;
1425
+ // Update buffer level from sync controller
1426
+ const syncState = this.syncController?.getState();
1427
+ if (syncState) {
1428
+ this._bufferMs = syncState.buffer.current;
1429
+ }
1430
+ // Emit timeupdate event
1431
+ this.emit("timeupdate", this._currentTime);
1432
+ // Update frame stats
1433
+ this._framesDecoded = metadata.presentedFrames;
1434
+ }
1435
+ /**
1436
+ * Cancel frame callback on cleanup
1437
+ */
1438
+ cancelFrameCallback() {
1439
+ if (this._frameCallbackId !== null && this.videoElement) {
1440
+ if ("cancelVideoFrameCallback" in HTMLVideoElement.prototype) {
1441
+ this.videoElement.cancelVideoFrameCallback(this._frameCallbackId);
1442
+ }
1443
+ this._frameCallbackId = null;
1444
+ }
1445
+ }
1446
+ // ============================================================================
1447
+ // Logging
1448
+ // ============================================================================
1449
+ log(message, level = "info") {
1450
+ if (!this.debugging && level === "info")
1451
+ return;
1452
+ console[level](`[WebCodecs] ${message}`);
1453
+ }
1454
+ }
1455
+ // Codec support cache - keyed by "codec|init_hash"
1456
+ WebCodecsPlayerImpl.codecCache = new Map();
1457
+
1458
+ exports.WebSocketController = WebSocketController.WebSocketController;
1459
+ exports.SyncController = SyncController.SyncController;
1460
+ exports.parseRawChunk = RawChunkParser.parseRawChunk;
1461
+ exports.LATENCY_PROFILES = LatencyProfiles.LATENCY_PROFILES;
1462
+ exports.getLatencyProfile = LatencyProfiles.getLatencyProfile;
1463
+ exports.mergeLatencyProfile = LatencyProfiles.mergeLatencyProfile;
1464
+ exports.JitterTracker = JitterBuffer.JitterTracker;
1465
+ exports.MultiTrackJitterTracker = JitterBuffer.MultiTrackJitterTracker;
1466
+ exports.WebCodecsPlayerImpl = WebCodecsPlayerImpl;
1467
+ //# sourceMappingURL=index.js.map