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,28 +1,86 @@
1
- import { LOG, delay, extractDomain, formatError, raceWithTimeout, registerAbortController, resolveFFmpegPath, retryOperation, runWithStreamContext, spawnFFmpeg, startTimer } from "../utils/index.js";
2
- import { getCurrentBrowser, getStream, minimizeBrowserWindow, registerManagedPage, unregisterManagedPage } from "../browser/index.js";
3
- import { getNextStreamId, getStreamCount } from "./registry.js";
4
- import { getProfileForChannel, getProfileForUrl, getProfiles, resolveProfile } from "../config/profiles.js";
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { BrowserSupersededError, BrowserUnavailableError, getBrowserInstance, getCurrentBrowser, getStream, invalidateBrowser, minimizeBrowserWindow, registerManagedPage, setCaptureProbe, unregisterManagedPage } from "../browser/index.js";
54
+ import { CaptureAbandonedError, createCaptureLock } from "./captureLock.js";
55
+ import { FINALIZE_SETTLE_DELAY, installManifestInterceptor } from "../browser/manifestInterceptor.js";
56
+ import { LOG, delay, extractDomain, formatError, getStreamContext, isStaleCaptureMutexError, maxRetryDuration, realClock, registerAbortController, resolveFFmpegPath, retryOperation, runWithStreamContext, spawnFFmpeg, startTimer, waitWithTimeout } from "../utils/index.js";
57
+ import { getAllStreams, getNextStreamId } from "./registry.js";
58
+ import { getAuthDomainForChannel, getServiceDisplayName, resolveServiceKey } from "../config/services.js";
59
+ import { getBuiltinProfile, getProfileForChannel, getProfileForUrl, resolveProfile } from "../config/profiles.js";
5
60
  import { getProviderByStrategy, invalidateDirectUrl, resolveDirectUrl } from "../browser/channelSelection.js";
6
- import { initializePlayback, injectVideoSelector, navigateToPage } from "../browser/video.js";
61
+ import { initializePlayback, injectVideoSelector, muteExistingVideos, navigateToPage } from "../browser/video.js";
7
62
  import { CONFIG } from "../config/index.js";
8
63
  import { chromeFetch } from "../utils/index.js";
64
+ import { createCaptureSession } from "./captureSession.js";
9
65
  import { getCachedEncryption } from "../native/probe.js";
10
66
  import { getCaptureMimeType } from "./codec.js";
67
+ import { getDomainAuthState } from "../config/health.js";
11
68
  import { getDomainConfig } from "../config/sites.js";
12
69
  import { getEffectiveViewport } from "../config/presets.js";
13
- import { getServiceDisplayName } from "../config/services.js";
14
- import { installManifestInterceptor } from "../browser/manifestInterceptor.js";
70
+ import { getUserProfiles } from "../config/userProfiles.js";
71
+ import { isCaptureInfrastructureError } from "./recovery.js";
15
72
  import { isChannelSelectionProfile } from "../types/index.js";
16
73
  import { monitorPlaybackHealth } from "./monitor.js";
17
74
  import { mutateChannels } from "../config/userChannels.js";
18
75
  import { pipeline } from "node:stream/promises";
19
76
  import { resizeAndMinimizeWindow } from "../browser/cdp.js";
77
+ import { startOverlayHandling } from "../browser/consent.js";
20
78
  /* This module contains the common stream setup logic for HLS streaming. The core logic is split into two functions:
21
79
  *
22
80
  * 1. createPageWithCapture(): Creates a browser page, starts media capture, navigates to the URL, and sets up video playback. This is the reusable core that both
23
81
  * initial stream setup and tab replacement recovery use.
24
82
  *
25
- * 2. setupStream(): Orchestrates stream creation by calling createPageWithCapture(), then registering the stream and starting the health monitor. This is the
83
+ * 2. setupStream(): Orchestrates stream creation by calling createPageWithCapture(), then starting the health monitor. This is the
26
84
  * entry point for new stream requests.
27
85
  *
28
86
  * The separation allows tab replacement recovery (in monitor.ts) to reuse the capture setup logic without duplicating code. When a browser tab becomes unresponsive,
@@ -35,24 +93,105 @@ import { resizeAndMinimizeWindow } from "../browser/cdp.js";
35
93
  * - Video element detection and playback setup
36
94
  *
37
95
  * setupStream() additionally handles:
38
- * - Request validation (URL format, concurrent stream limit)
39
- * - Stream registration
96
+ * - Request validation (URL format only)
40
97
  * - Health monitor startup
41
98
  * - Cleanup function creation
42
99
  */
43
100
  // Native fMP4 capture uses MP4/AAC for direct HLS segmentation without transcoding.
44
101
  const NATIVE_FMP4_MIME_TYPE = "video/mp4;codecs=avc1,mp4a.40.2";
45
- // Capture initialization queue. Chrome's tabCapture extension can only initialize one capture at a time - concurrent getStream() calls fail with "Cannot capture a
46
- // tab with an active stream." We serialize capture initialization using a promise chain so requests execute sequentially. Once a capture is established, it runs
47
- // concurrently with other captures without issue.
48
- let captureQueue = Promise.resolve();
49
- let captureQueueDepth = 0;
50
- // Threshold for logging a warning when the capture queue depth is unusually high. Under normal operation, the queue depth is 0-2. Higher values indicate
51
- // many simultaneous stream requests competing for Chrome's single-threaded capture initialization.
52
- const CAPTURE_QUEUE_DEPTH_WARNING = 5;
53
- // Maximum number of times createPageWithCapture() will retry when it detects that the page was closed while waiting in the capture queue (e.g., due to a browser
54
- // crash). An explicit guard prevents unbounded recursion.
102
+ // Capture initialization is serialized through a task-scoped lock. Chrome's tabCapture extension can only initialize one capture at a time - concurrent getStream()
103
+ // calls fail with "Cannot capture a tab with an active stream" - so every getStream initialization runs as a task on this one process-wide lock, which holds the
104
+ // turn until the task's promise settles. Once a capture is established it runs concurrently with other captures without issue.
105
+ // The wedge-derivation policy for the capture lock. A task held past max(CAPTURE_WEDGE_FLOOR_MS, deadline + CAPTURE_WEDGE_MARGIN_MS) without settling invokes its
106
+ // onWedge callback so the call site can route it into browser recovery. The floor is the wedge bound at the normal navigation timeout; the margin keeps the wedge
107
+ // strictly later than any larger caller deadline.
108
+ const CAPTURE_WEDGE_FLOOR_MS = 30000;
109
+ const CAPTURE_WEDGE_MARGIN_MS = 5000;
110
+ // The one production capture lock. It takes no config-derived values: setup.ts's module body runs before initializeConfiguration(), and streaming.* saves mutate the
111
+ // live CONFIG binding mid-process, so every timing bound is read per call at the call sites instead.
112
+ const captureLock = createCaptureLock({ clock: realClock, wedgeFloorMs: CAPTURE_WEDGE_FLOOR_MS, wedgeMarginMs: CAPTURE_WEDGE_MARGIN_MS });
113
+ // The delay puppeteer-stream's fire-and-forget STOP_RECORDING chain needs to settle after a raw capture stream is destroyed, before the owning page is closed.
114
+ // Destroying the stream triggers STOP_RECORDING via the close handler, but the async chain (STOP_RECORDING -> recorder.stop() -> onstop -> track.stop()) must finish
115
+ // while the browser is still connected, or Chrome's tabCapture state lingers and the next getStream() draws "Cannot capture a tab with an active stream".
116
+ const STOP_RECORDING_SETTLE_MS = 500;
117
+ // The teardown allowance the mid-life probe's outer lock deadline adds over its getStream criterion bound. The pass/fail criterion times getStream alone; this
118
+ // allowance lets the destroy-plus-STOP_RECORDING-settle teardown complete inside the turn without counting against that criterion. A teardown that hangs past the
119
+ // allowance trips the outer deadline and is reported as a capture-infrastructure failure.
120
+ const PROBE_TEARDOWN_ALLOWANCE_MS = 3000;
121
+ // The playback-initialization safety-net timeout. Channel selection plus video setup runs after navigation with no outer timeout racing its internal click
122
+ // retries; for guideGrid strategies a selection failure triggers an overlay dismiss and retry, which doubles the channel-selection budget. 45 seconds accommodates
123
+ // that retry while still preventing a pathological hang if multiple internal timeouts chain sequentially. Consumed by both the phase-2 race and the interception
124
+ // budget below.
125
+ const PLAYBACK_INIT_TIMEOUT = 45000;
126
+ // Margin folded into the interception budget beyond the phases it explicitly sizes: the small span between phase-2 finishing and finalize firing (the window
127
+ // resize and minimize, setupStream's pre-verification work) plus true slack. The interception window is a leak bound for a tune that dies without unwinding, not a
128
+ // latency bound - no healthy path waits on it - so its generosity costs nothing.
129
+ const INTERCEPTION_BUDGET_MARGIN_MS = 5000;
130
+ // The caller-visible capture-timeout messages, each defined once so the lock's deadline errors and any pin test read the exact text. Both match
131
+ // isCaptureInfrastructureError via its "timed out" substring, so they are exported for the classification pin that locks that contract.
132
+ export const STREAM_INIT_TIMEOUT_MESSAGE = "Stream initialization timed out.";
133
+ export const CAPTURE_PROBE_TIMEOUT_MESSAGE = "Capture probe timed out.";
134
+ /**
135
+ * Retires a raw capture stream: destroys it to fire STOP_RECORDING while the browser is still connected, then waits STOP_RECORDING_SETTLE_MS for that fire-and-forget
136
+ * chain to finish before the caller closes the page. This is the destroy-plus-settle core shared by the mid-life probe's success teardown and every path that must
137
+ * retire a capture stream produced after its caller had already abandoned the turn. It is the async sibling of captureSession.ts's disposer, which tears the composed
138
+ * FFmpeg pipeline down synchronously: this helper awaits the settle because a page close follows it directly, and unlike the composed disposer it is used on
139
+ * abandoned paths where the page may already be closed, so the STOP_RECORDING side is then best-effort.
140
+ * @param stream - The raw capture stream to destroy.
141
+ * @param clock - Clock used for the settle delay. Defaults to realClock; tests inject a fake.
142
+ */
143
+ async function retireRawStream(stream, clock = realClock) {
144
+ stream.destroy();
145
+ await clock.sleep(STOP_RECORDING_SETTLE_MS);
146
+ }
147
+ /**
148
+ * Escalates the stale-capture-mutex condition from a single home. A "Cannot capture a tab with an active stream" rejection leaks puppeteer-stream's module-level
149
+ * mutex permanently - a Chrome restart cannot clear module state, so only a fresh process recovers. This logs the one canonical diagnostic with the failing site as
150
+ * structured context, then schedules a deferred process exit. The exit is deferred briefly so the error log flushes to disk first; an immediate exit can truncate the
151
+ * buffered file write and lose the diagnostic that explains the restart. Every caller throws or exits its own path right after, so no further capture work runs in the
152
+ * interim.
153
+ * @param siteContext - A short phrase naming where the stale mutex surfaced, attached as structured log context.
154
+ */
155
+ function escalateStaleCaptureMutex(siteContext) {
156
+ LOG.error("Stale capture state detected. puppeteer-stream's internal capture mutex is now permanently locked and the capture system is unrecoverable. Exiting so " +
157
+ "the service manager can restart with a clean module state.", { site: siteContext });
158
+ setTimeout(() => process.exit(1), 100);
159
+ }
160
+ /**
161
+ * Thrown by the capture-lock task when it finds the page already closed at the instant its turn is granted (a browser crash during the turn-wait). createPageWithCapture
162
+ * catches it to drive the closed-page recursion outside the lock, so the turn releases at once rather than the recursion running while the turn is held. Module-private:
163
+ * it never crosses the function boundary.
164
+ */
165
+ class PageClosedDuringTurnError extends Error {
166
+ constructor() {
167
+ super("Page closed while waiting for its capture turn.");
168
+ this.name = "PageClosedDuringTurnError";
169
+ }
170
+ }
171
+ /**
172
+ * Thrown by createPageWithCapture when an establishment that navigated to a resolved direct watch URL failed on evidence against that URL - the coordinator's own
173
+ * verdict, read from invalidateDirectUrl, never re-derived here. It says one thing to the caller: the hint has already been evicted, and a fresh attempt down the
174
+ * guide path is worth making. setupStream is the one caller that acts on it; every other caller sees an ordinary establishment failure carrying the original
175
+ * rejection as its cause.
176
+ */
177
+ export class DirectUrlEstablishmentError extends Error {
178
+ constructor(cause) {
179
+ // The underlying failure travels in the message as well as in cause, because the callers that do not act on the type - tab replacement above all - log the
180
+ // message alone, and a wrapper that swallowed the reason would make this path harder to diagnose than an untyped throw.
181
+ super("Direct watch URL establishment failed: " + formatError(cause) + ".", { cause });
182
+ this.name = "DirectUrlEstablishmentError";
183
+ }
184
+ }
185
+ // Maximum number of times createPageWithCapture() will retry when it detects that the page was closed while waiting for its turn on the capture lock (e.g., due to a
186
+ // browser crash). An explicit guard prevents unbounded recursion.
55
187
  const MAX_PAGE_CLOSED_RETRIES = 3;
188
+ // Maximum time in milliseconds to wait for a single capture probe's getStream() to respond. Shared by the launch-gate verification (verifyCaptureSystem) and the
189
+ // mid-life re-verification, so both tiers exercise the capture path with the same bound.
190
+ const CAPTURE_PROBE_TIMEOUT_MS = 5000;
191
+ // Wire the capture-readiness probe into the browser launch gate. setup.ts owns getStream and the unrecoverable stale-mutex process.exit decision; browser/index.ts
192
+ // owns the launch lifecycle. Injecting verifyCaptureSystem here (setup.ts already depends on browser/index.ts) keeps the dependency one-directional and breaks the
193
+ // cycle, mirroring the browserAccessors boundary between login.ts and index.ts. It runs once at module load, before any browser launch.
194
+ setCaptureProbe(verifyCaptureSystem);
56
195
  /**
57
196
  * Error thrown when stream setup fails. Includes HTTP status code and user-friendly message for the response.
58
197
  */
