prismcast 1.12.0 → 1.13.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 (631) hide show
  1. package/dist/app.d.ts +41 -1
  2. package/dist/app.js +164 -76
  3. package/dist/app.js.map +1 -1
  4. package/dist/browser/blockedPage.d.ts +0 -1
  5. package/dist/browser/browserSupervisor.d.ts +34 -5
  6. package/dist/browser/browserSupervisor.js +39 -12
  7. package/dist/browser/browserSupervisor.js.map +1 -1
  8. package/dist/browser/cdp.d.ts +58 -6
  9. package/dist/browser/cdp.js +147 -9
  10. package/dist/browser/cdp.js.map +1 -1
  11. package/dist/browser/channelSelection.d.ts +18 -6
  12. package/dist/browser/channelSelection.js +47 -36
  13. package/dist/browser/channelSelection.js.map +1 -1
  14. package/dist/browser/consent.d.ts +33 -4
  15. package/dist/browser/consent.js +87 -50
  16. package/dist/browser/consent.js.map +1 -1
  17. package/dist/browser/display.d.ts +0 -1
  18. package/dist/browser/hlsPlaylistObserver.d.ts +3 -2
  19. package/dist/browser/hlsPlaylistObserver.js +11 -6
  20. package/dist/browser/hlsPlaylistObserver.js.map +1 -1
  21. package/dist/browser/index.d.ts +273 -59
  22. package/dist/browser/index.js +768 -255
  23. package/dist/browser/index.js.map +1 -1
  24. package/dist/browser/launchGovernor.d.ts +0 -1
  25. package/dist/browser/login.d.ts +11 -10
  26. package/dist/browser/login.js +42 -33
  27. package/dist/browser/login.js.map +1 -1
  28. package/dist/browser/manifestInterceptor.d.ts +14 -5
  29. package/dist/browser/manifestInterceptor.js +26 -13
  30. package/dist/browser/manifestInterceptor.js.map +1 -1
  31. package/dist/browser/navigation.d.ts +28 -0
  32. package/dist/browser/navigation.js +57 -0
  33. package/dist/browser/navigation.js.map +1 -0
  34. package/dist/browser/pageStaleness.d.ts +1 -2
  35. package/dist/browser/pageStaleness.js +9 -9
  36. package/dist/browser/pageStaleness.js.map +1 -1
  37. package/dist/browser/precaching.d.ts +27 -13
  38. package/dist/browser/precaching.js +219 -73
  39. package/dist/browser/precaching.js.map +1 -1
  40. package/dist/browser/tabCapture.d.ts +97 -0
  41. package/dist/browser/tabCapture.js +314 -0
  42. package/dist/browser/tabCapture.js.map +1 -0
  43. package/dist/browser/tabNetworkObserver.d.ts +0 -1
  44. package/dist/browser/tabSelection.d.ts +117 -0
  45. package/dist/browser/tabSelection.js +547 -0
  46. package/dist/browser/tabSelection.js.map +1 -0
  47. package/dist/browser/tuning/cache.d.ts +0 -1
  48. package/dist/browser/tuning/comcastPolymer.d.ts +0 -1
  49. package/dist/browser/tuning/comcastPolymer.js +18 -19
  50. package/dist/browser/tuning/comcastPolymer.js.map +1 -1
  51. package/dist/browser/tuning/cox.d.ts +0 -1
  52. package/dist/browser/tuning/directv.d.ts +0 -1
  53. package/dist/browser/tuning/directv.js +12 -13
  54. package/dist/browser/tuning/directv.js.map +1 -1
  55. package/dist/browser/tuning/fox.d.ts +0 -1
  56. package/dist/browser/tuning/fox.js +2 -2
  57. package/dist/browser/tuning/fox.js.map +1 -1
  58. package/dist/browser/tuning/gridSearch.d.ts +0 -1
  59. package/dist/browser/tuning/hbo.d.ts +0 -1
  60. package/dist/browser/tuning/hbo.js +2 -2
  61. package/dist/browser/tuning/hbo.js.map +1 -1
  62. package/dist/browser/tuning/hulu.d.ts +14 -2
  63. package/dist/browser/tuning/hulu.js +150 -24
  64. package/dist/browser/tuning/hulu.js.map +1 -1
  65. package/dist/browser/tuning/shared.d.ts +1 -2
  66. package/dist/browser/tuning/shared.js +1 -1
  67. package/dist/browser/tuning/sling.d.ts +0 -1
  68. package/dist/browser/tuning/sling.js +17 -22
  69. package/dist/browser/tuning/sling.js.map +1 -1
  70. package/dist/browser/tuning/spectrum.d.ts +0 -1
  71. package/dist/browser/tuning/thumbnailRow.d.ts +0 -1
  72. package/dist/browser/tuning/tileClick.d.ts +0 -1
  73. package/dist/browser/tuning/xfinity.d.ts +0 -1
  74. package/dist/browser/tuning/youtubeTv.d.ts +0 -1
  75. package/dist/browser/tuning/youtubeTv.js +1 -2
  76. package/dist/browser/tuning/youtubeTv.js.map +1 -1
  77. package/dist/browser/video.d.ts +55 -33
  78. package/dist/browser/video.js +213 -169
  79. package/dist/browser/video.js.map +1 -1
  80. package/dist/browser/windowSync.d.ts +45 -0
  81. package/dist/browser/windowSync.js +153 -0
  82. package/dist/browser/windowSync.js.map +1 -0
  83. package/dist/channels/index.d.ts +7 -1
  84. package/dist/channels/index.js +24 -8
  85. package/dist/channels/index.js.map +1 -1
  86. package/dist/config/channelForm.d.ts +0 -1
  87. package/dist/config/channelIdentity.d.ts +27 -0
  88. package/dist/config/channelIdentity.js +72 -0
  89. package/dist/config/channelIdentity.js.map +1 -0
  90. package/dist/config/channelSort.d.ts +25 -0
  91. package/dist/config/channelSort.js +101 -0
  92. package/dist/config/channelSort.js.map +1 -0
  93. package/dist/config/consistencyProbe.d.ts +0 -1
  94. package/dist/config/consistencyProbe.js +1 -1
  95. package/dist/config/consistencyProbe.js.map +1 -1
  96. package/dist/config/health.d.ts +3 -2
  97. package/dist/config/health.js +41 -31
  98. package/dist/config/health.js.map +1 -1
  99. package/dist/config/index.d.ts +12 -1
  100. package/dist/config/index.js +77 -31
  101. package/dist/config/index.js.map +1 -1
  102. package/dist/config/paths.d.ts +17 -2
  103. package/dist/config/paths.js +21 -1
  104. package/dist/config/paths.js.map +1 -1
  105. package/dist/config/persistence.context.d.ts +0 -1
  106. package/dist/config/persistence.d.ts +8 -4
  107. package/dist/config/persistence.js +143 -82
  108. package/dist/config/persistence.js.map +1 -1
  109. package/dist/config/presets.d.ts +0 -1
  110. package/dist/config/profiles.d.ts +9 -4
  111. package/dist/config/profiles.js +39 -6
  112. package/dist/config/profiles.js.map +1 -1
  113. package/dist/config/providerLineups.d.ts +0 -1
  114. package/dist/config/providerLineups.js +0 -3
  115. package/dist/config/providerLineups.js.map +1 -1
  116. package/dist/config/reactivity.d.ts +0 -1
  117. package/dist/config/servicePacks.d.ts +0 -1
  118. package/dist/config/services.d.ts +1 -47
  119. package/dist/config/services.js +4 -135
  120. package/dist/config/services.js.map +1 -1
  121. package/dist/config/sites.d.ts +0 -9
  122. package/dist/config/sites.js +82 -98
  123. package/dist/config/sites.js.map +1 -1
  124. package/dist/config/userChannels.d.ts +22 -29
  125. package/dist/config/userChannels.js +47 -65
  126. package/dist/config/userChannels.js.map +1 -1
  127. package/dist/config/userConfig.d.ts +19 -4
  128. package/dist/config/userConfig.js +123 -25
  129. package/dist/config/userConfig.js.map +1 -1
  130. package/dist/config/userProfiles.d.ts +2 -3
  131. package/dist/config/userProfiles.js +16 -13
  132. package/dist/config/userProfiles.js.map +1 -1
  133. package/dist/hdhr/channelMap.d.ts +0 -1
  134. package/dist/hdhr/deviceId.d.ts +0 -1
  135. package/dist/hdhr/discover.d.ts +0 -1
  136. package/dist/hdhr/getHandlers.d.ts +0 -1
  137. package/dist/hdhr/identity.d.ts +0 -1
  138. package/dist/hdhr/index.d.ts +0 -1
  139. package/dist/hdhr/protocol.d.ts +1 -2
  140. package/dist/hdhr/protocol.js +1 -1
  141. package/dist/hdhr/tunerState.d.ts +0 -1
  142. package/dist/hdhr/udp.d.ts +0 -1
  143. package/dist/hdhr/udp.js +16 -4
  144. package/dist/hdhr/udp.js.map +1 -1
  145. package/dist/identity.d.ts +0 -1
  146. package/dist/index.d.ts +0 -1
  147. package/dist/index.js +10 -3
  148. package/dist/index.js.map +1 -1
  149. package/dist/native/codecInference.d.ts +5 -4
  150. package/dist/native/codecInference.js +12 -7
  151. package/dist/native/codecInference.js.map +1 -1
  152. package/dist/native/decrypt.d.ts +10 -3
  153. package/dist/native/decrypt.js +9 -5
  154. package/dist/native/decrypt.js.map +1 -1
  155. package/dist/native/index.d.ts +26 -24
  156. package/dist/native/index.js +26 -20
  157. package/dist/native/index.js.map +1 -1
  158. package/dist/native/probe.d.ts +7 -10
  159. package/dist/native/probe.js +41 -37
  160. package/dist/native/probe.js.map +1 -1
  161. package/dist/native/proxy.d.ts +3 -4
  162. package/dist/native/proxy.js +91 -69
  163. package/dist/native/proxy.js.map +1 -1
  164. package/dist/native/tokenExpiry.d.ts +0 -1
  165. package/dist/routes/assets.d.ts +0 -1
  166. package/dist/routes/auth.d.ts +0 -1
  167. package/dist/routes/auth.js +1 -1
  168. package/dist/routes/auth.js.map +1 -1
  169. package/dist/routes/cdp.d.ts +11 -5
  170. package/dist/routes/cdp.js +91 -63
  171. package/dist/routes/cdp.js.map +1 -1
  172. package/dist/routes/channels.d.ts +0 -1
  173. package/dist/routes/clientActions.d.ts +0 -1
  174. package/dist/routes/components.d.ts +0 -1
  175. package/dist/routes/config/channels/endpoints/browse.d.ts +0 -1
  176. package/dist/routes/config/channels/endpoints/bulk.d.ts +0 -1
  177. package/dist/routes/config/channels/endpoints/bulk.js +3 -3
  178. package/dist/routes/config/channels/endpoints/bulk.js.map +1 -1
  179. package/dist/routes/config/channels/endpoints/crud.d.ts +0 -1
  180. package/dist/routes/config/channels/endpoints/crud.js +4 -3
  181. package/dist/routes/config/channels/endpoints/crud.js.map +1 -1
  182. package/dist/routes/config/channels/endpoints/importExport.d.ts +0 -1
  183. package/dist/routes/config/channels/endpoints/importExport.js +7 -3
  184. package/dist/routes/config/channels/endpoints/importExport.js.map +1 -1
  185. package/dist/routes/config/channels/endpoints/predefined.d.ts +0 -1
  186. package/dist/routes/config/channels/endpoints/prefs.d.ts +0 -1
  187. package/dist/routes/config/channels/endpoints/prefs.js +1 -1
  188. package/dist/routes/config/channels/endpoints/prefs.js.map +1 -1
  189. package/dist/routes/config/channels/endpoints/service.d.ts +0 -1
  190. package/dist/routes/config/channels/endpoints/service.js +8 -5
  191. package/dist/routes/config/channels/endpoints/service.js.map +1 -1
  192. package/dist/routes/config/channels/endpoints/tags.d.ts +0 -1
  193. package/dist/routes/config/channels/endpoints/tags.js +3 -3
  194. package/dist/routes/config/channels/endpoints/tags.js.map +1 -1
  195. package/dist/routes/config/channels/healthBridge.d.ts +4 -1
  196. package/dist/routes/config/channels/healthBridge.js +9 -5
  197. package/dist/routes/config/channels/healthBridge.js.map +1 -1
  198. package/dist/routes/config/channels/http/handler.d.ts +0 -1
  199. package/dist/routes/config/channels/http/playlistHint.d.ts +0 -1
  200. package/dist/routes/config/channels/http/serviceWarning.d.ts +0 -1
  201. package/dist/routes/config/channels/http/serviceWarning.js +1 -1
  202. package/dist/routes/config/channels/http/serviceWarning.js.map +1 -1
  203. package/dist/routes/config/channels/index.d.ts +0 -1
  204. package/dist/routes/config/channels/setup.d.ts +0 -1
  205. package/dist/routes/config/channels/table.d.ts +0 -1
  206. package/dist/routes/config/channels/table.js +66 -134
  207. package/dist/routes/config/channels/table.js.map +1 -1
  208. package/dist/routes/config/http/envelope.d.ts +1 -2
  209. package/dist/routes/config/index.d.ts +5 -5
  210. package/dist/routes/config/index.js +7 -5
  211. package/dist/routes/config/index.js.map +1 -1
  212. package/dist/routes/config/services.d.ts +0 -1
  213. package/dist/routes/config/services.js +17 -19
  214. package/dist/routes/config/services.js.map +1 -1
  215. package/dist/routes/config/settings.d.ts +6 -15
  216. package/dist/routes/config/settings.js +16 -35
  217. package/dist/routes/config/settings.js.map +1 -1
  218. package/dist/routes/debug.d.ts +0 -1
  219. package/dist/routes/debug.js +1 -1
  220. package/dist/routes/debug.js.map +1 -1
  221. package/dist/routes/health.d.ts +16 -8
  222. package/dist/routes/health.js +27 -12
  223. package/dist/routes/health.js.map +1 -1
  224. package/dist/routes/hls.d.ts +0 -1
  225. package/dist/routes/icons.d.ts +0 -1
  226. package/dist/routes/index.d.ts +0 -1
  227. package/dist/routes/logs.d.ts +3 -2
  228. package/dist/routes/logs.js +54 -14
  229. package/dist/routes/logs.js.map +1 -1
  230. package/dist/routes/mpegts.d.ts +0 -1
  231. package/dist/routes/play.d.ts +0 -1
  232. package/dist/routes/playlist.d.ts +0 -1
  233. package/dist/routes/playlist.js +2 -1
  234. package/dist/routes/playlist.js.map +1 -1
  235. package/dist/routes/root/content.d.ts +0 -1
  236. package/dist/routes/root/content.js +32 -11
  237. package/dist/routes/root/content.js.map +1 -1
  238. package/dist/routes/root/index.d.ts +10 -1
  239. package/dist/routes/root/index.js +17 -7
  240. package/dist/routes/root/index.js.map +1 -1
  241. package/dist/routes/root/scripts/channels.d.ts +0 -1
  242. package/dist/routes/root/scripts/channels.js +10 -20
  243. package/dist/routes/root/scripts/channels.js.map +1 -1
  244. package/dist/routes/root/scripts/clientEscape.d.ts +0 -1
  245. package/dist/routes/root/scripts/clientEscape.js +1 -1
  246. package/dist/routes/root/scripts/clientUrl.d.ts +0 -1
  247. package/dist/routes/root/scripts/config.d.ts +0 -1
  248. package/dist/routes/root/scripts/config.js +8 -29
  249. package/dist/routes/root/scripts/config.js.map +1 -1
  250. package/dist/routes/root/scripts/index.d.ts +0 -1
  251. package/dist/routes/root/scripts/shared.d.ts +0 -1
  252. package/dist/routes/root/scripts/shared.js +16 -13
  253. package/dist/routes/root/scripts/shared.js.map +1 -1
  254. package/dist/routes/root/scripts/status.d.ts +0 -1
  255. package/dist/routes/root/scripts/status.handlers.d.ts +3 -1
  256. package/dist/routes/root/scripts/status.handlers.js +52 -26
  257. package/dist/routes/root/scripts/status.handlers.js.map +1 -1
  258. package/dist/routes/root/styles.d.ts +0 -1
  259. package/dist/routes/root/styles.js +10 -18
  260. package/dist/routes/root/styles.js.map +1 -1
  261. package/dist/routes/services.d.ts +0 -1
  262. package/dist/routes/services.js +1 -1
  263. package/dist/routes/services.js.map +1 -1
  264. package/dist/routes/sse.d.ts +3 -2
  265. package/dist/routes/sse.js +7 -3
  266. package/dist/routes/sse.js.map +1 -1
  267. package/dist/routes/streams.d.ts +4 -2
  268. package/dist/routes/streams.js +8 -5
  269. package/dist/routes/streams.js.map +1 -1
  270. package/dist/routes/theme.d.ts +0 -1
  271. package/dist/routes/theme.js +3 -3
  272. package/dist/routes/theme.js.map +1 -1
  273. package/dist/routes/ui.d.ts +0 -1
  274. package/dist/routes/ui.js +5 -5
  275. package/dist/routes/ui.js.map +1 -1
  276. package/dist/routes/upgrade.d.ts +0 -1
  277. package/dist/routes/upgrade.js +2 -1
  278. package/dist/routes/upgrade.js.map +1 -1
  279. package/dist/service/commands.context.d.ts +0 -1
  280. package/dist/service/commands.context.js +7 -1
  281. package/dist/service/commands.context.js.map +1 -1
  282. package/dist/service/commands.d.ts +0 -1
  283. package/dist/service/generators.context.d.ts +0 -1
  284. package/dist/service/generators.d.ts +0 -1
  285. package/dist/service/index.d.ts +0 -1
  286. package/dist/streaming/captureLock.d.ts +1 -2
  287. package/dist/streaming/captureLock.js +8 -7
  288. package/dist/streaming/captureLock.js.map +1 -1
  289. package/dist/streaming/captureSession.d.ts +0 -1
  290. package/dist/streaming/clients.d.ts +4 -3
  291. package/dist/streaming/clients.js +5 -4
  292. package/dist/streaming/clients.js.map +1 -1
  293. package/dist/streaming/codec.d.ts +0 -1
  294. package/dist/streaming/fmp4Segmenter.d.ts +11 -6
  295. package/dist/streaming/fmp4Segmenter.js +39 -17
  296. package/dist/streaming/fmp4Segmenter.js.map +1 -1
  297. package/dist/streaming/hls.d.ts +96 -3
  298. package/dist/streaming/hls.js +282 -171
  299. package/dist/streaming/hls.js.map +1 -1
  300. package/dist/streaming/hlsResume.d.ts +8 -5
  301. package/dist/streaming/hlsResume.js +10 -8
  302. package/dist/streaming/hlsResume.js.map +1 -1
  303. package/dist/streaming/hlsSegments.d.ts +11 -8
  304. package/dist/streaming/hlsSegments.js +38 -18
  305. package/dist/streaming/hlsSegments.js.map +1 -1
  306. package/dist/streaming/lifecycle.d.ts +5 -4
  307. package/dist/streaming/lifecycle.js +43 -13
  308. package/dist/streaming/lifecycle.js.map +1 -1
  309. package/dist/streaming/monitor.d.ts +14 -3
  310. package/dist/streaming/monitor.js +447 -234
  311. package/dist/streaming/monitor.js.map +1 -1
  312. package/dist/streaming/mp4Parser.d.ts +0 -1
  313. package/dist/streaming/mpegts.d.ts +11 -1
  314. package/dist/streaming/mpegts.js +92 -28
  315. package/dist/streaming/mpegts.js.map +1 -1
  316. package/dist/streaming/playlistBuilder.d.ts +0 -1
  317. package/dist/streaming/preroll.d.ts +34 -15
  318. package/dist/streaming/preroll.js +32 -30
  319. package/dist/streaming/preroll.js.map +1 -1
  320. package/dist/streaming/pretune.d.ts +2 -3
  321. package/dist/streaming/pretune.js +45 -45
  322. package/dist/streaming/pretune.js.map +1 -1
  323. package/dist/streaming/pretuneTimers.d.ts +16 -12
  324. package/dist/streaming/pretuneTimers.js +27 -37
  325. package/dist/streaming/pretuneTimers.js.map +1 -1
  326. package/dist/streaming/recovery.d.ts +97 -9
  327. package/dist/streaming/recovery.js +96 -18
  328. package/dist/streaming/recovery.js.map +1 -1
  329. package/dist/streaming/registry.d.ts +102 -15
  330. package/dist/streaming/registry.js +90 -5
  331. package/dist/streaming/registry.js.map +1 -1
  332. package/dist/streaming/setup.d.ts +90 -32
  333. package/dist/streaming/setup.js +379 -248
  334. package/dist/streaming/setup.js.map +1 -1
  335. package/dist/streaming/showInfo.d.ts +8 -4
  336. package/dist/streaming/showInfo.js +54 -45
  337. package/dist/streaming/showInfo.js.map +1 -1
  338. package/dist/streaming/statusEmitter.d.ts +5 -3
  339. package/dist/streaming/statusEmitter.js +20 -8
  340. package/dist/streaming/statusEmitter.js.map +1 -1
  341. package/dist/types/channels.d.ts +0 -1
  342. package/dist/types/channels.js +0 -4
  343. package/dist/types/channels.js.map +1 -1
  344. package/dist/types/config.d.ts +0 -1
  345. package/dist/types/index.d.ts +4 -4
  346. package/dist/types/index.js +2 -1
  347. package/dist/types/index.js.map +1 -1
  348. package/dist/types/profiles.d.ts +51 -9
  349. package/dist/types/profiles.js +35 -1
  350. package/dist/types/profiles.js.map +1 -1
  351. package/dist/types/selection.d.ts +10 -1
  352. package/dist/types/shared.d.ts +0 -1
  353. package/dist/types/streaming.d.ts +8 -1
  354. package/dist/types/streaming.js +7 -0
  355. package/dist/types/streaming.js.map +1 -1
  356. package/dist/upgrade/commands.context.d.ts +0 -1
  357. package/dist/upgrade/commands.d.ts +0 -1
  358. package/dist/upgrade/commands.js +1 -1
  359. package/dist/upgrade/commands.js.map +1 -1
  360. package/dist/upgrade/detection.context.d.ts +0 -1
  361. package/dist/upgrade/detection.d.ts +1 -2
  362. package/dist/upgrade/index.d.ts +0 -1
  363. package/dist/upgrade/lifecycle.context.d.ts +2 -1
  364. package/dist/upgrade/lifecycle.context.js +9 -4
  365. package/dist/upgrade/lifecycle.context.js.map +1 -1
  366. package/dist/upgrade/lifecycle.d.ts +0 -1
  367. package/dist/upgrade/pathHandle.d.ts +0 -1
  368. package/dist/utils/bootSession.context.d.ts +0 -1
  369. package/dist/utils/bootSession.context.js +3 -2
  370. package/dist/utils/bootSession.context.js.map +1 -1
  371. package/dist/utils/bootSession.d.ts +0 -1
  372. package/dist/utils/chromeFetch.d.ts +0 -1
  373. package/dist/utils/cliOutput.d.ts +0 -1
  374. package/dist/utils/debugFilter.d.ts +0 -1
  375. package/dist/utils/debugFilter.js +5 -3
  376. package/dist/utils/debugFilter.js.map +1 -1
  377. package/dist/utils/delay.d.ts +65 -13
  378. package/dist/utils/delay.js +96 -23
  379. package/dist/utils/delay.js.map +1 -1
  380. package/dist/utils/errors.d.ts +0 -11
  381. package/dist/utils/errors.js +3 -14
  382. package/dist/utils/errors.js.map +1 -1
  383. package/dist/utils/evaluate.d.ts +11 -3
  384. package/dist/utils/evaluate.js +4 -3
  385. package/dist/utils/evaluate.js.map +1 -1
  386. package/dist/utils/ffmpeg.context.d.ts +0 -1
  387. package/dist/utils/ffmpeg.d.ts +1 -1
  388. package/dist/utils/ffmpeg.js +2 -1
  389. package/dist/utils/ffmpeg.js.map +1 -1
  390. package/dist/utils/fileLogger.d.ts +26 -7
  391. package/dist/utils/fileLogger.js +241 -102
  392. package/dist/utils/fileLogger.js.map +1 -1
  393. package/dist/utils/format.d.ts +36 -4
  394. package/dist/utils/format.js +66 -12
  395. package/dist/utils/format.js.map +1 -1
  396. package/dist/utils/index.d.ts +1 -2
  397. package/dist/utils/index.js +1 -1
  398. package/dist/utils/index.js.map +1 -1
  399. package/dist/utils/logEmitter.d.ts +0 -1
  400. package/dist/utils/logger.d.ts +0 -1
  401. package/dist/utils/m3u.d.ts +0 -1
  402. package/dist/utils/markup.d.ts +0 -1
  403. package/dist/utils/memo.d.ts +0 -1
  404. package/dist/utils/morganStream.d.ts +2 -3
  405. package/dist/utils/morganStream.js +6 -8
  406. package/dist/utils/morganStream.js.map +1 -1
  407. package/dist/utils/network.d.ts +0 -1
  408. package/dist/utils/never.d.ts +8 -0
  409. package/dist/utils/never.js +20 -0
  410. package/dist/utils/never.js.map +1 -0
  411. package/dist/utils/pid.d.ts +0 -1
  412. package/dist/utils/platform.d.ts +0 -1
  413. package/dist/utils/processInspector.context.d.ts +32 -2
  414. package/dist/utils/processInspector.context.js +117 -1
  415. package/dist/utils/processInspector.context.js.map +1 -1
  416. package/dist/utils/processInspector.d.ts +0 -32
  417. package/dist/utils/processInspector.js +10 -117
  418. package/dist/utils/processInspector.js.map +1 -1
  419. package/dist/utils/retry.d.ts +5 -4
  420. package/dist/utils/retry.js +43 -33
  421. package/dist/utils/retry.js.map +1 -1
  422. package/dist/utils/runtimeIdentity.context.d.ts +0 -1
  423. package/dist/utils/runtimeIdentity.context.js +3 -1
  424. package/dist/utils/runtimeIdentity.context.js.map +1 -1
  425. package/dist/utils/runtimeIdentity.d.ts +1 -1
  426. package/dist/utils/runtimeIdentity.js +1 -1
  427. package/dist/utils/runtimeIdentity.js.map +1 -1
  428. package/dist/utils/sanitize.d.ts +8 -1
  429. package/dist/utils/sanitize.js +19 -0
  430. package/dist/utils/sanitize.js.map +1 -1
  431. package/dist/utils/streamContext.d.ts +0 -1
  432. package/dist/utils/timing.d.ts +3 -4
  433. package/dist/utils/timing.js +4 -4
  434. package/dist/utils/timing.js.map +1 -1
  435. package/dist/utils/version.d.ts +14 -8
  436. package/dist/utils/version.js +35 -24
  437. package/dist/utils/version.js.map +1 -1
  438. package/package.json +7 -7
  439. package/dist/app.d.ts.map +0 -1
  440. package/dist/browser/blockedPage.d.ts.map +0 -1
  441. package/dist/browser/browserSupervisor.d.ts.map +0 -1
  442. package/dist/browser/cdp.d.ts.map +0 -1
  443. package/dist/browser/channelSelection.d.ts.map +0 -1
  444. package/dist/browser/consent.d.ts.map +0 -1
  445. package/dist/browser/display.d.ts.map +0 -1
  446. package/dist/browser/hlsPlaylistObserver.d.ts.map +0 -1
  447. package/dist/browser/index.d.ts.map +0 -1
  448. package/dist/browser/launchGovernor.d.ts.map +0 -1
  449. package/dist/browser/login.d.ts.map +0 -1
  450. package/dist/browser/manifestInterceptor.d.ts.map +0 -1
  451. package/dist/browser/pageStaleness.d.ts.map +0 -1
  452. package/dist/browser/precaching.d.ts.map +0 -1
  453. package/dist/browser/tabNetworkObserver.d.ts.map +0 -1
  454. package/dist/browser/tuning/cache.d.ts.map +0 -1
  455. package/dist/browser/tuning/comcastPolymer.d.ts.map +0 -1
  456. package/dist/browser/tuning/cox.d.ts.map +0 -1
  457. package/dist/browser/tuning/directv.d.ts.map +0 -1
  458. package/dist/browser/tuning/fox.d.ts.map +0 -1
  459. package/dist/browser/tuning/gridSearch.d.ts.map +0 -1
  460. package/dist/browser/tuning/hbo.d.ts.map +0 -1
  461. package/dist/browser/tuning/hulu.d.ts.map +0 -1
  462. package/dist/browser/tuning/shared.d.ts.map +0 -1
  463. package/dist/browser/tuning/sling.d.ts.map +0 -1
  464. package/dist/browser/tuning/spectrum.d.ts.map +0 -1
  465. package/dist/browser/tuning/thumbnailRow.d.ts.map +0 -1
  466. package/dist/browser/tuning/tileClick.d.ts.map +0 -1
  467. package/dist/browser/tuning/xfinity.d.ts.map +0 -1
  468. package/dist/browser/tuning/youtubeTv.d.ts.map +0 -1
  469. package/dist/browser/video.d.ts.map +0 -1
  470. package/dist/channels/index.d.ts.map +0 -1
  471. package/dist/config/channelForm.d.ts.map +0 -1
  472. package/dist/config/consistencyProbe.d.ts.map +0 -1
  473. package/dist/config/health.d.ts.map +0 -1
  474. package/dist/config/index.d.ts.map +0 -1
  475. package/dist/config/paths.d.ts.map +0 -1
  476. package/dist/config/persistence.context.d.ts.map +0 -1
  477. package/dist/config/persistence.d.ts.map +0 -1
  478. package/dist/config/presets.d.ts.map +0 -1
  479. package/dist/config/profiles.d.ts.map +0 -1
  480. package/dist/config/providerLineups.d.ts.map +0 -1
  481. package/dist/config/reactivity.d.ts.map +0 -1
  482. package/dist/config/servicePacks.d.ts.map +0 -1
  483. package/dist/config/services.d.ts.map +0 -1
  484. package/dist/config/sites.d.ts.map +0 -1
  485. package/dist/config/userChannels.d.ts.map +0 -1
  486. package/dist/config/userConfig.d.ts.map +0 -1
  487. package/dist/config/userProfiles.d.ts.map +0 -1
  488. package/dist/hdhr/channelMap.d.ts.map +0 -1
  489. package/dist/hdhr/deviceId.d.ts.map +0 -1
  490. package/dist/hdhr/discover.d.ts.map +0 -1
  491. package/dist/hdhr/getHandlers.d.ts.map +0 -1
  492. package/dist/hdhr/identity.d.ts.map +0 -1
  493. package/dist/hdhr/index.d.ts.map +0 -1
  494. package/dist/hdhr/protocol.d.ts.map +0 -1
  495. package/dist/hdhr/tunerState.d.ts.map +0 -1
  496. package/dist/hdhr/udp.d.ts.map +0 -1
  497. package/dist/identity.d.ts.map +0 -1
  498. package/dist/index.d.ts.map +0 -1
  499. package/dist/native/codecInference.d.ts.map +0 -1
  500. package/dist/native/decrypt.d.ts.map +0 -1
  501. package/dist/native/index.d.ts.map +0 -1
  502. package/dist/native/probe.d.ts.map +0 -1
  503. package/dist/native/proxy.d.ts.map +0 -1
  504. package/dist/native/tokenExpiry.d.ts.map +0 -1
  505. package/dist/routes/assets.d.ts.map +0 -1
  506. package/dist/routes/auth.d.ts.map +0 -1
  507. package/dist/routes/cdp.d.ts.map +0 -1
  508. package/dist/routes/channels.d.ts.map +0 -1
  509. package/dist/routes/clientActions.d.ts.map +0 -1
  510. package/dist/routes/components.d.ts.map +0 -1
  511. package/dist/routes/config/channels/endpoints/browse.d.ts.map +0 -1
  512. package/dist/routes/config/channels/endpoints/bulk.d.ts.map +0 -1
  513. package/dist/routes/config/channels/endpoints/crud.d.ts.map +0 -1
  514. package/dist/routes/config/channels/endpoints/importExport.d.ts.map +0 -1
  515. package/dist/routes/config/channels/endpoints/predefined.d.ts.map +0 -1
  516. package/dist/routes/config/channels/endpoints/prefs.d.ts.map +0 -1
  517. package/dist/routes/config/channels/endpoints/service.d.ts.map +0 -1
  518. package/dist/routes/config/channels/endpoints/tags.d.ts.map +0 -1
  519. package/dist/routes/config/channels/healthBridge.d.ts.map +0 -1
  520. package/dist/routes/config/channels/http/handler.d.ts.map +0 -1
  521. package/dist/routes/config/channels/http/playlistHint.d.ts.map +0 -1
  522. package/dist/routes/config/channels/http/serviceWarning.d.ts.map +0 -1
  523. package/dist/routes/config/channels/index.d.ts.map +0 -1
  524. package/dist/routes/config/channels/setup.d.ts.map +0 -1
  525. package/dist/routes/config/channels/table.d.ts.map +0 -1
  526. package/dist/routes/config/http/envelope.d.ts.map +0 -1
  527. package/dist/routes/config/index.d.ts.map +0 -1
  528. package/dist/routes/config/services.d.ts.map +0 -1
  529. package/dist/routes/config/settings.d.ts.map +0 -1
  530. package/dist/routes/debug.d.ts.map +0 -1
  531. package/dist/routes/health.d.ts.map +0 -1
  532. package/dist/routes/hls.d.ts.map +0 -1
  533. package/dist/routes/icons.d.ts.map +0 -1
  534. package/dist/routes/index.d.ts.map +0 -1
  535. package/dist/routes/logs.d.ts.map +0 -1
  536. package/dist/routes/mpegts.d.ts.map +0 -1
  537. package/dist/routes/play.d.ts.map +0 -1
  538. package/dist/routes/playlist.d.ts.map +0 -1
  539. package/dist/routes/root/content.d.ts.map +0 -1
  540. package/dist/routes/root/index.d.ts.map +0 -1
  541. package/dist/routes/root/scripts/channels.d.ts.map +0 -1
  542. package/dist/routes/root/scripts/clientEscape.d.ts.map +0 -1
  543. package/dist/routes/root/scripts/clientUrl.d.ts.map +0 -1
  544. package/dist/routes/root/scripts/config.d.ts.map +0 -1
  545. package/dist/routes/root/scripts/index.d.ts.map +0 -1
  546. package/dist/routes/root/scripts/shared.d.ts.map +0 -1
  547. package/dist/routes/root/scripts/status.d.ts.map +0 -1
  548. package/dist/routes/root/scripts/status.handlers.d.ts.map +0 -1
  549. package/dist/routes/root/styles.d.ts.map +0 -1
  550. package/dist/routes/services.d.ts.map +0 -1
  551. package/dist/routes/sse.d.ts.map +0 -1
  552. package/dist/routes/streams.d.ts.map +0 -1
  553. package/dist/routes/theme.d.ts.map +0 -1
  554. package/dist/routes/ui.d.ts.map +0 -1
  555. package/dist/routes/upgrade.d.ts.map +0 -1
  556. package/dist/service/commands.context.d.ts.map +0 -1
  557. package/dist/service/commands.d.ts.map +0 -1
  558. package/dist/service/generators.context.d.ts.map +0 -1
  559. package/dist/service/generators.d.ts.map +0 -1
  560. package/dist/service/index.d.ts.map +0 -1
  561. package/dist/streaming/captureLock.d.ts.map +0 -1
  562. package/dist/streaming/captureSession.d.ts.map +0 -1
  563. package/dist/streaming/clients.d.ts.map +0 -1
  564. package/dist/streaming/codec.d.ts.map +0 -1
  565. package/dist/streaming/fmp4Segmenter.d.ts.map +0 -1
  566. package/dist/streaming/hls.d.ts.map +0 -1
  567. package/dist/streaming/hlsResume.d.ts.map +0 -1
  568. package/dist/streaming/hlsSegments.d.ts.map +0 -1
  569. package/dist/streaming/lifecycle.d.ts.map +0 -1
  570. package/dist/streaming/monitor.d.ts.map +0 -1
  571. package/dist/streaming/mp4Parser.d.ts.map +0 -1
  572. package/dist/streaming/mpegts.d.ts.map +0 -1
  573. package/dist/streaming/playlistBuilder.d.ts.map +0 -1
  574. package/dist/streaming/preroll.d.ts.map +0 -1
  575. package/dist/streaming/pretune.d.ts.map +0 -1
  576. package/dist/streaming/pretuneTimers.d.ts.map +0 -1
  577. package/dist/streaming/recovery.d.ts.map +0 -1
  578. package/dist/streaming/registry.d.ts.map +0 -1
  579. package/dist/streaming/setup.d.ts.map +0 -1
  580. package/dist/streaming/showInfo.d.ts.map +0 -1
  581. package/dist/streaming/statusEmitter.d.ts.map +0 -1
  582. package/dist/types/channels.d.ts.map +0 -1
  583. package/dist/types/config.d.ts.map +0 -1
  584. package/dist/types/index.d.ts.map +0 -1
  585. package/dist/types/profiles.d.ts.map +0 -1
  586. package/dist/types/selection.d.ts.map +0 -1
  587. package/dist/types/shared.d.ts.map +0 -1
  588. package/dist/types/streaming.d.ts.map +0 -1
  589. package/dist/upgrade/commands.context.d.ts.map +0 -1
  590. package/dist/upgrade/commands.d.ts.map +0 -1
  591. package/dist/upgrade/detection.context.d.ts.map +0 -1
  592. package/dist/upgrade/detection.d.ts.map +0 -1
  593. package/dist/upgrade/index.d.ts.map +0 -1
  594. package/dist/upgrade/lifecycle.context.d.ts.map +0 -1
  595. package/dist/upgrade/lifecycle.d.ts.map +0 -1
  596. package/dist/upgrade/pathHandle.d.ts.map +0 -1
  597. package/dist/utils/bootSession.context.d.ts.map +0 -1
  598. package/dist/utils/bootSession.d.ts.map +0 -1
  599. package/dist/utils/chromeFetch.d.ts.map +0 -1
  600. package/dist/utils/cliOutput.d.ts.map +0 -1
  601. package/dist/utils/clock.d.ts +0 -15
  602. package/dist/utils/clock.d.ts.map +0 -1
  603. package/dist/utils/clock.js +0 -31
  604. package/dist/utils/clock.js.map +0 -1
  605. package/dist/utils/debugFilter.d.ts.map +0 -1
  606. package/dist/utils/delay.d.ts.map +0 -1
  607. package/dist/utils/errors.d.ts.map +0 -1
  608. package/dist/utils/evaluate.d.ts.map +0 -1
  609. package/dist/utils/ffmpeg.context.d.ts.map +0 -1
  610. package/dist/utils/ffmpeg.d.ts.map +0 -1
  611. package/dist/utils/fileLogger.d.ts.map +0 -1
  612. package/dist/utils/format.d.ts.map +0 -1
  613. package/dist/utils/index.d.ts.map +0 -1
  614. package/dist/utils/logEmitter.d.ts.map +0 -1
  615. package/dist/utils/logger.d.ts.map +0 -1
  616. package/dist/utils/m3u.d.ts.map +0 -1
  617. package/dist/utils/markup.d.ts.map +0 -1
  618. package/dist/utils/memo.d.ts.map +0 -1
  619. package/dist/utils/morganStream.d.ts.map +0 -1
  620. package/dist/utils/network.d.ts.map +0 -1
  621. package/dist/utils/pid.d.ts.map +0 -1
  622. package/dist/utils/platform.d.ts.map +0 -1
  623. package/dist/utils/processInspector.context.d.ts.map +0 -1
  624. package/dist/utils/processInspector.d.ts.map +0 -1
  625. package/dist/utils/retry.d.ts.map +0 -1
  626. package/dist/utils/runtimeIdentity.context.d.ts.map +0 -1
  627. package/dist/utils/runtimeIdentity.d.ts.map +0 -1
  628. package/dist/utils/sanitize.d.ts.map +0 -1
  629. package/dist/utils/streamContext.d.ts.map +0 -1
  630. package/dist/utils/timing.d.ts.map +0 -1
  631. package/dist/utils/version.d.ts.map +0 -1
