prismcast 1.10.2 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (693) hide show
  1. package/dist/app.d.ts +6 -3
  2. package/dist/app.d.ts.map +1 -0
  3. package/dist/app.js +140 -86
  4. package/dist/app.js.map +1 -1
  5. package/dist/browser/blockedPage.d.ts +80 -0
  6. package/dist/browser/blockedPage.d.ts.map +1 -0
  7. package/dist/browser/blockedPage.js +209 -0
  8. package/dist/browser/blockedPage.js.map +1 -0
  9. package/dist/browser/browserSupervisor.d.ts +80 -0
  10. package/dist/browser/browserSupervisor.d.ts.map +1 -0
  11. package/dist/browser/browserSupervisor.js +186 -0
  12. package/dist/browser/browserSupervisor.js.map +1 -0
  13. package/dist/browser/cdp.d.ts +1 -0
  14. package/dist/browser/cdp.d.ts.map +1 -0
  15. package/dist/browser/cdp.js +6 -5
  16. package/dist/browser/cdp.js.map +1 -1
  17. package/dist/browser/channelSelection.d.ts +45 -14
  18. package/dist/browser/channelSelection.d.ts.map +1 -0
  19. package/dist/browser/channelSelection.js +81 -18
  20. package/dist/browser/channelSelection.js.map +1 -1
  21. package/dist/browser/consent.d.ts +152 -0
  22. package/dist/browser/consent.d.ts.map +1 -0
  23. package/dist/browser/consent.js +418 -0
  24. package/dist/browser/consent.js.map +1 -0
  25. package/dist/browser/display.d.ts +1 -0
  26. package/dist/browser/display.d.ts.map +1 -0
  27. package/dist/browser/hlsPlaylistObserver.d.ts +55 -0
  28. package/dist/browser/hlsPlaylistObserver.d.ts.map +1 -0
  29. package/dist/browser/hlsPlaylistObserver.js +132 -0
  30. package/dist/browser/hlsPlaylistObserver.js.map +1 -0
  31. package/dist/browser/index.d.ts +67 -40
  32. package/dist/browser/index.d.ts.map +1 -0
  33. package/dist/browser/index.js +508 -300
  34. package/dist/browser/index.js.map +1 -1
  35. package/dist/browser/launchGovernor.d.ts +89 -0
  36. package/dist/browser/launchGovernor.d.ts.map +1 -0
  37. package/dist/browser/launchGovernor.js +96 -0
  38. package/dist/browser/launchGovernor.js.map +1 -0
  39. package/dist/browser/login.d.ts +10 -2
  40. package/dist/browser/login.d.ts.map +1 -0
  41. package/dist/browser/login.js +36 -5
  42. package/dist/browser/login.js.map +1 -1
  43. package/dist/browser/manifestInterceptor.d.ts +102 -49
  44. package/dist/browser/manifestInterceptor.d.ts.map +1 -0
  45. package/dist/browser/manifestInterceptor.js +219 -237
  46. package/dist/browser/manifestInterceptor.js.map +1 -1
  47. package/dist/browser/pageStaleness.d.ts +30 -0
  48. package/dist/browser/pageStaleness.d.ts.map +1 -0
  49. package/dist/browser/pageStaleness.js +76 -0
  50. package/dist/browser/pageStaleness.js.map +1 -0
  51. package/dist/browser/precaching.d.ts +95 -3
  52. package/dist/browser/precaching.d.ts.map +1 -0
  53. package/dist/browser/precaching.js +499 -65
  54. package/dist/browser/precaching.js.map +1 -1
  55. package/dist/browser/tabNetworkObserver.d.ts +49 -0
  56. package/dist/browser/tabNetworkObserver.d.ts.map +1 -0
  57. package/dist/browser/tabNetworkObserver.js +144 -0
  58. package/dist/browser/tabNetworkObserver.js.map +1 -0
  59. package/dist/browser/tuning/cache.d.ts +61 -0
  60. package/dist/browser/tuning/cache.d.ts.map +1 -0
  61. package/dist/browser/tuning/cache.js +62 -0
  62. package/dist/browser/tuning/cache.js.map +1 -0
  63. package/dist/browser/tuning/comcastPolymer.d.ts +3 -2
  64. package/dist/browser/tuning/comcastPolymer.d.ts.map +1 -0
  65. package/dist/browser/tuning/comcastPolymer.js +79 -102
  66. package/dist/browser/tuning/comcastPolymer.js.map +1 -1
  67. package/dist/browser/tuning/cox.d.ts +1 -0
  68. package/dist/browser/tuning/cox.d.ts.map +1 -0
  69. package/dist/browser/tuning/directv.d.ts +1 -0
  70. package/dist/browser/tuning/directv.d.ts.map +1 -0
  71. package/dist/browser/tuning/directv.js +52 -46
  72. package/dist/browser/tuning/directv.js.map +1 -1
  73. package/dist/browser/tuning/fox.d.ts +1 -0
  74. package/dist/browser/tuning/fox.d.ts.map +1 -0
  75. package/dist/browser/tuning/fox.js +13 -12
  76. package/dist/browser/tuning/fox.js.map +1 -1
  77. package/dist/browser/tuning/gridSearch.d.ts +68 -0
  78. package/dist/browser/tuning/gridSearch.d.ts.map +1 -0
  79. package/dist/browser/tuning/gridSearch.js +85 -0
  80. package/dist/browser/tuning/gridSearch.js.map +1 -0
  81. package/dist/browser/tuning/hbo.d.ts +1 -0
  82. package/dist/browser/tuning/hbo.d.ts.map +1 -0
  83. package/dist/browser/tuning/hbo.js +73 -45
  84. package/dist/browser/tuning/hbo.js.map +1 -1
  85. package/dist/browser/tuning/hulu.d.ts +1 -0
  86. package/dist/browser/tuning/hulu.d.ts.map +1 -0
  87. package/dist/browser/tuning/hulu.js +128 -173
  88. package/dist/browser/tuning/hulu.js.map +1 -1
  89. package/dist/browser/tuning/shared.d.ts +75 -5
  90. package/dist/browser/tuning/shared.d.ts.map +1 -0
  91. package/dist/browser/tuning/shared.js +216 -6
  92. package/dist/browser/tuning/shared.js.map +1 -1
  93. package/dist/browser/tuning/sling.d.ts +1 -0
  94. package/dist/browser/tuning/sling.d.ts.map +1 -0
  95. package/dist/browser/tuning/sling.js +105 -126
  96. package/dist/browser/tuning/sling.js.map +1 -1
  97. package/dist/browser/tuning/spectrum.d.ts +1 -0
  98. package/dist/browser/tuning/spectrum.d.ts.map +1 -0
  99. package/dist/browser/tuning/spectrum.js +80 -125
  100. package/dist/browser/tuning/spectrum.js.map +1 -1
  101. package/dist/browser/tuning/thumbnailRow.d.ts +1 -0
  102. package/dist/browser/tuning/thumbnailRow.d.ts.map +1 -0
  103. package/dist/browser/tuning/thumbnailRow.js +8 -2
  104. package/dist/browser/tuning/thumbnailRow.js.map +1 -1
  105. package/dist/browser/tuning/tileClick.d.ts +1 -0
  106. package/dist/browser/tuning/tileClick.d.ts.map +1 -0
  107. package/dist/browser/tuning/tileClick.js +3 -2
  108. package/dist/browser/tuning/tileClick.js.map +1 -1
  109. package/dist/browser/tuning/xfinity.d.ts +1 -0
  110. package/dist/browser/tuning/xfinity.d.ts.map +1 -0
  111. package/dist/browser/tuning/youtubeTv.d.ts +1 -0
  112. package/dist/browser/tuning/youtubeTv.d.ts.map +1 -0
  113. package/dist/browser/tuning/youtubeTv.js +74 -127
  114. package/dist/browser/tuning/youtubeTv.js.map +1 -1
  115. package/dist/browser/video.d.ts +54 -14
  116. package/dist/browser/video.d.ts.map +1 -0
  117. package/dist/browser/video.js +296 -138
  118. package/dist/browser/video.js.map +1 -1
  119. package/dist/channels/index.d.ts +1 -0
  120. package/dist/channels/index.d.ts.map +1 -0
  121. package/dist/channels/index.js +12 -9
  122. package/dist/channels/index.js.map +1 -1
  123. package/dist/config/channelForm.d.ts +15 -1
  124. package/dist/config/channelForm.d.ts.map +1 -0
  125. package/dist/config/channelForm.js +28 -12
  126. package/dist/config/channelForm.js.map +1 -1
  127. package/dist/config/consistencyProbe.d.ts +1 -0
  128. package/dist/config/consistencyProbe.d.ts.map +1 -0
  129. package/dist/config/consistencyProbe.js +27 -21
  130. package/dist/config/consistencyProbe.js.map +1 -1
  131. package/dist/config/health.d.ts +41 -8
  132. package/dist/config/health.d.ts.map +1 -0
  133. package/dist/config/health.js +189 -38
  134. package/dist/config/health.js.map +1 -1
  135. package/dist/config/index.d.ts +55 -3
  136. package/dist/config/index.d.ts.map +1 -0
  137. package/dist/config/index.js +320 -69
  138. package/dist/config/index.js.map +1 -1
  139. package/dist/config/paths.d.ts +10 -9
  140. package/dist/config/paths.d.ts.map +1 -0
  141. package/dist/config/paths.js +11 -11
  142. package/dist/config/paths.js.map +1 -1
  143. package/dist/config/persistence.context.d.ts +1 -0
  144. package/dist/config/persistence.context.d.ts.map +1 -0
  145. package/dist/config/persistence.d.ts +13 -10
  146. package/dist/config/persistence.d.ts.map +1 -0
  147. package/dist/config/persistence.js +40 -21
  148. package/dist/config/persistence.js.map +1 -1
  149. package/dist/config/presets.d.ts +1 -0
  150. package/dist/config/presets.d.ts.map +1 -0
  151. package/dist/config/profiles.d.ts +22 -15
  152. package/dist/config/profiles.d.ts.map +1 -0
  153. package/dist/config/profiles.js +81 -47
  154. package/dist/config/profiles.js.map +1 -1
  155. package/dist/config/providerLineups.d.ts +47 -0
  156. package/dist/config/providerLineups.d.ts.map +1 -0
  157. package/dist/config/providerLineups.js +137 -0
  158. package/dist/config/providerLineups.js.map +1 -0
  159. package/dist/config/reactivity.d.ts +84 -0
  160. package/dist/config/reactivity.d.ts.map +1 -0
  161. package/dist/config/reactivity.js +226 -0
  162. package/dist/config/reactivity.js.map +1 -0
  163. package/dist/config/servicePacks.d.ts +18 -2
  164. package/dist/config/servicePacks.d.ts.map +1 -0
  165. package/dist/config/servicePacks.js +53 -11
  166. package/dist/config/servicePacks.js.map +1 -1
  167. package/dist/config/services.d.ts +21 -15
  168. package/dist/config/services.d.ts.map +1 -0
  169. package/dist/config/services.js +35 -24
  170. package/dist/config/services.js.map +1 -1
  171. package/dist/config/sites.d.ts +6 -4
  172. package/dist/config/sites.d.ts.map +1 -0
  173. package/dist/config/sites.js +15 -11
  174. package/dist/config/sites.js.map +1 -1
  175. package/dist/config/userChannels.d.ts +129 -30
  176. package/dist/config/userChannels.d.ts.map +1 -0
  177. package/dist/config/userChannels.js +333 -68
  178. package/dist/config/userChannels.js.map +1 -1
  179. package/dist/config/userConfig.d.ts +14 -9
  180. package/dist/config/userConfig.d.ts.map +1 -0
  181. package/dist/config/userConfig.js +87 -25
  182. package/dist/config/userConfig.js.map +1 -1
  183. package/dist/config/userProfiles.d.ts +10 -6
  184. package/dist/config/userProfiles.d.ts.map +1 -0
  185. package/dist/config/userProfiles.js +64 -39
  186. package/dist/config/userProfiles.js.map +1 -1
  187. package/dist/hdhr/channelMap.d.ts +1 -0
  188. package/dist/hdhr/channelMap.d.ts.map +1 -0
  189. package/dist/hdhr/deviceId.d.ts +1 -0
  190. package/dist/hdhr/deviceId.d.ts.map +1 -0
  191. package/dist/hdhr/discover.d.ts +1 -0
  192. package/dist/hdhr/discover.d.ts.map +1 -0
  193. package/dist/hdhr/discover.js +48 -47
  194. package/dist/hdhr/discover.js.map +1 -1
  195. package/dist/hdhr/getHandlers.d.ts +29 -0
  196. package/dist/hdhr/getHandlers.d.ts.map +1 -0
  197. package/dist/hdhr/getHandlers.js +196 -0
  198. package/dist/hdhr/getHandlers.js.map +1 -0
  199. package/dist/hdhr/identity.d.ts +36 -0
  200. package/dist/hdhr/identity.d.ts.map +1 -0
  201. package/dist/hdhr/identity.js +51 -0
  202. package/dist/hdhr/identity.js.map +1 -0
  203. package/dist/hdhr/index.d.ts +22 -4
  204. package/dist/hdhr/index.d.ts.map +1 -0
  205. package/dist/hdhr/index.js +229 -54
  206. package/dist/hdhr/index.js.map +1 -1
  207. package/dist/hdhr/protocol.d.ts +75 -0
  208. package/dist/hdhr/protocol.d.ts.map +1 -0
  209. package/dist/hdhr/protocol.js +264 -0
  210. package/dist/hdhr/protocol.js.map +1 -0
  211. package/dist/hdhr/tunerState.d.ts +24 -0
  212. package/dist/hdhr/tunerState.d.ts.map +1 -0
  213. package/dist/hdhr/tunerState.js +67 -0
  214. package/dist/hdhr/tunerState.js.map +1 -0
  215. package/dist/hdhr/udp.d.ts +43 -0
  216. package/dist/hdhr/udp.d.ts.map +1 -0
  217. package/dist/hdhr/udp.js +277 -0
  218. package/dist/hdhr/udp.js.map +1 -0
  219. package/dist/identity.d.ts +1 -0
  220. package/dist/identity.d.ts.map +1 -0
  221. package/dist/identity.js +13 -11
  222. package/dist/identity.js.map +1 -1
  223. package/dist/index.d.ts +1 -0
  224. package/dist/index.d.ts.map +1 -0
  225. package/dist/index.js +11 -12
  226. package/dist/index.js.map +1 -1
  227. package/dist/native/codecInference.d.ts +1 -0
  228. package/dist/native/codecInference.d.ts.map +1 -0
  229. package/dist/native/codecInference.js +50 -5
  230. package/dist/native/codecInference.js.map +1 -1
  231. package/dist/native/decrypt.d.ts +6 -2
  232. package/dist/native/decrypt.d.ts.map +1 -0
  233. package/dist/native/decrypt.js +15 -8
  234. package/dist/native/decrypt.js.map +1 -1
  235. package/dist/native/index.d.ts +40 -17
  236. package/dist/native/index.d.ts.map +1 -0
  237. package/dist/native/index.js +281 -120
  238. package/dist/native/index.js.map +1 -1
  239. package/dist/native/probe.d.ts +114 -18
  240. package/dist/native/probe.d.ts.map +1 -0
  241. package/dist/native/probe.js +452 -115
  242. package/dist/native/probe.js.map +1 -1
  243. package/dist/native/proxy.d.ts +57 -5
  244. package/dist/native/proxy.d.ts.map +1 -0
  245. package/dist/native/proxy.js +604 -170
  246. package/dist/native/proxy.js.map +1 -1
  247. package/dist/native/tokenExpiry.d.ts +1 -0
  248. package/dist/native/tokenExpiry.d.ts.map +1 -0
  249. package/dist/native/tokenExpiry.js +1 -1
  250. package/dist/routes/assets.d.ts +1 -0
  251. package/dist/routes/assets.d.ts.map +1 -0
  252. package/dist/routes/assets.js +4 -2
  253. package/dist/routes/assets.js.map +1 -1
  254. package/dist/routes/auth.d.ts +1 -0
  255. package/dist/routes/auth.d.ts.map +1 -0
  256. package/dist/routes/auth.js +0 -1
  257. package/dist/routes/auth.js.map +1 -1
  258. package/dist/routes/cdp.d.ts +5 -5
  259. package/dist/routes/cdp.d.ts.map +1 -0
  260. package/dist/routes/cdp.js +59 -20
  261. package/dist/routes/cdp.js.map +1 -1
  262. package/dist/routes/channels.d.ts +1 -0
  263. package/dist/routes/channels.d.ts.map +1 -0
  264. package/dist/routes/channels.js +2 -1
  265. package/dist/routes/channels.js.map +1 -1
  266. package/dist/routes/clientActions.d.ts +98 -0
  267. package/dist/routes/clientActions.d.ts.map +1 -0
  268. package/dist/routes/clientActions.js +113 -0
  269. package/dist/routes/clientActions.js.map +1 -0
  270. package/dist/routes/components.d.ts +5 -3
  271. package/dist/routes/components.d.ts.map +1 -0
  272. package/dist/routes/components.js +36 -39
  273. package/dist/routes/components.js.map +1 -1
  274. package/dist/routes/config/channels/endpoints/browse.d.ts +1 -0
  275. package/dist/routes/config/channels/endpoints/browse.d.ts.map +1 -0
  276. package/dist/routes/config/channels/endpoints/browse.js +23 -10
  277. package/dist/routes/config/channels/endpoints/browse.js.map +1 -1
  278. package/dist/routes/config/channels/endpoints/bulk.d.ts +1 -0
  279. package/dist/routes/config/channels/endpoints/bulk.d.ts.map +1 -0
  280. package/dist/routes/config/channels/endpoints/bulk.js +5 -3
  281. package/dist/routes/config/channels/endpoints/bulk.js.map +1 -1
  282. package/dist/routes/config/channels/endpoints/crud.d.ts +1 -0
  283. package/dist/routes/config/channels/endpoints/crud.d.ts.map +1 -0
  284. package/dist/routes/config/channels/endpoints/crud.js +85 -93
  285. package/dist/routes/config/channels/endpoints/crud.js.map +1 -1
  286. package/dist/routes/config/channels/endpoints/importExport.d.ts +1 -0
  287. package/dist/routes/config/channels/endpoints/importExport.d.ts.map +1 -0
  288. package/dist/routes/config/channels/endpoints/importExport.js +6 -3
  289. package/dist/routes/config/channels/endpoints/importExport.js.map +1 -1
  290. package/dist/routes/config/channels/endpoints/predefined.d.ts +1 -0
  291. package/dist/routes/config/channels/endpoints/predefined.d.ts.map +1 -0
  292. package/dist/routes/config/channels/endpoints/prefs.d.ts +1 -0
  293. package/dist/routes/config/channels/endpoints/prefs.d.ts.map +1 -0
  294. package/dist/routes/config/channels/endpoints/prefs.js +2 -1
  295. package/dist/routes/config/channels/endpoints/prefs.js.map +1 -1
  296. package/dist/routes/config/channels/endpoints/service.d.ts +2 -1
  297. package/dist/routes/config/channels/endpoints/service.d.ts.map +1 -0
  298. package/dist/routes/config/channels/endpoints/service.js +3 -1
  299. package/dist/routes/config/channels/endpoints/service.js.map +1 -1
  300. package/dist/routes/config/channels/endpoints/tags.d.ts +1 -0
  301. package/dist/routes/config/channels/endpoints/tags.d.ts.map +1 -0
  302. package/dist/routes/config/channels/endpoints/tags.js +13 -7
  303. package/dist/routes/config/channels/endpoints/tags.js.map +1 -1
  304. package/dist/routes/config/channels/healthBridge.d.ts +24 -0
  305. package/dist/routes/config/channels/healthBridge.d.ts.map +1 -0
  306. package/dist/routes/config/channels/healthBridge.js +71 -0
  307. package/dist/routes/config/channels/healthBridge.js.map +1 -0
  308. package/dist/routes/config/channels/http/handler.d.ts +1 -0
  309. package/dist/routes/config/channels/http/handler.d.ts.map +1 -0
  310. package/dist/routes/config/channels/http/playlistHint.d.ts +2 -1
  311. package/dist/routes/config/channels/http/playlistHint.d.ts.map +1 -0
  312. package/dist/routes/config/channels/http/playlistHint.js +16 -2
  313. package/dist/routes/config/channels/http/playlistHint.js.map +1 -1
  314. package/dist/routes/config/channels/http/serviceWarning.d.ts +4 -2
  315. package/dist/routes/config/channels/http/serviceWarning.d.ts.map +1 -0
  316. package/dist/routes/config/channels/http/serviceWarning.js +3 -2
  317. package/dist/routes/config/channels/http/serviceWarning.js.map +1 -1
  318. package/dist/routes/config/channels/index.d.ts +1 -0
  319. package/dist/routes/config/channels/index.d.ts.map +1 -0
  320. package/dist/routes/config/channels/setup.d.ts +1 -0
  321. package/dist/routes/config/channels/setup.d.ts.map +1 -0
  322. package/dist/routes/config/channels/table.d.ts +3 -3
  323. package/dist/routes/config/channels/table.d.ts.map +1 -0
  324. package/dist/routes/config/channels/table.js +148 -107
  325. package/dist/routes/config/channels/table.js.map +1 -1
  326. package/dist/routes/config/http/envelope.d.ts +1 -0
  327. package/dist/routes/config/http/envelope.d.ts.map +1 -0
  328. package/dist/routes/config/index.d.ts +35 -3
  329. package/dist/routes/config/index.d.ts.map +1 -0
  330. package/dist/routes/config/index.js +51 -2
  331. package/dist/routes/config/index.js.map +1 -1
  332. package/dist/routes/config/services.d.ts +5 -4
  333. package/dist/routes/config/services.d.ts.map +1 -0
  334. package/dist/routes/config/services.js +57 -43
  335. package/dist/routes/config/services.js.map +1 -1
  336. package/dist/routes/config/settings.d.ts +1 -0
  337. package/dist/routes/config/settings.d.ts.map +1 -0
  338. package/dist/routes/config/settings.js +98 -49
  339. package/dist/routes/config/settings.js.map +1 -1
  340. package/dist/routes/debug.d.ts +1 -0
  341. package/dist/routes/debug.d.ts.map +1 -0
  342. package/dist/routes/debug.js +277 -139
  343. package/dist/routes/debug.js.map +1 -1
  344. package/dist/routes/health.d.ts +39 -1
  345. package/dist/routes/health.d.ts.map +1 -0
  346. package/dist/routes/health.js +33 -27
  347. package/dist/routes/health.js.map +1 -1
  348. package/dist/routes/hls.d.ts +1 -0
  349. package/dist/routes/hls.d.ts.map +1 -0
  350. package/dist/routes/icons.d.ts +1 -0
  351. package/dist/routes/icons.d.ts.map +1 -0
  352. package/dist/routes/icons.js +1 -1
  353. package/dist/routes/index.d.ts +1 -0
  354. package/dist/routes/index.d.ts.map +1 -0
  355. package/dist/routes/logs.d.ts +1 -0
  356. package/dist/routes/logs.d.ts.map +1 -0
  357. package/dist/routes/logs.js +14 -4
  358. package/dist/routes/logs.js.map +1 -1
  359. package/dist/routes/mpegts.d.ts +1 -0
  360. package/dist/routes/mpegts.d.ts.map +1 -0
  361. package/dist/routes/play.d.ts +1 -0
  362. package/dist/routes/play.d.ts.map +1 -0
  363. package/dist/routes/playlist.d.ts +1 -0
  364. package/dist/routes/playlist.d.ts.map +1 -0
  365. package/dist/routes/playlist.js +7 -2
  366. package/dist/routes/playlist.js.map +1 -1
  367. package/dist/routes/root/content.d.ts +8 -5
  368. package/dist/routes/root/content.d.ts.map +1 -0
  369. package/dist/routes/root/content.js +29 -26
  370. package/dist/routes/root/content.js.map +1 -1
  371. package/dist/routes/root/index.d.ts +2 -1
  372. package/dist/routes/root/index.d.ts.map +1 -0
  373. package/dist/routes/root/index.js +26 -21
  374. package/dist/routes/root/index.js.map +1 -1
  375. package/dist/routes/root/scripts/channels.d.ts +6 -0
  376. package/dist/routes/root/scripts/channels.d.ts.map +1 -0
  377. package/dist/routes/root/scripts/channels.js +197 -118
  378. package/dist/routes/root/scripts/channels.js.map +1 -1
  379. package/dist/routes/root/scripts/clientEscape.d.ts +22 -0
  380. package/dist/routes/root/scripts/clientEscape.d.ts.map +1 -0
  381. package/dist/routes/root/scripts/clientEscape.js +41 -0
  382. package/dist/routes/root/scripts/clientEscape.js.map +1 -0
  383. package/dist/routes/root/scripts/clientUrl.d.ts +23 -0
  384. package/dist/routes/root/scripts/clientUrl.d.ts.map +1 -0
  385. package/dist/routes/root/scripts/clientUrl.js +44 -0
  386. package/dist/routes/root/scripts/clientUrl.js.map +1 -0
  387. package/dist/routes/root/scripts/config.d.ts +6 -0
  388. package/dist/routes/root/scripts/config.d.ts.map +1 -0
  389. package/dist/routes/root/scripts/config.js +110 -24
  390. package/dist/routes/root/scripts/config.js.map +1 -1
  391. package/dist/routes/root/scripts/index.d.ts +1 -0
  392. package/dist/routes/root/scripts/index.d.ts.map +1 -0
  393. package/dist/routes/root/scripts/shared.d.ts +1 -0
  394. package/dist/routes/root/scripts/shared.d.ts.map +1 -0
  395. package/dist/routes/root/scripts/shared.js +112 -26
  396. package/dist/routes/root/scripts/shared.js.map +1 -1
  397. package/dist/routes/root/scripts/status.d.ts +1 -0
  398. package/dist/routes/root/scripts/status.d.ts.map +1 -0
  399. package/dist/routes/root/scripts/status.handlers.d.ts +23 -33
  400. package/dist/routes/root/scripts/status.handlers.d.ts.map +1 -0
  401. package/dist/routes/root/scripts/status.handlers.js +46 -92
  402. package/dist/routes/root/scripts/status.handlers.js.map +1 -1
  403. package/dist/routes/root/scripts/status.js +21 -8
  404. package/dist/routes/root/scripts/status.js.map +1 -1
  405. package/dist/routes/root/styles.d.ts +1 -0
  406. package/dist/routes/root/styles.d.ts.map +1 -0
  407. package/dist/routes/root/styles.js +7 -4
  408. package/dist/routes/root/styles.js.map +1 -1
  409. package/dist/routes/services.d.ts +13 -1
  410. package/dist/routes/services.d.ts.map +1 -0
  411. package/dist/routes/services.js +81 -68
  412. package/dist/routes/services.js.map +1 -1
  413. package/dist/routes/sse.d.ts +1 -0
  414. package/dist/routes/sse.d.ts.map +1 -0
  415. package/dist/routes/sse.js +1 -1
  416. package/dist/routes/sse.js.map +1 -1
  417. package/dist/routes/streams.d.ts +2 -1
  418. package/dist/routes/streams.d.ts.map +1 -0
  419. package/dist/routes/streams.js +6 -4
  420. package/dist/routes/streams.js.map +1 -1
  421. package/dist/routes/theme.d.ts +3 -2
  422. package/dist/routes/theme.d.ts.map +1 -0
  423. package/dist/routes/theme.js +6 -5
  424. package/dist/routes/theme.js.map +1 -1
  425. package/dist/routes/ui.d.ts +3 -1
  426. package/dist/routes/ui.d.ts.map +1 -0
  427. package/dist/routes/ui.js +16 -4
  428. package/dist/routes/ui.js.map +1 -1
  429. package/dist/routes/upgrade.d.ts +1 -0
  430. package/dist/routes/upgrade.d.ts.map +1 -0
  431. package/dist/routes/upgrade.js +6 -2
  432. package/dist/routes/upgrade.js.map +1 -1
  433. package/dist/service/commands.context.d.ts +1 -0
  434. package/dist/service/commands.context.d.ts.map +1 -0
  435. package/dist/service/commands.context.js +4 -3
  436. package/dist/service/commands.context.js.map +1 -1
  437. package/dist/service/commands.d.ts +1 -0
  438. package/dist/service/commands.d.ts.map +1 -0
  439. package/dist/service/commands.js +10 -13
  440. package/dist/service/commands.js.map +1 -1
  441. package/dist/service/generators.context.d.ts +1 -0
  442. package/dist/service/generators.context.d.ts.map +1 -0
  443. package/dist/service/generators.context.js +1 -1
  444. package/dist/service/generators.context.js.map +1 -1
  445. package/dist/service/generators.d.ts +4 -3
  446. package/dist/service/generators.d.ts.map +1 -0
  447. package/dist/service/generators.js +111 -31
  448. package/dist/service/generators.js.map +1 -1
  449. package/dist/service/index.d.ts +1 -0
  450. package/dist/service/index.d.ts.map +1 -0
  451. package/dist/streaming/captureLock.d.ts +44 -0
  452. package/dist/streaming/captureLock.d.ts.map +1 -0
  453. package/dist/streaming/captureLock.js +95 -0
  454. package/dist/streaming/captureLock.js.map +1 -0
  455. package/dist/streaming/captureSession.d.ts +31 -0
  456. package/dist/streaming/captureSession.d.ts.map +1 -0
  457. package/dist/streaming/captureSession.js +54 -0
  458. package/dist/streaming/captureSession.js.map +1 -0
  459. package/dist/streaming/clients.d.ts +1 -0
  460. package/dist/streaming/clients.d.ts.map +1 -0
  461. package/dist/streaming/codec.d.ts +1 -0
  462. package/dist/streaming/codec.d.ts.map +1 -0
  463. package/dist/streaming/fmp4Segmenter.d.ts +29 -2
  464. package/dist/streaming/fmp4Segmenter.d.ts.map +1 -0
  465. package/dist/streaming/fmp4Segmenter.js +79 -26
  466. package/dist/streaming/fmp4Segmenter.js.map +1 -1
  467. package/dist/streaming/hls.d.ts +18 -7
  468. package/dist/streaming/hls.d.ts.map +1 -0
  469. package/dist/streaming/hls.js +220 -120
  470. package/dist/streaming/hls.js.map +1 -1
  471. package/dist/streaming/hlsResume.d.ts +1 -0
  472. package/dist/streaming/hlsResume.d.ts.map +1 -0
  473. package/dist/streaming/hlsResume.js +1 -1
  474. package/dist/streaming/hlsResume.js.map +1 -1
  475. package/dist/streaming/hlsSegments.d.ts +59 -2
  476. package/dist/streaming/hlsSegments.d.ts.map +1 -0
  477. package/dist/streaming/hlsSegments.js +147 -17
  478. package/dist/streaming/hlsSegments.js.map +1 -1
  479. package/dist/streaming/lifecycle.d.ts +4 -1
  480. package/dist/streaming/lifecycle.d.ts.map +1 -0
  481. package/dist/streaming/lifecycle.js +65 -85
  482. package/dist/streaming/lifecycle.js.map +1 -1
  483. package/dist/streaming/monitor.d.ts +6 -4
  484. package/dist/streaming/monitor.d.ts.map +1 -0
  485. package/dist/streaming/monitor.js +343 -145
  486. package/dist/streaming/monitor.js.map +1 -1
  487. package/dist/streaming/mp4Parser.d.ts +5 -4
  488. package/dist/streaming/mp4Parser.d.ts.map +1 -0
  489. package/dist/streaming/mp4Parser.js +36 -13
  490. package/dist/streaming/mp4Parser.js.map +1 -1
  491. package/dist/streaming/mpegts.d.ts +16 -0
  492. package/dist/streaming/mpegts.d.ts.map +1 -0
  493. package/dist/streaming/mpegts.js +61 -17
  494. package/dist/streaming/mpegts.js.map +1 -1
  495. package/dist/streaming/playlistBuilder.d.ts +1 -0
  496. package/dist/streaming/playlistBuilder.d.ts.map +1 -0
  497. package/dist/streaming/preroll.d.ts +32 -5
  498. package/dist/streaming/preroll.d.ts.map +1 -0
  499. package/dist/streaming/preroll.js +70 -32
  500. package/dist/streaming/preroll.js.map +1 -1
  501. package/dist/streaming/pretune.d.ts +26 -1
  502. package/dist/streaming/pretune.d.ts.map +1 -0
  503. package/dist/streaming/pretune.js +22 -24
  504. package/dist/streaming/pretune.js.map +1 -1
  505. package/dist/streaming/pretuneTimers.d.ts +24 -0
  506. package/dist/streaming/pretuneTimers.d.ts.map +1 -0
  507. package/dist/streaming/pretuneTimers.js +56 -0
  508. package/dist/streaming/pretuneTimers.js.map +1 -0
  509. package/dist/streaming/recovery.d.ts +139 -5
  510. package/dist/streaming/recovery.d.ts.map +1 -0
  511. package/dist/streaming/recovery.js +160 -16
  512. package/dist/streaming/recovery.js.map +1 -1
  513. package/dist/streaming/registry.d.ts +27 -9
  514. package/dist/streaming/registry.d.ts.map +1 -0
  515. package/dist/streaming/registry.js +21 -3
  516. package/dist/streaming/registry.js.map +1 -1
  517. package/dist/streaming/setup.d.ts +157 -29
  518. package/dist/streaming/setup.d.ts.map +1 -0
  519. package/dist/streaming/setup.js +1034 -458
  520. package/dist/streaming/setup.js.map +1 -1
  521. package/dist/streaming/showInfo.d.ts +21 -1
  522. package/dist/streaming/showInfo.d.ts.map +1 -0
  523. package/dist/streaming/showInfo.js +31 -16
  524. package/dist/streaming/showInfo.js.map +1 -1
  525. package/dist/streaming/statusEmitter.d.ts +10 -10
  526. package/dist/streaming/statusEmitter.d.ts.map +1 -0
  527. package/dist/streaming/statusEmitter.js +5 -8
  528. package/dist/streaming/statusEmitter.js.map +1 -1
  529. package/dist/types/channels.d.ts +16 -4
  530. package/dist/types/channels.d.ts.map +1 -0
  531. package/dist/types/channels.js +4 -2
  532. package/dist/types/channels.js.map +1 -1
  533. package/dist/types/config.d.ts +16 -5
  534. package/dist/types/config.d.ts.map +1 -0
  535. package/dist/types/index.d.ts +3 -2
  536. package/dist/types/index.d.ts.map +1 -0
  537. package/dist/types/profiles.d.ts +6 -4
  538. package/dist/types/profiles.d.ts.map +1 -0
  539. package/dist/types/selection.d.ts +31 -3
  540. package/dist/types/selection.d.ts.map +1 -0
  541. package/dist/types/selection.js +4 -2
  542. package/dist/types/selection.js.map +1 -1
  543. package/dist/types/shared.d.ts +1 -0
  544. package/dist/types/shared.d.ts.map +1 -0
  545. package/dist/types/streaming.d.ts +8 -0
  546. package/dist/types/streaming.d.ts.map +1 -0
  547. package/dist/upgrade/commands.context.d.ts +1 -0
  548. package/dist/upgrade/commands.context.d.ts.map +1 -0
  549. package/dist/upgrade/commands.context.js +14 -10
  550. package/dist/upgrade/commands.context.js.map +1 -1
  551. package/dist/upgrade/commands.d.ts +8 -15
  552. package/dist/upgrade/commands.d.ts.map +1 -0
  553. package/dist/upgrade/commands.js +27 -28
  554. package/dist/upgrade/commands.js.map +1 -1
  555. package/dist/upgrade/detection.context.d.ts +1 -0
  556. package/dist/upgrade/detection.context.d.ts.map +1 -0
  557. package/dist/upgrade/detection.context.js +4 -1
  558. package/dist/upgrade/detection.context.js.map +1 -1
  559. package/dist/upgrade/detection.d.ts +9 -7
  560. package/dist/upgrade/detection.d.ts.map +1 -0
  561. package/dist/upgrade/detection.js +23 -54
  562. package/dist/upgrade/detection.js.map +1 -1
  563. package/dist/upgrade/index.d.ts +2 -0
  564. package/dist/upgrade/index.d.ts.map +1 -0
  565. package/dist/upgrade/index.js +1 -0
  566. package/dist/upgrade/index.js.map +1 -1
  567. package/dist/upgrade/lifecycle.context.d.ts +20 -0
  568. package/dist/upgrade/lifecycle.context.d.ts.map +1 -0
  569. package/dist/upgrade/lifecycle.context.js +47 -0
  570. package/dist/upgrade/lifecycle.context.js.map +1 -0
  571. package/dist/upgrade/lifecycle.d.ts +72 -0
  572. package/dist/upgrade/lifecycle.d.ts.map +1 -0
  573. package/dist/upgrade/lifecycle.js +175 -0
  574. package/dist/upgrade/lifecycle.js.map +1 -0
  575. package/dist/upgrade/pathHandle.d.ts +30 -0
  576. package/dist/upgrade/pathHandle.d.ts.map +1 -0
  577. package/dist/upgrade/pathHandle.js +102 -0
  578. package/dist/upgrade/pathHandle.js.map +1 -0
  579. package/dist/utils/bootSession.context.d.ts +16 -0
  580. package/dist/utils/bootSession.context.d.ts.map +1 -0
  581. package/dist/utils/bootSession.context.js +60 -0
  582. package/dist/utils/bootSession.context.js.map +1 -0
  583. package/dist/utils/bootSession.d.ts +17 -0
  584. package/dist/utils/bootSession.d.ts.map +1 -0
  585. package/dist/utils/bootSession.js +20 -0
  586. package/dist/utils/bootSession.js.map +1 -0
  587. package/dist/utils/chromeFetch.d.ts +1 -0
  588. package/dist/utils/chromeFetch.d.ts.map +1 -0
  589. package/dist/utils/cliOutput.d.ts +1 -0
  590. package/dist/utils/cliOutput.d.ts.map +1 -0
  591. package/dist/utils/clock.d.ts +5 -4
  592. package/dist/utils/clock.d.ts.map +1 -0
  593. package/dist/utils/clock.js +19 -16
  594. package/dist/utils/clock.js.map +1 -1
  595. package/dist/utils/debugFilter.d.ts +9 -1
  596. package/dist/utils/debugFilter.d.ts.map +1 -0
  597. package/dist/utils/debugFilter.js +60 -33
  598. package/dist/utils/debugFilter.js.map +1 -1
  599. package/dist/utils/delay.d.ts +48 -15
  600. package/dist/utils/delay.d.ts.map +1 -0
  601. package/dist/utils/delay.js +73 -20
  602. package/dist/utils/delay.js.map +1 -1
  603. package/dist/utils/errors.d.ts +32 -0
  604. package/dist/utils/errors.d.ts.map +1 -0
  605. package/dist/utils/errors.js +45 -4
  606. package/dist/utils/errors.js.map +1 -1
  607. package/dist/utils/evaluate.d.ts +1 -0
  608. package/dist/utils/evaluate.d.ts.map +1 -0
  609. package/dist/utils/evaluate.js +29 -23
  610. package/dist/utils/evaluate.js.map +1 -1
  611. package/dist/utils/ffmpeg.context.d.ts +1 -0
  612. package/dist/utils/ffmpeg.context.d.ts.map +1 -0
  613. package/dist/utils/ffmpeg.context.js.map +1 -1
  614. package/dist/utils/ffmpeg.d.ts +29 -7
  615. package/dist/utils/ffmpeg.d.ts.map +1 -0
  616. package/dist/utils/ffmpeg.js +32 -14
  617. package/dist/utils/ffmpeg.js.map +1 -1
  618. package/dist/utils/fileLogger.d.ts +3 -1
  619. package/dist/utils/fileLogger.d.ts.map +1 -0
  620. package/dist/utils/fileLogger.js +64 -29
  621. package/dist/utils/fileLogger.js.map +1 -1
  622. package/dist/utils/format.d.ts +14 -1
  623. package/dist/utils/format.d.ts.map +1 -0
  624. package/dist/utils/format.js +19 -5
  625. package/dist/utils/format.js.map +1 -1
  626. package/dist/utils/index.d.ts +4 -0
  627. package/dist/utils/index.d.ts.map +1 -0
  628. package/dist/utils/index.js +3 -0
  629. package/dist/utils/index.js.map +1 -1
  630. package/dist/utils/logEmitter.d.ts +1 -0
  631. package/dist/utils/logEmitter.d.ts.map +1 -0
  632. package/dist/utils/logger.d.ts +1 -0
  633. package/dist/utils/logger.d.ts.map +1 -0
  634. package/dist/utils/logger.js +10 -6
  635. package/dist/utils/logger.js.map +1 -1
  636. package/dist/utils/m3u.d.ts +3 -2
  637. package/dist/utils/m3u.d.ts.map +1 -0
  638. package/dist/utils/m3u.js +4 -3
  639. package/dist/utils/m3u.js.map +1 -1
  640. package/dist/utils/markup.d.ts +13 -1
  641. package/dist/utils/markup.d.ts.map +1 -0
  642. package/dist/utils/markup.js +28 -4
  643. package/dist/utils/markup.js.map +1 -1
  644. package/dist/utils/memo.d.ts +3 -2
  645. package/dist/utils/memo.d.ts.map +1 -0
  646. package/dist/utils/memo.js +3 -3
  647. package/dist/utils/morganStream.d.ts +1 -0
  648. package/dist/utils/morganStream.d.ts.map +1 -0
  649. package/dist/utils/network.d.ts +1 -0
  650. package/dist/utils/network.d.ts.map +1 -0
  651. package/dist/utils/network.js +2 -2
  652. package/dist/utils/pid.d.ts +3 -25
  653. package/dist/utils/pid.d.ts.map +1 -0
  654. package/dist/utils/pid.js +10 -49
  655. package/dist/utils/pid.js.map +1 -1
  656. package/dist/utils/platform.d.ts +3 -1
  657. package/dist/utils/platform.d.ts.map +1 -0
  658. package/dist/utils/platform.js +2 -1
  659. package/dist/utils/platform.js.map +1 -1
  660. package/dist/utils/processInspector.context.d.ts +8 -0
  661. package/dist/utils/processInspector.context.d.ts.map +1 -0
  662. package/dist/utils/processInspector.context.js +106 -0
  663. package/dist/utils/processInspector.context.js.map +1 -0
  664. package/dist/utils/processInspector.d.ts +55 -0
  665. package/dist/utils/processInspector.d.ts.map +1 -0
  666. package/dist/utils/processInspector.js +127 -0
  667. package/dist/utils/processInspector.js.map +1 -0
  668. package/dist/utils/retry.d.ts +25 -2
  669. package/dist/utils/retry.d.ts.map +1 -0
  670. package/dist/utils/retry.js +33 -6
  671. package/dist/utils/retry.js.map +1 -1
  672. package/dist/utils/runtimeIdentity.context.d.ts +7 -0
  673. package/dist/utils/runtimeIdentity.context.d.ts.map +1 -0
  674. package/dist/utils/runtimeIdentity.context.js +43 -0
  675. package/dist/utils/runtimeIdentity.context.js.map +1 -0
  676. package/dist/utils/runtimeIdentity.d.ts +98 -0
  677. package/dist/utils/runtimeIdentity.d.ts.map +1 -0
  678. package/dist/utils/runtimeIdentity.js +181 -0
  679. package/dist/utils/runtimeIdentity.js.map +1 -0
  680. package/dist/utils/sanitize.d.ts +1 -0
  681. package/dist/utils/sanitize.d.ts.map +1 -0
  682. package/dist/utils/streamContext.d.ts +2 -1
  683. package/dist/utils/streamContext.d.ts.map +1 -0
  684. package/dist/utils/streamContext.js +1 -1
  685. package/dist/utils/timing.d.ts +2 -1
  686. package/dist/utils/timing.d.ts.map +1 -0
  687. package/dist/utils/timing.js +0 -5
  688. package/dist/utils/timing.js.map +1 -1
  689. package/dist/utils/version.d.ts +1 -0
  690. package/dist/utils/version.d.ts.map +1 -0
  691. package/dist/utils/version.js +47 -13
  692. package/dist/utils/version.js.map +1 -1
  693. package/package.json +26 -20