@@ -66,6 +205,28 @@ export class StreamSetupError extends Error {
66
205
  this.userMessage = userMessage;
67
206
  }
68
207
  }
208
+ /**
209
+ * Prepends sign-in guidance to a user-facing failure message when the failing channel's service domain is currently marked needs-sign-in. A confirmed
210
+ * authentication wall on the service is the most likely cause of a failed tune there, and the channel table's login icon is the remedy - the underlying error
211
+ * stays in the message so the original failure remains identifiable. Returns the message unchanged for ad-hoc URL streams without a channel identity, for
212
+ * channels whose domain cannot be resolved, and for domains not marked needs-sign-in.
213
+ *
214
+ * Exported for unit-test coverage of the guidance composition. Production callers reach this only through setupStream's failure paths, never directly.
215
+ * @param userMessage - The user-facing failure message being composed.
216
+ * @param channelKey - The failing channel's key, or null/undefined for ad-hoc URL streams.
217
+ * @param serviceName - The service's display name, used to name who needs the sign-in.
218
+ * @returns The user message, led by sign-in guidance when the channel's domain is marked needs-sign-in.
219
+ */
220
+ export function withSignInGuidance(userMessage, channelKey, serviceName) {
221
+ if (!channelKey) {
222
+ return userMessage;
223
+ }
224
+ const domain = getAuthDomainForChannel(resolveServiceKey(channelKey));
225
+ if (!domain || (getDomainAuthState(domain)?.status !== "needsLogin")) {
226
+ return userMessage;
227
+ }
228
+ return serviceName + " needs sign-in. Open PrismCast's channel table and click this channel's login icon to sign in. " + userMessage;
229
+ }
69
230
  // Request ID Generation.
70
231
  /**
71
232
  * Generates a short alphanumeric request ID for log correlation. The ID is 6 characters to keep log messages readable while providing enough uniqueness for
@@ -130,6 +291,21 @@ export function validateStreamUrl(url) {
130
291
  }
131
292
  }
132
293
  // Page and Capture Creation.
294
+ /**
295
+ * Disposes a browser page acquired during stream setup: unregisters it from managed-page tracking and closes it if still open. The close is fire-and-forget with a
296
+ * debug log on error, matching the long-standing setup-failure cleanup behavior. Used as the DisposableStack disposer for pages in createPageWithCapture and
297
+ * setupStream, and by the setup-result cleanup closure, so every setup-phase page teardown flows through one definition.
298
+ * @param page - The page to dispose.
299
+ */
300
+ function disposePage(page) {
301
+ unregisterManagedPage(page);
302
+ if (!page.isClosed()) {
303
+ page.close().catch((error) => {
304
+ LOG.debug("streaming:setup", "Page close error during setup cleanup: %s.", formatError(error));
305
+ });
306
+ }
307
+ }
308
+ const defaultCreatePageWithCaptureDeps = { getCurrentBrowser, getStream, startOverlayHandling };
133
309
  /**
134
310
  * Creates a browser page with media capture and navigates to the URL. This is the reusable core function used by both initial stream setup and tab replacement
135
311
  * recovery. It handles:
@@ -139,260 +315,300 @@ export function validateStreamUrl(url) {
139
315
  * - Setting up video playback via navigateToPage() + initializePlayback()
140
316
  *
141
317
  * The caller is responsible for:
142
- * - Creating the segmenter and piping captureStream to it
318
+ * - Creating the segmenter and attaching it to the capture session via captureSession.attachSegmenter()
143
319
  * - Registering/updating the stream in the registry
144
320
  * - Starting/updating the health monitor
145
321
  * - Handling cleanup on failure
146
322
  *
147
323
  * @param options - Options for page and capture creation.
148
- * @returns The page, context, capture stream, and FFmpeg process (if any).
324
+ * @param deps - The injected browser and overlay-poll collaborators; defaults to defaultCreatePageWithCaptureDeps. Threaded so a test drives this function without a
325
+ * live Chrome by substituting the shared-browser accessor, the capture launcher, and the static-capture overlay poll.
326
+ * @returns The page, context, and capture session (which owns the raw capture stream and any FFmpeg child).
149
327
  * @throws Error if page creation, capture initialization, or navigation fails.
150
328
  */