@@ -50,31 +50,31 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
50
50
  var e = new Error(message);
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
- import { BrowserSupersededError, BrowserUnavailableError, getBrowserInstance, getCurrentBrowser, getStream, invalidateBrowser, minimizeBrowserWindow, registerManagedPage, setCaptureProbe, unregisterManagedPage } from "../browser/index.js";
54
- import { CaptureAbandonedError, createCaptureLock } from "./captureLock.js";
53
+ import { BrowserCaptureImpairedError, BrowserSupersededError, BrowserUnavailableError, acquireCaptureStream, confirmSharedWindowPlacement, emulateCaptureSurface, emulateLayoutSurface, getBrowserInstance, getCaptureImpairment, getCurrentBrowser, installActivationHeal, noteBrowserCaptureImpaired, registerManagedPage, resolveSharedWindowCarrier, setCaptureProbe, syncWindowVisibility, unregisterManagedPage } from "../browser/index.js";
54
+ import { CAPTURE_SOURCE_UNAVAILABLE_MESSAGE, isChannelSelectionProfile } from "../types/index.js";
55
+ import { CaptureAbandonedError, CaptureTurnTimeoutError, createCaptureLock } from "./captureLock.js";
55
56
  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";
57
+ import { LOG, chromeFetch, delay, extractDomain, formatError, getStreamContext, maxRetryDuration, registerAbortController, resolveFFmpegPath, retryOperation, runWithStreamContext, spawnFFmpeg, startTimer, timeoutSignal, waitWithTimeout } from "../utils/index.js";
58
58
  import { getAuthDomainForChannel, getServiceDisplayName, resolveServiceKey } from "../config/services.js";