@@ -1,23 +1,19 @@
1
- /* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
- *
3
- * hls.ts: HLS streaming request handlers for PrismCast.
4
- */
5
- import { getEffectiveCaptureCodec, isCaptureHardwareAccelerated } from "./codec.js";
6
1
  import { LOG, formatError, runWithStreamContext, startTimer } from "../utils/index.js";
7
- import { StreamSetupError, createPageWithCapture, generateStreamId, setupStream } from "./setup.js";
8
- import { createHLSState, getAllStreams, getNextStreamId, getStream, getStreamCount, registerStream, updateLastAccess } from "./registry.js";
2
+ import { StreamSetupError, createPageWithCapture, generateStreamId, reestablishChannelManifest, setupStream, validateStreamUrl } from "./setup.js";
3
+ import { buildProbeCacheStamp, clearProbeCache } from "../native/probe.js";
4
+ import { cancelPrerollTimer, createHLSState, getAllStreams, getNextStreamId, getStream, getStreamCount, registerStream, updateLastAccess } from "./registry.js";
9
5
  import { createInitialStreamStatus, emitStreamAdded } from "./statusEmitter.js";
10
6
  import { deleteResumeData, getResumeSegmentIndex, peekResumeData } from "./hlsResume.js";
11
7
  import { emitCurrentSystemStatus, isLoginModeActive, unregisterManagedPage } from "../browser/index.js";