151
- export async function createPageWithCapture(options) {
152
- const captureElapsed = startTimer();
153
- const { comment, onFFmpegError, profile, streamId, url } = options;
154
- // Create browser page.
155
- const browser = await getCurrentBrowser();
156
- const page = await browser.newPage();
157
- registerManagedPage(page);
158
- await page.setBypassCSP(true);
159
- // Inject the shared video selector helper into the browser context. This must happen before navigation so the helper is available when evaluate calls run during
160
- // initializePlayback (startVideoPlayback, applyVideoStyles, verifyFullscreen, lockVolumeProperties) and subsequent health monitoring (getVideoState).
161
- await injectVideoSelector(page);
162
- // Install CDP manifest interceptor before navigation. This listener captures .m3u8 URLs from the browser's network requests, enabling native HLS streaming for
163
- // services that use clear or AES-128 encrypted streams. Skipped for tab replacements (native proxy is independent of capture) and for channels already known to
164
- // use DRM (avoids 15 seconds of wasted CDP overhead per tune). The await ensures the CDP session and Network domain are ready before navigation begins.
165
- const manifestInterception = (!options.tabReplacement && !options.skipManifestInterception) ? await installManifestInterceptor(page) : null;
166
- // Select MIME type based on capture mode. FFmpeg mode is more stable for long recordings because Chrome's native fMP4 MediaRecorder can become unstable. The
167
- // codec decision (H.264 vs HEVC) is delegated to the codec module, which considers the user's allowlist and GPU hardware capabilities.
168
- const useFFmpeg = CONFIG.streaming.captureMode === "ffmpeg";
169
- const captureMimeType = useFFmpeg ? getCaptureMimeType() : NATIVE_FMP4_MIME_TYPE;
170
- // Track the output stream that will be sent to the segmenter and FFmpeg process if used. Also track the raw capture stream separately - it must be destroyed
171
- // before closing the page to ensure chrome.tabCapture releases the capture.
172
- let outputStream;
173
- let rawCaptureStream = null;
174
- let ffmpegProcess = null;
175
- // Capture queue release function, hoisted here so both the try and catch blocks can access it. The promise and resolver are created together via
176
- // withResolvers when the queue entry is registered below. The once-guard prevents double-releasing from multiple code paths (success, catch, timeout).
177
- let captureQueueReleased = false;
178
- let releaseCaptureQueue;
179
- const releaseCaptureOnce = () => {
180
- if (!captureQueueReleased) {
181
- captureQueueReleased = true;
182
- captureQueueDepth--;
183
- releaseCaptureQueue();
184
- }
185
- };
186
- // Initialize media stream capture.
329
+ export async function createPageWithCapture(options, deps = defaultCreatePageWithCaptureDeps) {
330
+ const env_1 = { stack: [], error: void 0, hasError: false };
187
331
  try {
188
- const streamOptions = {
189
- audio: true,
190
- audioBitsPerSecond: CONFIG.streaming.audioBitsPerSecond,
191
- mimeType: captureMimeType,
192
- video: true,
193
- videoBitsPerSecond: CONFIG.streaming.videoBitsPerSecond,
194
- videoConstraints: {
195
- mandatory: {
196
- maxFrameRate: 60,
197
- maxHeight: getEffectiveViewport(CONFIG).height,
198
- maxWidth: getEffectiveViewport(CONFIG).width,
199
- minFrameRate: Math.max(30, Math.min(60, CONFIG.streaming.frameRate)),
200
- minHeight: getEffectiveViewport(CONFIG).height,
201
- minWidth: getEffectiveViewport(CONFIG).width
202
- }
203
- }
204
- };
205
- // Serialize capture initialization. Wait for any previous capture to finish before calling getStream(), because Chrome's tabCapture extension rejects
206
- // concurrent initialization attempts. On success, the lock is released immediately so the next caller can proceed. On failure, the lock is held until the
207
- // catch block decides what to do - the catch block releases the lock after handling the error.
208
- const previousCapture = captureQueue;
209
- captureQueueDepth++;
210
- if (captureQueueDepth >= CAPTURE_QUEUE_DEPTH_WARNING) {
211
- LOG.warn("Capture queue depth is %d. Multiple stream requests are competing for Chrome's capture initialization.", captureQueueDepth);
212
- }
213
- // eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- Standard pattern for signal promises.
214
- const queued = Promise.withResolvers();
215
- captureQueue = queued.promise;
216
- releaseCaptureQueue = queued.resolve;
217
- // Guard against a permanently hung predecessor. If the previous capture doesn't complete within the navigation timeout, release our queue position and let the
218
- // caller's error handling deal with it. This prevents a single stuck getStream() from blocking all future captures indefinitely.
332
+ const captureElapsed = startTimer();
333
+ const { comment, numericStreamId, onFFmpegError, profile, streamId, url } = options;
334
+ // Acquire every resource on a DisposableStack so that any throw - in capture initialization, navigation, or playback setup - disposes them structurally as the
335
+ // function unwinds, in last-acquired-first order (capture session before page, so the capture stream is destroyed and STOP_RECORDING fires while the browser is
336
+ // still connected, before the page closes). On success we move() the stack to disarm it and transfer ownership to the caller. This centralizes teardown that
337
+ // would otherwise be repeated in each failure path, and closes the navigation-path leak of the manifest interceptor.
338
+ const resources = __addDisposableResource(env_1, new DisposableStack(), false);
339
+ // Create browser page.
340
+ const browser = await deps.getCurrentBrowser();
341
+ const page = await browser.newPage();
342
+ // Register in-flight: the registry does not record this page against the stream until setup finishes, so the mark is what keeps stale page cleanup from closing
343
+ // it mid-tune.
344
+ registerManagedPage(page, { inFlightSetup: true });
345
+ resources.adopt(page, disposePage);
346
+ await page.setBypassCSP(true);
347
+ // Inject the shared video selector helper into the browser context. This must happen before navigation so the helper is available when evaluate calls run during
348
+ // initializePlayback (startVideoPlayback, applyVideoStyles, verifyFullscreen, lockVolumeProperties) and subsequent health monitoring (getVideoState).
349
+ await injectVideoSelector(page);
350
+ // Select MIME type based on capture mode. FFmpeg mode is more stable for long recordings because Chrome's native fMP4 MediaRecorder can become unstable. The
351
+ // codec decision (H.264 vs HEVC) is delegated to the codec module, which considers the user's allowlist and GPU hardware capabilities.
352
+ const useFFmpeg = CONFIG.streaming.captureMode === "ffmpeg";
353
+ const captureMimeType = useFFmpeg ? getCaptureMimeType() : NATIVE_FMP4_MIME_TYPE;
354
+ // Resolve the FFmpeg binary path up front, before the raw capture stream is acquired below. resolveFFmpegPath is a memoized resolver that can sticky-reject; if its
355
+ // await sat between acquiring the raw capture stream and wrapping it in a CaptureSession, a rejection would strand the stream undestroyed (STOP_RECORDING never
356
+ // fires, leaving chrome.tabCapture active). Resolving it here keeps any rejection on a path with no capture resource yet acquired, so the CaptureSession remains
357
+ // the single owner from the instant the stream exists. Falls back to "ffmpeg" so spawn() defers to PATH lookup; only meaningful in FFmpeg mode.
358
+ const ffmpegBin = useFFmpeg ? ((await resolveFFmpegPath()) ?? "ffmpeg") : "ffmpeg";
359
+ // The capture-pipeline composite, assigned once the raw capture stream and optional FFmpeg child exist and registered on the DisposableStack the moment it is built.
360
+ let captureSession;
361
+ // Initialize media stream capture. The whole capture-init phase runs inside one try so a browser crash detected at turn grant drives the closed-page recursion,
362
+ // while every other failure unwinds through the DisposableStack.
219
363
  try {
220
- await raceWithTimeout(previousCapture, CONFIG.streaming.navigationTimeout, new Error("Capture queue wait timed out."));
221
- }
222
- catch (error) {
223
- // Release our queue position so subsequent captures aren't blocked by our failure.
224
- releaseCaptureOnce();
225
- throw error;
226
- }
227
- // After the queue wait, verify our page is still connected. If Chrome crashed while we were waiting, our page is dead and we need to start over with a
228
- // fresh page on the new browser. Release our queue position first so subsequent callers aren't blocked.
229
- if (page.isClosed()) {
230
- releaseCaptureOnce();
231
- unregisterManagedPage(page);
232
- const retryCount = options._pageClosedRetries ?? 0;
233
- if (retryCount >= MAX_PAGE_CLOSED_RETRIES) {
234
- throw new Error("Browser crashed too many times during capture initialization.");
235
- }
236
- return await createPageWithCapture({ ...options, _pageClosedRetries: retryCount + 1 });
237
- }
238
- const streamPromise = getStream(page, streamOptions);
239
- // Release the queue on success only. On failure, the catch block handles the release. The rejection handler is a no-op to suppress unhandled rejection
240
- // warnings; the actual error handling happens in the catch block below.
241
- void streamPromise.then(() => { releaseCaptureOnce(); }, () => { });
242
- const stream = await raceWithTimeout(streamPromise, CONFIG.streaming.navigationTimeout, new Error("Stream initialization timed out."));
243
- // Store the raw capture stream. This must be destroyed before closing the page.
244
- rawCaptureStream = stream;
245
- // For FFmpeg mode, spawn FFmpeg to transcode the Matroska stream to fMP4. FFmpeg copies the H264 video and transcodes Opus audio to AAC. The resolved binary
246
- // path comes from the production-cached resolver - first call probes; subsequent calls return the memoized result. Falls back to "ffmpeg" so spawn() defers
247
- // to PATH lookup if the resolver couldn't find a path (matching the previous behavior; the spawn will then fail with ENOENT if PATH is also empty).
248
- if (useFFmpeg) {
249
- const ffmpegBin = (await resolveFFmpegPath()) ?? "ffmpeg";
250
- const ffmpeg = spawnFFmpeg(ffmpegBin, CONFIG.streaming.audioBitsPerSecond, (error) => {
251
- LOG.error("FFmpeg process error: %s.", formatError(error));
252
- if (onFFmpegError) {
253
- onFFmpegError(error);
364
+ const streamOptions = {
365
+ audio: true,
366
+ audioBitsPerSecond: CONFIG.streaming.audioBitsPerSecond,
367
+ mimeType: captureMimeType,
368
+ video: true,
369
+ videoBitsPerSecond: CONFIG.streaming.videoBitsPerSecond,
370
+ // Constrain capture frame rate to a 30-60 fps band: 60 is the live-TV ceiling, and a 30 floor keeps motion smooth even when the user configures a lower rate.
371
+ // The ceiling is fixed at 60 while the floor follows the user's configured rate (clamped into the band), so the encoder favours the requested rate but never
372
+ // drops below 30. The readiness probe (attemptCaptureProbe) instead pins both bounds to a flat 30 because its getStream() fails or succeeds at the tabCapture
373
+ // API level before encoding matters, so a representative-but-minimal constraint set suffices there.
374
+ videoConstraints: {
375
+ mandatory: {
376
+ maxFrameRate: 60,
377
+ maxHeight: getEffectiveViewport(CONFIG).height,
378
+ maxWidth: getEffectiveViewport(CONFIG).width,
379
+ minFrameRate: Math.max(30, Math.min(60, CONFIG.streaming.frameRate)),
380
+ minHeight: getEffectiveViewport(CONFIG).height,
381
+ minWidth: getEffectiveViewport(CONFIG).width
382
+ }
254
383
  }
255
- }, streamId, comment);
256
- ffmpegProcess = ffmpeg;
257
- // Handle pipe errors on stdout. Stdin errors are handled by pipeline() below.
258
- ffmpeg.stdout.on("error", (error) => {
259
- const errorMessage = formatError(error);
260
- if (errorMessage.includes("EPIPE")) {
261
- LOG.debug("streaming:ffmpeg", "FFmpeg stdout pipe closed: %s.", errorMessage);
384
+ };
385
+ // Acquire the raw capture stream through the capture lock. The lock serializes getStream against every other capture init process-wide and holds the turn until
386
+ // this task's promise settles, so no two inits collide. Every timing bound is read here, at the call, because the module-scope lock captured nothing from CONFIG.
387
+ const stream = await captureLock.run(async (signal) => {
388
+ // If Chrome crashed while this task waited for its turn, the page is dead. Throw a typed error so the turn releases at once and the closed-page recursion runs
389
+ // OUTSIDE the lock, on a fresh page.
390
+ if (page.isClosed()) {
391
+ throw new PageClosedDuringTurnError();
262
392
  }
263
- else {
264
- LOG.error("FFmpeg stdout pipe error: %s.", errorMessage);
265
- ffmpeg.kill();
266
- if (onFFmpegError) {
267
- onFFmpegError(error);
268
- }
393
+ // Initialize capture. On a stale-mutex rejection - including one that arrives after this task was abandoned at the caller deadline - escalate from the single
394
+ // home before rethrowing, so the process-exit safety net fires even on the abandoned path, where a late stale-mutex rejection would otherwise go unescalated.
395
+ let raw;
396
+ try {
397
+ raw = await deps.getStream(page, streamOptions);
269
398
  }
270
- });
271
- // Pipe the Matroska capture stream to FFmpeg's stdin using pipeline() for proper cleanup. When FFmpeg is killed during tab replacement, pipeline() automatically
272
- // destroys the source stream, preventing "write after end" errors that would occur with .pipe().
273
- pipeline(stream, ffmpeg.stdin).catch((error) => {
274
- const errorMessage = formatError(error);
275
- // EPIPE, "write after end", and "Premature close" errors are expected during cleanup when FFmpeg is killed or the capture stream is destroyed.
276
- if (errorMessage.includes("EPIPE") || errorMessage.includes("write after end") || errorMessage.includes("Premature close")) {
277
- return;
399
+ catch (error) {
400
+ if (isStaleCaptureMutexError(error)) {
401
+ escalateStaleCaptureMutex("stream initialization");
402
+ }
403
+ throw error;
278
404
  }
279
- // Unexpected pipeline errors require cleanup.
280
- LOG.error("Capture pipeline error: %s.", errorMessage);
281
- ffmpeg.kill();
282
- if (onFFmpegError) {
283
- onFFmpegError(error instanceof Error ? error : new Error(String(error)));
405
+ // The caller deadline fired while getStream was still running: retire the stream this task just produced - destroy plus the STOP_RECORDING settle - inside the
406
+ // turn, then reject, so no path strands a live capture on a closing page or mistakes a retired stream for a usable one.
407
+ if (signal.aborted) {
408
+ await retireRawStream(raw, realClock);
409
+ throw new CaptureAbandonedError();
284
410
  }
411
+ return raw;
412
+ }, {
413
+ deadlineMessage: STREAM_INIT_TIMEOUT_MESSAGE,
414
+ deadlineMs: CONFIG.streaming.navigationTimeout,
415
+ // A wedged capture init is decisive only when no OTHER stream is active - the same judgment, from the same predicate, as the mid-life detector: any other
416
+ // active stream means the browser is demonstrably capturing, so tearing it down would violate per-stream failure isolation. reverificationInProgress is passed
417
+ // as the literal false because an in-flight mid-life probe is itself queued BEHIND this wedged task on the same lock, so deferring to it would double the
418
+ // recovery bound, and a redundant invalidate is identity-guarded and converges harmlessly.
419
+ onWedge: () => {
420
+ if (shouldReverifyCapture({ activeStreamIds: getAllStreams().map((entry) => entry.id), failingStreamId: numericStreamId, hasBrowser: true,
421
+ reverificationInProgress: false })) {
422
+ // Route the wedge into the existing browser-recovery ladder, targeting the exact instance this task ran against; invalidateBrowser no-ops if it was already
423
+ // superseded. A rejection is warn-logged rather than rethrown out of the wedge callback.
424
+ void invalidateBrowser(browser, "capture initialization wedged past the recovery bound").catch((error) => {
425
+ LOG.warn("Invalidating a wedged capture browser failed: %s.", formatError(error));
426
+ });
427
+ }
428
+ else {
429
+ // Other streams are active, so hold rather than tear the browser down: waiters fail fast on their own turn-wait bounds, the wedged operation settles at
430
+ // worst at Puppeteer's protocolTimeout, and once the other streams drain a later wedge or setup failure reaches the zero-other case.
431
+ LOG.warn("Capture initialization has wedged past the recovery bound, but other streams are active; holding rather than invalidating the browser.", { failingStreamId: numericStreamId });
432
+ }
433
+ },
434
+ turnWaitMs: CONFIG.streaming.navigationTimeout
285
435
  });
286
- // Use FFmpeg's stdout (fMP4 output) as the output stream for segmentation.
287
- outputStream = ffmpeg.stdout;
436
+ // For FFmpeg mode, spawn FFmpeg to transcode the Matroska stream to fMP4. FFmpeg copies the H264 video and transcodes Opus audio to AAC. The binary path was
437
+ // resolved up front (above) so no throwable await sits between acquiring the raw capture stream and wrapping it in the CaptureSession that owns it. The spawn and
438
+ // pipeline wiring below are synchronous, so the stream is owned the instant it exists.
439
+ let ffmpegProcess = null;
440
+ if (useFFmpeg) {
441
+ const ffmpeg = spawnFFmpeg(ffmpegBin, CONFIG.streaming.audioBitsPerSecond, (error) => {
442
+ LOG.error("FFmpeg process error: %s.", formatError(error));
443
+ if (onFFmpegError) {
444
+ onFFmpegError(error);
445
+ }
446
+ }, streamId, comment);
447
+ ffmpegProcess = ffmpeg;
448
+ // Handle pipe errors on stdout. Stdin errors are handled by pipeline() below.
449
+ ffmpeg.stdout.on("error", (error) => {
450
+ const errorMessage = formatError(error);
451
+ if (errorMessage.includes("EPIPE")) {
452
+ LOG.debug("streaming:ffmpeg", "FFmpeg stdout pipe closed: %s.", errorMessage);
453
+ }
454
+ else {
455
+ LOG.error("FFmpeg stdout pipe error: %s.", errorMessage);
456
+ ffmpeg.kill();
457
+ if (onFFmpegError) {
458
+ onFFmpegError(error);
459
+ }
460
+ }
461
+ });
462
+ // Pipe the Matroska capture stream to FFmpeg's stdin using pipeline() for proper cleanup. When FFmpeg is killed during tab replacement, pipeline() automatically
463
+ // destroys the source stream, preventing "write after end" errors that would occur with .pipe().
464
+ pipeline(stream, ffmpeg.stdin).catch((error) => {
465
+ const errorMessage = formatError(error);
466
+ // EPIPE, "write after end", and "Premature close" errors are expected during cleanup when FFmpeg is killed or the capture stream is destroyed.
467
+ if (errorMessage.includes("EPIPE") || errorMessage.includes("write after end") || errorMessage.includes("Premature close")) {
468
+ return;
469
+ }
470
+ // Unexpected pipeline errors require cleanup.
471
+ LOG.error("Capture pipeline error: %s.", errorMessage);
472
+ ffmpeg.kill();
473
+ if (onFFmpegError) {
474
+ onFFmpegError(error instanceof Error ? error : new Error(String(error)));
475
+ }
476
+ });
477
+ }
478
+ // Wrap the raw capture stream and optional FFmpeg child as one self-disposing pipeline unit, and register it for structural teardown. The session derives the
479
+ // segmenter input internally (FFmpeg's stdout in FFmpeg mode, the raw stream in native-fMP4 mode); the caller attaches the segmenter once it is created.
480
+ captureSession = createCaptureSession({ ffmpegProcess, rawCaptureStream: stream });
481
+ resources.use(captureSession);
288
482
  }
289
- else {
290
- // Native fMP4 mode: Use the raw capture stream directly. In this mode, rawCaptureStream and outputStream are the same object.
291
- outputStream = rawCaptureStream;
483
+ catch (error) {
484
+ // A browser crash detected at turn grant surfaces as PageClosedDuringTurnError: the page is dead, so unregister it, bump the retry counter, and start over on a
485
+ // fresh page on the new browser. The recursion runs here, outside the lock, so the turn was already released the instant the task threw. The retry cap prevents
486
+ // unbounded recursion during a crash loop.
487
+ if (error instanceof PageClosedDuringTurnError) {
488
+ unregisterManagedPage(page);
489
+ const retryCount = options._pageClosedRetries ?? 0;
490
+ if (retryCount >= MAX_PAGE_CLOSED_RETRIES) {
491
+ throw new Error("Browser crashed too many times during capture initialization.");
492
+ }
493
+ return await createPageWithCapture({ ...options, _pageClosedRetries: retryCount + 1 }, deps);
494
+ }
495
+ // Every other rejection - a stale-mutex escalation already fired inside the task, a caller-deadline CaptureDeadlineError, or any other capture-init failure - just
496
+ // unwinds. Resource teardown (page, interceptor, and the capture session once built) is handled by the DisposableStack as this throw unwinds the function scope.
497
+ throw error;
292
498
  }
293
- }
294
- catch (error) {
295
- // Clean up on capture initialization failure. Destroy the raw capture stream first to ensure chrome.tabCapture releases the capture.
296
- if (rawCaptureStream && !rawCaptureStream.destroyed) {
297
- rawCaptureStream.destroy();
499
+ // Navigate and set up playback. For static capture profiles, just navigate without video setup.
500
+ let context;
501
+ let strategyDirectTune = false;
502
+ let usedDirectUrl = false;
503
+ // Install the CDP manifest interceptor immediately before the navigate-and-tune fork, so its observation window opens after the capture-lock and getStream phase
504
+ // (which the observer would otherwise idle through) and spans exactly the phases that produce manifests: navigation with retry, channel selection, and video
505
+ // setup. The install decision is gated on tab replacement and the skip flag only - static-capture profiles install too, since they stay native-HLS eligible
506
+ // through the interception's presence, so the guard sits upstream of the fork and both branches inherit it. The navigation allowance handed to the budget is
507
+ // this path's worst-case retry duration, backoff sleeps included; establishmentBudgetMs owns why the window has to outlive that allowance and every phase
508
+ // after it, direct tunes included.
509
+ const interceptionBudgetMs = establishmentBudgetMs(maxRetryDuration({
510
+ backoffJitter: CONFIG.recovery.backoffJitter,
511
+ maxAttempts: CONFIG.streaming.maxNavigationRetries,
512
+ maxBackoffDelay: CONFIG.recovery.maxBackoffDelay,
513
+ timeoutMs: CONFIG.streaming.navigationTimeout
514
+ }));
515
+ const manifestInterception = (!options.tabReplacement && !options.skipManifestInterception) ? await installManifestInterceptor(page, interceptionBudgetMs) : null;
516
+ // Register the interception on the resource stack after the capture session, so on an unwind it disposes first. Its disposal is a CDP observer detach with no
517
+ // ordering dependency on the capture session or the page; the only ordered teardown pair is capture-session-before-page (STOP_RECORDING while the browser is
518
+ // still connected), which both this stack and the later owned stack preserve.
519
+ if (manifestInterception) {
520
+ resources.use(manifestInterception);
298
521
  }
299
- unregisterManagedPage(page);
300
- if (!page.isClosed()) {
301
- page.close().catch(() => { });
522
+ try {
523
+ if (!profile.staticCapture) {
524
+ // Check for a direct watch URL, unless the caller has asked for the guide path outright. When one is available, navigate directly to it and skip channel
525
+ // selection, avoiding guide page navigation entirely. On a failure the coordinator blames the URL for, the catch block below evicts the hint and leaves this
526
+ // function typed, and setupStream re-invokes once with the resolution skipped so the tune still gets its guide attempt.
527
+ const directUrl = options.skipDirectUrl ? null : await resolveDirectUrl(profile, page);
528
+ usedDirectUrl = !!directUrl;
529
+ const navigationUrl = directUrl ?? url;
530
+ /* Phase 1 (navigation) and phase 2 (channel selection + video setup) run through the shared establishment composition, so the step order this path uses -
531
+ * navigate, stamp the observation epoch, initialize playback under the module-scope PLAYBACK_INIT_TIMEOUT safety net - is the same order the
532
+ * re-establishment path runs. When navigating to a cached direct URL, channel selection is skipped because the URL already targets the correct channel.
533
+ * The composition places no outer timeout around channel selection: each sub-step (selectChannel, waitForVideoReady, etc.) carries its own internal
534
+ * timeout via videoTimeout and the click retry constants.
535
+ */
536
+ const tuneResult = await establishChannelPlayback(page, profile, manifestInterception, {
537
+ initOptions: { persistResolution: options.persistResolution, requestedUrl: navigationUrl, skipChannelSelection: usedDirectUrl },
538
+ // Navigate with retry. The 10-second navigationTimeout is appropriate for page loads, and retryOperation correctly reloads the page on genuine navigation
539
+ // failures. The retry ladder is this path's own policy, kept separate from channel selection so its timeout does not race the internal click retry loops
540
+ // in channel selection strategies (guideGrid can take 15-20 seconds for binary search + click retries).
541
+ navigate: async () => {
542
+ await retryOperation({
543
+ backoffJitter: CONFIG.recovery.backoffJitter,
544
+ description: "page navigation for " + navigationUrl,
545
+ maxAttempts: CONFIG.streaming.maxNavigationRetries,
546
+ maxBackoffDelay: CONFIG.recovery.maxBackoffDelay,
547
+ operation: async () => {
548
+ await navigateToPage(page, navigationUrl, profile);
549
+ },
550
+ shouldAbort: () => page.isClosed(),
551
+ timeoutMs: CONFIG.streaming.navigationTimeout
552
+ });
553
+ }
554
+ });
555
+ strategyDirectTune = tuneResult.directTune ?? false;
556
+ context = tuneResult.context;
557
+ }
558
+ else {
559
+ await page.goto(url);
560
+ // A static capture navigates once and takes the page as-is, with no channel selection or video wait, so it never reaches the tune path's overlay poll. Launch
561
+ // a bounded staticCapture poll so a cookie banner or per-site modal is dismissed on the captured page. There is no controller: the phase's window bounds it and
562
+ // a closed page stops it via the tick-error taxonomy. Any dismissal click lands in the captured pixels, which is exactly the intent for a static capture.
563
+ void deps.startOverlayHandling(page, profile, { phase: "staticCapture" });
564
+ context = page;
565
+ }
302
566
  }
303
- const errorMessage = formatError(error);
304
- // Stale capture state is unrecoverable. The "Cannot capture a tab with an active stream" error occurs inside puppeteer-stream's second lock section, which
305
- // has no try/finally. The internal mutex is permanently leaked - all subsequent getStream() calls will hang on it. Chrome restart cannot fix module-level
306
- // state, so the only recourse is a full process restart. Release the capture queue so other callers aren't left hanging, then exit.
307
- if (errorMessage.includes("Cannot capture a tab with an active stream")) {
308
- LOG.error("Stale capture state detected. puppeteer-stream's internal capture mutex is now permanently locked. The capture system is unrecoverable. " +
309
- "Exiting so the service manager can restart with a clean module state.");
310
- releaseCaptureOnce();
311
- setTimeout(() => process.exit(1), 100);
567
+ catch (error) {
568
+ // If a direct watch URL was used, offer the failure to the cache coordinator, which evicts the live and persisted hints when the failure is evidence against
569
+ // the URL and reports that verdict back. The retention policy lives there and is read from there - this file never re-derives which failures count.
570
+ const urlEvidence = usedDirectUrl && invalidateDirectUrl(profile, error);
571
+ // Re-minimize the browser window. Navigation may have un-minimized it (new tab activation on macOS), and without this the window stays visible after the failed
572
+ // attempt. Fire-and-forget since we're about to throw. Resource teardown (capture session, interceptor, page) is handled by the DisposableStack as this throw
573
+ // unwinds the function scope; the capture session disposes first, destroying the capture stream before the page closes, so STOP_RECORDING ordering is preserved.
574
+ minimizeBrowserWindow().catch(() => { });
575
+ /* A failure the coordinator blamed on the URL is worth one more attempt down the guide path, so it leaves this function typed for setupStream to act on. An
576
+ * establishment timeout qualifies: the retry is a whole fresh invocation, so there is no abandoned first attempt still driving a page, no one-shot manifest
577
+ * interception handle to reuse, and no direct-tune marker to reset - the retry gets its own page, handle, and markers, which is exactly what a fresh tune
578
+ * would get. Page-death and abort failures, the ones the retention policy keeps the URL for, rethrow raw: nothing about them says the URL is wrong.
579
+ */
580
+ if (urlEvidence) {
581
+ throw new DirectUrlEstablishmentError(error);
582
+ }
312
583
  throw error;
313
584
  }
314
- // For non-stale errors, release the capture queue so subsequent callers can proceed.
315
- releaseCaptureOnce();
316
- throw error;
317
- }
318
- // Navigate and set up playback. For static capture profiles, just navigate without video setup.
319
- let context;
320
- let strategyDirectTune = false;
321
- let usedDirectUrl = false;
322
- try {
323
- if (!profile.staticCapture) {
324
- // Check for a direct watch URL. If available, navigate directly to it and skip channel selection, avoiding guide page navigation entirely. On failure,
325
- // the cache entry is invalidated in the catch block so the outer retry loop (in streaming/hls.ts) re-invokes with the guide URL.
326
- const directUrl = await resolveDirectUrl(profile, page);
327
- usedDirectUrl = !!directUrl;
328
- const navigationUrl = directUrl ?? url;
329
- // Phase 1: Navigate to the page with retry. The 10-second navigationTimeout is appropriate for page loads, and retryOperation correctly reloads the page on
330
- // genuine navigation failures. Navigation is wrapped in retryOperation separately from channel selection so the timeout does not race with the internal click
331
- // retry loops in channel selection strategies (guideGrid can take 15-20 seconds for binary search + click retries).
332
- await retryOperation({
333
- backoffJitter: CONFIG.recovery.backoffJitter,
334
- description: "page navigation for " + navigationUrl,
335
- maxAttempts: CONFIG.streaming.maxNavigationRetries,
336
- maxBackoffDelay: CONFIG.recovery.maxBackoffDelay,
337
- operation: async () => {
338
- await navigateToPage(page, navigationUrl, profile);
339
- },
340
- shouldAbort: () => page.isClosed(),
341
- timeoutMs: CONFIG.streaming.navigationTimeout
342
- });
343
- // Phase 2: Channel selection + video setup. When navigating to a cached direct URL, skip channel selection since the URL already targets the correct
344
- // channel. Runs after navigation succeeds with no outer timeout racing against internal click retries. Each sub-step (selectChannel, waitForVideoReady,
345
- // etc.) has its own internal timeout via videoTimeout and click retry constants. For guideGrid strategies, a channel selection failure triggers an overlay
346
- // dismiss and retry, which doubles the channel selection time budget. The 45-second safety-net timeout accommodates this retry while still preventing
347
- // pathological hangs if multiple internal timeouts chain sequentially.
348
- const PLAYBACK_INIT_TIMEOUT = 45000;
349
- const tuneResult = await raceWithTimeout(initializePlayback(page, profile, { persistResolution: options.persistResolution, skipChannelSelection: usedDirectUrl }), PLAYBACK_INIT_TIMEOUT, new Error("Playback initialization timed out after " + String(PLAYBACK_INIT_TIMEOUT) + "ms."));
350
- strategyDirectTune = tuneResult.directTune ?? false;
351
- context = tuneResult.context;
352
- }
353
- else {
354
- await page.goto(url);
355
- context = page;
585
+ // During tab replacement, allow Chrome's compositor to fully stabilize the fullscreen video surface before minimizing. Without this delay, the compositor may
586
+ // snapshot an incorrect scaling state during the minimize transition, causing the captured content to appear zoomed into the top-left corner.
587
+ if (options.tabReplacement && !profile.staticCapture) {
588
+ await delay(500);
356
589
  }
590
+ // Resize and minimize window.
591
+ await resizeAndMinimizeWindow(page);
592
+ LOG.debug("timing:startup", "Page with capture ready. Total: %sms.", captureElapsed());
593
+ // Success: transfer ownership of the page, interceptor, and capture session out of the scope guard. move() empties the stack so its scope-exit disposal is a no-op
594
+ // and the caller becomes responsible for disposing what it now holds.
595
+ resources.move();
596
+ return {
597
+ captureSession,
598
+ context,
599
+ // The kind the interception is finalized against, derived by the shared formula from this path's tune facts and the profile.
600
+ directTune: computeDirectTuneKind({ profile, strategyDirectTune, usedDirectUrl }),
601
+ manifestInterception,
602
+ page
603
+ };
357
604
  }
358
- catch (error) {
359
- // If a cached direct URL was used, invalidate it so the next attempt falls through to guide navigation.
360
- if (usedDirectUrl) {
361
- invalidateDirectUrl(profile);
362
- }
363
- // Clean up on navigation or playback initialization failure. Destroy the raw capture stream first to ensure chrome.tabCapture releases the capture.
364
- if (!rawCaptureStream.destroyed) {
365
- rawCaptureStream.destroy();
366
- }
367
- if (ffmpegProcess) {
368
- ffmpegProcess.kill();
369
- }
370
- unregisterManagedPage(page);
371
- if (!page.isClosed()) {
372
- page.close().catch(() => { });
373
- }
374
- // Re-minimize the browser window. Navigation may have un-minimized it (new tab activation on macOS), and without this the window stays visible after the
375
- // failed attempt. Fire-and-forget since we're about to throw.
376
- minimizeBrowserWindow().catch(() => { });
377
- throw error;
605
+ catch (e_1) {
606
+ env_1.error = e_1;
607
+ env_1.hasError = true;
378
608
  }
379
- // During tab replacement, allow Chrome's compositor to fully stabilize the fullscreen video surface before minimizing. Without this delay, the compositor may
380
- // snapshot an incorrect scaling state during the minimize transition, causing the captured content to appear zoomed into the top-left corner.
381
- if (options.tabReplacement && !profile.staticCapture) {
382
- await delay(500);
609
+ finally {
610
+ __disposeResources(env_1);
383
611
  }
384
- // Resize and minimize window.
385
- await resizeAndMinimizeWindow(page);
386
- LOG.debug("timing:startup", "Page with capture ready. Total: %sms.", captureElapsed());
387
- return {
388
- captureStream: outputStream,
389
- context,
390
- directTune: usedDirectUrl || strategyDirectTune || !isChannelSelectionProfile(profile),
391
- ffmpegProcess,
392
- manifestInterception,
393
- page,
394
- rawCaptureStream
395
- };
396
612
  }
397
613
  // URL Redirect Resolution.
398
614
  /**
@@ -421,8 +637,8 @@ async function resolveRedirectUrl(url) {
421
637
  * Promise that resolves once the write is committed; resolution-layer errors thrown from the underlying file store are surfaced for caller-side logging but do not
422
638
  * abort the tune (selectChannel attaches a .catch on the returned promise).
423
639
  *
424
- * Idempotent at the storage layer: writing the same selector twice is a no-op (the file store deduplicates identical deltas via normalization). The closure does
425
- * not pre-check whether the value differs - the underlying store handles that.
640
+ * Safe to call more than once at the storage layer: writing the same selector twice is a no-op (the file store deduplicates identical deltas via normalization).
641
+ * The closure does not pre-check whether the value differs - the underlying store handles that.
426
642
  * @param canonicalKey - The canonical channel key (e.g., "fox").
427
643
  * @param serviceTag - The active service tag (e.g., "foxone").
428
644
  * @returns Async callback that persists the resolved selector to the channel store.
@@ -437,21 +653,116 @@ function buildPersistResolutionCallback(canonicalKey, serviceTag) {
437
653
  LOG.debug("tuning", "Persisted resolved selector \"%s\" to channel store as \"%s\".", resolvedSelector, variantKey);
438
654
  };
439
655
  }
656
+ /* Decides whether the manifest a tune's interception selected belongs to the channel the profile selects, using the provider's own verifier when one exists. The
657
+ * provider and channelSelector gates run before the interception promise is awaited, so a stream with nothing to verify never waits on the interception
658
+ * here...the promise's settle time belongs only to the paths that consume it. Verification is gated to master-kind selections because a provider verifier like
659
+ * Fox's reads the channel call sign from a fixed segment of the master CDN URL, and a media (chunklist) URL has a different path shape the verifier was never
660
+ * calibrated for - a false tune-failure on a correct stream. Providers without a verifier, profiles without a channelSelector, non-master selections, and a null
661
+ * interception all verify vacuously: the gates upstream are the only identity signal we have there, so the honest answer is no objection rather than a guess.
662
+ *
663
+ * @param interceptionPromise - The interception promise from the handle the tune finalized, awaited only once the gates above admit a verifier.
664
+ * @param profile - The resolved site profile whose strategy names the provider and whose channelSelector names the expected channel.
665
+ * @returns A human-readable failure reason when the manifest belongs to a different channel, or null when it verifies (including every vacuous case).
666
+ */
667
+ export async function verifyManifestSelection(interceptionPromise, profile) {
668
+ const provider = getProviderByStrategy(profile.channelSelection.strategy);
669
+ if (!provider?.verifyManifestForChannel || !profile.channelSelector) {
670
+ return null;
671
+ }
672
+ const interception = await interceptionPromise;
673
+ if (interception?.selectedKind !== "master") {
674
+ return null;
675
+ }
676
+ return provider.verifyManifestForChannel(interception.manifestUrl, profile.channelSelector);
677
+ }
678
+ // Channel Establishment.
679
+ /**
680
+ * Sizes the interception observation window for one establishment run. The observer has to outlive every phase that can feed it: the caller's own navigation
681
+ * allowance (retry-aware on the tune path, a single attempt on the refresh path), the bounded playback initialization, the settle the interceptor waits out
682
+ * before it resolves, and a margin covering the small span between those phases. A window that expired mid-establishment would resolve with whatever the page
683
+ * load captured rather than the channel the selection landed on, which is the outcome this budget forecloses. Because no healthy path waits on the timer - it is
684
+ * a leak bound for an establishment that dies without unwinding - its generosity costs nothing.
685
+ * @param navigationAllowanceMs - The caller's worst-case navigation allowance, including any retry backoff its own policy performs.
686
+ * @returns The observation window, in milliseconds.
687
+ */
688
+ export function establishmentBudgetMs(navigationAllowanceMs) {
689
+ return navigationAllowanceMs + PLAYBACK_INIT_TIMEOUT + FINALIZE_SETTLE_DELAY + INTERCEPTION_BUDGET_MARGIN_MS;
690
+ }
691
+ /**
692
+ * Derives the direct-tune kind the interception is finalized against. A tune is direct when the route was a cached direct watch URL, when the strategy itself
693
+ * resolved a direct tune through API interception, or when the profile has no DOM-based channel-selection step to run at all. The refresh path always navigates
694
+ * the configured channel url with full selection, so it never has a cached direct URL to report and omits that term entirely.
695
+ * @param options - The resolved profile plus the tune facts the kind reads: whether a cached direct URL was taken (absent on paths that cannot take one) and
696
+ * whether the strategy resolved a direct tune of its own.
697
+ * @returns True when the interception should adjudicate as a direct tune.
698
+ */
699
+ export function computeDirectTuneKind(options) {
700
+ return (options.usedDirectUrl ?? false) || options.strategyDirectTune || !isChannelSelectionProfile(options.profile);
701
+ }
702
+ const defaultEstablishChannelPlaybackDeps = { initializePlayback };
703
+ /**
704
+ * Establishes a channel on a page: navigate under the caller's own policy, stamp the interception's observation epoch, then run playback initialization under
705
+ * the module's safety-net bound. The tune path and the native refresh capability both run this, so the sequence and its ordering are written exactly once and
706
+ * only per-path policy differs.
707
+ * @param page - The page being established.
708
+ * @param profile - The resolved site profile whose strategy drives navigation and channel selection.
709
+ * @param handle - The interceptor observing this establishment, or null when nothing is observing it (a tab replacement, or an install that failed).
710
+ * @param options - The caller's navigation policy, its initialization options, and an optional settlement hook. See EstablishChannelPlaybackOptions.
711
+ * @param deps - Injected browser-boundary collaborator; defaults to the real playback initializer in production.
712
+ * @returns The result playback initialization produced: the video context for subsequent monitoring, and the strategy's direct-tune flag where it resolved one.
713
+ * @throws The caller's navigation failure, the initialization's own failure, or the bound's timeout error when the bound lapses first.
714
+ */
715
+ export async function establishChannelPlayback(page, profile, handle, options, deps = defaultEstablishChannelPlaybackDeps) {
716
+ const { initOptions, navigate, onInitSettled } = options;
717
+ await navigate();
718
+ // Channel selection begins here, so the observation epoch is stamped now - manifests seen earlier belong to page load (a guide page's auto-played default
719
+ // channel), and guide-tune selection prefers a master observed after this point. A null handle means nothing is observing, so there is no epoch to stamp.
720
+ handle?.markChannelSelectionStart();
721
+ const initPromise = deps.initializePlayback(page, profile, initOptions);
722
+ /* The settlement hook rides the initialization's own settlement rather than a fixed step after the wait below, because only the promise knows which of
723
+ * success, failure, or a late completion actually happened - and a late completion is reachable, since the wait abandons rather than cancels: a lapsed
724
+ * initialization is not stopped, it winds down on its own bounded internal phases and can finish afterward. The trailing catch is the whole safety net for
725
+ * the hook itself, absorbing a hook that throws as well as a structurally-permitted async hook that rejects, so this chain can never become a rejection
726
+ * source of its own.
727
+ */
728
+ if (onInitSettled) {
729
+ void initPromise.finally(() => onInitSettled()).catch(() => { });
730
+ }
731
+ return waitWithTimeout(initPromise, PLAYBACK_INIT_TIMEOUT, new Error("Playback initialization timed out after " + String(PLAYBACK_INIT_TIMEOUT) + "ms."));
732
+ }
733
+ /**
734
+ * Adjudicates what an establishment's interception selected: finalize it against the honest direct-tune kind, then confirm the manifest belongs to the channel
735
+ * the profile names. The tune path and the native refresh capability both run this, so finalize-then-verify is written exactly once.
736
+ *
737
+ * The stage hands on the interception promise rather than a resolved result, which is what keeps verifyManifestSelection's latency rule holding - its provider
738
+ * and selector gates run before the promise is awaited, so a stream with nothing to verify never waits on the interception here.
739
+ * @param handle - The interceptor this establishment installed, still observing.
740
+ * @param profile - The resolved site profile whose strategy names the provider and whose channelSelector names the expected channel.
741
+ * @param directTune - The direct-tune kind to finalize against.
742
+ * @returns A human-readable failure reason when the manifest belongs to a different channel, or null when it verifies (including every vacuous case).
743
+ */
744
+ export async function adjudicateChannelSelection(handle, profile, directTune) {
745
+ handle.finalize(directTune);
746
+ return verifyManifestSelection(handle.promise, profile);
747
+ }
440
748
  /**
441
749
  * Sets up a stream: validates input, creates browser page, initializes capture, navigates to URL, and starts health monitoring.
442
750
  *
443
751
  * This function handles all common stream setup logic. The caller is responsible for:
444
- * - Connecting the returned captureStream to the appropriate output (HTTP response, FFmpeg, etc.)
752
+ * - Creating the segmenter and attaching it to the returned capture session (via captureSession.attachSegmenter), or upgrading to native streaming
445
753
  * - Registering the stream in the registry
446
754
  * - Triggering cleanup when the stream ends
447
755
  *
448
756
  * @param options - Stream configuration options.
449
757
  * @param onCircuitBreak - Callback invoked when the circuit breaker trips (stream unrecoverable).
450
- * @returns Setup result with capture stream, cleanup function, and metadata.
758
+ * @param deps - The injected browser and overlay-poll collaborators, forwarded to every establishment attempt this function makes; defaults to
759
+ * defaultCreatePageWithCaptureDeps. Threaded for the same reason createPageWithCapture takes them: a test drives the establishment sequence - including its guide
760
+ * fallback - without a live Chrome by substituting the shared-browser accessor, the capture launcher, and the overlay poll.
761
+ * @returns Setup result with capture session, cleanup function, and metadata.
451
762
  * @throws StreamSetupError if setup fails with appropriate status code and message.
452
763
  */
453
- export async function setupStream(options, onCircuitBreak) {
454
- const { channel, channelName, channelSelector, clickSelector, clickToPlay, onTabReplacementFactory, profileOverride, staticCapture, url } = options;
764
+ export async function setupStream(options, onCircuitBreak, deps = defaultCreatePageWithCaptureDeps) {
765
+ const { channel, channelName, channelSelector, clickSelector, clickToPlay, onTabReplacementFactory, probeIdentity, profileOverride, staticCapture, url } = options;
455
766
  // Use pre-allocated IDs from a pending registry entry when available, or generate new ones. Pre-allocated IDs ensure the abort controller, health monitor, and
456
767
  // tab replacement handler all reference the same stream identity as the pending entry in the registry.
457
768
  const streamId = options.streamId ?? generateStreamId(channelName, url);
@@ -480,218 +791,348 @@ export async function setupStream(options, onCircuitBreak) {
480
791
  let profileName = profileResult.profileName;
481
792
  // Wrap the setup in stream context for log correlation.
482
793
  return runWithStreamContext({ channelName: channel?.name, streamId, url }, async () => {
483
- // Apply profile override if specified.
484
- if (profileOverride) {
485
- const validProfiles = getProfiles().map((p) => p.name);
486
- if (validProfiles.includes(profileOverride)) {
487
- profile = resolveProfile(profileOverride);
488
- profileName = profileOverride;
489
- LOG.debug("streaming:setup", "Profile overridden to '%s' via query parameter.", profileOverride);
794
+ const env_2 = { stack: [], error: void 0, hasError: false };
795
+ try {
796
+ // Apply profile override if specified.
797
+ if (profileOverride) {
798
+ // An override may name any profile that exists: a builtin from any source through the single lookup, or one of the user's own. The UI profile catalog is
799
+ // not the oracle here - it omits the provider profiles, which a direct override may legitimately ask for.
800
+ if (Boolean(getBuiltinProfile(profileOverride)) || (profileOverride in getUserProfiles())) {
801
+ profile = resolveProfile(profileOverride);
802
+ profileName = profileOverride;
803
+ LOG.debug("streaming:setup", "Profile overridden to '%s' via query parameter.", profileOverride);
804
+ }
805
+ else {
806
+ LOG.warn("Unknown profile override '%s', using resolved profile.", profileOverride);
807
+ }
490
808
  }
491
- else {
492
- LOG.warn("Unknown profile override '%s', using resolved profile.", profileOverride);
809
+ /* A channel that named a profile and did not get it should hear about it once, at the moment it matters. Resolution reports the substitution on its return
810
+ * rather than logging it, because the playlist render and the channel table call the same resolver and would repeat the message on every fetch and every
811
+ * draw. Comparing profileName against the resolver's keeps a query-parameter override quiet: when ?profile= replaces the resolution wholesale, the
812
+ * substitution the resolver reported is not what tunes.
813
+ */
814
+ if (profileResult.overriddenProfile && (profileName === profileResult.profileName)) {
815
+ LOG.warn("Channel %s specifies the %s profile, which requires a channel selector the channel does not define; tuning with the %s profile instead.", channel?.name ?? channelName ?? url, profileResult.overriddenProfile, profileName);
493
816
  }
494
- }
495
- // Apply static capture override if specified.
496
- if (staticCapture) {
497
- profile = { ...profile, staticCapture: true };
498
- }
499
- // Merge the ad-hoc channel selector into the profile if provided. This must happen after the profile override block above, which replaces the profile object
500
- // wholesale and would discard an earlier merge. For predefined channels, getProfileForChannel already handles the merge from channel.channelSelector.
501
- if (channelSelector) {
502
- profile = { ...profile, channelSelector };
503
- }
504
- // Merge the ad-hoc clickToPlay and clickSelector options into the profile. clickSelector implies clickToPlay. For ad-hoc streams, these enable clicking an
505
- // element to start playback - either the video element (clickToPlay alone) or a play button overlay (clickToPlay + clickSelector).
506
- if (clickToPlay || clickSelector) {
507
- profile = { ...profile, clickToPlay: true, ...(clickSelector ? { clickSelector } : {}) };
508
- }
509
- // Compute the metadata comment for FFmpeg. Prefer the friendly channel name, fall back to the channel key, or extract the domain from the URL.
510
- const metadataComment = channel?.name ?? channelName ?? extractDomain(url);
511
- // Compute the friendly service display name once for use in both the monitor and the setup result.
512
- const serviceName = getServiceDisplayName(url);
513
- // Create the tab replacement handler if a factory was provided. This is done after profile resolution so the handler has access to the final profile.
514
- const onTabReplacement = onTabReplacementFactory ? onTabReplacementFactory(numericStreamId, streamId, profile, metadataComment) : undefined;
515
- // Validate URL.
516
- const validation = validateStreamUrl(url);
517
- if (!validation.valid) {
518
- LOG.error("Invalid URL requested: %s - %s.", url, validation.reason ?? "Unknown error");
519
- throw new StreamSetupError("Invalid URL: " + (validation.reason ?? "Unknown error"), 400, validation.reason ?? "Invalid URL.");
520
- }
521
- // Check concurrent stream limit.
522
- if (getStreamCount() >= CONFIG.streaming.maxConcurrentStreams) {
523
- LOG.warn("Concurrent stream limit reached (%s/%s). Rejecting request.", getStreamCount(), CONFIG.streaming.maxConcurrentStreams);
524
- throw new StreamSetupError("Concurrent stream limit reached.", 503, "Maximum concurrent streams (" + String(CONFIG.streaming.maxConcurrentStreams) + ") reached. Try again later.");
525
- }
526
- // Create page and start capture using the shared function. This handles browser page creation, capture initialization, FFmpeg spawning, and navigation with retry.
527
- let captureResult;
528
- try {
529
- // Skip CDP manifest interception if the probe cache already knows this channel uses DRM. This avoids creating a CDP session that sits idle for 15 seconds
530
- // before the interceptor timeout cleans it up.
531
- const skipInterception = channelName ? (getCachedEncryption(channelName) === "drm") : false;
532
- // Build the persistResolution closure for the active channel. When the resolution layer in selectChannel() converts a category selector to a concrete call
533
- // sign, this closure writes the result to the user's channel store as a per-service-variant override - the same shape produced when a user manually edits
534
- // the selector via the web UI. Omitted for ad-hoc URL streams (no channel record to update).
535
- const serviceTag = getDomainConfig(url)?.serviceTag;
536
- const persistResolution = (channelName && serviceTag) ?
537
- buildPersistResolutionCallback(channelName, serviceTag) :
538
- undefined;
539
- captureResult = await createPageWithCapture({
540
- comment: metadataComment,
541
- onFFmpegError: onCircuitBreak,
542
- persistResolution,
817
+ // Apply static capture override if specified.
818
+ if (staticCapture) {
819
+ profile = { ...profile, staticCapture: true };
820
+ }
821
+ // Merge the ad-hoc channel selector into the profile if provided. This must happen after the profile override block above, which replaces the profile object
822
+ // wholesale and would discard an earlier merge. For predefined channels, getProfileForChannel already handles the merge from channel.channelSelector.
823
+ if (channelSelector) {
824
+ profile = { ...profile, channelSelector };
825
+ }
826
+ // Merge the ad-hoc clickToPlay and clickSelector options into the profile. clickSelector implies clickToPlay. For ad-hoc streams, these enable clicking an
827
+ // element to start playback - either the video element (clickToPlay alone) or a play button overlay (clickToPlay + clickSelector).
828
+ if (clickToPlay || clickSelector) {
829
+ profile = { ...profile, clickToPlay: true, ...(clickSelector ? { clickSelector } : {}) };
830
+ }
831
+ // Compute the metadata comment for FFmpeg. Prefer the friendly channel name, fall back to the channel key, or extract the domain from the URL.
832
+ const metadataComment = channel?.name ?? channelName ?? extractDomain(url);
833
+ // Compute the friendly service display name once for use in both the monitor and the setup result.
834
+ const serviceName = getServiceDisplayName(url);
835
+ // Create the tab replacement handler if a factory was provided. This is done after profile resolution so the handler has access to the final profile.
836
+ const onTabReplacement = onTabReplacementFactory ? onTabReplacementFactory(numericStreamId, streamId, profile, metadataComment) : undefined;
837
+ // Validate URL.
838
+ const validation = validateStreamUrl(url);
839
+ if (!validation.valid) {
840
+ LOG.error("Invalid URL requested: %s - %s.", url, validation.reason ?? "Unknown error");
841
+ throw new StreamSetupError("Invalid URL: " + (validation.reason ?? "Unknown error"), 400, validation.reason ?? "Invalid URL.");
842
+ }
843
+ // Concurrent-stream capacity is reserved upstream at the registration site (reserveStreamSlot in hls.ts) before this stream's pending entry is registered, so
844
+ // the new stream is excluded from its own check. We deliberately do NOT re-check here: by the time setupStream runs, getStreamCount() already includes this
845
+ // stream's pending entry, so a count-based check would double-count it against its own slot and reject at the legitimate boundary - after the client has
846
+ // already received a preroll playlist. reserveStreamSlot is the single source of truth for the capacity decision; setupStream's sole caller (completeStreamSetup)
847
+ // always reserves before reaching here.
848
+ // Create page and start capture using the shared function. This handles browser page creation, capture initialization, FFmpeg spawning, and navigation with retry.
849
+ let captureResult;
850
+ try {
851
+ /* Skip CDP manifest interception when the channel is pinned to screen capture, or when the probe cache already knows this stream's binding resolves to
852
+ * DRM. The per-channel override short-circuits first, so a forced channel never installs the interceptor: nothing intercepts a manifest, no native
853
+ * attempt runs, no probe fires, and the encryption cache stays untouched by that stream. The cache half avoids creating a CDP session that sits idle for
854
+ * 15 seconds before the interceptor timeout cleans it up; every stream carries an identity, ad-hoc URLs included, so that lookup needs no guard.
855
+ */
856
+ const skipInterception = (channel?.forceCapture === true) || (getCachedEncryption(probeIdentity) === "drm");
857
+ // Build the persistResolution closure for the active channel. When the resolution layer in selectChannel() converts a category selector to a concrete call
858
+ // sign, this closure writes the result to the user's channel store as a per-service-variant override - the same shape produced when a user manually edits
859
+ // the selector via the web UI. Omitted for ad-hoc URL streams (no channel record to update).
860
+ const serviceTag = getDomainConfig(url)?.serviceTag;
861
+ const persistResolution = (channelName && serviceTag) ?
862
+ buildPersistResolutionCallback(channelName, serviceTag) :
863
+ undefined;
864
+ const attemptOptions = {
865
+ comment: metadataComment,
866
+ numericStreamId,
867
+ onFFmpegError: onCircuitBreak,
868
+ persistResolution,
869
+ profile,
870
+ skipManifestInterception: skipInterception,
871
+ streamId,
872
+ url
873
+ };
874
+ /* The establishment, with its one guide fallback. A failure the coordinator blamed on a direct watch URL arrives here typed, with the hint already evicted;
875
+ * one more invocation with the resolution skipped is exactly what the next tune would do, and doing it now spares the client a whole failed request. The
876
+ * fallback runs after the first attempt's DisposableStack has unwound, so it gets a fresh page, a fresh manifest-interception handle, and usedDirectUrl
877
+ * naturally false - the direct-tune marker and the manifest finalizer are computed per invocation and need no reset. Only the typed error is caught here;
878
+ * every other failure, the fallback's own included, falls to setupStream's catch below so one classification block serves both attempts identically.
879
+ *
880
+ * The worst case it can produce: a stale hint plus a genuinely broken guide costs two establishments before the classified failure. On the preroll-fed HLS
881
+ * path the client stays fed throughout; on the blocking callers - pretune, MPEG-TS, an ad-hoc play - the alternative is the same second establishment run by
882
+ * the caller's own retry, one request later.
883
+ */
884
+ const establish = async () => {
885
+ try {
886
+ return await createPageWithCapture(attemptOptions, deps);
887
+ }
888
+ catch (error) {
889
+ if (!(error instanceof DirectUrlEstablishmentError)) {
890
+ throw error;
891
+ }
892
+ LOG.warn("The direct watch URL for %s did not establish playback. Retrying once through the provider's guide.", metadataComment);
893
+ return await createPageWithCapture({ ...attemptOptions, skipDirectUrl: true }, deps);
894
+ }
895
+ };
896
+ captureResult = await establish();
897
+ }
898
+ catch (error) {
899
+ // The browser supervisor's acquire() rejects with these while the capture system is recovering: BrowserUnavailableError when the relaunch governor is cooling
900
+ // (degraded), BrowserSupersededError when an in-flight launch was abandoned by a readiness-loss. Both are transient "retry me" conditions, so they map to a 503
901
+ // back-off (Channels DVR honors the Retry-After the route attaches to a 503). We handle them first and WITHOUT an error log: the supervisor raises the loud
902
+ // degraded alarm once on the transition, so the per-request 503s during the cooldown must stay quiet rather than spam an error on every Channels DVR retry.
903
+ // Their messages carry no capture-infrastructure signature, so without this explicit branch the classifier below would make them a 500 the client never backs
904
+ // off from. This must precede isCaptureInfrastructureError.
905
+ if ((error instanceof BrowserUnavailableError) || (error instanceof BrowserSupersededError)) {
906
+ throw new StreamSetupError("Browser temporarily unavailable.", 503, "The capture system is recovering. Please retry shortly.", { cause: error });
907
+ }
908
+ // createPageWithCapture handles its own cleanup on failure (closes page, kills FFmpeg).
909
+ const errorMessage = formatError(error);
910
+ const lowerMessage = errorMessage.toLowerCase();
911
+ const benignPatterns = ["abort", "session closed"];
912
+ const isBenign = benignPatterns.some((pattern) => lowerMessage.includes(pattern));
913
+ if (!isBenign) {
914
+ LOG.error("Stream setup failed for %s: %s.", url, errorMessage);
915
+ }
916
+ // Capture infrastructure errors should return 503 to signal Channels DVR to back off. These include Chrome capture state issues, capture-lock turn-wait
917
+ // timeouts, and stream initialization failures. Using 503 with Retry-After prevents retry storms when there's a systemic issue. isCaptureInfrastructureError
918
+ // (recovery.ts) is the single source of truth for this classification, shared with the browser supervisor's readiness detection.
919
+ const isCaptureError = isCaptureInfrastructureError(errorMessage);
920
+ // A capture-infrastructure failure may mean the browser, though still connected, can no longer capture. Hand it to the passive mid-life detector, which (guarded
921
+ // and single-flight, in the background) re-verifies capture readiness and invalidates the browser for a governed relaunch if confirmed. Fire-and-forget: it must
922
+ // never delay this response.
923
+ if (isCaptureError) {
924
+ noteCaptureInfrastructureFailure(numericStreamId);
925
+ }
926
+ // A failed tune on a service currently marked needs-sign-in most likely failed AT the auth wall, so the user-facing message leads with the remedy.
927
+ throw new StreamSetupError("Stream error.", isCaptureError ? 503 : 500, withSignInGuidance("Failed to start stream.", channelName, serviceName), { cause: error });
928
+ }
929
+ const { captureSession, context, directTune, manifestInterception, page } = captureResult;
930
+ // Hold the page, capture session, and interceptor on a scope guard so a tune-verification failure below disposes them structurally rather than repeating the
931
+ // teardown inline. Push order mirrors the capture-setup resource stack (page, capture session, interception): the interception is registered last so it disposes
932
+ // first, but its disposal is a CDP observer detach with no ordering dependency on either peer; the pair that must stay ordered is capture-session-before-page,
933
+ // so the capture stream is destroyed and STOP_RECORDING fires while the browser is still connected, ahead of the page close. On success we move() the guard and
934
+ // hand ownership to the cleanup closure (and, once the session is installed on the registry entry, to terminateStream).
935
+ const owned = __addDisposableResource(env_2, new DisposableStack(), false);
936
+ owned.adopt(page, disposePage);
937
+ owned.use(captureSession);
938
+ if (manifestInterception) {
939
+ owned.use(manifestInterception);
940
+ }
941
+ // Tune verification. The shared adjudication stage finalizes the manifest interceptor and confirms the captured master manifest URL belongs to the channel
942
+ // that was just tuned. This step makes setupStream "verified by construction" - every consumer of StreamSetupResult (HLS preroll, HLS blocking, MPEG-TS,
943
+ // native proxy, capture mode) receives a stream guaranteed to be on the requested channel without having to opt in or coordinate. Streams with no manifest
944
+ // interception at all (DRM-cached channels, tab replacements) have nothing to adjudicate and skip the step entirely; verifyManifestSelection owns which of
945
+ // the remaining ones it can speak to. A failure reason throws StreamSetupError so the existing failure path marks channel health, terminates the pending
946
+ // registry entry, and surfaces a clear error - never silently delivers the wrong channel. The scope guard disposes the capture session, interceptor, and page
947
+ // as the throw unwinds.
948
+ if (manifestInterception) {
949
+ const verifyError = await adjudicateChannelSelection(manifestInterception, profile, directTune);
950
+ if (verifyError) {
951
+ await minimizeBrowserWindow();
952
+ const failureLabel = channel?.name ?? channelName ?? url;
953
+ throw new StreamSetupError("Tune verification failed: " + verifyError, 502, withSignInGuidance("Tune verification failed for " + failureLabel + ". " + verifyError, channelName, serviceName));
954
+ }
955
+ }
956
+ // Monitor stream info for status updates. The serviceTag enables service-specific monitoring flags (e.g., tinySegmentThreshold).
957
+ const monitorStreamInfo = {
958
+ channelName: channel?.name ?? null,
959
+ numericStreamId,
960
+ serviceName,
961
+ serviceTag: getDomainConfig(url)?.serviceTag,
962
+ startTime
963
+ };
964
+ // Start the health monitor for this stream.
965
+ const monitor = monitorPlaybackHealth(page, context, profile, url, streamId, monitorStreamInfo, onCircuitBreak, onTabReplacement);
966
+ // Cleanup function. Releases all resources associated with the stream. Safe to call more than once. completeStreamSetup uses it as the fallback teardown
967
+ // when the pending entry is terminated before the capture session is installed on it; once installed, terminateStream disposes the same session (safe to
968
+ // call disposal more than once). Disposes the capture session (kill -> destroy -> stop) before closing the page so STOP_RECORDING fires while the browser is
969
+ // connected.
970
+ let cleanupCompleted = false;
971
+ const cleanup = async () => {
972
+ if (cleanupCompleted) {
973
+ return;
974
+ }
975
+ cleanupCompleted = true;
976
+ monitor.dispose();
977
+ captureSession.dispose();
978
+ disposePage(page);
979
+ // Re-minimize the browser window.
980
+ await minimizeBrowserWindow();
981
+ };
982
+ // Success: transfer ownership out of the scope guard. The cleanup closure and, once the session is installed on the registry entry, terminateStream become
983
+ // responsible for disposing the page and capture session; the interceptor continues into tune verification and native streaming.
984
+ owned.move();
985
+ // Return the setup result.
986
+ return {
987
+ captureSession,
988
+ channelName: channel?.name ?? null,
989
+ cleanup,
990
+ directTune,
991
+ manifestInterception,
992
+ monitor,
993
+ numericStreamId,
994
+ page,
995
+ probeIdentity,
543
996
  profile,
544
- skipManifestInterception: skipInterception,
997
+ profileName,
998
+ serviceName,
999
+ startTime,
545
1000
  streamId,
546
1001
  url
547
- });
1002
+ };
548
1003
  }
549
- catch (error) {
550
- // createPageWithCapture handles its own cleanup on failure (closes page, kills FFmpeg).
551
- const errorMessage = formatError(error);
552
- const lowerMessage = errorMessage.toLowerCase();
553
- const benignPatterns = ["abort", "session closed"];
554
- const isBenign = benignPatterns.some((pattern) => lowerMessage.includes(pattern));
555
- if (!isBenign) {
556
- LOG.error("Stream setup failed for %s: %s.", url, errorMessage);
557
- }
558
- // Capture infrastructure errors should return 503 to signal Channels DVR to back off. These include Chrome capture state issues, queue timeouts, and stream
559
- // initialization failures. Using 503 with Retry-After prevents retry storms when there's a systemic issue.
560
- const captureErrorPatterns = ["Cannot capture", "timed out", "Capture queue"];
561
- const isCaptureError = captureErrorPatterns.some((pattern) => errorMessage.includes(pattern));
562
- throw new StreamSetupError("Stream error.", isCaptureError ? 503 : 500, "Failed to start stream.", { cause: error });
1004
+ catch (e_2) {
1005
+ env_2.error = e_2;
1006
+ env_2.hasError = true;
563
1007
  }
564
- const { captureStream, context, directTune, ffmpegProcess, manifestInterception, page, rawCaptureStream } = captureResult;
565
- // Tune verification. Finalize the manifest interceptor and confirm the captured master manifest URL belongs to the channel that was just tuned. This step
566
- // makes setupStream "verified by construction" - every consumer of StreamSetupResult (HLS preroll, HLS blocking, MPEG-TS, native proxy, capture mode) receives
567
- // a stream guaranteed to be on the requested channel without having to opt in or coordinate.
568
- //
569
- // The verifier is a per-provider hook on ProviderModule.verifyManifestForChannel. Today only foxProvider implements it (Fox's CDN URL encodes the channel call
570
- // sign in the path). Verification is opportunistic: providers without a verifier and streams without a manifest interception (e.g., DRM-cached channels, tab
571
- // replacements) skip the check. When a verifier returns a failure reason, we tear down the capture and throw StreamSetupError so the existing failure path
572
- // marks channel health, terminates the pending registry entry, and surfaces a clear error - never silently delivers the wrong channel.
573
- if (manifestInterception) {
574
- manifestInterception.finalize(directTune);
575
- const provider = getProviderByStrategy(profile.channelSelection.strategy);
576
- if (provider?.verifyManifestForChannel && profile.channelSelector) {
577
- const interception = await manifestInterception.promise;
578
- if (interception) {
579
- const verifyError = provider.verifyManifestForChannel(interception.masterManifestUrl, profile.channelSelector);
580
- if (verifyError) {
581
- // Verification failed. Tear down the capture before throwing so we do not leak the page, FFmpeg process, or browser-side capture session. We replicate
582
- // the relevant cleanup steps inline because the cleanup() closure has not been constructed yet at this point in the function.
583
- if (!rawCaptureStream.destroyed) {
584
- rawCaptureStream.destroy();
585
- }
586
- if (ffmpegProcess) {
587
- ffmpegProcess.kill();
588
- }
589
- unregisterManagedPage(page);
590
- if (!page.isClosed()) {
591
- page.close().catch((closeError) => {
592
- LOG.debug("streaming:setup", "Page close error during verification failure cleanup: %s.", formatError(closeError));
593
- });
594
- }
595
- await minimizeBrowserWindow();
596
- const failureLabel = channel?.name ?? channelName ?? url;
597
- throw new StreamSetupError("Tune verification failed: " + verifyError, 502, "Tune verification failed for " + failureLabel + ". " + verifyError);
1008
+ finally {
1009
+ __disposeResources(env_2);
1010
+ }
1011
+ });
1012
+ }
1013
+ /* Re-establishes a stream's channel on its page and returns the fresh manifest interception, for the native token-refresh path. It runs the same establishment
1014
+ * composition the tune path runs - navigate under this path's own policy, stamp the observation epoch, initialize playback under the shared bound, then adjudicate
1015
+ * what the interception selected - so there is exactly one establishment sequence in this system. The deliberate divergences from the tune path are each owned here.
1016
+ * Navigation is single-attempt: both callers of the refresh chain bring their own retry ladders (the proactive timer re-arms failures with bounded backoff, and the
1017
+ * monitor escalates a persisting stall to capture fallback), so retrying inside would stack ladders and stretch a recovery the project wants fast. The route is
1018
+ * always the configured channel url with full channel selection: a cached direct URL and a skipped selection belong to the tune's own optimization, and the
1019
+ * re-establishment takes the click-verified route instead, so the kind it finalizes against carries no cached-direct-URL term. And a category selector's
1020
+ * re-resolved call sign is not persisted here: persistence belongs to the tune lifecycle, and the recovery paths already re-tune without it. Playback
1021
+ * initialization is bounded by the same race the tune uses, with the same abandon-on-timeout semantics; the remnant is bounded and act-limited - its phases expire
1022
+ * on their own internal timeouts, its consent poll can only reject cookie banners and dismiss per-site modals within its fixed window, and a later navigation on
1023
+ * the page force-settles whatever remains - so a subsequent refresh attempt starting on this page meets at worst a dying consent poll, never a competing channel
1024
+ * click. Failures normalize to null with a warning rather than throwing - the caller is a background refresh cycle whose ladder already owns the endgame. The body
1025
+ * runs under the stream's log context so the composed primitives' own lines carry the stream prefix: the ambient context is kept when a caller already established
1026
+ * one (the monitor's recovery path carries a richer context, show-name resolution included, that a nested run would replace rather than merge), and is supplied
1027
+ * only on the proactive timer's path, which has none. The page's audio is re-muted on the initialization's own settlement rather than at any fixed step: playback
1028
+ * establishment unmutes by direct property write, so an already-playing element stays audible until re-muted, and attaching the re-mute to the initialization
1029
+ * promise covers every outcome - success, failure, and a timed-out attempt whose establishment completes late - while the play-override the native upgrade
1030
+ * registered keeps future play() calls muted without re-registration.
1031
+ *
1032
+ * @param options - The stream facts the re-establishment closes over. See ReestablishChannelManifestOptions.
1033
+ * @returns The verified manifest interception, or null when the channel could not be re-established.
1034
+ */
1035
+ export async function reestablishChannelManifest(options) {
1036
+ const { channelName, page, profile, streamIdStr, url } = options;
1037
+ const establish = async () => {
1038
+ try {
1039
+ const env_3 = { stack: [], error: void 0, hasError: false };
1040
+ try {
1041
+ // The navigation allowance handed to the budget is a single attempt's timeout, since this path navigates once by design; establishmentBudgetMs owns why
1042
+ // the observer has to outlive that allowance and every step after it.
1043
+ const budgetMs = establishmentBudgetMs(CONFIG.streaming.navigationTimeout);
1044
+ // Scope-bind the interceptor with "using" so its CDP observer is disposed on every exit from this function, including the early returns below.
1045
+ const handle = __addDisposableResource(env_3, await installManifestInterceptor(page, budgetMs), false);
1046
+ if (!handle) {
1047
+ LOG.warn("The channel for %s could not be re-established: the manifest interceptor did not install.", channelName ?? url);
1048
+ return null;
1049
+ }
1050
+ // Establish the channel through the shared composition: navigate, stamp the observation epoch - which fences anything the reloaded page auto-played so it
1051
+ // cannot win adjudication over the channel the selection lands on - then run playback initialization under the same bound the tune path uses.
1052
+ const tuneResult = await establishChannelPlayback(page, profile, handle, {
1053
+ initOptions: { requestedUrl: url },
1054
+ // Navigate through the profile's own wait strategy, single-attempt by design. A navigation failure throws to the catch below and normalizes to null.
1055
+ navigate: async () => {
1056
+ await navigateToPage(page, url, profile);
1057
+ },
1058
+ // Restore the page's mute when the initialization settles. Playback establishment unmutes by direct property write, so an element that is already
1059
+ // playing stays audible until it is re-muted, and attaching the restore to the settlement is what covers every outcome rather than only the happy one.
1060
+ onInitSettled: () => {
1061
+ void muteExistingVideos(page);
598
1062
  }
1063
+ });
1064
+ // Adjudicate honestly: the kind formula gets no cached-direct-URL term here, because the route above never takes one.
1065
+ const verifyError = await adjudicateChannelSelection(handle, profile, computeDirectTuneKind({ profile, strategyDirectTune: tuneResult.directTune ?? false }));
1066
+ // On a verifier-bearing master path the adjudication has already awaited this promise; on the vacuous path this await is the one that waits out the
1067
+ // finalize settle. Reading it before the verification result is contract rather than preference: an establishment that intercepted nothing has to report
1068
+ // exactly that, and the verification gates raise no objection for a null interception anyway, so the check order is what keeps each warning accurate to
1069
+ // its own outcome.
1070
+ const interception = await handle.promise;
1071
+ if (!interception) {
1072
+ LOG.warn("The channel for %s could not be re-established: no manifest was intercepted.", channelName ?? url);
1073
+ return null;
599
1074
  }
1075
+ if (verifyError) {
1076
+ LOG.warn("The re-established channel for %s did not verify: %s", channelName ?? url, verifyError);
1077
+ return null;
1078
+ }
1079
+ return interception;
600
1080
  }
601
- }
602
- // Monitor stream info for status updates. The serviceTag enables service-specific monitoring flags (e.g., tinySegmentThreshold).
603
- const monitorStreamInfo = {
604
- channelName: channel?.name ?? null,
605
- numericStreamId,
606
- serviceName,
607
- serviceTag: getDomainConfig(url)?.serviceTag,
608
- startTime
609
- };
610
- // Start the health monitor for this stream.
611
- const stopMonitor = monitorPlaybackHealth(page, context, profile, url, streamId, monitorStreamInfo, onCircuitBreak, onTabReplacement);
612
- // Cleanup function. Releases all resources associated with the stream. Idempotent - safe to call multiple times.
613
- let cleanupCompleted = false;
614
- const cleanup = async () => {
615
- if (cleanupCompleted) {
616
- return;
617
- }
618
- cleanupCompleted = true;
619
- // Stop the health monitor first.
620
- stopMonitor();
621
- // Destroy the raw capture stream BEFORE closing the page. This triggers puppeteer-stream's close handler while the browser is still connected, ensuring
622
- // STOP_RECORDING is called and chrome.tabCapture releases the capture. Without this, subsequent getStream() calls may hang with "active stream" errors.
623
- if (!rawCaptureStream.destroyed) {
624
- rawCaptureStream.destroy();
625
- }
626
- // Kill the FFmpeg process if using Matroska+FFmpeg mode.
627
- if (ffmpegProcess) {
628
- ffmpegProcess.kill();
1081
+ catch (e_3) {
1082
+ env_3.error = e_3;
1083
+ env_3.hasError = true;
629
1084
  }
630
- // Unregister from managed pages.
631
- unregisterManagedPage(page);
632
- // Close the browser page (fire-and-forget to avoid blocking on stuck pages).
633
- if (!page.isClosed()) {
634
- page.close().catch((error) => {
635
- LOG.debug("streaming:setup", "Page close error during cleanup: %s.", formatError(error));
636
- });
1085
+ finally {
1086
+ __disposeResources(env_3);
637
1087
  }
638
- // Re-minimize the browser window.
639
- await minimizeBrowserWindow();
640
- };
641
- // Return the setup result.
642
- return {
643
- captureStream,
644
- channelName: channel?.name ?? null,
645
- cleanup,
646
- directTune,
647
- ffmpegProcess,
648
- manifestInterception,
649
- numericStreamId,
650
- page,
651
- profile,
652
- profileName,
653
- rawCaptureStream,
654
- serviceName,
655
- startTime,
656
- stopMonitor,
657
- streamId,
658
- url
659
- };
660
- });
1088
+ }
1089
+ catch (error) {
1090
+ LOG.warn("The channel for %s could not be re-established: %s.", channelName ?? url, formatError(error));
1091
+ return null;
1092
+ }
1093
+ };
1094
+ // Keep a caller's own context rather than nesting a thinner one inside it - the monitor's recovery context carries show-name resolution this frame cannot
1095
+ // rebuild - and establish one only where none exists, which is the proactive refresh timer's bare callback.
1096
+ if (getStreamContext()) {
1097
+ return establish();
1098
+ }
1099
+ return runWithStreamContext({ channelName: channelName ?? undefined, streamId: streamIdStr, url }, establish);
661
1100
  }
662
- // Startup Capture Verification.
1101
+ // Capture Readiness Verification.
663
1102
  /**
664
- * Verifies that Chrome's capture system is functional before the server starts accepting requests. This detects stale tabCapture state left over from a previous
665
- * Chrome process - common during quick service restarts where the old process hasn't fully exited before the new one launches. Without this probe, the first stream
666
- * request would trigger the runtime stale capture handler, which exits the process because the puppeteer-stream mutex is permanently leaked.
1103
+ * Verifies that a freshly-launched Chrome instance can actually capture, by running a real getStream against a throwaway page on it. This is the capability tier of
1104
+ * the browser launch gate: browser/index.ts injects it via setCaptureProbe and runs it inside launchReadyBrowser, so a browser is published as ready only after its
1105
+ * capture capability is verified - at boot AND at every relaunch, not just startup. It exercises the exact getStream path that hangs when the puppeteer-stream
1106
+ * extension is unregistered, so a dead extension is detected immediately rather than causing every subsequent stream request to hang.
667
1107
  *
668
- * The probe creates a temporary page, attempts a short capture, and tears down both cleanly. A 500ms delay after destroying the capture stream allows
669
- * puppeteer-stream's fire-and-forget STOP_RECORDING chain to complete before closing the page, preventing the stale capture cascade on the first real request.
1108
+ * Each attempt creates a temporary page, attempts a short capture, and tears down both cleanly. A 500ms delay after destroying the capture stream allows
1109
+ * puppeteer-stream's fire-and-forget STOP_RECORDING chain to complete before closing the page, preventing a stale capture cascade on the first real request.
670
1110
  *
671
- * After a system reboot, Chrome's display stack or capture extension may not be ready when the service manager starts PrismCast. The probe retries up to
672
- * PROBE_MAX_ATTEMPTS times with a delay between attempts, giving the system time to settle before giving up. This prevents a rapid restart storm where the service
673
- * manager relaunches PrismCast repeatedly, each attempt orphaning a Chrome process and degrading the environment further.
1111
+ * After a system reboot or a fresh relaunch, Chrome's display stack or capture extension may not be ready immediately. The probe retries up to PROBE_MAX_ATTEMPTS
1112
+ * times with a delay between attempts, giving the system time to settle before giving up. At boot this prevents a rapid restart storm where the service manager
1113
+ * relaunches PrismCast repeatedly, each attempt orphaning a Chrome process; at relaunch it provides in-launch settling before the supervisor counts a launch failure.
674
1114
  *
675
- * If stale capture state is detected, the process exits immediately - Chrome restart cannot fix the leaked mutex, only a fresh process can.
1115
+ * If stale capture state is detected, the process exits immediately - a Chrome restart cannot fix the leaked module-level mutex, only a fresh process can.
1116
+ * @param browser - The Chrome instance to verify (the local instance being launched, passed in rather than re-acquired to avoid re-entering the launch in flight).
676
1117
  */
677
- export async function verifyCaptureSystem() {
1118
+ export async function verifyCaptureSystem(browser) {
678
1119
  const PROBE_MAX_ATTEMPTS = 3;
679
1120
  const PROBE_RETRY_DELAY = 5000;
680
- const PROBE_TIMEOUT = 5000;
681
1121
  for (let attempt = 1; attempt <= PROBE_MAX_ATTEMPTS; attempt++) {
1122
+ // The launch gate runs its capture probe OFF the capture lock, deliberately: it fires pre-publish, when supervisor.current() is null, so a wedged gate task would
1123
+ // have no recovery target and would jam the shared lock. GATE mode keeps the internal getStream bound, and this path keeps its own poison exit below.
682
1124
  // eslint-disable-next-line no-await-in-loop -- Sequential retries are intentional; each probe must complete before deciding whether to retry.
683
- const result = await attemptCaptureProbe(PROBE_TIMEOUT);
1125
+ const result = await attemptCaptureProbe(browser, { boundMs: CAPTURE_PROBE_TIMEOUT_MS, kind: "gate" });
684
1126
  // Probe succeeded.
685
1127
  if (result === null) {
686
1128
  return;
687
1129
  }
688
- // Stale capture state is unrecoverable. The error occurs inside puppeteer-stream's second lock section, which has no try/finally - the internal mutex is
689
- // permanently leaked. All subsequent getStream() calls will hang on it. Chrome restart cannot fix module-level state, so exit and let the service manager
690
- // restart with a clean process.
691
- if (result.includes("Cannot capture a tab with an active stream")) {
692
- LOG.error("Startup probe detected stale capture state. puppeteer-stream's internal capture mutex is now permanently locked. Exiting so the service " +
693
- "manager can restart with a clean module state.");
694
- process.exit(1);
1130
+ // Stale capture state is unrecoverable: puppeteer-stream's second lock section has no try/finally, so the mutex is permanently leaked and every subsequent
1131
+ // getStream() hangs. Chrome restart cannot fix module-level state, so escalate from the single home (which logs and schedules the deferred exit), then throw so no
1132
+ // further probe attempts run before the process exits.
1133
+ if (isStaleCaptureMutexError(result)) {
1134
+ escalateStaleCaptureMutex("the launch-gate capture probe");
1135
+ throw new Error("Capture system verification failed: stale capture state detected.");
695
1136
  }
696
1137
  // If we have retries remaining, log a warning and wait before the next attempt.
697
1138
  if (attempt < PROBE_MAX_ATTEMPTS) {
@@ -705,17 +1146,29 @@ export async function verifyCaptureSystem() {
705
1146
  }
706
1147
  }
707
1148
  /**
708
- * Executes a single capture probe attempt. Creates a temporary page, tries to start a capture stream, and tears everything down cleanly.
709
- * @param timeout - Maximum time in milliseconds to wait for getStream() to respond.
1149
+ * Executes a single capture probe attempt. Creates a temporary page on the given browser, tries to start a capture stream, and tears everything down cleanly. It
1150
+ * NEVER throws in either mode: it returns null on success or an error-message string on failure, so callers branch on the string. The two modes differ only in how
1151
+ * getStream is bounded - see CaptureProbeMode.
1152
+ * @param browser - The Chrome instance to probe.
1153
+ * @param mode - The operating mode: gate (internal getStream race) or midlife (self-timed getStream on the lock).
1154
+ * @param clock - Clock used for the mid-life self-timing and the teardown settle. Defaults to realClock.
710
1155
  * @returns Null on success, or an error message string on failure.
711
1156
  */
712
- async function attemptCaptureProbe(timeout) {
713
- const browser = await getCurrentBrowser();
1157
+ async function attemptCaptureProbe(browser, mode, clock = realClock) {
714
1158
  const page = await browser.newPage();
715
1159
  registerManagedPage(page);
1160
+ // Tears the probe page down cleanly: retire the raw capture stream (destroy plus the STOP_RECORDING settle) while the browser is still connected, unregister the
1161
+ // managed page, then close it. Shared by every success and self-timed-failure path in both modes.
1162
+ const teardown = async (stream) => {
1163
+ await retireRawStream(stream, clock);
1164
+ unregisterManagedPage(page);
1165
+ if (!page.isClosed()) {
1166
+ await page.close();
1167
+ }
1168
+ };
716
1169
  try {
717
- // Use the same capture MIME type and viewport constraints as the runtime. The stale state error occurs at the tabCapture API level before encoding matters,
718
- // but matching the runtime configuration ensures the probe exercises the exact same getStream() parameters.
1170
+ // Use the same capture MIME type and viewport (height/width) as the runtime. The stale state error occurs at the tabCapture API level before encoding matters,
1171
+ // so matching those runtime constraints ensures the probe exercises a representative getStream() call.
719
1172
  const useFFmpeg = CONFIG.streaming.captureMode === "ffmpeg";
720
1173
  const captureMimeType = useFFmpeg ? getCaptureMimeType() : NATIVE_FMP4_MIME_TYPE;
721
1174
  const streamOptions = {
@@ -733,17 +1186,41 @@ async function attemptCaptureProbe(timeout) {
733
1186
  }
734
1187
  }
735
1188
  };
736
- const stream = await raceWithTimeout(getStream(page, streamOptions), timeout, new Error("Capture probe timed out."));
737
- // Capture succeeded - the system is functional. Destroy the stream before closing the page to ensure chrome.tabCapture releases the capture cleanly.
738
- const readable = stream;
739
- readable.destroy();
740
- // Wait for puppeteer-stream's capture cleanup chain to complete. readable.destroy() triggers STOP_RECORDING via the close handler, but the call is
741
- // fire-and-forget. The async chain (STOP_RECORDING -> recorder.stop() -> onstop -> track.stop()) must finish before closing the page, or Chrome's tabCapture
742
- // state may linger and cause "Cannot capture a tab with an active stream" errors on the first real stream request.
743
- await delay(500);
744
- unregisterManagedPage(page);
745
- if (!page.isClosed()) {
746
- await page.close();
1189
+ // GATE mode: bound getStream with an internal timeout. On a lapse the getStream promise is still pending, so attach a both-callback handler that retires a
1190
+ // late-arriving stream (best-effort; the page may already be closing) and consumes a late rejection. The bounded wait already observes the promise's rejection,
1191
+ // so a fulfillment-only handler would create unhandled-rejection noise. This cleans up the orphan without serializing successive gate attempts against one another.
1192
+ if (mode.kind === "gate") {
1193
+ const streamPromise = getStream(page, streamOptions);
1194
+ const timeoutError = new Error(CAPTURE_PROBE_TIMEOUT_MESSAGE);
1195
+ let stream;
1196
+ try {
1197
+ stream = await waitWithTimeout(streamPromise, mode.boundMs, timeoutError);
1198
+ }
1199
+ catch (error) {
1200
+ // Only the internal timeout leaves getStream pending; an in-time rejection produced no stream to clean up and is already observed by the bounded wait.
1201
+ if (error === timeoutError) {
1202
+ void streamPromise.then((late) => {
1203
+ void retireRawStream(late, clock);
1204
+ }, (reason) => {
1205
+ LOG.debug("streaming:setup", "A late gate capture stream rejected after the probe timeout: %s.", formatError(reason));
1206
+ });
1207
+ }
1208
+ throw error;
1209
+ }
1210
+ await teardown(stream);
1211
+ LOG.info("Capture system verified successfully.");
1212
+ return null;
1213
+ }
1214
+ // MID-LIFE mode: await getStream raw and self-time it. The turn (owned by the lock) spans the whole task, but the pass/fail CRITERION is getStream's own latency
1215
+ // against boundMs, measured without racing or abandoning anything, so the teardown settle that follows never counts against it.
1216
+ const startedAt = clock.now();
1217
+ const stream = (await getStream(page, streamOptions));
1218
+ const elapsed = clock.now() - startedAt;
1219
+ await teardown(stream);
1220
+ // Report failure when the caller abandoned this probe at the lock's outer deadline (unobservable in practice - the lock already rejected the caller - but it keeps
1221
+ // the never-throw contract and prevents stranding a capture), or when getStream's own latency exceeded the criterion bound.
1222
+ if (mode.signal.aborted || (elapsed > mode.boundMs)) {
1223
+ return CAPTURE_PROBE_TIMEOUT_MESSAGE;
747
1224
  }
748
1225
  LOG.info("Capture system verified successfully.");
749
1226
  return null;
@@ -758,4 +1235,103 @@ async function attemptCaptureProbe(timeout) {
758
1235
  return errorMessage;
759
1236
  }
760
1237
  }
1238
+ // Passive Mid-Life Capture-Death Detection.
1239
+ /* A browser can be capture-ready at launch and lose its capture capability later - the extension wedges, tabCapture stalls - without ever firing a "disconnected"
1240
+ * event, so neither the launch gate nor the disconnect handler would catch it. This detector rides a signal that is already happening: a stream-setup failure
1241
+ * carrying a capture-infrastructure signature. It is deliberately conservative. The guard (the failing stream is the only active stream) preserves per-stream
1242
+ * isolation - if any other stream is active, the browser is either demonstrably capturing or those streams will trip their own circuit breakers and drain. The probe
1243
+ * is the authoritative arbiter, serialized through the capture lock so it can never race a real stream's getStream init, and it runs in the background, single-flight,
1244
+ * so it never delays a response or stacks up. On a confirmed failure, the one recovery action runs: invalidate the browser for a governed relaunch.
1245
+ */
1246
+ // At most one mid-life re-verification runs at a time across the process, so a burst of capture-infrastructure failures triggers a single probe, not a storm.
1247
+ let captureReverificationInProgress = false;
1248
+ /**
1249
+ * Runs one capture probe as a task on the capture lock, so it cannot race a concurrent getStream initialization (which would draw a spurious "Cannot capture a tab
1250
+ * with an active stream"). The lock holds the turn across the probe's full task - getStream plus the STOP_RECORDING settle inside attemptCaptureProbe - so the next
1251
+ * initialization does not start until the probe's capture is fully released. The mid-life probe self-times its getStream against `timeout`; the lock's outer deadline
1252
+ * adds a teardown allowance over it as a safety net.
1253
+ * @param browser - The Chrome instance to probe.
1254
+ * @param timeout - Maximum time in milliseconds to wait for the probe's getStream() to respond (the self-timed criterion).
1255
+ * @returns Null when the browser captured successfully, or an error message when it could not (including a wedged-lock timeout).
1256
+ */
1257
+ async function probeCaptureSerialized(browser, timeout) {
1258
+ try {
1259
+ return await captureLock.run((signal) => attemptCaptureProbe(browser, { boundMs: timeout, kind: "midlife", signal }), {
1260
+ deadlineMessage: CAPTURE_PROBE_TIMEOUT_MESSAGE,
1261
+ deadlineMs: timeout + PROBE_TEARDOWN_ALLOWANCE_MS,
1262
+ // The probe's wedge is a loud warning only, never an invalidate: by wedge time the detector has already invalidated this browser via the returned failure
1263
+ // string (the outer deadline fires roughly 22s earlier), so a wedge here means the kill-immune leaked-mutex hang, where a second invalidate is a guaranteed
1264
+ // identity-guard no-op.
1265
+ onWedge: () => {
1266
+ LOG.warn("A mid-life capture probe has wedged past the recovery bound; the capture mutex is likely leaked and awaiting a process restart.");
1267
+ },
1268
+ turnWaitMs: CONFIG.streaming.navigationTimeout
1269
+ });
1270
+ }
1271
+ catch (error) {
1272
+ // The only throw here is a lock failure - a turn-wait timeout or the outer deadline; attemptCaptureProbe returns its own failures as strings. A jammed capture
1273
+ // lock is itself evidence the browser cannot capture, so surface it as a probe failure the detector routes into invalidateBrowser.
1274
+ return formatError(error);
1275
+ }
1276
+ }
1277
+ /**
1278
+ * Pure decision for whether a mid-life capture-infrastructure failure warrants re-verifying the browser. Re-verification is warranted iff no other re-verification
1279
+ * is already in flight, a browser is published to probe, and the failing stream is the only active stream - any other active stream means either the browser is
1280
+ * demonstrably capturing (so this failure is stream-specific - never invalidate) or those streams will trip their own circuit breakers and drain, after which a
1281
+ * later failure reaches the zero-other case. An empty registry (setup failed before the stream was registered) satisfies the isolation check vacuously and is the
1282
+ * common real-world shape of this failure.
1283
+ * @param inputs - The decision inputs.
1284
+ * @param inputs.activeStreamIds - The ids of every stream currently in the registry.
1285
+ * @param inputs.failingStreamId - The id of the stream whose setup just failed, excluded from the isolation check.
1286
+ * @param inputs.hasBrowser - Whether a browser instance is currently published to probe.
1287
+ * @param inputs.reverificationInProgress - Whether a re-verification is already deciding the browser's fate.
1288
+ * @returns True when the failure should trigger a re-verification probe.
1289
+ */
1290
+ export function shouldReverifyCapture(inputs) {
1291
+ return !inputs.reverificationInProgress && inputs.hasBrowser && inputs.activeStreamIds.every((id) => id === inputs.failingStreamId);
1292
+ }
1293
+ /**
1294
+ * Passive mid-life capture-death detection, called from the stream-setup failure path when the failure carries a capture-infrastructure signature. If the failing
1295
+ * stream is the only active stream, it re-verifies the browser's capture capability with a lock-serialized probe in the background and, on confirmed failure,
1296
+ * invalidates the browser for a governed relaunch - catching a browser that is still connected (no "disconnected" event) but can no longer capture. Fire-and-forget
1297
+ * so the failing request's response is not delayed; single-flight so a burst of failures triggers at most one probe.
1298
+ * @param failingStreamId - The numeric id of the stream whose setup just failed, excluded from the active-stream guard.
1299
+ */
1300
+ function noteCaptureInfrastructureFailure(failingStreamId) {
1301
+ // Single-flight: a re-verification is already deciding the browser's fate; do not stack another. Read first so a re-verify already in flight short-circuits
1302
+ // before the registry and browser lookups below are even performed.
1303
+ if (captureReverificationInProgress) {
1304
+ return;
1305
+ }
1306
+ // A readiness probe needs a connected browser to exercise. If none is published, a disconnect already handled the readiness loss. The !browser check is
1307
+ // combined into this one condition (rather than a separate guard) so TypeScript narrows browser from Nullable<Browser> to Browser for the probe and
1308
+ // invalidateBrowser calls below. hasBrowser and reverificationInProgress are passed as the values their guards already guarantee here - true (the !browser
1309
+ // guard has passed) and false (the single-flight guard above already returned if it were true).
1310
+ const browser = getBrowserInstance();
1311
+ if (!browser || !shouldReverifyCapture({ activeStreamIds: getAllStreams().map((entry) => entry.id), failingStreamId, hasBrowser: true,
1312
+ reverificationInProgress: false })) {
1313
+ return;
1314
+ }
1315
+ captureReverificationInProgress = true;
1316
+ // Run in the background so the failing request's 503 response is not delayed by the probe, which can take up to the probe timeout.
1317
+ void (async () => {
1318
+ try {
1319
+ const failure = await probeCaptureSerialized(browser, CAPTURE_PROBE_TIMEOUT_MS);
1320
+ if (failure !== null) {
1321
+ // The probe confirmed the browser cannot capture though it is still connected. invalidateBrowser is the single recovery action - relinquish readiness,
1322
+ // terminate the now-doomed streams, and close Chrome - so the next request relaunches a fresh, gate-verified browser. We pass the exact instance we probed:
1323
+ // invalidateBrowser no-ops if it was already superseded by a disconnect-and-relaunch during the probe, so we never tear down a healthy replacement. A
1324
+ // genuinely leaked module mutex, if that was the cause, surfaces again at the relaunch's gate probe and exits there; a merely-slow getStream that finally
1325
+ // settles instead lets the relaunch recover.
1326
+ await invalidateBrowser(browser, "a capture probe failed after a stream setup failure with no other active streams");
1327
+ }
1328
+ }
1329
+ catch (error) {
1330
+ LOG.debug("streaming:setup", "Mid-life capture re-verification aborted: %s.", formatError(error));
1331
+ }
1332
+ finally {
1333
+ captureReverificationInProgress = false;
1334
+ }
1335
+ })();
1336
+ }
761
1337
  //# sourceMappingURL=setup.js.map