59
- import { getBuiltinProfile, getProfileForChannel, getProfileForUrl, resolveProfile } from "../config/profiles.js";
59
+ import { getDomainConfig, getProfileForChannel, getProfileForUrl, resolveProfile } from "../config/profiles.js";
60
60
  import { getProviderByStrategy, invalidateDirectUrl, resolveDirectUrl } from "../browser/channelSelection.js";
61
61
  import { initializePlayback, injectVideoSelector, muteExistingVideos, navigateToPage } from "../browser/video.js";
62
62
  import { CONFIG } from "../config/index.js";
63
- import { chromeFetch } from "../utils/index.js";
64
63
  import { createCaptureSession } from "./captureSession.js";
64
+ import { getBuiltinProfile } from "../config/sites.js";
65
65
  import { getCachedEncryption } from "../native/probe.js";
66
66
  import { getCaptureMimeType } from "./codec.js";
67
67
  import { getDomainAuthState } from "../config/health.js";
68
- import { getDomainConfig } from "../config/sites.js";
69
- import { getPresetViewport } from "../config/presets.js";
68
+ import { getNextStreamId } from "./registry.js";
70
69
  import { getUserProfiles } from "../config/userProfiles.js";
71
70
  import { isCaptureInfrastructureError } from "./recovery.js";