12
8
  import { generatePrerollPlaylist, getPrerollCodec, getPrerollSegmentCount, isPrerollReady } from "./preroll.js";
13
9
  import { getAllChannels, getChannelLogo, isPredefinedChannelDisabled } from "../config/userChannels.js";
14
- import { getAudioPlaylist, getAudioSegment, getInitSegment, getPlaylist, getSegment, getVideoPlaylist, waitForPlaylist } from "./hlsSegments.js";
10
+ import { getAudioPlaylist, getAudioSegment, getInitSegment, getNamedInitSegment, getPlaylist, getSegment, getVideoPlaylist, waitForPlaylist } from "./hlsSegments.js";
15
11
  import { getAuthDomainForChannel, getResolvedChannel, getServiceTagForChannel, isChannelAvailableByService, resolveServiceKey } from "../config/services.js";
16
12
  import { getChannelStreamId, isTerminationInitiated, setChannelStreamId, terminateStream } from "./lifecycle.js";
13
+ import { getEffectiveCaptureCodec, isCaptureHardwareAccelerated } from "./codec.js";
17
14
  import { markChannelFailure, markChannelSuccess } from "../config/health.js";
18
15
  import { CONFIG } from "../config/index.js";
19
16
  import { attemptNativeStreaming } from "../native/index.js";
20
- import { clearProbeCache } from "../native/probe.js";
21
17
  import { createFMP4Segmenter } from "./fmp4Segmenter.js";
22
18
  import { createHash } from "node:crypto";
23
19
  import { getProviderBySlug } from "../browser/channelSelection.js";
@@ -41,14 +37,15 @@ import { triggerShowNameUpdate } from "./showInfo.js";
41
37
  * the proxy takes over. Otherwise, createCaptureSegmenter() creates the fMP4 segmenter and pipes the capture stream. When the first real playlist arrives
42
38
  * (from either the segmenter or native proxy), the preroll timer is cancelled and the client receives live content on the next poll.
43
39
  *
44
- * Shared streams: If multiple clients request the same channel (or the same ad-hoc URL with the same profile), they share one stream. The first client triggers
45
- * stream creation, and subsequent clients get the existing playlist and segments. Ad-hoc streams are identified by a synthetic key ("play-<hash>") derived from the
46
- * URL and profile, allowing them to use the same channelToStreamId deduplication mechanism as predefined channels.
40
+ * Shared streams: If multiple clients request the same channel (or the same ad-hoc URL with the same profile, selector, clickToPlay, and clickSelector), they share
41
+ * one stream. The first client triggers stream creation, and subsequent clients get the existing playlist and segments. Ad-hoc streams are identified by a
42
+ * synthetic key ("play-<hash>") derived from the URL, profile, selector, clickToPlay, and clickSelector, allowing them to use the same channelToStreamId
43
+ * deduplication mechanism as predefined channels.
47
44
  */