72
- import { isChannelSelectionProfile } from "../types/index.js";
73
- import { minimizeWindow } from "../browser/cdp.js";
74
71
  import { monitorPlaybackHealth } from "./monitor.js";
75
72
  import { mutateChannels } from "../config/userChannels.js";
73
+ import { openSharedWindowTab } from "../browser/tabSelection.js";
76
74
  import { pipeline } from "node:stream/promises";
75
+ import { reaffirmCaptureSurface } from "../browser/cdp.js";
77
76
  import { startOverlayHandling } from "../browser/consent.js";
77
+ import { systemClock } from "homebridge-plugin-utils";
78
78
  /* This module contains the common stream setup logic for HLS streaming. The core logic is split into two functions:
79
79
  *
80
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
@@ -99,8 +99,8 @@ import { startOverlayHandling } from "../browser/consent.js";
99
99
  */
100
100
  // Native fMP4 capture uses MP4/AAC for direct HLS segmentation without transcoding.
101
101
  const NATIVE_FMP4_MIME_TYPE = "video/mp4;codecs=avc1,mp4a.40.2";
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
102
+ // Capture initialization is serialized through a task-scoped lock. Chrome's tabCapture extension can only initialize one capture at a time - a second start while
103
+ // one is in flight fails with "Cannot capture a tab with an active stream" - so every acquisition runs as a task on this one process-wide lock, which holds the
104
104
  // turn until the task's promise settles. Once a capture is established it runs concurrently with other captures without issue.
105
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
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
@@ -109,15 +109,21 @@ const CAPTURE_WEDGE_FLOOR_MS = 30000;
109
109
  const CAPTURE_WEDGE_MARGIN_MS = 5000;
110
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
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;
112
+ const captureLock = createCaptureLock({ clock: systemClock, wedgeFloorMs: CAPTURE_WEDGE_FLOOR_MS, wedgeMarginMs: CAPTURE_WEDGE_MARGIN_MS });
113
+ // The bound on how long the capture extension may take to confirm a recording stopped, after a raw capture stream is destroyed and before the owning page is
114
+ // closed. Destroying the stream sends the stop request; the extension answers by closing its socket once the recorder has stopped, its tracks have stopped, and
115
+ // its pending sends have drained, and that answer is what CaptureStream.stopped carries. The stop has to land while the browser is still connected, or Chrome's
116
+ // tabCapture state lingers and the next acquisition draws "Cannot capture a tab with an active stream". The ceiling bounds the case where the answer can never
117
+ // arrive - a browser that is already gone - so a page close is never held hostage to it.
118
+ const STOP_RECORDING_CEILING_MS = 3000;
119
+ const STOP_RECORDING_CEILING_MESSAGE = "The capture extension did not confirm the recording stopped in time.";
120
+ // The slack the probe's teardown gets beyond the stop confirmation itself: the managed-page unregister and the page close that follow it.
121
+ const PAGE_CLOSE_MARGIN_MS = 2000;
122
+ // The teardown allowance the mid-life probe's outer lock deadline adds over its acquisition criterion bound. The pass/fail criterion times the acquisition alone;
123
+ // this allowance lets the destroy-and-confirm teardown complete inside the turn without counting against that criterion. It is derived from the stop ceiling it
124
+ // has to outlast rather than written as a second number beside it, so the two cannot drift apart. A teardown that hangs past the allowance trips the outer
125
+ // deadline and is reported as a capture-infrastructure failure.
126
+ const PROBE_TEARDOWN_ALLOWANCE_MS = STOP_RECORDING_CEILING_MS + PAGE_CLOSE_MARGIN_MS;
121
127
  // The playback-initialization safety-net timeout. Channel selection plus video setup runs after navigation with no outer timeout racing its internal click
122
128
  // retries; for guideGrid strategies a selection failure triggers an overlay dismiss and retry, which doubles the channel-selection budget. 45 seconds accommodates
123
129
  // that retry while still preventing a pathological hang if multiple internal timeouts chain sequentially. Consumed by both the phase-2 race and the interception
@@ -127,35 +133,30 @@ const PLAYBACK_INIT_TIMEOUT = 45000;
127
133
  // 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
134
  // latency bound - no healthy path waits on it - so its generosity costs nothing.
129
135
  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.
136
+ // The caller-visible capture-timeout messages, each defined once so the lock's deadline errors and any assertion test read the exact text. Both match
137
+ // isCaptureInfrastructureError via its "timed out" substring, so they are exported for the classification assertion that locks that contract.
132
138
  export const STREAM_INIT_TIMEOUT_MESSAGE = "Stream initialization timed out.";
133
139
  export const CAPTURE_PROBE_TIMEOUT_MESSAGE = "Capture probe timed out.";
134
140
  /**
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.
141
+ * Retires a raw capture stream: destroys it so the stop request goes out while the browser is still connected, then waits for the extension to confirm the
142
+ * recording stopped before the caller closes the page. This is the destroy-and-confirm core shared by the mid-life probe's success teardown and every path that
143
+ * must 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
144
+ * composed FFmpeg pipeline down synchronously: this helper awaits the confirmation because a page close follows it directly, and unlike the composed disposer it
145
+ * is used on abandoned paths where the page may already be closed, so the stop side is then best-effort.
146
+ *
147
+ * It never throws. A confirmation that does not arrive inside the ceiling is warned about and then stepped past, because holding a page open for a browser that
148
+ * has stopped answering helps nobody.
149
+ * @param stream - The raw capture stream to retire.
150
+ * @param clock - Clock bounding the stop confirmation. Defaults to the system clock; tests inject a virtual clock.
142
151
  */
143
- async function retireRawStream(stream, clock = realClock) {
152
+ export async function retireRawStream(stream, clock = systemClock) {
144
153
  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);
154
+ try {
155
+ await waitWithTimeout(stream.stopped, STOP_RECORDING_CEILING_MS, { clock, reason: new Error(STOP_RECORDING_CEILING_MESSAGE) });
156
+ }
157
+ catch {
158
+ LOG.warn("The capture extension did not confirm the recording stopped within %dms; closing the page regardless.", STOP_RECORDING_CEILING_MS);
159
+ }
159
160
  }
160
161
  /**
161
162
  * 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
@@ -185,12 +186,12 @@ export class DirectUrlEstablishmentError extends Error {
185
186
  // 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
187
  // browser crash). An explicit guard prevents unbounded recursion.
187
188
  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
+ // Maximum time in milliseconds to wait for a single capture probe's acquisition to respond. Shared by the launch-gate verification (verifyCaptureSystem) and the
189
190
  // mid-life re-verification, so both tiers exercise the capture path with the same bound.
190
191
  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.
192
+ // Wire the capture-readiness probe into the browser launch gate. setup.ts owns the capture lock and the readiness probe; browser/index.ts owns the launch
193
+ // lifecycle. Injecting verifyCaptureSystem here (setup.ts already depends on browser/index.ts) keeps the dependency one-directional and breaks the
194
+ // cycle, mirroring the loginDeps boundary between login.ts and index.ts. It runs once at module load, before any browser launch.
194
195
  setCaptureProbe(verifyCaptureSystem);
195
196
  /**
196
197
  * Error thrown when stream setup fails. Includes HTTP status code and user-friendly message for the response.
@@ -305,7 +306,14 @@ function disposePage(page) {
305
306
  });
306
307
  }
307
308
  }
308
- const defaultCreatePageWithCaptureDeps = { getCurrentBrowser, getStream, startOverlayHandling };
309
+ const defaultCreatePageWithCaptureDeps = { acquireCaptureStream, awaitCaptureVerdict: noteClassifiedCaptureFailure,
310
+ emulateCaptureSurface, getCurrentBrowser, installActivationHeal, openSharedWindowTab, reaffirmCaptureSurface, spawnFFmpeg, startOverlayHandling,
311
+ syncWindowVisibility };
312
+ /* The window-topology answers the open primitive needs and cannot reach for itself: tabSelection.ts speaks to the capture extension alone, so the CDP-side carrier
313
+ * resolution and placement confirmation arrive from here, where both modules are already in view. One record, referenced by both call sites, because the two call
314
+ * sites want the identical pair.
315
+ */
316
+ const SHARED_WINDOW_TOPOLOGY = { confirmPlacement: confirmSharedWindowPlacement, resolveCarrier: resolveSharedWindowCarrier };
309
317
  /**
310
318
  * 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
311
319
  * recovery. It handles:
@@ -313,6 +321,8 @@ const defaultCreatePageWithCaptureDeps = { getCurrentBrowser, getStream, startOv
313
321
  * - Initializing media capture (native fMP4 or Matroska+FFmpeg)
314
322
  * - Navigating to the URL with retry
315
323
  * - Setting up video playback via navigateToPage() + initializePlayback()
324
+ * - Re-entering itself once, on a fresh page through a freshly acquired browser, when Chrome refuses the capture start and the mid-life probe's verdict says a
325
+ * second attempt has something to start from
316
326
  *
317
327
  * The caller is responsible for:
318
328
  * - Creating the segmenter and attaching it to the capture session via captureSession.attachSegmenter()
@@ -322,7 +332,7 @@ const defaultCreatePageWithCaptureDeps = { getCurrentBrowser, getStream, startOv
322
332
  *
323
333
  * @param options - Options for page and capture creation.
324
334
  * @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.
335
+ * live Chrome by substituting the shared-browser accessor, the capture acquisition, and the static-capture overlay poll.
326
336
  * @returns The page, context, and capture session (which owns the raw capture stream and any FFmpeg child).
327
337
  * @throws Error if page creation, capture initialization, or navigation fails.
328
338
  */
@@ -330,23 +340,39 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
330
340
  const env_1 = { stack: [], error: void 0, hasError: false };
331
341
  try {
332
342
  const captureElapsed = startTimer();
333
- const { comment, numericStreamId, onFFmpegError, profile, streamId, url } = options;
343
+ const { comment, onFFmpegError, profile, streamId, url } = options;
344
+ /* Bring the window on screen before anything else. Capture reads the compositor's output for the shared window, and that output is only composed correctly for a
345
+ * window the desktop is presenting, so the sync has to land ahead of capture acquisition rather than alongside it. The pending registry entry is already in
346
+ * capture mode by the time any caller reaches here, on both request paths, so the policy reads capture-active and this resolves to a window on screen.
347
+ */
348
+ await deps.syncWindowVisibility();
334
349
  // Acquire every resource on a DisposableStack so that any throw - in capture initialization, navigation, or playback setup - disposes them structurally as the
335
350
  // 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
351
  // 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
352
  // would otherwise be repeated in each failure path, and closes the navigation-path leak of the manifest interceptor.
338
353
  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 });
354
+ /* Create browser page. The establishment goes on to start a capture, so a browser that can no longer start one refuses here, before a page exists. The page is
355
+ * opened as a tab of the shared window rather than wherever Chrome would place it, because a capture tab rooted in a discovery window keeps that window alive
356
+ * and leaves the visibility policy managing a window the capture is not in. The tab is selected for the moment it arrives and the selection goes straight back,
357
+ * so a user working in another tab is never moved out of it.
358
+ */
359
+ const browser = await deps.getCurrentBrowser("capture");
360
+ const page = await deps.openSharedWindowTab(browser, { deps: SHARED_WINDOW_TOPOLOGY });
361
+ // Register the page as held in flight: the registry does not record it against the stream until setup finishes, so the mark is what keeps stale page cleanup
362
+ // from closing it mid-tune.
363
+ registerManagedPage(page, { inFlight: true });
345
364
  resources.adopt(page, disposePage);