48
45
  // Delay before seeding the preroll playlist in milliseconds. If stream setup completes before this timer fires, the preroll is skipped and the client receives real
49
46
  // content directly. This ensures fast-tuning services (native HLS at 2-3s, most capture services at 4-7s) never see preroll, while slow services (Xfinity/Cox at
50
47
  // 13-15s) get preroll content after the delay to prevent HTTP timeouts.
51
- const PREROLL_DELAY_MS = 9_000;
48
+ const PREROLL_DELAY_MS = 9000;
52
49
  /**
53
50
  * Builds the onError/onStop callbacks for createFMP4Segmenter. Both callbacks share the same termination chain (skip-if-already-terminating guard, log error,
54
51
  * terminate stream, emit current system status); the only differences between callsites are the log-message and termination-reason suffixes used to mark the
@@ -83,8 +80,8 @@ function buildSegmenterTerminationHandlers(streamId, channelName, options = {})
83
80
  // Login mode error body used by both validateChannel() and handlePlayStream() to ensure consistent response format.
84
81
  const LOGIN_MODE_BODY = { error: "Login in progress", message: "Please complete authentication before starting new streams." };
85
82
  /**
86
- * Validates a channel name for streaming. Performs all fast, synchronous checks: disabled status, service resolution, channel lookup, and login mode. Returns a
87
- * discriminated union so callers can handle success and failure without coupling to Express response objects.
83
+ * Validates a channel name for streaming. Performs all fast, synchronous checks: disabled status, service resolution, channel lookup, service-filter availability,
84
+ * and login mode. Returns a discriminated union so callers can handle success and failure without coupling to Express response objects.
88
85
  *
89
86
  * This is extracted from ensureChannelStream() so it can be called by both HLS and MPEG-TS code paths without duplicating the validation logic.
90
87
  *
@@ -107,7 +104,8 @@ export function validateChannel(channelName) {
107
104
  if (!channel && (resolvedKey !== channelName)) {
108
105
  LOG.warn("Service '%s' not found for channel '%s'. Using default service.", resolvedKey, channelName);
109
106
  }
110
- // Runtime check needed even though TypeScript thinks channel is always defined (Record indexing quirk).
107
+ // effectiveChannel may be undefined: getResolvedChannel can return undefined and the getAllChannels() fallback is also possibly-undefined under
108
+ // noUncheckedIndexedAccess, so we guard before use.
111
109
  if (!effectiveChannel) {
112
110
  return { body: "Channel not found.", statusCode: 404, valid: false };
113
111
  }
@@ -139,6 +137,34 @@ export function sendValidationError(validation, res) {
139
137
  res.status(validation.statusCode).send(validation.body);
140
138
  }
141
139
  }
140
+ // Capacity Reservation.
141
+ /**
142
+ * Pure predicate for the concurrent-stream capacity decision. Returns true when a new stream may be admitted given the current active count and the configured
143
+ * limit. Extracted as a standalone function so the boundary arithmetic is pinned by a unit test without driving a browser. The count passed in must be the count
144
+ * BEFORE the new stream's pending entry is registered, so the new stream is naturally excluded from its own capacity check; admitting the final slot
145
+ * (active === maxConcurrent - 1) returns true.
146
+ * @param activeCount - Number of streams currently registered, excluding the stream being admitted.
147
+ * @param maxConcurrent - The configured concurrent-stream limit.
148
+ * @returns True if a slot is available for the new stream.
149
+ */
150
+ export function hasStreamCapacity(activeCount, maxConcurrent) {
151
+ return activeCount < maxConcurrent;
152
+ }
153
+ /**
154
+ * Reserves a concurrent-stream slot at the registration site - the single source of truth for the capacity decision across both the preroll path
155
+ * (ensureChannelStream) and the blocking path (initializeStream). It evaluates capacity against the current registry count BEFORE the new stream's pending entry is
156
+ * registered, so the new stream is excluded from its own check. setupStream never re-checks capacity; this registration-site reservation is the sole authority for
157
+ * the decision, so at the boundary (one slot free) the registration-site gate admits the stream and no downstream check can double-count it against its own slot.
158
+ * When the limit is reached, a single idle stream is reclaimed to free a slot; reservation succeeds when either a slot was already free or one was reclaimed.
159
+ * @returns True if a slot was reserved (room existed or an idle stream was reclaimed), false if the limit is reached and no idle stream could be reclaimed.
160
+ */
161
+ function reserveStreamSlot() {
162
+ if (hasStreamCapacity(getStreamCount(), CONFIG.streaming.maxConcurrentStreams)) {
163
+ return true;
164
+ }
165
+ // At capacity. Reclaim the longest-idle stream to make room; reservation succeeds only if one was actually freed.
166
+ return reclaimIdleStream();
167
+ }
142
168
  // Public Endpoint Handlers.
143
169
  /**
144
170
  * Ensures a stream is running for a channel. If no stream exists, registers a pending stream with a preroll playlist (when available) and launches async setup. If a
@@ -174,15 +200,15 @@ export async function ensureChannelStream(channelName, req, res) {
174
200
  // stream setup runs asynchronously. When the preroll is not available (FFmpeg missing or failed), fall back to blocking initialization.
175
201
  const prerollCodec = getPrerollCodec();
176
202
  if (isPrerollReady(prerollCodec)) {
177
- // Check capacity before registering the pending entry. The pending entry occupies a registry slot, so registering it at max capacity would cause setupStream's
178
- // capacity check to reject - after we've already returned a preroll playlist to the client. By gating here, we can still send a proper 503 error response.
179
- if (getStreamCount() >= CONFIG.streaming.maxConcurrentStreams) {
180
- if (!reclaimIdleStream()) {
181
- res.setHeader("Retry-After", "10");
182
- res.setHeader("X-HDHomeRun-Error", "All Tuners In Use");
183
- res.status(503).send("Maximum concurrent streams (" + String(CONFIG.streaming.maxConcurrentStreams) + ") reached. Try again later.");
184
- return null;
185
- }
203
+ // Reserve a capacity slot before registering the pending entry. The pending entry occupies a registry slot the instant it is registered, so the capacity
204
+ // decision must happen here - while the new stream is still excluded from the count - to avoid a later self-counting check rejecting it after the client has
205
+ // already received a preroll playlist. reserveStreamSlot is the single source of truth for this decision and reclaims an idle stream when at the limit. On
206
+ // failure we send a proper 503 here, before any registration or preroll response.
207
+ if (!reserveStreamSlot()) {
208
+ res.setHeader("Retry-After", "10");
209
+ res.setHeader("X-HDHomeRun-Error", "All Tuners In Use");
210
+ res.status(503).send("Maximum concurrent streams (" + String(CONFIG.streaming.maxConcurrentStreams) + ") reached. Try again later.");
211
+ return null;
186
212
  }
187
213
  const clientAddress = req.ip ?? req.socket.remoteAddress ?? null;
188
214
  const pending = registerPendingStream(channelName, validation.channel, clientAddress, req, prerollCodec);
@@ -258,8 +284,10 @@ export function handleHLSSegment(req, res) {
258
284
  sendSegment(initSegment, "init.mp4", res);
259
285
  return;
260
286
  }
261
- // Handle media segments (.m4s and .ts). Check both video and audio segment stores.
262
- const segment = getSegment(streamId, segmentName) ?? getAudioSegment(streamId, segmentName);
287
+ // Handle media segments (.m4s and .ts) and the named initialization segments a native fMP4 relay serves. Check the video and audio segment stores, then the
288
+ // relay's per-track init store - a name the relay minted for an upstream #EXT-X-MAP reference resolves here rather than through the literal branch above,
289
+ // which belongs to the capture path's single init slot.
290
+ const segment = getSegment(streamId, segmentName) ?? getAudioSegment(streamId, segmentName) ?? getNamedInitSegment(streamId, segmentName);
263
291
  if (!segment) {
264
292
  res.status(404).send("Segment not found.");
265
293
  return;
@@ -307,13 +335,13 @@ export function handleHLSVariantPlaylist(req, res) {
307
335
  }
308
336
  // Ad-Hoc Streaming.
309
337
  /**
310
- * Handles ad-hoc stream requests for arbitrary URLs. Generates a deterministic synthetic key from the URL and profile, starts a stream if none exists, and redirects
311
- * to the standard HLS playlist path. This enables streaming URLs that are not predefined as channels.
338
+ * Handles ad-hoc stream requests for arbitrary URLs. Generates a deterministic synthetic key from the URL, profile, selector, clickToPlay, and clickSelector, starts
339
+ * a stream if none exists, and redirects to the standard HLS playlist path. This enables streaming URLs that are not predefined as channels.
312
340
  *
313
- * The synthetic key includes the profile so that the same URL with different profiles produces separate streams. The "play-" prefix prevents collisions with
314
- * predefined channel names.
341
+ * The synthetic key includes the profile, selector, clickToPlay, and clickSelector so that the same URL with different options produces separate streams. The
342
+ * "play-" prefix prevents collisions with predefined channel names.
315
343
  *
316
- * Route: GET /play?url=<url>&profile=<name>
344
+ * Route: GET /play?url=<url>&profile=<name>&selector=<selector>&clickToPlay=<bool>&clickSelector=<selector>
317
345
  *
318
346
  * @param req - Express request object.
319
347
  * @param res - Express response object.
@@ -342,6 +370,18 @@ export async function handlePlayStream(req, res) {
342
370
  res.status(503).json(LOGIN_MODE_BODY);
343
371
  return;
344
372
  }
373
+ /* Reject a URL that cannot work before anything is reserved on its behalf. initializeStream takes a concurrent-stream slot and registers a pending entry
374
+ * before the deeper validation inside setupStream runs, so a URL destined to be refused would otherwise hold capacity a real stream could need, and would
375
+ * reach the redirect probe that issues an outbound request on its behalf first. The placement is deliberate in both directions: ahead of initializeStream so
376
+ * nothing is reserved, and behind the login-mode check so a URL submitted during login still answers 503 rather than 400. The same validator also runs inside
377
+ * setupStream, which is where channel-based streams meet it; one shared rule called from each entry point, not a second copy of the rule.
378
+ */
379
+ const validation = validateStreamUrl(url);
380
+ if (!validation.valid) {
381
+ LOG.error("Invalid URL requested: %s - %s.", url, validation.reason ?? "Unknown error");
382
+ res.status(400).send(validation.reason ?? "Invalid URL.");
383
+ return;
384
+ }
345
385
  // Capture client IP for Channels DVR API integration.
346
386
  const clientAddress = req.ip ?? req.socket.remoteAddress ?? null;
347
387
  // Start a new ad-hoc stream. initializeStream handles pending registration, capture setup, segmenter creation, and event emission.