365
+ // Emulate the capture surface before anything else touches the page and well before capture acquires it: the dimensions the encoder is held to and the pixel
366
+ // density the compositor rasters at are both settled here, on a page nothing has overridden yet, and a failure unwinds through the DisposableStack exactly as
367
+ // every later step does.
368
+ const surface = await deps.emulateCaptureSurface(page);
346
369
  await page.setBypassCSP(true);
347
370
  // 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
371
  // initializePlayback (startVideoPlayback, applyVideoStyles, verifyFullscreen, lockVolumeProperties) and subsequent health monitoring (getVideoState).
349
372
  await injectVideoSelector(page);
373
+ // Install the tab-activation heal alongside it, before capture is acquired, so the page is enrolled and carries its listener from its first document onward:
374
+ // whichever way this tab becomes the selected one, the capture's composition moves back to the emulated surface about a second later.
375
+ await deps.installActivationHeal(page);
350
376
  // 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
377
  // codec decision (H.264 vs HEVC) is delegated to the codec module, which considers the user's allowlist and GPU hardware capabilities.
352
378
  const useFFmpeg = CONFIG.streaming.captureMode === "ffmpeg";
@@ -361,76 +387,60 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
361
387
  // 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
388
  // while every other failure unwinds through the DisposableStack.
363
389
  try {
364
- const viewport = getPresetViewport(CONFIG);
365
390
  const streamOptions = {
366
391
  audio: true,
367
392
  audioBitsPerSecond: CONFIG.streaming.audioBitsPerSecond,
368
393
  mimeType: captureMimeType,
369
394
  video: true,
370
395
  videoBitsPerSecond: CONFIG.streaming.videoBitsPerSecond,
371
- // 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.
372
- // 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
373
- // drops below 30. The readiness probe (attemptCaptureProbe) instead pins both bounds to a flat 30 because its getStream() fails or succeeds at the tabCapture
374
- // API level before encoding matters, so a representative-but-minimal constraint set suffices there.
396
+ /* The dimension bounds hold the track to exactly the surface emulateCaptureSurface declared on this page, rather than to a second read of the configured
397
+ * preset, so a preset saved mid-establishment cannot leave the encoder constrained to dimensions the page was never emulated at.
398
+ *
399
+ * The frame rate is constrained 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
400
+ * rate. 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
401
+ * never drops below 30. The readiness probe (attemptCaptureProbe) instead holds both bounds to a flat 30 because its acquisition fails or succeeds at the
402
+ * tabCapture API level before encoding matters, so a representative-but-minimal constraint set suffices there.
403
+ */
375
404
  videoConstraints: {
376
405
  mandatory: {
377
406
  maxFrameRate: 60,
378
- maxHeight: viewport.height,
379
- maxWidth: viewport.width,
407
+ maxHeight: surface.height,
408
+ maxWidth: surface.width,
380
409
  minFrameRate: Math.max(30, Math.min(60, CONFIG.streaming.frameRate)),
381
- minHeight: viewport.height,
382
- minWidth: viewport.width
410
+ minHeight: surface.height,
411
+ minWidth: surface.width
383
412
  }
384
413
  }
385
414
  };
386
- // 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
387
- // 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.
415
+ // Acquire the raw capture stream through the capture lock. The lock serializes acquisition against every other capture init process-wide and holds the turn
416
+ // until 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
417
+ // from CONFIG.
388
418
  const stream = await captureLock.run(async (signal) => {
389
419
  // 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
390
420
  // OUTSIDE the lock, on a fresh page.
391
421
  if (page.isClosed()) {
392
422
  throw new PageClosedDuringTurnError();
393
423
  }
394
- // Initialize capture. On a stale-mutex rejection - including one that arrives after this task was abandoned at the caller deadline - escalate from the single
395
- // 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.
396
- let raw;
397
- try {
398
- raw = await deps.getStream(page, streamOptions);
399
- }
400
- catch (error) {
401
- if (isStaleCaptureMutexError(error)) {
402
- escalateStaleCaptureMutex("stream initialization");
403
- }
404
- throw error;
405
- }
406
- // The caller deadline fired while getStream was still running: retire the stream this task just produced - destroy plus the STOP_RECORDING settle - inside the
407
- // turn, then reject, so no path strands a live capture on a closing page or mistakes a retired stream for a usable one.
424
+ // Initialize capture. The acquisition makes one start and reports what came back, so it is handed no deadline signal of its own; the check immediately
425
+ // below is where this task decides what becomes of a capture that arrived after the caller stopped waiting for it.
426
+ const raw = await deps.acquireCaptureStream(page, streamOptions);
427
+ // The caller deadline fired while the acquisition was still running: retire the stream this task just produced - destroy plus the stop confirmation - inside
428
+ // the turn, then reject, so no path strands a live capture on a closing page or mistakes a retired stream for a usable one.
408
429
  if (signal.aborted) {
409
- await retireRawStream(raw, realClock);
430
+ await retireRawStream(raw, systemClock);
410
431
  throw new CaptureAbandonedError();
411
432
  }
412
433
  return raw;
413
434
  }, {
414
435
  deadlineMessage: STREAM_INIT_TIMEOUT_MESSAGE,
415
436
  deadlineMs: CONFIG.streaming.navigationTimeout,
416
- // 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
417
- // active stream means the browser is demonstrably capturing, so tearing it down would violate per-stream failure isolation. reverificationInProgress is passed
418
- // 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
419
- // recovery bound, and a redundant invalidate is identity-guarded and converges harmlessly.
437
+ /* A capture initialization that holds the lock this long is evidence about the browser rather than about the stream: the wedge fires only after this task's
438
+ * own deadline has already failed the tune, so the tune is lost either way and what is left to decide is what the browser can still be trusted with.
439
+ * Recording it marks the instance this task ran against, which refuses later capture starts at once and relaunches the browser when nothing depends on it -
440
+ * the same response the probe's verdict takes, reached from the other direction.
441
+ */
420
442
  onWedge: () => {
421
- if (shouldReverifyCapture({ activeStreamIds: getAllStreams().map((entry) => entry.id), failingStreamId: numericStreamId, hasBrowser: true,
422
- reverificationInProgress: false })) {
423
- // Route the wedge into the existing browser-recovery ladder, targeting the exact instance this task ran against; invalidateBrowser no-ops if it was already
424
- // superseded. A rejection is warn-logged rather than rethrown out of the wedge callback.
425
- void invalidateBrowser(browser, "capture initialization wedged past the recovery bound").catch((error) => {
426
- LOG.warn("Invalidating a wedged capture browser failed: %s.", formatError(error));
427
- });
428
- }
429
- else {
430
- // 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
431
- // worst at Puppeteer's protocolTimeout, and once the other streams drain a later wedge or setup failure reaches the zero-other case.
432
- LOG.warn("Capture initialization has wedged past the recovery bound, but other streams are active; holding rather than invalidating the browser.", { failingStreamId: numericStreamId });
433
- }
443
+ void noteBrowserCaptureImpaired(browser, "capture initialization wedged past the recovery bound");
434
444
  },
435
445
  turnWaitMs: CONFIG.streaming.navigationTimeout
436
446
  });
@@ -439,17 +449,26 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
439
449
  // pipeline wiring below are synchronous, so the stream is owned the instant it exists.
440
450
  let ffmpegProcess = null;
441
451
  if (useFFmpeg) {
442
- const ffmpeg = spawnFFmpeg(ffmpegBin, CONFIG.streaming.audioBitsPerSecond, (error) => {
452
+ const ffmpeg = deps.spawnFFmpeg(ffmpegBin, CONFIG.streaming.audioBitsPerSecond, (error) => {
443
453
  LOG.error("FFmpeg process error: %s.", formatError(error));
444
454
  if (onFFmpegError) {
445
455
  onFFmpegError(error);
446
456
  }
447
457
  }, streamId, comment);
448
458
  ffmpegProcess = ffmpeg;
459
+ /* A disposed pipeline must never fire its error callback. The two listeners below sit on the process's own streams rather than inside the spawn wrapper,
460
+ * so they are the two places a post-kill teardown event can still reach a caller, and each gates on the same teardown flag the wrapper's own handlers use.
461
+ *
462
+ * The gate is what keeps a tab replacement's discard silent. A replacement disposes the outgoing pipeline in the same synchronous frame it installs the
463
+ * incoming one, so a stray event from the killed FFmpeg lands a tick later - against a registry that already holds a healthy new pipeline - and this
464
+ * closure's callback would break the circuit on the very stream the replacement just saved. String filters cannot cover that: they name the errors
465
+ * teardown is known to produce, and the point here is every error a killed pipeline produces.
466
+ */
467
+ const isTornDown = () => ffmpeg.isShuttingDown();
449
468
  // Handle pipe errors on stdout. Stdin errors are handled by pipeline() below.
450
469
  ffmpeg.stdout.on("error", (error) => {
451
470
  const errorMessage = formatError(error);
452
- if (errorMessage.includes("EPIPE")) {
471
+ if (errorMessage.includes("EPIPE") || isTornDown()) {
453
472
  LOG.debug("streaming:ffmpeg", "FFmpeg stdout pipe closed: %s.", errorMessage);
454
473
  }
455
474
  else {
@@ -464,8 +483,9 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
464
483
  // destroys the source stream, preventing "write after end" errors that would occur with .pipe().
465
484
  pipeline(stream, ffmpeg.stdin).catch((error) => {
466
485
  const errorMessage = formatError(error);
467
- // EPIPE, "write after end", and "Premature close" errors are expected during cleanup when FFmpeg is killed or the capture stream is destroyed.
468
- if (errorMessage.includes("EPIPE") || errorMessage.includes("write after end") || errorMessage.includes("Premature close")) {
486
+ // EPIPE, "write after end", and "Premature close" errors are expected during cleanup when FFmpeg is killed or the capture stream is destroyed, and a
487
+ // pipeline whose FFmpeg is being torn down produces whatever it produces.
488
+ if (errorMessage.includes("EPIPE") || errorMessage.includes("write after end") || errorMessage.includes("Premature close") || isTornDown()) {
469
489
  return;
470
490
  }
471
491
  // Unexpected pipeline errors require cleanup.
@@ -493,15 +513,42 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
493
513
  }
494
514
  return await createPageWithCapture({ ...options, _pageClosedRetries: retryCount + 1 }, deps);
495
515
  }
496
- // Every other rejection - a stale-mutex escalation already fired inside the task, a caller-deadline CaptureDeadlineError, or any other capture-init failure - just
497
- // unwinds. Resource teardown (page, interceptor, and the capture session once built) is handled by the DisposableStack as this throw unwinds the function scope.
516
+ /* Chrome refuses a first capture start now and then, browser-wide, and a fresh Chrome accepts at once - so this refusal is a question about the browser, and
517
+ * the probe is what answers it. The wait is worth it because the answer is exactly what the tune needs: every one of its outcomes IS the retry policy, read
518
+ * off getCurrentBrowser("capture") on the way back in. A `captured` verdict means the browser is healthy and the re-entry meets the same instance. A `failed`
519
+ * verdict on an otherwise idle browser means the relaunch has already run by the time the verdict settles, so the re-entry meets the fresh one. A `failed`
520
+ * verdict while a stream is established or another tune is mid-setup means the relaunch could not run, and the re-entry draws the impaired refusal the
521
+ * supervisor already answers with - a quiet 503 rather than a second failed establishment.
522
+ *
523
+ * The failed attempt's resources are released BEFORE the wait rather than on the way out. The relaunch a failed verdict triggers waits for every in-flight
524
+ * page to be gone, and this page is one of them, so a tune still holding its page would be waiting on a relaunch its own page forbids. Disposing here empties
525
+ * the stack, which leaves its scope-exit disposal a no-op on every path out of this block.
526
+ *
527
+ * One retry, and the marker travels through the closed-page recursion's options spread on purpose: a browser crash recovery that follows a refusal retry must
528
+ * not buy a second one, because the bound is per logical establishment rather than per invocation.
529
+ */
530
+ if (formatError(error).includes(CAPTURE_SOURCE_UNAVAILABLE_MESSAGE) && !options._refusalRetried) {
531
+ resources.dispose();
532
+ const verdict = await deps.awaitCaptureVerdict(error);
533
+ // No published browser, or a probe that never obtained its turn: neither is evidence about the browser, so there is nothing a second establishment would
534
+ // be starting from. The refusal travels as the tune's own failure.
535
+ if (!verdict || (verdict.kind === "inconclusive")) {
536
+ throw error;
537
+ }
538
+ LOG.info("Chrome refused the capture start; retrying once on the probe's verdict.", { verdict: verdict.kind });
539
+ return await createPageWithCapture({ ...options, _refusalRetried: true }, deps);
540
+ }
541
+ // Every other rejection - a caller-deadline CaptureDeadlineError, a refusal the one retry has already been spent on, or any other capture-init failure - just
542
+ // unwinds. The verdict still gets the failure, because a browser that cannot start captures is worth marking whoever asked; nothing waits on it here.
543
+ // Resource teardown (page, interceptor, and the capture session once built) is handled by the DisposableStack as this throw unwinds the function scope.
544
+ void deps.awaitCaptureVerdict(error);
498
545
  throw error;
499
546
  }
500
547
  // Navigate and set up playback. For static capture profiles, just navigate without video setup.
501
548
  let context;
502
549
  let strategyDirectTune = false;
503
550
  let usedDirectUrl = false;
504
- // Install the CDP manifest interceptor immediately before the navigate-and-tune fork, so its observation window opens after the capture-lock and getStream phase
551
+ // Install the CDP manifest interceptor immediately before the navigate-and-tune fork, so its observation window opens after the capture-lock and acquisition
505
552
  // (which the observer would otherwise idle through) and spans exactly the phases that produce manifests: navigation with retry, channel selection, and video
506
553
  // 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
507
554
  // 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
@@ -569,10 +616,10 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
569
616
  // 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
570
617
  // 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.
571
618
  const urlEvidence = usedDirectUrl && invalidateDirectUrl(profile, error);
572
- // 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
573
- // attempt. Fire-and-forget since we're about to throw. Resource teardown (capture session, interceptor, page) is handled by the DisposableStack as this throw
574
- // unwinds the function scope; the capture session disposes first, destroying the capture stream before the page closes, so STOP_RECORDING ordering is preserved.
575
- minimizeBrowserWindow().catch(() => { });
619
+ // Window presentation is deliberately left alone here. The failing stream's registry entry is still registered in capture mode at this point, so the policy
620
+ // would read capture-active and hold the window on screen anyway; the terminate path that tears the entry down is the moment that genuinely settles it.
621
+ // Resource teardown (capture session, interceptor, page) is handled by the DisposableStack as this throw unwinds the function scope; the capture session
622
+ // disposes first, destroying the capture stream before the page closes, so STOP_RECORDING ordering is preserved.
576
623
  /* 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
577
624
  * 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
578
625
  * 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
@@ -581,15 +628,27 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
581
628
  if (urlEvidence) {
582
629
  throw new DirectUrlEstablishmentError(error);
583
630
  }
631
+ /* Classification sits below that branch, on the standing attempt, and not above it. A failure that leaves this function typed is not a verdict on the
632
+ * establishment at all - it is a request for one more attempt - and the attempt that follows raises its own failure through this same catch. Classifying
633
+ * above the branch would hand the readiness detector both of them for a single logical setup, which is one setup speaking twice about one browser's fate.
634
+ * The establishment timeout is exactly the shape where that matters, because it is both a failure the URL can be blamed for and one that carries a
635
+ * capture-infrastructure signature.
636
+ *
637
+ * The verdict is started here and not waited on. A failure this far in has already spent a navigation and a channel selection, so a second whole tune would
638
+ * double the client's wait for something no verdict can change: the capture start, the one step a fresh browser would do differently, already succeeded.
639
+ */
640
+ void deps.awaitCaptureVerdict(error);
584
641
  throw error;
585
642
  }
586
- // During tab replacement, allow Chrome's compositor to fully stabilize the fullscreen video surface before minimizing. Without this delay, the compositor may
587
- // snapshot an incorrect scaling state during the minimize transition, causing the captured content to appear zoomed into the top-left corner.
588
- if (options.tabReplacement && !profile.staticCapture) {
589
- await delay(500);
590
- }
591
- // Minimize the window now that the page is fully established.
592
- await minimizeWindow(page);
643
+ /* Both establishment branches join here, which is why the re-affirmation sits at this point: capture acquisition selects this tab for the length of the start
644
+ * and hands the selection back, and a tune of a fullscreen-activating profile selects it again for its fullscreen sequence and hands it back once more.
645
+ * Re-issuing the page's own declared metrics is what leaves the capture composing the emulated surface rather than the window's fitted view of it, whatever the
646
+ * establishment did with the selection.
647
+ */
648
+ await deps.reaffirmCaptureSurface(page);
649
+ // Re-settle the window now that the page is established, handing the sync the page it should use for the CDP session. Navigation activates the window on macOS,
650
+ // so a pass here settles it against the policy after the tune, as every other transition does.
651
+ await deps.syncWindowVisibility(page);
593
652
  LOG.debug("timing:startup", "Page with capture ready. Total: %sms.", captureElapsed());
594
653
  // 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
595
654
  // and the caller becomes responsible for disposing what it now holds.
@@ -617,18 +676,23 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
617
676
  * FruitDeepLinks) whose domain has no profile mapping. By following redirects, we discover the actual streaming site's domain and can resolve the correct profile.
618
677
  *
619
678
  * Uses a HEAD request to avoid downloading response bodies. The 3-second timeout ensures stream startup isn't blocked by slow or unreachable indirection services.
679
+ * This resolver holds no clock of its own, so that bound takes the port's default, the system clock.
620
680
  *
621
681
  * @param url - The URL to resolve.
622
682
  * @returns The final URL after following all redirects, or null on any error.
623
683
  */
624
684
  async function resolveRedirectUrl(url) {
685
+ const bound = timeoutSignal(3000);
625
686
  try {
626
- const response = await chromeFetch(url, { method: "HEAD", signal: AbortSignal.timeout(3000) });
687
+ const response = await chromeFetch(url, { method: "HEAD", signal: bound.signal });
627
688
  return response.url;
628
689
  }
629
690
  catch {
630
691
  return null;
631
692
  }
693
+ finally {
694
+ bound.cancel();
695
+ }
632
696
  }
633
697
  // Stream Setup.
634
698
  /**
@@ -651,7 +715,7 @@ function buildPersistResolutionCallback(canonicalKey, serviceTag) {
651
715
  const existing = data.channels[variantKey] ?? {};
652
716
  data.channels[variantKey] = { ...existing, channelSelector: resolvedSelector };
653
717
  });
654
- LOG.debug("tuning", "Persisted resolved selector \"%s\" to channel store as \"%s\".", resolvedSelector, variantKey);
718
+ LOG.debug("tuning:selector", "Persisted resolved selector \"%s\" to channel store as \"%s\".", resolvedSelector, variantKey);
655
719
  };
656
720
  }
657
721
  /* 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
@@ -659,7 +723,7 @@ function buildPersistResolutionCallback(canonicalKey, serviceTag) {
659
723
  * 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
660
724
  * 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
661
725
  * calibrated for - a false tune-failure on a correct stream. Providers without a verifier, profiles without a channelSelector, non-master selections, and a null
662
- * 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.
726
+ * interception all verify vacuously: the gates upstream are the only identity signal we have there, so the answer the state supports is no objection rather than a guess.
663
727
  *
664
728
  * @param interceptionPromise - The interception promise from the handle the tune finalized, awaited only once the gates above admit a verifier.
665
729
  * @param profile - The resolved site profile whose strategy names the provider and whose channelSelector names the expected channel.
@@ -720,7 +784,7 @@ export async function establishChannelPlayback(page, profile, handle, options, d
720
784
  // 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.
721
785
  handle?.markChannelSelectionStart();
722
786
  const initPromise = deps.initializePlayback(page, profile, initOptions);
723
- /* 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
787
+ /* The settlement hook runs on the initialization's own settlement rather than a fixed step after the wait below, because only the promise knows which of
724
788
  * success, failure, or a late completion actually happened - and a late completion is reachable, since the wait abandons rather than cancels: a lapsed
725
789
  * 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
726
790
  * 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
@@ -729,10 +793,10 @@ export async function establishChannelPlayback(page, profile, handle, options, d
729
793
  if (onInitSettled) {
730
794
  void initPromise.finally(() => onInitSettled()).catch(() => { });
731
795
  }
732
- return waitWithTimeout(initPromise, PLAYBACK_INIT_TIMEOUT, new Error("Playback initialization timed out after " + String(PLAYBACK_INIT_TIMEOUT) + "ms."));
796
+ return waitWithTimeout(initPromise, PLAYBACK_INIT_TIMEOUT, { reason: new Error("Playback initialization timed out after " + String(PLAYBACK_INIT_TIMEOUT) + "ms.") });
733
797
  }
734
798
  /**
735
- * Adjudicates what an establishment's interception selected: finalize it against the honest direct-tune kind, then confirm the manifest belongs to the channel
799
+ * Adjudicates what an establishment's interception selected: finalize it against the accurate direct-tune kind, then confirm the manifest belongs to the channel
736
800
  * the profile names. The tune path and the native refresh capability both run this, so finalize-then-verify is written exactly once.
737
801
  *
738
802
  * The stage hands on the interception promise rather than a resolved result, which is what keeps verifyManifestSelection's latency rule holding - its provider
@@ -768,7 +832,8 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
768
832
  // tab replacement handler all reference the same stream identity as the pending entry in the registry.
769
833
  const streamId = options.streamId ?? generateStreamId(channelName, url);
770
834
  const numericStreamId = options.numericStreamId ?? getNextStreamId();
771
- const startTime = new Date();
835
+ // Stream setup is a composition point - it builds the capture lock, the health monitor, and the page - so it reads the system clock at its own boundary.
836
+ const startTime = systemClock.now();
772
837
  // Create and register the AbortController for this stream. This allows pending evaluate calls to be cancelled immediately when the stream is terminated.
773
838
  const abortController = new AbortController();
774
839
  registerAbortController(streamId, abortController);
@@ -849,12 +914,12 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
849
914
  // Create page and start capture using the shared function. This handles browser page creation, capture initialization, FFmpeg spawning, and navigation with retry.
850
915
  let captureResult;
851
916
  try {
852
- /* 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
917
+ /* Skip CDP manifest interception when the channel is locked to screen capture, or when the probe cache already knows this stream's binding resolves to
853
918
  * DRM. The per-channel override short-circuits first, so a forced channel never installs the interceptor: nothing intercepts a manifest, no native
854
919
  * 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
855
920
  * 15 seconds before the interceptor timeout cleans it up; every stream carries an identity, ad-hoc URLs included, so that lookup needs no guard.
856
921
  */
857
- const skipInterception = (channel?.forceCapture === true) || (getCachedEncryption(probeIdentity) === "drm");
922
+ const skipInterception = (channel?.forceCapture === true) || (getCachedEncryption(probeIdentity, systemClock.now()) === "drm");
858
923
  // Build the persistResolution closure for the active channel. When the resolution layer in selectChannel() converts a category selector to a concrete call
859
924
  // 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
860
925
  // the selector via the web UI. Omitted for ad-hoc URL streams (no channel record to update).
@@ -864,7 +929,6 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
864
929
  undefined;
865
930
  const attemptOptions = {
866
931
  comment: metadataComment,
867
- numericStreamId,
868
932
  onFFmpegError: onCircuitBreak,
869
933
  persistResolution,
870
934
  profile,
@@ -906,6 +970,13 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
906
970
  if ((error instanceof BrowserUnavailableError) || (error instanceof BrowserSupersededError)) {
907
971
  throw new StreamSetupError("Browser temporarily unavailable.", 503, "The capture system is recovering. Please retry shortly.", { cause: error });
908
972
  }
973
+ // The browser is alive and still serving the captures it started, but it can no longer start another, so this tune is refused before a page is even opened.
974
+ // Quiet like its sibling above, because the alarm fired once when the mark was recorded. The message differs because the wait ends differently: the client is
975
+ // waiting on the browser to be released by whatever still holds it rather than on a cooldown to elapse.
976
+ if (error instanceof BrowserCaptureImpairedError) {
977
+ throw new StreamSetupError("Browser temporarily unavailable.", 503, "The browser can no longer start captures and will relaunch as soon as nothing is " +
978
+ "using it. Please retry shortly.", { cause: error });
979
+ }
909
980
  // createPageWithCapture handles its own cleanup on failure (closes page, kills FFmpeg).
910
981
  const errorMessage = formatError(error);
911
982
  const lowerMessage = errorMessage.toLowerCase();
@@ -914,16 +985,14 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
914
985
  if (!isBenign) {
915
986
  LOG.error("Stream setup failed for %s: %s.", url, errorMessage);
916
987
  }
917
- // Capture infrastructure errors should return 503 to signal Channels DVR to back off. These include Chrome capture state issues, capture-lock turn-wait
918
- // timeouts, and stream initialization failures. Using 503 with Retry-After prevents retry storms when there's a systemic issue. isCaptureInfrastructureError
919
- // (recovery.ts) is the single source of truth for this classification, shared with the browser supervisor's readiness detection.
988
+ /* Capture infrastructure errors should return 503 to signal Channels DVR to back off. These include Chrome capture state issues, capture-lock turn-wait
989
+ * timeouts, and stream initialization failures. Using 503 with Retry-After prevents retry storms when there's a systemic issue. isCaptureInfrastructureError
990
+ * (recovery.ts) is the single source of truth for this judgment, shared with the browser supervisor's readiness detection.
991
+ *
992
+ * The read here is purely for the client-facing status. The side effect that judgment also drives - handing the failure to the passive mid-life capture
993
+ * detector - fires inside createPageWithCapture's own catches, where every caller of the acquisition reaches it rather than only this one.
994
+ */
920
995
  const isCaptureError = isCaptureInfrastructureError(errorMessage);
921
- // 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
922
- // 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
923
- // never delay this response.
924
- if (isCaptureError) {
925
- noteCaptureInfrastructureFailure(numericStreamId);
926
- }
927
996
  // 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.
928
997
  throw new StreamSetupError("Stream error.", isCaptureError ? 503 : 500, withSignInGuidance("Failed to start stream.", channelName, serviceName), { cause: error });
929
998
  }
@@ -949,7 +1018,6 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
949
1018
  if (manifestInterception) {
950
1019
  const verifyError = await adjudicateChannelSelection(manifestInterception, profile, directTune);
951
1020
  if (verifyError) {
952
- await minimizeBrowserWindow();
953
1021
  const failureLabel = channel?.name ?? channelName ?? url;
954
1022
  throw new StreamSetupError("Tune verification failed: " + verifyError, 502, withSignInGuidance("Tune verification failed for " + failureLabel + ". " + verifyError, channelName, serviceName));
955
1023
  }
@@ -977,8 +1045,8 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
977
1045
  monitor.dispose();
978
1046
  captureSession.dispose();
979
1047
  disposePage(page);
980
- // Re-minimize the browser window.
981
- await minimizeBrowserWindow();
1048
+ // Settle the window against the policy now that this stream's resources are gone.
1049
+ await deps.syncWindowVisibility();
982
1050
  };
983
1051
  // Success: transfer ownership out of the scope guard. The cleanup closure and, once the session is installed on the registry entry, terminateStream become
984
1052
  // responsible for disposing the page and capture session; the interceptor continues into tune verification and native streaming.
@@ -1062,7 +1130,7 @@ export async function reestablishChannelManifest(options) {
1062
1130
  void muteExistingVideos(page);
1063
1131
  }
1064
1132
  });
1065
- // Adjudicate honestly: the kind formula gets no cached-direct-URL term here, because the route above never takes one.
1133
+ // Adjudicate against this route's facts: the kind formula gets no cached-direct-URL term here, because the route above never takes one.
1066
1134
  const verifyError = await adjudicateChannelSelection(handle, profile, computeDirectTuneKind({ profile, strategyDirectTune: tuneResult.directTune ?? false }));
1067
1135
  // 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
1068
1136
  // finalize settle. Reading it before the verification result is contract rather than preference: an establishment that intercepted nothing has to report
@@ -1101,19 +1169,17 @@ export async function reestablishChannelManifest(options) {
1101
1169
  }
1102
1170
  // Capture Readiness Verification.
1103
1171
  /**
1104
- * 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
1105
- * 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
1106
- * 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
1107
- * extension is unregistered, so a dead extension is detected immediately rather than causing every subsequent stream request to hang.
1172
+ * Verifies that a freshly-launched Chrome instance can actually capture, by running a real capture acquisition against a throwaway page on it. This is the
1173
+ * capability tier of the browser launch gate: browser/index.ts injects it via setCaptureProbe and runs it inside launchReadyBrowser, so a browser is published as
1174
+ * ready only after its capture capability is verified - at boot AND at every relaunch, not just startup. It exercises the exact acquisition path that fails when
1175
+ * the capture extension is unregistered, so a dead extension is detected immediately rather than causing every subsequent stream request to fail.
1108
1176
  *
1109
- * Each attempt creates a temporary page, attempts a short capture, and tears down both cleanly. A 500ms delay after destroying the capture stream allows
1110
- * 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.
1177
+ * Each attempt creates a temporary page, attempts a short capture, and tears down both cleanly. The teardown waits for the extension to confirm the recording
1178
+ * stopped, bounded by a ceiling, before closing the page, so Chrome's tabCapture state is released rather than lingering into the first real request.
1111
1179
  *
1112
1180
  * 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
1113
1181
  * 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
1114
1182
  * relaunches PrismCast repeatedly, each attempt orphaning a Chrome process; at relaunch it provides in-launch settling before the supervisor counts a launch failure.
1115
- *
1116
- * 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.
1117
1183
  * @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).
1118
1184
  */
1119
1185
  export async function verifyCaptureSystem(browser) {
@@ -1121,20 +1187,13 @@ export async function verifyCaptureSystem(browser) {
1121
1187
  const PROBE_RETRY_DELAY = 5000;
1122
1188
  for (let attempt = 1; attempt <= PROBE_MAX_ATTEMPTS; attempt++) {
1123
1189
  // 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
1124
- // 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.
1190
+ // have no recovery target and would jam the shared lock. GATE mode keeps the acquisition bounded internally instead.
1125
1191
  // eslint-disable-next-line no-await-in-loop -- Sequential retries are intentional; each probe must complete before deciding whether to retry.
1126
1192
  const result = await attemptCaptureProbe(browser, { boundMs: CAPTURE_PROBE_TIMEOUT_MS, kind: "gate" });
1127
1193
  // Probe succeeded.
1128
1194
  if (result === null) {
1129
1195
  return;
1130
1196
  }
1131
- // Stale capture state is unrecoverable: puppeteer-stream's second lock section has no try/finally, so the mutex is permanently leaked and every subsequent
1132
- // 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
1133
- // further probe attempts run before the process exits.
1134
- if (isStaleCaptureMutexError(result)) {
1135
- escalateStaleCaptureMutex("the launch-gate capture probe");
1136
- throw new Error("Capture system verification failed: stale capture state detected.");
1137
- }
1138
1197
  // If we have retries remaining, log a warning and wait before the next attempt.
1139
1198
  if (attempt < PROBE_MAX_ATTEMPTS) {
1140
1199
  LOG.warn("Capture probe attempt %d of %d failed: %s. Retrying in %ds.", attempt, PROBE_MAX_ATTEMPTS, result, PROBE_RETRY_DELAY / 1000);
@@ -1158,16 +1217,24 @@ export async function verifyCaptureSystem(browser) {
1158
1217
  /**
1159
1218
  * 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
1160
1219
  * 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
1161
- * getStream is bounded - see CaptureProbeMode.
1220
+ * the acquisition is bounded - see CaptureProbeMode.
1162
1221
  * @param browser - The Chrome instance to probe.
1163
- * @param mode - The operating mode: gate (internal getStream race) or midlife (self-timed getStream on the lock).
1164
- * @param clock - Clock used for the mid-life self-timing and the teardown settle. Defaults to realClock.
1222
+ * @param mode - The operating mode: gate (internal acquisition race) or midlife (self-timed acquisition on the lock).
1223
+ * @param clock - Clock used for the mid-life self-timing and the teardown confirmation. Defaults to the system clock.
1165
1224
  * @returns Null on success, or an error message string on failure.
1166
1225
  */
1167
- async function attemptCaptureProbe(browser, mode, clock = realClock) {
1168
- const page = await browser.newPage();
1226
+ async function attemptCaptureProbe(browser, mode, clock = systemClock) {
1227
+ /* The mid-life probe runs against a browser that is serving streams, so its page is opened as a tab of the shared window for the reason a stream's page is: a
1228
+ * probe tab rooted in a discovery window would hold that window open for as long as the probe took. The launch gate keeps the plain create: it runs pre-publish,
1229
+ * when the browser has exactly one window and no discovery window can exist yet, so its tab has no wrong window to land in - and a gate that has to stay bounded
1230
+ * gains nothing from queuing behind the selection executor to prove it.
1231
+ */
1232
+ const page = (mode.kind === "midlife") ? await openSharedWindowTab(browser, { deps: SHARED_WINDOW_TOPOLOGY }) : await browser.newPage({ background: true });
1169
1233
  registerManagedPage(page);
1170
- // 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
1234
+ // The probe page carries the preset-sized layout at the display's density, the surface a capture page starts from before its own declaration, so the probe's
1235
+ // acquisition keeps the shape the field validated.
1236
+ const surface = await emulateLayoutSurface(page);
1237
+ // Tears the probe page down cleanly: retire the raw capture stream (destroy plus the stop confirmation) while the browser is still connected, unregister the
1171
1238
  // managed page, then close it. Shared by every success and self-timed-failure path in both modes.
1172
1239
  const teardown = async (stream) => {
1173
1240
  await retireRawStream(stream, clock);
@@ -1177,12 +1244,11 @@ async function attemptCaptureProbe(browser, mode, clock = realClock) {
1177
1244
  }
1178
1245
  };
1179
1246
  try {
1180
- // 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,
1181
- // so matching those runtime constraints ensures the probe exercises a representative getStream() call. Both read the configured preset, so they agree by
1182
- // construction rather than by two call sites happening to pick the same numbers.
1247
+ // Use the same capture MIME type and surface as the runtime. The stale state error occurs at the tabCapture API level before encoding matters, so matching
1248
+ // those runtime constraints ensures the probe exercises a representative acquisition. The constraints are held to the dimensions the declaration above
1249
+ // returned, so the probe holds its track to the surface the page actually carries rather than to a second read of the preset.
1183
1250
  const useFFmpeg = CONFIG.streaming.captureMode === "ffmpeg";
1184
1251
  const captureMimeType = useFFmpeg ? getCaptureMimeType() : NATIVE_FMP4_MIME_TYPE;
1185
- const viewport = getPresetViewport(CONFIG);
1186
1252
  const streamOptions = {
1187
1253
  audio: true,
1188
1254
  mimeType: captureMimeType,
@@ -1190,26 +1256,26 @@ async function attemptCaptureProbe(browser, mode, clock = realClock) {
1190
1256
  videoConstraints: {
1191
1257
  mandatory: {
1192
1258
  maxFrameRate: 30,
1193
- maxHeight: viewport.height,
1194
- maxWidth: viewport.width,
1259
+ maxHeight: surface.height,
1260
+ maxWidth: surface.width,
1195
1261
  minFrameRate: 30,
1196
- minHeight: viewport.height,
1197
- minWidth: viewport.width
1262
+ minHeight: surface.height,
1263
+ minWidth: surface.width
1198
1264
  }
1199
1265
  }
1200
1266
  };
1201
- // 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
1267
+ // GATE mode: bound the acquisition with an internal timeout. On a lapse the acquisition is still running, so attach a both-callback handler that retires a
1202
1268
  // 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,
1203
1269
  // so a fulfillment-only handler would create unhandled-rejection noise. This cleans up the orphan without serializing successive gate attempts against one another.
1204
1270
  if (mode.kind === "gate") {
1205
- const streamPromise = getStream(page, streamOptions);
1271
+ const streamPromise = acquireCaptureStream(page, streamOptions);
1206
1272
  const timeoutError = new Error(CAPTURE_PROBE_TIMEOUT_MESSAGE);
1207
1273
  let stream;
1208
1274
  try {
1209
- stream = await waitWithTimeout(streamPromise, mode.boundMs, timeoutError);
1275
+ stream = await waitWithTimeout(streamPromise, mode.boundMs, { reason: timeoutError });
1210
1276
  }
1211
1277
  catch (error) {
1212
- // 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.
1278
+ // Only the internal timeout leaves the acquisition running; an in-time rejection produced no stream to clean up and is already observed by the bounded wait.
1213
1279
  if (error === timeoutError) {
1214
1280
  void streamPromise.then((late) => {
1215
1281
  void retireRawStream(late, clock);
@@ -1223,14 +1289,14 @@ async function attemptCaptureProbe(browser, mode, clock = realClock) {
1223
1289
  LOG.info("Capture system verified successfully.");
1224
1290
  return null;
1225
1291
  }
1226
- // 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
1227
- // against boundMs, measured without racing or abandoning anything, so the teardown settle that follows never counts against it.
1292
+ // MID-LIFE mode: await the acquisition raw and self-time it. The turn (owned by the lock) spans the whole task, but the pass/fail CRITERION is the
1293
+ // acquisition's own latency against boundMs, measured without racing or abandoning anything, so the teardown that follows never counts against it.
1228
1294
  const startedAt = clock.now();
1229
- const stream = (await getStream(page, streamOptions));
1295
+ const stream = await acquireCaptureStream(page, streamOptions);
1230
1296
  const elapsed = clock.now() - startedAt;
1231
1297
  await teardown(stream);
1232
1298
  // 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
1233
- // the never-throw contract and prevents stranding a capture), or when getStream's own latency exceeded the criterion bound.
1299
+ // the never-throw contract and prevents stranding a capture), or when the acquisition's own latency exceeded the criterion bound.
1234
1300
  if (mode.signal.aborted || (elapsed > mode.boundMs)) {
1235
1301
  return CAPTURE_PROBE_TIMEOUT_MESSAGE;
1236
1302
  }
@@ -1246,104 +1312,169 @@ async function attemptCaptureProbe(browser, mode, clock = realClock) {
1246
1312
  }
1247
1313
  return errorMessage;
1248
1314
  }
1315
+ finally {
1316
+ /* The mid-life open restores the shared window on its way in, so the presentation is settled back here, on every exit this arm has: the success return, the
1317
+ * self-timed bound return, and the catch. It settles inside the probe rather than in the detector above it because the capture lock's outer deadline can
1318
+ * abandon this task while it runs on detached - a settle placed in the detector would fire before a detached probe's open had restored anything. The launch
1319
+ * gate opens no such tab and touches no window, so the settle is the mid-life arm's alone.
1320
+ */
1321
+ if (mode.kind === "midlife") {
1322
+ await syncWindowVisibility();
1323
+ }
1324
+ }
1249
1325
  }
1250
1326
  // Passive Mid-Life Capture-Death Detection.
1251
- /* A browser can be capture-ready at launch and lose its capture capability later - the extension wedges, tabCapture stalls - without ever firing a "disconnected"
1252
- * 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
1253
- * carrying a capture-infrastructure signature. It is deliberately conservative. The guard (the failing stream is the only active stream) preserves per-stream
1254
- * 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
1255
- * 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,
1256
- * 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.
1327
+ /* A browser can be capture-ready at launch and lose its capture capability later - the extension wedges, tabCapture stalls, a display reconfiguration leaves the
1328
+ * process unable to start another capture while the captures already running continue - without ever firing a "disconnected" event, so neither the launch gate nor
1329
+ * the disconnect handler would catch it. This detector uses a signal that is already happening: a stream-setup failure carrying a capture-infrastructure
1330
+ * signature. The probe is the arbiter, serialized through the capture lock so it can never race a real stream's capture acquisition. A probe that never obtained a
1331
+ * turn is no verdict at all, because a busy lock is evidence about load rather than about the browser. On a confirmed failure the one recovery action runs: mark
1332
+ * the browser, which leaves its running captures alone, refuses new capture starts at acquire(), and leaves the adapter to decide when the relaunch is safe.
1333
+ *
1334
+ * The verdict is a shared promise rather than a task nobody holds, and that is what lets a single caller wait on it. A tune Chrome refused the capture start for
1335
+ * is that caller: it has nothing to serve until the browser's fate is settled, and a relaunched browser is exactly what would serve it. Every other failure hands
1336
+ * its error over and walks on, so no healthy response is ever delayed by a probe. One slot holds the verdict for as long as it is unsettled, which is what makes
1337
+ * a burst cheap: a dozen tunes refused in the same second share one probe, one verdict, and one relaunch.
1257
1338
  */
1258
- // 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.
1259
- let captureReverificationInProgress = false;
1339
+ // The verdict being reached right now, or null when none is. Every caller that arrives while one is unsettled is handed this same promise.
1340
+ let captureVerdictInFlight = null;
1341
+ /**
1342
+ * Classifies a throw from the probe's run on the capture lock into a verdict or the absence of one. A turn-wait timeout means the probe never got to run: with
1343
+ * other streams tuning at the same moment it queued behind their legitimate acquisitions, and a lock that is busy says something about load, not about the browser
1344
+ * - a holder that is genuinely hung marks the browser through its own wedge. Every other value, the outer deadline above all, means the probe held its turn and its
1345
+ * own acquisition did not settle, which is exactly the failure the mark exists for. Pure and total, so the distinction the detector rests on is pinnable without a
1346
+ * browser.
1347
+ * @param error - The value the lock run threw.
1348
+ * @returns The outcome the detector acts on.
1349
+ */
1350
+ export function classifyCaptureProbeFailure(error) {
1351
+ if (error instanceof CaptureTurnTimeoutError) {
1352
+ return { kind: "inconclusive", reason: formatError(error) };
1353
+ }
1354
+ return { kind: "failed", reason: formatError(error) };
1355
+ }
1260
1356
  /**
1261
- * 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
1262
- * 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
1263
- * 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
1264
- * adds a teardown allowance over it as a safety net.
1357
+ * Runs one capture probe as a task on the capture lock, so it cannot race a concurrent capture acquisition (which would draw a spurious "Cannot capture a tab
1358
+ * with an active stream"). The lock holds the turn across the probe's full task - the acquisition plus the stop confirmation inside attemptCaptureProbe - so the
1359
+ * next initialization does not start until the probe's capture is fully released. The mid-life probe self-times its acquisition against `timeout`; the lock's
1360
+ * outer deadline adds a teardown allowance over it as a safety net.
1265
1361
  * @param browser - The Chrome instance to probe.
1266
- * @param timeout - Maximum time in milliseconds to wait for the probe's getStream() to respond (the self-timed criterion).
1267
- * @returns Null when the browser captured successfully, or an error message when it could not (including a wedged-lock timeout).
1362
+ * @param timeout - Maximum time in milliseconds to wait for the probe's acquisition to respond (the self-timed criterion).
1363
+ * @returns Captured when the browser started a capture, failed when it could not, inconclusive when the probe never obtained its turn.
1268
1364
  */
1269
1365
  async function probeCaptureSerialized(browser, timeout) {
1270
1366
  try {
1271
- return await captureLock.run((signal) => attemptCaptureProbe(browser, { boundMs: timeout, kind: "midlife", signal }), {
1367
+ const failure = await captureLock.run((signal) => attemptCaptureProbe(browser, { boundMs: timeout, kind: "midlife",
1368
+ signal }), {
1272
1369
  deadlineMessage: CAPTURE_PROBE_TIMEOUT_MESSAGE,
1273
1370
  deadlineMs: timeout + PROBE_TEARDOWN_ALLOWANCE_MS,
1274
- // 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
1275
- // 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
1276
- // identity-guard no-op.
1371
+ // The probe's wedge is a loud warning only, never a second verdict: by wedge time the detector has already marked this browser from the returned failure
1372
+ // (the outer deadline fires roughly 22s earlier), so a mark raised here would land on an instance that already carries one.
1277
1373
  onWedge: () => {
1278
- LOG.warn("A mid-life capture probe has wedged past the recovery bound; the capture mutex is likely leaked and awaiting a process restart.");
1374
+ LOG.warn("A mid-life capture probe has wedged past the recovery bound; the browser was already marked when the probe's deadline fired, so this wedge " +
1375
+ "is informational.");
1279
1376
  },
1280
1377
  turnWaitMs: CONFIG.streaming.navigationTimeout
1281
1378
  });
1379
+ return (failure === null) ? { kind: "captured" } : { kind: "failed", reason: failure };
1282
1380
  }
1283
1381
  catch (error) {
1284
- // 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
1285
- // lock is itself evidence the browser cannot capture, so surface it as a probe failure the detector routes into invalidateBrowser.
1286
- return formatError(error);
1382
+ return classifyCaptureProbeFailure(error);
1287
1383
  }
1288
1384
  }
1289
1385
  /**
1290
- * Pure decision for whether a mid-life capture-infrastructure failure warrants re-verifying the browser. Re-verification is warranted iff no other re-verification
1291
- * 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
1292
- * demonstrably capturing (so this failure is stream-specific - never invalidate) or those streams will trip their own circuit breakers and drain, after which a
1293
- * 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
1294
- * common real-world shape of this failure.
1295
- * @param inputs - The decision inputs.
1296
- * @param inputs.activeStreamIds - The ids of every stream currently in the registry.
1297
- * @param inputs.failingStreamId - The id of the stream whose setup just failed, excluded from the isolation check.
1298
- * @param inputs.hasBrowser - Whether a browser instance is currently published to probe.
1299
- * @param inputs.reverificationInProgress - Whether a re-verification is already deciding the browser's fate.
1300
- * @returns True when the failure should trigger a re-verification probe.
1386
+ * Classifies an establishment failure and, when it carries a capture-infrastructure signature, hands it to the passive mid-life detector.
1387
+ *
1388
+ * This sits at the acquisition chokepoint rather than in one caller's catch, so every path that establishes a capture gets the detection: a fresh stream's setup,
1389
+ * a tab replacement, and anything added later. Both of createPageWithCapture's own catch blocks call it, which is what extends the coverage across phases - the
1390
+ * acquisition catch sees a refused capture start, the establishment catch sees the playback-initialization timeout the pattern list also names.
1391
+ *
1392
+ * Every logical setup classifies at most once. What holds that rule at the two call sites is where each call sits: each classifies the failure it is about to
1393
+ * rethrow raw, and never one it is about to wrap for a retry. So a failure blamed on a direct watch URL classifies nothing, and the standing attempt that follows
1394
+ * it classifies for both. The alternative would hand the readiness detector two failures from one setup, which is two background probes deciding a single
1395
+ * browser's fate.
1396
+ *
1397
+ * DirectUrlEstablishmentError is excluded here as well, as the backstop that keeps the rule true of a call site added later rather than as a filter the current
1398
+ * two depend on: neither of them can reach this with one, because the type exists only to leave this function, and the path that wraps into it throws before
1399
+ * the classification line is reached.
1400
+ * @param error - The failure the establishment is about to rethrow.
1401
+ * @returns The verdict the detector reached, or null when this failure called for none.
1301
1402
  */
1302
- export function shouldReverifyCapture(inputs) {
1303
- return !inputs.reverificationInProgress && inputs.hasBrowser && inputs.activeStreamIds.every((id) => id === inputs.failingStreamId);
1403
+ async function noteClassifiedCaptureFailure(error) {
1404
+ if ((error instanceof DirectUrlEstablishmentError) || !isCaptureInfrastructureError(error)) {
1405
+ return null;
1406
+ }
1407
+ return await noteCaptureInfrastructureFailure();
1304
1408
  }
1409
+ // The production collaborators, with the probe bound to the criterion every mid-life verdict is judged against.
1410
+ const defaultCaptureVerdictDeps = { getBrowserInstance, getCaptureImpairment, noteBrowserCaptureImpaired,
1411
+ probe: (browser) => probeCaptureSerialized(browser, CAPTURE_PROBE_TIMEOUT_MS) };
1305
1412
  /**
1306
- * Passive mid-life capture-death detection, called from the stream-setup failure path when the failure carries a capture-infrastructure signature. If the failing
1307
- * 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,
1308
- * 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
1309
- * so the failing request's response is not delayed; single-flight so a burst of failures triggers at most one probe.
1310
- * @param failingStreamId - The numeric id of the stream whose setup just failed, excluded from the active-stream guard.
1413
+ * Passive mid-life capture-death detection, called from the stream-setup failure path when the failure carries a capture-infrastructure signature. It re-verifies
1414
+ * the browser's capture capability with a lock-serialized probe and, on a confirmed failure, marks the browser: its running captures continue, new capture starts
1415
+ * are refused at acquire(), and the adapter relaunches it once nothing depends on it. Any refused start is worth a probe - a browser that is demonstrably
1416
+ * capturing for someone else can still be unable to start another - while a browser that already carries a mark needs no second verdict, so the reason on record
1417
+ * is handed back instead.
1418
+ *
1419
+ * The returned promise settles only once everything the verdict set in motion has settled, the relaunch a `failed` outcome triggers included. That is the whole
1420
+ * reason a caller can act on it: a tune that re-acquired the browser the instant the mark landed would meet the retiring instance's drain, where acquire() rejects
1421
+ * rather than joins, while one that waits for this promise meets the fresh browser or a refusal that means what it says. The promise never rejects; the absence of
1422
+ * a verdict is a null rather than a throw.
1423
+ *
1424
+ * It is deliberately not async: every caller in a burst has to hand back the SAME promise object, because what they are waiting on is the browser's one verdict
1425
+ * rather than their own call's. An async wrapper would give each caller a distinct promise that merely adopts it, and the shared identity the single-flight slot
1426
+ * exists to provide would stop being observable at this boundary.
1427
+ * @param deps - The injected collaborators; defaults to the production ones.
1428
+ * @returns The verdict, or null when no browser is published to reach one about.
1311
1429
  */
1312
- function noteCaptureInfrastructureFailure(failingStreamId) {
1313
- // 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
1314
- // before the registry and browser lookups below are even performed.
1315
- if (captureReverificationInProgress) {
1316
- return;
1430
+ export function noteCaptureInfrastructureFailure(deps = defaultCaptureVerdictDeps) {
1431
+ // A verdict already being reached is the verdict every later caller gets. Read first so a burst short-circuits before the browser lookup below is even
1432
+ // performed, and so no second probe can ever spend the capture lock on a question that is already being answered.
1433
+ if (captureVerdictInFlight) {
1434
+ return captureVerdictInFlight;
1317
1435
  }
1318
- // A readiness probe needs a connected browser to exercise. If none is published, a disconnect already handled the readiness loss. The !browser check is
1319
- // combined into this one condition (rather than a separate guard) so TypeScript narrows browser from Nullable<Browser> to Browser for the probe and
1320
- // invalidateBrowser calls below. hasBrowser and reverificationInProgress are passed as the values their guards already guarantee here - true (the !browser
1321
- // guard has passed) and false (the single-flight guard above already returned if it were true).
1322
- const browser = getBrowserInstance();
1323
- if (!browser || !shouldReverifyCapture({ activeStreamIds: getAllStreams().map((entry) => entry.id), failingStreamId, hasBrowser: true,
1324
- reverificationInProgress: false })) {
1325
- return;
1436
+ // A readiness probe needs a connected browser to exercise. If none is published, a disconnect already handled the readiness loss and there is nothing to judge.
1437
+ const browser = deps.getBrowserInstance();
1438
+ if (!browser) {
1439
+ return Promise.resolve(null);
1326
1440
  }
1327
- captureReverificationInProgress = true;
1328
- // 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.
1329
- void (async () => {
1441
+ /* A refused start against a browser that is already marked belongs to a tune that acquired it an instant before the mark landed. The verdict is recorded and
1442
+ * the relaunch is scheduled, so a probe here would spend the capture lock re-establishing what is already known. The recorded reason answers instead, and what
1443
+ * happens next is the caller's own re-acquire to discover from the supervisor.
1444
+ */
1445
+ const impairment = deps.getCaptureImpairment();
1446
+ if (impairment) {
1447
+ return Promise.resolve({ kind: "failed", reason: impairment.reason });
1448
+ }
1449
+ captureVerdictInFlight = (async () => {
1330
1450
  try {
1331
- const failure = await probeCaptureSerialized(browser, CAPTURE_PROBE_TIMEOUT_MS);
1332
- if (failure !== null) {
1333
- // The probe confirmed the browser cannot capture though it is still connected. invalidateBrowser is the single recovery action - relinquish readiness,
1334
- // 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:
1335
- // 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
1336
- // 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
1337
- // settles instead lets the relaunch recover.
1338
- await invalidateBrowser(browser, "a capture probe failed after a stream setup failure with no other active streams");
1451
+ const outcome = await deps.probe(browser);
1452
+ switch (outcome.kind) {
1453
+ case "failed": {
1454
+ /* The probe confirmed the browser cannot start a capture though it is still connected. We pass the exact instance we probed, so a disconnect and
1455
+ * relaunch during the probe leaves the fresh browser unmarked. The mark is awaited rather than fired off, because the relaunch it starts is the thing a
1456
+ * waiting tune is waiting for: settling this promise ahead of that relaunch would hand the tune a browser mid-teardown.
1457
+ */
1458
+ await deps.noteBrowserCaptureImpaired(browser, outcome.reason);
1459
+ break;
1460
+ }
1461
+ case "inconclusive": {
1462
+ LOG.debug("streaming:setup", "Mid-life capture re-verification could not obtain a turn on the capture lock (%s); no verdict.", outcome.reason);
1463
+ break;
1464
+ }
1465
+ case "captured": {
1466
+ // The browser started a capture, so it is healthy and the setup failure that brought us here belonged to the stream rather than to the browser.
1467
+ break;
1468
+ }
1339
1469
  }
1340
- }
1341
- catch (error) {
1342
- LOG.debug("streaming:setup", "Mid-life capture re-verification aborted: %s.", formatError(error));
1470
+ return outcome;
1343
1471
  }
1344
1472
  finally {
1345
- captureReverificationInProgress = false;
1473
+ // The slot empties as this verdict settles, so the next capture-infrastructure failure asks the browser afresh rather than reading an answer about a
1474
+ // moment that has passed.
1475
+ captureVerdictInFlight = null;
1346
1476
  }
1347
1477
  })();
1478
+ return captureVerdictInFlight;
1348
1479
  }
1349
1480
  //# sourceMappingURL=setup.js.map