@@ -381,7 +421,7 @@ async function sendPlaylistResponse(streamId, clientAddress, res) {
381
421
  // The playlist may not be populated yet if the deferred preroll timer hasn't fired and the segmenter hasn't produced content. Wait for the playlistReady promise
382
422
  // which resolves when either source provides a playlist. The 30-second timeout covers pathological cases like setup hanging.
383
423
  if (!playlist) {
384
- const ready = await waitForPlaylist(streamId, 30_000);
424
+ const ready = await waitForPlaylist(streamId, 30000);
385
425
  if (ready) {
386
426
  playlist = getPlaylist(streamId);
387
427
  }
@@ -431,18 +471,6 @@ function sendSegment(data, segmentName, res) {
431
471
  res.send(data);
432
472
  }
433
473
  // Stream Lifecycle.
434
- /**
435
- * Cleans up resources from a stream that was terminated during setup before the segmenter was stored in the registry. This handles the rare race condition where
436
- * another code path (e.g., circuit breaker) calls terminateStream() between registerStream() and the segmenter assignment.
437
- *
438
- * terminateStream() already cleaned up the page, monitor, channel mapping, and registry entry. We only need to stop the segmenter, which was created after
439
- * termination occurred and therefore wasn't cleaned up.
440
- * @param segmenter - The orphaned fMP4 segmenter instance to stop.
441
- */
442
- function cleanupOrphanedSetup(segmenter) {
443
- LOG.debug("streaming:setup", "Stream was terminated during setup. Stopping orphaned segmenter.");
444
- segmenter.stop();
445
- }
446
474
  // Maps vertical resolution heights to standard display labels. Used by formatNativeQuality() to convert "1920x1080" -> "1080p".
447
475
  const RESOLUTION_LABELS = { "1080": "1080p", "2160": "4K", "360": "360p", "480": "480p", "720": "720p" };
448
476
  /**
@@ -506,26 +534,18 @@ function createTabReplacementHandler(numericStreamId, streamId, channelName, url
506
534
  // Get the current init segment, segment index, and per-track timestamps from the old segmenter before stopping it. The init segment enables discontinuity
507
535
  // suppression when codec parameters are unchanged, the segment index allows the new segmenter to continue numbering, and the track timestamps ensure monotonic
508
536
  // baseMediaDecodeTime across capture restarts.
509
- const currentInitSegment = stream.segmenter?.getInitSegment();
510
- const currentInitVersion = stream.segmenter?.getInitVersion() ?? 0;
511
- const currentSegmentIndex = stream.segmenter?.getSegmentIndex() ?? 0;
512
- const currentSessionStats = stream.segmenter?.getSessionStats();
513
- const currentTrackTimestamps = stream.segmenter?.getTrackTimestamps();
514
- // Destroy the OLD capture stream first. This MUST happen before closing the page to ensure chrome.tabCapture releases the capture. Without this, the new
515
- // getStream() call would hang with "Cannot capture a tab with an active stream" error.
516
- if (stream.rawCaptureStream && !stream.rawCaptureStream.destroyed) {
517
- LOG.debug("recovery:tab", "Destroying old capture stream for tab replacement.");
518
- stream.rawCaptureStream.destroy();
519
- }
520
- // Stop the current segmenter if it exists.
521
- if (stream.segmenter) {
522
- LOG.debug("recovery:tab", "Stopping current segmenter for tab replacement.");
523
- stream.segmenter.stop();
524
- }
525
- // Stop the FFmpeg process if it exists.
526
- if (stream.ffmpegProcess) {
527
- LOG.debug("recovery:tab", "Stopping FFmpeg process for tab replacement.");
528
- stream.ffmpegProcess.kill();
537
+ const oldSegmenter = stream.captureSession?.segmenter;
538
+ const currentInitSegment = oldSegmenter?.getInitSegment();
539
+ const currentInitVersion = oldSegmenter?.getInitVersion() ?? 0;
540
+ const currentSegmentIndex = oldSegmenter?.getSegmentIndex() ?? 0;
541
+ const currentSessionStats = oldSegmenter?.getSessionStats();
542
+ const currentTrackTimestamps = oldSegmenter?.getTrackTimestamps();
543
+ // Dispose the OLD capture pipeline. The CaptureSession kills the FFmpeg child first (setting its shuttingDown flag before the capture stream's EOF can reach
544
+ // FFmpeg's stdin), then destroys the capture stream (which MUST happen before the old page is closed below, so chrome.tabCapture releases the capture and the
545
+ // new getStream() does not hang with "Cannot capture a tab with an active stream"), then stops the segmenter. The new pipeline is constructed fresh further down.
546
+ if (stream.captureSession) {
547
+ LOG.debug("recovery:tab", "Disposing old capture pipeline for tab replacement.");
548
+ stream.captureSession.dispose();
529
549
  }
530
550
  // Close the current page. The page may be null for pending entries whose async setup has not yet completed.
531
551
  const oldPage = stream.page;
@@ -544,6 +564,7 @@ function createTabReplacementHandler(numericStreamId, streamId, channelName, url
544
564
  try {
545
565
  captureResult = await createPageWithCapture({
546
566
  comment: metadataComment,
567
+ numericStreamId,
547
568
  onFFmpegError: (error) => {
548
569
  LOG.error("FFmpeg error during tab replacement recovery: %s.", formatError(error));
549
570
  onCircuitBreak();
@@ -559,6 +580,21 @@ function createTabReplacementHandler(numericStreamId, streamId, channelName, url
559
580
  return null;
560
581
  }
561
582
  LOG.debug("timing:tab", "New page with capture created. (+%sms)", tabElapsed());
583
+ // Re-check the stream before adopting the fresh resources. The awaited createPageWithCapture above yields the event loop, so the stream can terminate mid-await;
584
+ // the entry writes below would then attach the new page and capture session to an entry that termination already disposed, orphaning both. Tear the fresh resources
585
+ // down in the same order the old-pipeline teardown above uses - dispose the capture session first so chrome.tabCapture releases cleanly, then unregister the
586
+ // managed page, then close it - and abandon the replacement.
587
+ if (!getStream(numericStreamId)) {
588
+ captureResult.captureSession.dispose();
589
+ unregisterManagedPage(captureResult.page);
590
+ if (!captureResult.page.isClosed()) {
591
+ captureResult.page.close().catch((error) => {
592
+ LOG.debug("recovery:tab", "Page close error during aborted tab replacement: %s.", formatError(error));
593
+ });
594
+ }
595
+ LOG.debug("recovery:tab", "Stream %s terminated during tab replacement - discarded the fresh page and capture session.", streamId);
596
+ return null;
597
+ }
562
598
  // Create a new segmenter for the new capture stream. Continue from the current segment index for playlist continuity, pass the per-track timestamp counters
563
599
  // for monotonic baseMediaDecodeTime, and mark the first segment with a discontinuity tag so clients know the stream parameters may have changed.
564
600
  const newSegmenter = createFMP4Segmenter({
@@ -571,13 +607,11 @@ function createTabReplacementHandler(numericStreamId, streamId, channelName, url
571
607
  startingSegmentIndex: currentSegmentIndex,
572
608
  streamId: numericStreamId
573
609
  });
574
- // Pipe the new capture to the new segmenter.
575
- newSegmenter.pipe(captureResult.captureStream);
576
- // Update the registry entry with the new resources.
577
- stream.ffmpegProcess = captureResult.ffmpegProcess;
610
+ // Wire the new segmenter into the new capture session (attachSegmenter pipes the session's capture output into it), then install the session and page on the
611
+ // registry entry.
612
+ captureResult.captureSession.attachSegmenter(newSegmenter);
613
+ stream.captureSession = captureResult.captureSession;
578
614
  stream.page = captureResult.page;
579
- stream.rawCaptureStream = captureResult.rawCaptureStream;
580
- stream.segmenter = newSegmenter;
581
615
  LOG.info("Tab replacement complete. New capture started with segment continuity.");
582
616
  LOG.debug("timing:tab", "Tab replacement complete. Total: %sms.", tabElapsed());
583
617
  return {
@@ -598,6 +632,13 @@ function createTabReplacementHandler(numericStreamId, streamId, channelName, url
598
632
  */
599
633
  export async function initializeStream(options) {
600
634
  const { channel, channelName, url } = options;
635
+ // Reserve a capacity slot before registering the pending entry. This is the same single-source-of-truth gate used by the preroll path, applied here so the
636
+ // blocking callers (pretune, MPEG-TS, ad-hoc play) gate capacity at the registration site - while the new stream is still excluded from the count - rather than
637
+ // relying on a downstream self-counting check that would double-count this stream against its own slot and spuriously reject the final slot. On failure we throw
638
+ // the same StreamSetupError(503) the callers already handle, so their error responses and HDHomeRun headers are unchanged.
639
+ if (!reserveStreamSlot()) {
640
+ throw new StreamSetupError("Concurrent stream limit reached.", 503, "Maximum concurrent streams (" + String(CONFIG.streaming.maxConcurrentStreams) + ") reached. Try again later.");
641
+ }
601
642
  // Allocate stream IDs. For predefined channels, use the channel name for the stream ID prefix. For ad-hoc streams, omit it so generateStreamId derives a prefix
602
643
  // from the URL (e.g., "foxsports-abc123"), which is more informative in logs.
603
644
  const numericStreamId = getNextStreamId();
@@ -636,7 +677,8 @@ function registerPendingStream(channelName, channel, clientAddress, req, codec)
636
677
  const baseUrl = protocol + "://" + (host ?? fallbackHost);
637
678
  // Create HLS state with a deferred preroll timer. The timer fires after PREROLL_DELAY_MS - if stream setup hasn't completed by then, preroll is seeded and the
638
679
  // playlist response is unblocked. For resume streams, the preroll's MEDIA-SEQUENCE is offset by the saved segment index so it continues from the prior session's
639
- // sequence range. The timer is cancelled by completeStreamSetup() when the segmenter or native proxy is created, preventing races.
680
+ // sequence range. The timer is disarmed through cancelPrerollTimer() once real content arrives or preroll state is otherwise invalidated, so it cannot fire against
681
+ // state that has moved on.
640
682
  const hls = createHLSState();
641
683
  // Snapshot the resume segment index once at registration. Both the preroll timer callback and completeStreamSetup() use this single snapshot, eliminating the TTL
642
684
  // race that would occur if each read the resume map independently at different times.
@@ -673,9 +715,9 @@ function createPendingEntry(options) {
673
715
  const { channel, channelName, hls, numericStreamId, streamIdStr, url } = options;
674
716
  registerStream({
675
717
  captureCodec: null,
718
+ captureSession: null,
676
719
  channelName: channel?.name ?? null,
677
720
  clientAddress: options.clientAddress ?? null,
678
- ffmpegProcess: null,
679
721
  hardwareAccelerated: false,
680
722
  hls,
681
723
  id: numericStreamId,
@@ -683,17 +725,18 @@ function createPendingEntry(options) {
683
725
  lastPlaylistRequest: Date.now(),
684
726
  storeKey: channelName
685
727
  },
728
+ monitor: null,
686
729
  mpegTsClientCount: 0,
687
730
  nativeBandwidth: 0,
731
+ nativeContainer: null,
688
732
  nativeProxy: null,
689
733
  nativeResolution: null,
690
734
  page: null,
691
735
  preTuned: options.preTuned ?? false,
736
+ probeIdentity: null,
692
737
  profile: null,
693
- rawCaptureStream: null,
694
- segmenter: null,
738
+ reestablishManifest: null,
695
739
  startTime: options.streamStartTime ?? new Date(),
696
- stopMonitor: null,
697
740
  streamIdStr,
698
741
  streamingMode: "capture",
699
742
  url
@@ -730,8 +773,9 @@ function handleSetupFailure(numericStreamId, channelName, channel, error, logErr
730
773
  }
731
774
  }
732
775
  /**
733
- * Attempts to upgrade the stream from capture to native HLS. Finalizes the manifest interception, probes the manifest, creates a native proxy, stops the capture
734
- * pipeline, suppresses page audio, and updates the registry entry. Returns the codec and formatted quality string on success, or null if native is not viable.
776
+ * Attempts to upgrade the stream from capture to native HLS. Consumes the manifest interception already finalized by setupStream(), probes the manifest, creates a
777
+ * native proxy, stops the capture pipeline, suppresses page audio, and updates the registry entry. Returns the codec and formatted quality string on success, or
778
+ * null if native is not viable.
735
779
  * @param setup - The stream setup result from setupStream().
736
780
  * @param numericStreamId - The stream's numeric ID.
737
781
  * @param channelName - The channel key for logging and cache operations.
@@ -750,6 +794,12 @@ async function startNativeProxy(setup, numericStreamId, channelName, url, mpegTs
750
794
  // is read dynamically from the stream's HLS state at playlist generation time.
751
795
  const pendingForNative = getStream(numericStreamId);
752
796
  const nativePrerollSegmentCount = pendingForNative?.hls.prerollSegmentCount ?? 0;
797
+ // The re-establishment capability closes over the stream's own tune facts here, in the layer that owns them: a token-refresh reload re-acquires its
798
+ // manifest through the stream's real tune rather than a bare navigation. The page stays a parameter rather than a captured value because the callers
799
+ // own the page reference - the monitor hands its own current page to every recovery action it drives.
800
+ const reestablishManifest = (page) => {
801
+ return reestablishChannelManifest({ channelName: setup.channelName, page, profile: setup.profile, streamIdStr: setup.streamId, url });
802
+ };
753
803
  const nativeResult = await attemptNativeStreaming({
754
804
  channelName,
755
805
  interceptionPromise: setup.manifestInterception.promise,
@@ -763,9 +813,23 @@ async function startNativeProxy(setup, numericStreamId, channelName, url, mpegTs
763
813
  LOG.warn("Native proxy error for %s: %s. Falling back to capture.", channelName, error);
764
814
  clearProbeCache(channelName);
765
815
  },
816
+ onFeedApplied: (metadata) => {
817
+ // The registry write stays in the streaming layer, exactly as the error report above does: native reports upward and the layer that owns the entry is the
818
+ // layer that writes it. A refresh can land on a different rung of the service's ladder, so the status display reads the quality the stream is actually
819
+ // serving rather than the one it tuned to. An entry that is already gone means the stream is terminating, which is nothing to record.
820
+ const refreshed = getStream(numericStreamId);
821
+ if (!refreshed) {
822
+ return;
823
+ }
824
+ refreshed.captureCodec = metadata.codec;
825
+ refreshed.nativeBandwidth = metadata.bandwidth;
826
+ refreshed.nativeResolution = metadata.resolution;
827
+ },
766
828
  page: setup.page,
767
829
  prerollCodec: pendingForNative?.hls.prerollCodec ?? "h264",
768
830
  prerollSegmentCount: nativePrerollSegmentCount,
831
+ probeIdentity: setup.probeIdentity,
832
+ reestablishManifest,
769
833
  streamId: numericStreamId,
770
834
  streamIdStr: setup.streamId,
771
835
  url
@@ -778,18 +842,25 @@ async function startNativeProxy(setup, numericStreamId, channelName, url, mpegTs
778
842
  nativeResult.proxy.stop();
779
843
  return null;
780
844
  }
781
- // Stop the capture pipeline - native streaming replaces it entirely.
782
- if (!setup.rawCaptureStream.destroyed) {
783
- setup.rawCaptureStream.destroy();
784
- }
785
- if (setup.ffmpegProcess) {
786
- setup.ffmpegProcess.kill();
787
- }
845
+ // Dispose the capture pipeline - native streaming replaces it entirely. No segmenter has been attached yet (native is attempted before createCaptureSegmenter), so
846
+ // disposal kills the FFmpeg child and destroys the capture stream.
847
+ setup.captureSession.dispose();
848
+ // Consume the persisted capture-mode resume data for this channel. Resume data seeds an fMP4 segmenter's starting sequence and init segment for capture continuity
849
+ // across restarts; createCaptureSegmenter consumes it on the capture path. On this native-upgrade path that segmenter is never created, so without this delete the
850
+ // resume entry would linger until TTL and could be mis-applied to a later capture-mode stream for the same channel. Native streaming carries its own MPEG-TS
851
+ // sequencing, so the capture resume state is obsolete the moment we commit to native.
852
+ deleteResumeData(channelName);
788
853
  // Update the registry entry to reflect native mode. For streams with separate audio, clear preroll state - preroll is muxed video+audio and can't be
789
854
  // split into separate renditions. For muxed-audio streams, preserve preroll state so the proxy can build composite playlists with preroll entries.
790
- currentStream.ffmpegProcess = null;
855
+ //
856
+ // The separate-audio path disarms the preroll timer alongside nulling the fields. The timer's callback closed over its own base-URL and codec copies at
857
+ // registration, so nulling the fields silences only sendPlaylistResponse's per-poll regeneration, not the one-shot overwrite the callback would still perform when
858
+ // it fires. Disarming here completes the invalidation. One residual is inherent to preroll-for-hasAudio: if the native tune already exceeded PREROLL_DELAY_MS the
859
+ // timer has already fired, so the nulled fields freeze the already-written preroll playlist until the proxy's first real content arrives - bounded, not a stall.
860
+ currentStream.captureSession = null;
791
861
  currentStream.hls.hasAudio = nativeResult.hasAudio;
792
862
  if (nativeResult.hasAudio) {
863
+ cancelPrerollTimer(currentStream.hls);
793
864
  currentStream.hls.prerollBaseUrl = null;
794
865
  currentStream.hls.prerollCodec = null;
795
866
  currentStream.hls.prerollSegmentCount = 0;
@@ -797,15 +868,20 @@ async function startNativeProxy(setup, numericStreamId, channelName, url, mpegTs
797
868
  currentStream.captureCodec = nativeResult.codec;
798
869
  currentStream.hardwareAccelerated = false;
799
870
  currentStream.nativeBandwidth = nativeResult.bandwidth;
871
+ currentStream.nativeContainer = nativeResult.container;
800
872
  currentStream.nativeProxy = nativeResult.proxy;
801
873
  currentStream.nativeResolution = nativeResult.resolution;
802
- currentStream.rawCaptureStream = null;
874
+ currentStream.reestablishManifest = reestablishManifest;
803
875
  currentStream.streamingMode = "native";
804
- // Start the native proxy. Signal init segment readiness immediately - native MPEG-TS segments carry their own PAT/PMT codec configuration in every
805
- // segment, so there is no separate init segment to wait for. Without this, MPEG-TS clients block on waitForInitSegment() and time out before the proxy's
806
- // first poll cycle completes.
876
+ // Start the native proxy. Init readiness is conditional on the container. An MPEG-TS source carries its own PAT/PMT codec configuration in every segment and
877
+ // has no separate init to wait for, so readiness is signaled here and MPEG-TS clients are released immediately rather than blocking on waitForInitSegment()
878
+ // until it times out. An fMP4 source does have an init, and the store signals readiness when the video track's first one lands - video because the consumer
879
+ // waiting on this signal is the remux path, which resolves the video init. A null container reaches this only on a feed the DRM path abandoned, so it takes
880
+ // the immediate signal rather than deadlocking readiness on an init that will never arrive.
807
881
  nativeResult.proxy.start();
808
- currentStream.hls.signalInitSegmentReady();
882
+ if (nativeResult.container !== "fmp4") {
883
+ currentStream.hls.signalInitSegmentReady();
884
+ }
809
885
  // Suppress audio on the browser page. The page stays alive for token refresh but the video element's audio is not part of the native stream - without
810
886
  // suppression, it plays audibly on the local machine.
811
887
  await suppressPageAudio(setup.page);
@@ -813,12 +889,13 @@ async function startNativeProxy(setup, numericStreamId, channelName, url, mpegTs
813
889
  return { codec: nativeResult.codec, quality: formatNativeQuality(nativeResult.bandwidth, nativeResult.codec, nativeResult.resolution) };
814
890
  }
815
891
  /**
816
- * Creates the fMP4 segmenter for capture mode streams. Reads resume data, creates the segmenter with preroll and resume configuration, pipes the capture stream,
817
- * and stores the segmenter in the registry. Resume data is consumed only after the segmenter is successfully stored, ensuring it survives if creation fails.
892
+ * Creates the fMP4 segmenter for capture mode streams. Reads resume data, creates the segmenter with preroll and resume configuration, and attaches it to the
893
+ * capture session (which pipes the session's capture output into it). Resume data is consumed only after the segmenter is successfully attached to a non-disposed
894
+ * session, ensuring it survives if the stream was terminated during setup.
818
895
  * @param setup - The stream setup result from setupStream().
819
896
  * @param numericStreamId - The stream's numeric ID.
820
897
  * @param channelName - The channel key for resume data and logging.
821
- * @returns True if the segmenter was created and stored, false if the stream was terminated during setup.
898
+ * @returns True if the segmenter was attached, false if the stream was terminated during setup (the session was already disposed).
822
899
  */
823
900
  function createCaptureSegmenter(setup, numericStreamId, channelName) {
824
901
  // Peek at resume data from a previous shutdown without consuming it. The data is consumed (deleted) only after the segmenter is successfully created and
@@ -852,25 +929,25 @@ function createCaptureSegmenter(setup, numericStreamId, channelName) {
852
929
  ...buildSegmenterTerminationHandlers(numericStreamId, channelName),
853
930
  streamId: numericStreamId
854
931
  });
855
- // Pipe the capture stream to the segmenter.
856
- segmenter.pipe(setup.captureStream);
857
- // Store the segmenter reference in the registry.
858
- const captureStream = getStream(numericStreamId);
859
- if (captureStream) {
860
- captureStream.segmenter = segmenter;
861
- // Now that the segmenter is created, piped, and stored, consume the resume data so it's not used again on a subsequent stream start for the same channel.
862
- if (resumeData) {
863
- deleteResumeData(channelName);
864
- }
865
- return true;
932
+ // Attach the segmenter to the capture session. attachSegmenter pipes the session's capture output into the segmenter. If the stream was terminated mid-setup, the
933
+ // session is already disposed and attachSegmenter stops the orphan segmenter instead of wiring it - the single home for that rare-race cleanup.
934
+ setup.captureSession.attachSegmenter(segmenter);
935
+ if (setup.captureSession.disposed) {
936
+ // The stream was terminated during setup (rare race). attachSegmenter already stopped the orphan; leave the resume data intact so the next stream start for this
937
+ // channel can retry with it rather than losing it to an HLS sequence reset.
938
+ return false;
939
+ }
940
+ // The segmenter is created, piped, and owned by the session. Consume the resume data so it is not reused on a subsequent stream start for the same channel.
941
+ if (resumeData) {
942
+ deleteResumeData(channelName);
866
943
  }
867
- // Stream was terminated during setup (rare race condition). Clean up the orphaned segmenter.
868
- cleanupOrphanedSetup(segmenter);
869
- return false;
944
+ return true;
870
945
  }
871
946
  /**
872
947
  * Completes the async portion of stream initialization. Creates the browser page, navigates to the URL, sets up capture, creates the segmenter, and fills in the
873
- * pending registry entry. On failure, cleans up the pending entry via terminateStream().
948
+ * pending registry entry. If the pending entry was terminated during setup (e.g., idle timeout) before this function reaches it, it releases the now-orphaned
949
+ * setup resources via setup.cleanup() and returns null. On a thrown error, it does not clean up the pending entry itself - callers reach handleSetupFailure(), which
950
+ * terminates the pending entry via terminateStream().
874
951
  *
875
952
  * This is the Phase 2 of the two-phase stream initialization. For the non-blocking HLS path, it runs as fire-and-forget via `void`. For the blocking path
876
953
  * (initializeStream), it is awaited directly.
@@ -881,11 +958,31 @@ function createCaptureSegmenter(setup, numericStreamId, channelName) {
881
958
  */
882
959
  async function completeStreamSetup(options) {
883
960
  const { channel, channelName, channelSelector, clickSelector, clickToPlay, mpegTsClient, numericStreamId, profileOverride, streamIdStr, url } = options;
884
- // Circuit breaker callback - terminate the stream on unrecoverable errors.
961
+ /* Build the probe-cache identity once, here, and hand it onward as a value. This is the only frame that holds both halves of it on every entry path: the true
962
+ * per-stream key (channelName is the route key for a predefined tune, the binding hash for an ad-hoc /play, the channel id for a pretune) and the binding
963
+ * inputs the stamp is derived from. Nothing downstream may build one - setupStream's own channelName is deliberately undefined for ad-hoc streams, and a
964
+ * stamp assembled from a frame holding only part of the binding would silently describe a different stream than the one the tune reaches.
965
+ *
966
+ * The effective profile is the query override when one was supplied, otherwise the stored profile: an overridden tune reaches a different stream than the
967
+ * stored profile would, so the stamp honestly differs and an entry cached under the stored profile re-probes once under the override rather than answering
968
+ * for a page it never described.
969
+ */
970
+ const probeIdentity = {
971
+ key: channelName,
972
+ stamp: buildProbeCacheStamp(channel ? { channelSelector: channel.channelSelector, profile: profileOverride ?? channel.profile, url } :
973
+ { channelSelector, profile: profileOverride, url })
974
+ };
975
+ /* Circuit breaker callback - terminates this stream on unrecoverable errors, by the id the closure was built for. Resolving the id through the channel index
976
+ * at fire time could reach a different stream: a channel re-tuned while this callback was in flight is registered under the same name with a newer id, and the
977
+ * breaker's verdict belongs to the stream whose monitor reached it.
978
+ *
979
+ * The registry-presence guard is what makes a repeated or late invocation silent. terminateStream's own guard covers only a termination already underway - its
980
+ * tracking entry is removed when that termination completes - so a second call from an independent site (an FFmpeg error handler firing after the monitor
981
+ * already terminated the stream) would otherwise log a second termination summary and re-emit the stream-removed event for a stream long gone.
982
+ */
885
983
  const onCircuitBreak = () => {
886
- const currentStreamId = getChannelStreamId(channelName);
887
- if (currentStreamId !== undefined) {
888
- terminateStream(currentStreamId, channelName, "too many errors");
984
+ if (getStream(numericStreamId)) {
985
+ terminateStream(numericStreamId, channelName, "too many errors");
889
986
  void emitCurrentSystemStatus();
890
987
  }
891
988
  };
@@ -893,10 +990,10 @@ async function completeStreamSetup(options) {
893
990
  const tabReplacementFactory = (_, streamId, profile, metadataComment) => {
894
991
  return createTabReplacementHandler(numericStreamId, streamId, channelName, url, profile, metadataComment, onCircuitBreak);
895
992
  };
896
- // If at capacity, try to reclaim an idle stream before starting setup. This avoids rejecting new requests when idle streams can be freed.
897
- if (getStreamCount() >= CONFIG.streaming.maxConcurrentStreams) {
898
- reclaimIdleStream();
899
- }
993
+ // Capacity was already reserved at the registration site (reserveStreamSlot in ensureChannelStream for the preroll path, and in initializeStream for the blocking
994
+ // path) before this stream's pending entry was registered, so the new stream is excluded from its own capacity check. We deliberately do NOT re-check or reclaim
995
+ // here: the pending entry is now counted, so a count-based check would double-count this stream against its own slot and could evict a healthy peer at the
996
+ // legitimate boundary. The registration-site reservation is the single source of truth for the capacity decision.
900
997
  // Pass the pre-allocated IDs to setupStream so it uses them instead of generating new ones. This ensures the abort controller, health monitor, and tab replacement
901
998
  // handler all reference the same stream identity as the pending registry entry. Pass channelName only for predefined channels - for ad-hoc streams, omitting it
902
999
  // causes generateStreamId to derive the stream ID string from the URL (e.g., "foxsports-abc123"), which is more informative in logs.
@@ -908,6 +1005,7 @@ async function completeStreamSetup(options) {
908
1005
  clickToPlay: channel ? undefined : clickToPlay,
909
1006
  numericStreamId,
910
1007
  onTabReplacementFactory: tabReplacementFactory,
1008
+ probeIdentity,
911
1009
  profileOverride,
912
1010
  streamId: streamIdStr,
913
1011
  url
@@ -921,12 +1019,12 @@ async function completeStreamSetup(options) {
921
1019
  });
922
1020
  return null;
923
1021
  }
924
- stream.ffmpegProcess = setup.ffmpegProcess;
1022
+ stream.captureSession = setup.captureSession;
1023
+ stream.monitor = setup.monitor;
925
1024
  stream.page = setup.page;
1025
+ stream.probeIdentity = setup.probeIdentity;
926
1026
  stream.profile = setup.profile;
927
- stream.rawCaptureStream = setup.rawCaptureStream;
928
1027
  stream.startTime = setup.startTime;
929
- stream.stopMonitor = setup.stopMonitor;
930
1028
  stream.url = setup.url;
931
1029
  // Continue within stream context for consistent logging.
932
1030
  return runWithStreamContext({ channelName: channel?.name, streamId: setup.streamId, url: setup.url }, async () => {
@@ -953,6 +1051,8 @@ async function completeStreamSetup(options) {
953
1051
  }
954
1052
  const effectiveCodec = getEffectiveCaptureCodec();
955
1053
  const captureHwAccel = isCaptureHardwareAccelerated();
1054
+ // Prefix the codec label with U+26A1, the high-voltage / lightning-bolt glyph, as a visual marker in the "Streaming..." log line that capture is
1055
+ // hardware-accelerated. The glyph is written as an escape so the source stays ASCII; it renders as the lightning bolt in the log output.
956
1056
  const ffmpegCodec = captureHwAccel ? ("\u26A1 " + effectiveCodec.toUpperCase()) : effectiveCodec.toUpperCase();
957
1057
  const captureMode = (streamingMode === "native") ? ("native HLS" + nativeQuality) :
958
1058
  (CONFIG.streaming.captureMode === "ffmpeg" ? "FFmpeg [" + ffmpegCodec + "]" : "Native fMP4");