prismcast 1.10.2 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (693) hide show
  1. package/dist/app.d.ts +6 -3
  2. package/dist/app.d.ts.map +1 -0
  3. package/dist/app.js +140 -86
  4. package/dist/app.js.map +1 -1
  5. package/dist/browser/blockedPage.d.ts +80 -0
  6. package/dist/browser/blockedPage.d.ts.map +1 -0
  7. package/dist/browser/blockedPage.js +209 -0
  8. package/dist/browser/blockedPage.js.map +1 -0
  9. package/dist/browser/browserSupervisor.d.ts +80 -0
  10. package/dist/browser/browserSupervisor.d.ts.map +1 -0
  11. package/dist/browser/browserSupervisor.js +186 -0
  12. package/dist/browser/browserSupervisor.js.map +1 -0
  13. package/dist/browser/cdp.d.ts +1 -0
  14. package/dist/browser/cdp.d.ts.map +1 -0
  15. package/dist/browser/cdp.js +6 -5
  16. package/dist/browser/cdp.js.map +1 -1
  17. package/dist/browser/channelSelection.d.ts +45 -14
  18. package/dist/browser/channelSelection.d.ts.map +1 -0
  19. package/dist/browser/channelSelection.js +81 -18
  20. package/dist/browser/channelSelection.js.map +1 -1
  21. package/dist/browser/consent.d.ts +152 -0
  22. package/dist/browser/consent.d.ts.map +1 -0
  23. package/dist/browser/consent.js +418 -0
  24. package/dist/browser/consent.js.map +1 -0
  25. package/dist/browser/display.d.ts +1 -0
  26. package/dist/browser/display.d.ts.map +1 -0
  27. package/dist/browser/hlsPlaylistObserver.d.ts +55 -0
  28. package/dist/browser/hlsPlaylistObserver.d.ts.map +1 -0
  29. package/dist/browser/hlsPlaylistObserver.js +132 -0
  30. package/dist/browser/hlsPlaylistObserver.js.map +1 -0
  31. package/dist/browser/index.d.ts +67 -40
  32. package/dist/browser/index.d.ts.map +1 -0
  33. package/dist/browser/index.js +508 -300
  34. package/dist/browser/index.js.map +1 -1
  35. package/dist/browser/launchGovernor.d.ts +89 -0
  36. package/dist/browser/launchGovernor.d.ts.map +1 -0
  37. package/dist/browser/launchGovernor.js +96 -0
  38. package/dist/browser/launchGovernor.js.map +1 -0
  39. package/dist/browser/login.d.ts +10 -2
  40. package/dist/browser/login.d.ts.map +1 -0
  41. package/dist/browser/login.js +36 -5
  42. package/dist/browser/login.js.map +1 -1
  43. package/dist/browser/manifestInterceptor.d.ts +102 -49
  44. package/dist/browser/manifestInterceptor.d.ts.map +1 -0
  45. package/dist/browser/manifestInterceptor.js +219 -237
  46. package/dist/browser/manifestInterceptor.js.map +1 -1
  47. package/dist/browser/pageStaleness.d.ts +30 -0
  48. package/dist/browser/pageStaleness.d.ts.map +1 -0
  49. package/dist/browser/pageStaleness.js +76 -0
  50. package/dist/browser/pageStaleness.js.map +1 -0
  51. package/dist/browser/precaching.d.ts +95 -3
  52. package/dist/browser/precaching.d.ts.map +1 -0
  53. package/dist/browser/precaching.js +499 -65
  54. package/dist/browser/precaching.js.map +1 -1
  55. package/dist/browser/tabNetworkObserver.d.ts +49 -0
  56. package/dist/browser/tabNetworkObserver.d.ts.map +1 -0
  57. package/dist/browser/tabNetworkObserver.js +144 -0
  58. package/dist/browser/tabNetworkObserver.js.map +1 -0
  59. package/dist/browser/tuning/cache.d.ts +61 -0
  60. package/dist/browser/tuning/cache.d.ts.map +1 -0
  61. package/dist/browser/tuning/cache.js +62 -0
  62. package/dist/browser/tuning/cache.js.map +1 -0
  63. package/dist/browser/tuning/comcastPolymer.d.ts +3 -2
  64. package/dist/browser/tuning/comcastPolymer.d.ts.map +1 -0
  65. package/dist/browser/tuning/comcastPolymer.js +79 -102
  66. package/dist/browser/tuning/comcastPolymer.js.map +1 -1
  67. package/dist/browser/tuning/cox.d.ts +1 -0
  68. package/dist/browser/tuning/cox.d.ts.map +1 -0
  69. package/dist/browser/tuning/directv.d.ts +1 -0
  70. package/dist/browser/tuning/directv.d.ts.map +1 -0
  71. package/dist/browser/tuning/directv.js +52 -46
  72. package/dist/browser/tuning/directv.js.map +1 -1
  73. package/dist/browser/tuning/fox.d.ts +1 -0
  74. package/dist/browser/tuning/fox.d.ts.map +1 -0
  75. package/dist/browser/tuning/fox.js +13 -12
  76. package/dist/browser/tuning/fox.js.map +1 -1
  77. package/dist/browser/tuning/gridSearch.d.ts +68 -0
  78. package/dist/browser/tuning/gridSearch.d.ts.map +1 -0
  79. package/dist/browser/tuning/gridSearch.js +85 -0
  80. package/dist/browser/tuning/gridSearch.js.map +1 -0
  81. package/dist/browser/tuning/hbo.d.ts +1 -0
  82. package/dist/browser/tuning/hbo.d.ts.map +1 -0
  83. package/dist/browser/tuning/hbo.js +73 -45
  84. package/dist/browser/tuning/hbo.js.map +1 -1
  85. package/dist/browser/tuning/hulu.d.ts +1 -0
  86. package/dist/browser/tuning/hulu.d.ts.map +1 -0
  87. package/dist/browser/tuning/hulu.js +128 -173
  88. package/dist/browser/tuning/hulu.js.map +1 -1
  89. package/dist/browser/tuning/shared.d.ts +75 -5
  90. package/dist/browser/tuning/shared.d.ts.map +1 -0
  91. package/dist/browser/tuning/shared.js +216 -6
  92. package/dist/browser/tuning/shared.js.map +1 -1
  93. package/dist/browser/tuning/sling.d.ts +1 -0
  94. package/dist/browser/tuning/sling.d.ts.map +1 -0
  95. package/dist/browser/tuning/sling.js +105 -126
  96. package/dist/browser/tuning/sling.js.map +1 -1
  97. package/dist/browser/tuning/spectrum.d.ts +1 -0
  98. package/dist/browser/tuning/spectrum.d.ts.map +1 -0
  99. package/dist/browser/tuning/spectrum.js +80 -125
  100. package/dist/browser/tuning/spectrum.js.map +1 -1
  101. package/dist/browser/tuning/thumbnailRow.d.ts +1 -0
  102. package/dist/browser/tuning/thumbnailRow.d.ts.map +1 -0
  103. package/dist/browser/tuning/thumbnailRow.js +8 -2
  104. package/dist/browser/tuning/thumbnailRow.js.map +1 -1
  105. package/dist/browser/tuning/tileClick.d.ts +1 -0
  106. package/dist/browser/tuning/tileClick.d.ts.map +1 -0
  107. package/dist/browser/tuning/tileClick.js +3 -2
  108. package/dist/browser/tuning/tileClick.js.map +1 -1
  109. package/dist/browser/tuning/xfinity.d.ts +1 -0
  110. package/dist/browser/tuning/xfinity.d.ts.map +1 -0
  111. package/dist/browser/tuning/youtubeTv.d.ts +1 -0
  112. package/dist/browser/tuning/youtubeTv.d.ts.map +1 -0
  113. package/dist/browser/tuning/youtubeTv.js +74 -127
  114. package/dist/browser/tuning/youtubeTv.js.map +1 -1
  115. package/dist/browser/video.d.ts +54 -14
  116. package/dist/browser/video.d.ts.map +1 -0
  117. package/dist/browser/video.js +296 -138
  118. package/dist/browser/video.js.map +1 -1
  119. package/dist/channels/index.d.ts +1 -0
  120. package/dist/channels/index.d.ts.map +1 -0
  121. package/dist/channels/index.js +12 -9
  122. package/dist/channels/index.js.map +1 -1
  123. package/dist/config/channelForm.d.ts +15 -1
  124. package/dist/config/channelForm.d.ts.map +1 -0
  125. package/dist/config/channelForm.js +28 -12
  126. package/dist/config/channelForm.js.map +1 -1
  127. package/dist/config/consistencyProbe.d.ts +1 -0
  128. package/dist/config/consistencyProbe.d.ts.map +1 -0
  129. package/dist/config/consistencyProbe.js +27 -21
  130. package/dist/config/consistencyProbe.js.map +1 -1
  131. package/dist/config/health.d.ts +41 -8
  132. package/dist/config/health.d.ts.map +1 -0
  133. package/dist/config/health.js +189 -38
  134. package/dist/config/health.js.map +1 -1
  135. package/dist/config/index.d.ts +55 -3
  136. package/dist/config/index.d.ts.map +1 -0
  137. package/dist/config/index.js +320 -69
  138. package/dist/config/index.js.map +1 -1
  139. package/dist/config/paths.d.ts +10 -9
  140. package/dist/config/paths.d.ts.map +1 -0
  141. package/dist/config/paths.js +11 -11
  142. package/dist/config/paths.js.map +1 -1
  143. package/dist/config/persistence.context.d.ts +1 -0
  144. package/dist/config/persistence.context.d.ts.map +1 -0
  145. package/dist/config/persistence.d.ts +13 -10
  146. package/dist/config/persistence.d.ts.map +1 -0
  147. package/dist/config/persistence.js +40 -21
  148. package/dist/config/persistence.js.map +1 -1
  149. package/dist/config/presets.d.ts +1 -0
  150. package/dist/config/presets.d.ts.map +1 -0
  151. package/dist/config/profiles.d.ts +22 -15
  152. package/dist/config/profiles.d.ts.map +1 -0
  153. package/dist/config/profiles.js +81 -47
  154. package/dist/config/profiles.js.map +1 -1
  155. package/dist/config/providerLineups.d.ts +47 -0
  156. package/dist/config/providerLineups.d.ts.map +1 -0
  157. package/dist/config/providerLineups.js +137 -0
  158. package/dist/config/providerLineups.js.map +1 -0
  159. package/dist/config/reactivity.d.ts +84 -0
  160. package/dist/config/reactivity.d.ts.map +1 -0
  161. package/dist/config/reactivity.js +226 -0
  162. package/dist/config/reactivity.js.map +1 -0
  163. package/dist/config/servicePacks.d.ts +18 -2
  164. package/dist/config/servicePacks.d.ts.map +1 -0
  165. package/dist/config/servicePacks.js +53 -11
  166. package/dist/config/servicePacks.js.map +1 -1
  167. package/dist/config/services.d.ts +21 -15
  168. package/dist/config/services.d.ts.map +1 -0
  169. package/dist/config/services.js +35 -24
  170. package/dist/config/services.js.map +1 -1
  171. package/dist/config/sites.d.ts +6 -4
  172. package/dist/config/sites.d.ts.map +1 -0
  173. package/dist/config/sites.js +15 -11
  174. package/dist/config/sites.js.map +1 -1
  175. package/dist/config/userChannels.d.ts +129 -30
  176. package/dist/config/userChannels.d.ts.map +1 -0
  177. package/dist/config/userChannels.js +333 -68
  178. package/dist/config/userChannels.js.map +1 -1
  179. package/dist/config/userConfig.d.ts +14 -9
  180. package/dist/config/userConfig.d.ts.map +1 -0
  181. package/dist/config/userConfig.js +87 -25
  182. package/dist/config/userConfig.js.map +1 -1
  183. package/dist/config/userProfiles.d.ts +10 -6
  184. package/dist/config/userProfiles.d.ts.map +1 -0
  185. package/dist/config/userProfiles.js +64 -39
  186. package/dist/config/userProfiles.js.map +1 -1
  187. package/dist/hdhr/channelMap.d.ts +1 -0
  188. package/dist/hdhr/channelMap.d.ts.map +1 -0
  189. package/dist/hdhr/deviceId.d.ts +1 -0
  190. package/dist/hdhr/deviceId.d.ts.map +1 -0
  191. package/dist/hdhr/discover.d.ts +1 -0
  192. package/dist/hdhr/discover.d.ts.map +1 -0
  193. package/dist/hdhr/discover.js +48 -47
  194. package/dist/hdhr/discover.js.map +1 -1
  195. package/dist/hdhr/getHandlers.d.ts +29 -0
  196. package/dist/hdhr/getHandlers.d.ts.map +1 -0
  197. package/dist/hdhr/getHandlers.js +196 -0
  198. package/dist/hdhr/getHandlers.js.map +1 -0
  199. package/dist/hdhr/identity.d.ts +36 -0
  200. package/dist/hdhr/identity.d.ts.map +1 -0
  201. package/dist/hdhr/identity.js +51 -0
  202. package/dist/hdhr/identity.js.map +1 -0
  203. package/dist/hdhr/index.d.ts +22 -4
  204. package/dist/hdhr/index.d.ts.map +1 -0
  205. package/dist/hdhr/index.js +229 -54
  206. package/dist/hdhr/index.js.map +1 -1
  207. package/dist/hdhr/protocol.d.ts +75 -0
  208. package/dist/hdhr/protocol.d.ts.map +1 -0
  209. package/dist/hdhr/protocol.js +264 -0
  210. package/dist/hdhr/protocol.js.map +1 -0
  211. package/dist/hdhr/tunerState.d.ts +24 -0
  212. package/dist/hdhr/tunerState.d.ts.map +1 -0
  213. package/dist/hdhr/tunerState.js +67 -0
  214. package/dist/hdhr/tunerState.js.map +1 -0
  215. package/dist/hdhr/udp.d.ts +43 -0
  216. package/dist/hdhr/udp.d.ts.map +1 -0
  217. package/dist/hdhr/udp.js +277 -0
  218. package/dist/hdhr/udp.js.map +1 -0
  219. package/dist/identity.d.ts +1 -0
  220. package/dist/identity.d.ts.map +1 -0
  221. package/dist/identity.js +13 -11
  222. package/dist/identity.js.map +1 -1
  223. package/dist/index.d.ts +1 -0
  224. package/dist/index.d.ts.map +1 -0
  225. package/dist/index.js +11 -12
  226. package/dist/index.js.map +1 -1
  227. package/dist/native/codecInference.d.ts +1 -0
  228. package/dist/native/codecInference.d.ts.map +1 -0
  229. package/dist/native/codecInference.js +50 -5
  230. package/dist/native/codecInference.js.map +1 -1
  231. package/dist/native/decrypt.d.ts +6 -2
  232. package/dist/native/decrypt.d.ts.map +1 -0
  233. package/dist/native/decrypt.js +15 -8
  234. package/dist/native/decrypt.js.map +1 -1
  235. package/dist/native/index.d.ts +40 -17
  236. package/dist/native/index.d.ts.map +1 -0
  237. package/dist/native/index.js +281 -120
  238. package/dist/native/index.js.map +1 -1
  239. package/dist/native/probe.d.ts +114 -18
  240. package/dist/native/probe.d.ts.map +1 -0
  241. package/dist/native/probe.js +452 -115
  242. package/dist/native/probe.js.map +1 -1
  243. package/dist/native/proxy.d.ts +57 -5
  244. package/dist/native/proxy.d.ts.map +1 -0
  245. package/dist/native/proxy.js +604 -170
  246. package/dist/native/proxy.js.map +1 -1
  247. package/dist/native/tokenExpiry.d.ts +1 -0
  248. package/dist/native/tokenExpiry.d.ts.map +1 -0
  249. package/dist/native/tokenExpiry.js +1 -1
  250. package/dist/routes/assets.d.ts +1 -0
  251. package/dist/routes/assets.d.ts.map +1 -0
  252. package/dist/routes/assets.js +4 -2
  253. package/dist/routes/assets.js.map +1 -1
  254. package/dist/routes/auth.d.ts +1 -0
  255. package/dist/routes/auth.d.ts.map +1 -0
  256. package/dist/routes/auth.js +0 -1
  257. package/dist/routes/auth.js.map +1 -1
  258. package/dist/routes/cdp.d.ts +5 -5
  259. package/dist/routes/cdp.d.ts.map +1 -0
  260. package/dist/routes/cdp.js +59 -20
  261. package/dist/routes/cdp.js.map +1 -1
  262. package/dist/routes/channels.d.ts +1 -0
  263. package/dist/routes/channels.d.ts.map +1 -0
  264. package/dist/routes/channels.js +2 -1
  265. package/dist/routes/channels.js.map +1 -1
  266. package/dist/routes/clientActions.d.ts +98 -0
  267. package/dist/routes/clientActions.d.ts.map +1 -0
  268. package/dist/routes/clientActions.js +113 -0
  269. package/dist/routes/clientActions.js.map +1 -0
  270. package/dist/routes/components.d.ts +5 -3
  271. package/dist/routes/components.d.ts.map +1 -0
  272. package/dist/routes/components.js +36 -39
  273. package/dist/routes/components.js.map +1 -1
  274. package/dist/routes/config/channels/endpoints/browse.d.ts +1 -0
  275. package/dist/routes/config/channels/endpoints/browse.d.ts.map +1 -0
  276. package/dist/routes/config/channels/endpoints/browse.js +23 -10
  277. package/dist/routes/config/channels/endpoints/browse.js.map +1 -1
  278. package/dist/routes/config/channels/endpoints/bulk.d.ts +1 -0
  279. package/dist/routes/config/channels/endpoints/bulk.d.ts.map +1 -0
  280. package/dist/routes/config/channels/endpoints/bulk.js +5 -3
  281. package/dist/routes/config/channels/endpoints/bulk.js.map +1 -1
  282. package/dist/routes/config/channels/endpoints/crud.d.ts +1 -0
  283. package/dist/routes/config/channels/endpoints/crud.d.ts.map +1 -0
  284. package/dist/routes/config/channels/endpoints/crud.js +85 -93
  285. package/dist/routes/config/channels/endpoints/crud.js.map +1 -1
  286. package/dist/routes/config/channels/endpoints/importExport.d.ts +1 -0
  287. package/dist/routes/config/channels/endpoints/importExport.d.ts.map +1 -0
  288. package/dist/routes/config/channels/endpoints/importExport.js +6 -3
  289. package/dist/routes/config/channels/endpoints/importExport.js.map +1 -1
  290. package/dist/routes/config/channels/endpoints/predefined.d.ts +1 -0
  291. package/dist/routes/config/channels/endpoints/predefined.d.ts.map +1 -0
  292. package/dist/routes/config/channels/endpoints/prefs.d.ts +1 -0
  293. package/dist/routes/config/channels/endpoints/prefs.d.ts.map +1 -0
  294. package/dist/routes/config/channels/endpoints/prefs.js +2 -1
  295. package/dist/routes/config/channels/endpoints/prefs.js.map +1 -1
  296. package/dist/routes/config/channels/endpoints/service.d.ts +2 -1
  297. package/dist/routes/config/channels/endpoints/service.d.ts.map +1 -0
  298. package/dist/routes/config/channels/endpoints/service.js +3 -1
  299. package/dist/routes/config/channels/endpoints/service.js.map +1 -1
  300. package/dist/routes/config/channels/endpoints/tags.d.ts +1 -0
  301. package/dist/routes/config/channels/endpoints/tags.d.ts.map +1 -0
  302. package/dist/routes/config/channels/endpoints/tags.js +13 -7
  303. package/dist/routes/config/channels/endpoints/tags.js.map +1 -1
  304. package/dist/routes/config/channels/healthBridge.d.ts +24 -0
  305. package/dist/routes/config/channels/healthBridge.d.ts.map +1 -0
  306. package/dist/routes/config/channels/healthBridge.js +71 -0
  307. package/dist/routes/config/channels/healthBridge.js.map +1 -0
  308. package/dist/routes/config/channels/http/handler.d.ts +1 -0
  309. package/dist/routes/config/channels/http/handler.d.ts.map +1 -0
  310. package/dist/routes/config/channels/http/playlistHint.d.ts +2 -1
  311. package/dist/routes/config/channels/http/playlistHint.d.ts.map +1 -0
  312. package/dist/routes/config/channels/http/playlistHint.js +16 -2
  313. package/dist/routes/config/channels/http/playlistHint.js.map +1 -1
  314. package/dist/routes/config/channels/http/serviceWarning.d.ts +4 -2
  315. package/dist/routes/config/channels/http/serviceWarning.d.ts.map +1 -0
  316. package/dist/routes/config/channels/http/serviceWarning.js +3 -2
  317. package/dist/routes/config/channels/http/serviceWarning.js.map +1 -1
  318. package/dist/routes/config/channels/index.d.ts +1 -0
  319. package/dist/routes/config/channels/index.d.ts.map +1 -0
  320. package/dist/routes/config/channels/setup.d.ts +1 -0
  321. package/dist/routes/config/channels/setup.d.ts.map +1 -0
  322. package/dist/routes/config/channels/table.d.ts +3 -3
  323. package/dist/routes/config/channels/table.d.ts.map +1 -0
  324. package/dist/routes/config/channels/table.js +148 -107
  325. package/dist/routes/config/channels/table.js.map +1 -1
  326. package/dist/routes/config/http/envelope.d.ts +1 -0
  327. package/dist/routes/config/http/envelope.d.ts.map +1 -0
  328. package/dist/routes/config/index.d.ts +35 -3
  329. package/dist/routes/config/index.d.ts.map +1 -0
  330. package/dist/routes/config/index.js +51 -2
  331. package/dist/routes/config/index.js.map +1 -1
  332. package/dist/routes/config/services.d.ts +5 -4
  333. package/dist/routes/config/services.d.ts.map +1 -0
  334. package/dist/routes/config/services.js +57 -43
  335. package/dist/routes/config/services.js.map +1 -1
  336. package/dist/routes/config/settings.d.ts +1 -0
  337. package/dist/routes/config/settings.d.ts.map +1 -0
  338. package/dist/routes/config/settings.js +98 -49
  339. package/dist/routes/config/settings.js.map +1 -1
  340. package/dist/routes/debug.d.ts +1 -0
  341. package/dist/routes/debug.d.ts.map +1 -0
  342. package/dist/routes/debug.js +277 -139
  343. package/dist/routes/debug.js.map +1 -1
  344. package/dist/routes/health.d.ts +39 -1
  345. package/dist/routes/health.d.ts.map +1 -0
  346. package/dist/routes/health.js +33 -27
  347. package/dist/routes/health.js.map +1 -1
  348. package/dist/routes/hls.d.ts +1 -0
  349. package/dist/routes/hls.d.ts.map +1 -0
  350. package/dist/routes/icons.d.ts +1 -0
  351. package/dist/routes/icons.d.ts.map +1 -0
  352. package/dist/routes/icons.js +1 -1
  353. package/dist/routes/index.d.ts +1 -0
  354. package/dist/routes/index.d.ts.map +1 -0
  355. package/dist/routes/logs.d.ts +1 -0
  356. package/dist/routes/logs.d.ts.map +1 -0
  357. package/dist/routes/logs.js +14 -4
  358. package/dist/routes/logs.js.map +1 -1
  359. package/dist/routes/mpegts.d.ts +1 -0
  360. package/dist/routes/mpegts.d.ts.map +1 -0
  361. package/dist/routes/play.d.ts +1 -0
  362. package/dist/routes/play.d.ts.map +1 -0
  363. package/dist/routes/playlist.d.ts +1 -0
  364. package/dist/routes/playlist.d.ts.map +1 -0
  365. package/dist/routes/playlist.js +7 -2
  366. package/dist/routes/playlist.js.map +1 -1
  367. package/dist/routes/root/content.d.ts +8 -5
  368. package/dist/routes/root/content.d.ts.map +1 -0
  369. package/dist/routes/root/content.js +29 -26
  370. package/dist/routes/root/content.js.map +1 -1
  371. package/dist/routes/root/index.d.ts +2 -1
  372. package/dist/routes/root/index.d.ts.map +1 -0
  373. package/dist/routes/root/index.js +26 -21
  374. package/dist/routes/root/index.js.map +1 -1
  375. package/dist/routes/root/scripts/channels.d.ts +6 -0
  376. package/dist/routes/root/scripts/channels.d.ts.map +1 -0
  377. package/dist/routes/root/scripts/channels.js +197 -118
  378. package/dist/routes/root/scripts/channels.js.map +1 -1
  379. package/dist/routes/root/scripts/clientEscape.d.ts +22 -0
  380. package/dist/routes/root/scripts/clientEscape.d.ts.map +1 -0
  381. package/dist/routes/root/scripts/clientEscape.js +41 -0
  382. package/dist/routes/root/scripts/clientEscape.js.map +1 -0
  383. package/dist/routes/root/scripts/clientUrl.d.ts +23 -0
  384. package/dist/routes/root/scripts/clientUrl.d.ts.map +1 -0
  385. package/dist/routes/root/scripts/clientUrl.js +44 -0
  386. package/dist/routes/root/scripts/clientUrl.js.map +1 -0
  387. package/dist/routes/root/scripts/config.d.ts +6 -0
  388. package/dist/routes/root/scripts/config.d.ts.map +1 -0
  389. package/dist/routes/root/scripts/config.js +110 -24
  390. package/dist/routes/root/scripts/config.js.map +1 -1
  391. package/dist/routes/root/scripts/index.d.ts +1 -0
  392. package/dist/routes/root/scripts/index.d.ts.map +1 -0
  393. package/dist/routes/root/scripts/shared.d.ts +1 -0
  394. package/dist/routes/root/scripts/shared.d.ts.map +1 -0
  395. package/dist/routes/root/scripts/shared.js +112 -26
  396. package/dist/routes/root/scripts/shared.js.map +1 -1
  397. package/dist/routes/root/scripts/status.d.ts +1 -0
  398. package/dist/routes/root/scripts/status.d.ts.map +1 -0
  399. package/dist/routes/root/scripts/status.handlers.d.ts +23 -33
  400. package/dist/routes/root/scripts/status.handlers.d.ts.map +1 -0
  401. package/dist/routes/root/scripts/status.handlers.js +46 -92
  402. package/dist/routes/root/scripts/status.handlers.js.map +1 -1
  403. package/dist/routes/root/scripts/status.js +21 -8
  404. package/dist/routes/root/scripts/status.js.map +1 -1
  405. package/dist/routes/root/styles.d.ts +1 -0
  406. package/dist/routes/root/styles.d.ts.map +1 -0
  407. package/dist/routes/root/styles.js +7 -4
  408. package/dist/routes/root/styles.js.map +1 -1
  409. package/dist/routes/services.d.ts +13 -1
  410. package/dist/routes/services.d.ts.map +1 -0
  411. package/dist/routes/services.js +81 -68
  412. package/dist/routes/services.js.map +1 -1
  413. package/dist/routes/sse.d.ts +1 -0
  414. package/dist/routes/sse.d.ts.map +1 -0
  415. package/dist/routes/sse.js +1 -1
  416. package/dist/routes/sse.js.map +1 -1
  417. package/dist/routes/streams.d.ts +2 -1
  418. package/dist/routes/streams.d.ts.map +1 -0
  419. package/dist/routes/streams.js +6 -4
  420. package/dist/routes/streams.js.map +1 -1
  421. package/dist/routes/theme.d.ts +3 -2
  422. package/dist/routes/theme.d.ts.map +1 -0
  423. package/dist/routes/theme.js +6 -5
  424. package/dist/routes/theme.js.map +1 -1
  425. package/dist/routes/ui.d.ts +3 -1
  426. package/dist/routes/ui.d.ts.map +1 -0
  427. package/dist/routes/ui.js +16 -4
  428. package/dist/routes/ui.js.map +1 -1
  429. package/dist/routes/upgrade.d.ts +1 -0
  430. package/dist/routes/upgrade.d.ts.map +1 -0
  431. package/dist/routes/upgrade.js +6 -2
  432. package/dist/routes/upgrade.js.map +1 -1
  433. package/dist/service/commands.context.d.ts +1 -0
  434. package/dist/service/commands.context.d.ts.map +1 -0
  435. package/dist/service/commands.context.js +4 -3
  436. package/dist/service/commands.context.js.map +1 -1
  437. package/dist/service/commands.d.ts +1 -0
  438. package/dist/service/commands.d.ts.map +1 -0
  439. package/dist/service/commands.js +10 -13
  440. package/dist/service/commands.js.map +1 -1
  441. package/dist/service/generators.context.d.ts +1 -0
  442. package/dist/service/generators.context.d.ts.map +1 -0
  443. package/dist/service/generators.context.js +1 -1
  444. package/dist/service/generators.context.js.map +1 -1
  445. package/dist/service/generators.d.ts +4 -3
  446. package/dist/service/generators.d.ts.map +1 -0
  447. package/dist/service/generators.js +111 -31
  448. package/dist/service/generators.js.map +1 -1
  449. package/dist/service/index.d.ts +1 -0
  450. package/dist/service/index.d.ts.map +1 -0
  451. package/dist/streaming/captureLock.d.ts +44 -0
  452. package/dist/streaming/captureLock.d.ts.map +1 -0
  453. package/dist/streaming/captureLock.js +95 -0
  454. package/dist/streaming/captureLock.js.map +1 -0
  455. package/dist/streaming/captureSession.d.ts +31 -0
  456. package/dist/streaming/captureSession.d.ts.map +1 -0
  457. package/dist/streaming/captureSession.js +54 -0
  458. package/dist/streaming/captureSession.js.map +1 -0
  459. package/dist/streaming/clients.d.ts +1 -0
  460. package/dist/streaming/clients.d.ts.map +1 -0
  461. package/dist/streaming/codec.d.ts +1 -0
  462. package/dist/streaming/codec.d.ts.map +1 -0
  463. package/dist/streaming/fmp4Segmenter.d.ts +29 -2
  464. package/dist/streaming/fmp4Segmenter.d.ts.map +1 -0
  465. package/dist/streaming/fmp4Segmenter.js +79 -26
  466. package/dist/streaming/fmp4Segmenter.js.map +1 -1
  467. package/dist/streaming/hls.d.ts +18 -7
  468. package/dist/streaming/hls.d.ts.map +1 -0
  469. package/dist/streaming/hls.js +220 -120
  470. package/dist/streaming/hls.js.map +1 -1
  471. package/dist/streaming/hlsResume.d.ts +1 -0
  472. package/dist/streaming/hlsResume.d.ts.map +1 -0
  473. package/dist/streaming/hlsResume.js +1 -1
  474. package/dist/streaming/hlsResume.js.map +1 -1
  475. package/dist/streaming/hlsSegments.d.ts +59 -2
  476. package/dist/streaming/hlsSegments.d.ts.map +1 -0
  477. package/dist/streaming/hlsSegments.js +147 -17
  478. package/dist/streaming/hlsSegments.js.map +1 -1
  479. package/dist/streaming/lifecycle.d.ts +4 -1
  480. package/dist/streaming/lifecycle.d.ts.map +1 -0
  481. package/dist/streaming/lifecycle.js +65 -85
  482. package/dist/streaming/lifecycle.js.map +1 -1
  483. package/dist/streaming/monitor.d.ts +6 -4
  484. package/dist/streaming/monitor.d.ts.map +1 -0
  485. package/dist/streaming/monitor.js +343 -145
  486. package/dist/streaming/monitor.js.map +1 -1
  487. package/dist/streaming/mp4Parser.d.ts +5 -4
  488. package/dist/streaming/mp4Parser.d.ts.map +1 -0
  489. package/dist/streaming/mp4Parser.js +36 -13
  490. package/dist/streaming/mp4Parser.js.map +1 -1
  491. package/dist/streaming/mpegts.d.ts +16 -0
  492. package/dist/streaming/mpegts.d.ts.map +1 -0
  493. package/dist/streaming/mpegts.js +61 -17
  494. package/dist/streaming/mpegts.js.map +1 -1
  495. package/dist/streaming/playlistBuilder.d.ts +1 -0
  496. package/dist/streaming/playlistBuilder.d.ts.map +1 -0
  497. package/dist/streaming/preroll.d.ts +32 -5
  498. package/dist/streaming/preroll.d.ts.map +1 -0
  499. package/dist/streaming/preroll.js +70 -32
  500. package/dist/streaming/preroll.js.map +1 -1
  501. package/dist/streaming/pretune.d.ts +26 -1
  502. package/dist/streaming/pretune.d.ts.map +1 -0
  503. package/dist/streaming/pretune.js +22 -24
  504. package/dist/streaming/pretune.js.map +1 -1
  505. package/dist/streaming/pretuneTimers.d.ts +24 -0
  506. package/dist/streaming/pretuneTimers.d.ts.map +1 -0
  507. package/dist/streaming/pretuneTimers.js +56 -0
  508. package/dist/streaming/pretuneTimers.js.map +1 -0
  509. package/dist/streaming/recovery.d.ts +139 -5
  510. package/dist/streaming/recovery.d.ts.map +1 -0
  511. package/dist/streaming/recovery.js +160 -16
  512. package/dist/streaming/recovery.js.map +1 -1
  513. package/dist/streaming/registry.d.ts +27 -9
  514. package/dist/streaming/registry.d.ts.map +1 -0
  515. package/dist/streaming/registry.js +21 -3
  516. package/dist/streaming/registry.js.map +1 -1
  517. package/dist/streaming/setup.d.ts +157 -29
  518. package/dist/streaming/setup.d.ts.map +1 -0
  519. package/dist/streaming/setup.js +1034 -458
  520. package/dist/streaming/setup.js.map +1 -1
  521. package/dist/streaming/showInfo.d.ts +21 -1
  522. package/dist/streaming/showInfo.d.ts.map +1 -0
  523. package/dist/streaming/showInfo.js +31 -16
  524. package/dist/streaming/showInfo.js.map +1 -1
  525. package/dist/streaming/statusEmitter.d.ts +10 -10
  526. package/dist/streaming/statusEmitter.d.ts.map +1 -0
  527. package/dist/streaming/statusEmitter.js +5 -8
  528. package/dist/streaming/statusEmitter.js.map +1 -1
  529. package/dist/types/channels.d.ts +16 -4
  530. package/dist/types/channels.d.ts.map +1 -0
  531. package/dist/types/channels.js +4 -2
  532. package/dist/types/channels.js.map +1 -1
  533. package/dist/types/config.d.ts +16 -5
  534. package/dist/types/config.d.ts.map +1 -0
  535. package/dist/types/index.d.ts +3 -2
  536. package/dist/types/index.d.ts.map +1 -0
  537. package/dist/types/profiles.d.ts +6 -4
  538. package/dist/types/profiles.d.ts.map +1 -0
  539. package/dist/types/selection.d.ts +31 -3
  540. package/dist/types/selection.d.ts.map +1 -0
  541. package/dist/types/selection.js +4 -2
  542. package/dist/types/selection.js.map +1 -1
  543. package/dist/types/shared.d.ts +1 -0
  544. package/dist/types/shared.d.ts.map +1 -0
  545. package/dist/types/streaming.d.ts +8 -0
  546. package/dist/types/streaming.d.ts.map +1 -0
  547. package/dist/upgrade/commands.context.d.ts +1 -0
  548. package/dist/upgrade/commands.context.d.ts.map +1 -0
  549. package/dist/upgrade/commands.context.js +14 -10
  550. package/dist/upgrade/commands.context.js.map +1 -1
  551. package/dist/upgrade/commands.d.ts +8 -15
  552. package/dist/upgrade/commands.d.ts.map +1 -0
  553. package/dist/upgrade/commands.js +27 -28
  554. package/dist/upgrade/commands.js.map +1 -1
  555. package/dist/upgrade/detection.context.d.ts +1 -0
  556. package/dist/upgrade/detection.context.d.ts.map +1 -0
  557. package/dist/upgrade/detection.context.js +4 -1
  558. package/dist/upgrade/detection.context.js.map +1 -1
  559. package/dist/upgrade/detection.d.ts +9 -7
  560. package/dist/upgrade/detection.d.ts.map +1 -0
  561. package/dist/upgrade/detection.js +23 -54
  562. package/dist/upgrade/detection.js.map +1 -1
  563. package/dist/upgrade/index.d.ts +2 -0
  564. package/dist/upgrade/index.d.ts.map +1 -0
  565. package/dist/upgrade/index.js +1 -0
  566. package/dist/upgrade/index.js.map +1 -1
  567. package/dist/upgrade/lifecycle.context.d.ts +20 -0
  568. package/dist/upgrade/lifecycle.context.d.ts.map +1 -0
  569. package/dist/upgrade/lifecycle.context.js +47 -0
  570. package/dist/upgrade/lifecycle.context.js.map +1 -0
  571. package/dist/upgrade/lifecycle.d.ts +72 -0
  572. package/dist/upgrade/lifecycle.d.ts.map +1 -0
  573. package/dist/upgrade/lifecycle.js +175 -0
  574. package/dist/upgrade/lifecycle.js.map +1 -0
  575. package/dist/upgrade/pathHandle.d.ts +30 -0
  576. package/dist/upgrade/pathHandle.d.ts.map +1 -0
  577. package/dist/upgrade/pathHandle.js +102 -0
  578. package/dist/upgrade/pathHandle.js.map +1 -0
  579. package/dist/utils/bootSession.context.d.ts +16 -0
  580. package/dist/utils/bootSession.context.d.ts.map +1 -0
  581. package/dist/utils/bootSession.context.js +60 -0
  582. package/dist/utils/bootSession.context.js.map +1 -0
  583. package/dist/utils/bootSession.d.ts +17 -0
  584. package/dist/utils/bootSession.d.ts.map +1 -0
  585. package/dist/utils/bootSession.js +20 -0
  586. package/dist/utils/bootSession.js.map +1 -0
  587. package/dist/utils/chromeFetch.d.ts +1 -0
  588. package/dist/utils/chromeFetch.d.ts.map +1 -0
  589. package/dist/utils/cliOutput.d.ts +1 -0
  590. package/dist/utils/cliOutput.d.ts.map +1 -0
  591. package/dist/utils/clock.d.ts +5 -4
  592. package/dist/utils/clock.d.ts.map +1 -0
  593. package/dist/utils/clock.js +19 -16
  594. package/dist/utils/clock.js.map +1 -1
  595. package/dist/utils/debugFilter.d.ts +9 -1
  596. package/dist/utils/debugFilter.d.ts.map +1 -0
  597. package/dist/utils/debugFilter.js +60 -33
  598. package/dist/utils/debugFilter.js.map +1 -1
  599. package/dist/utils/delay.d.ts +48 -15
  600. package/dist/utils/delay.d.ts.map +1 -0
  601. package/dist/utils/delay.js +73 -20
  602. package/dist/utils/delay.js.map +1 -1
  603. package/dist/utils/errors.d.ts +32 -0
  604. package/dist/utils/errors.d.ts.map +1 -0
  605. package/dist/utils/errors.js +45 -4
  606. package/dist/utils/errors.js.map +1 -1
  607. package/dist/utils/evaluate.d.ts +1 -0
  608. package/dist/utils/evaluate.d.ts.map +1 -0
  609. package/dist/utils/evaluate.js +29 -23
  610. package/dist/utils/evaluate.js.map +1 -1
  611. package/dist/utils/ffmpeg.context.d.ts +1 -0
  612. package/dist/utils/ffmpeg.context.d.ts.map +1 -0
  613. package/dist/utils/ffmpeg.context.js.map +1 -1
  614. package/dist/utils/ffmpeg.d.ts +29 -7
  615. package/dist/utils/ffmpeg.d.ts.map +1 -0
  616. package/dist/utils/ffmpeg.js +32 -14
  617. package/dist/utils/ffmpeg.js.map +1 -1
  618. package/dist/utils/fileLogger.d.ts +3 -1
  619. package/dist/utils/fileLogger.d.ts.map +1 -0
  620. package/dist/utils/fileLogger.js +64 -29
  621. package/dist/utils/fileLogger.js.map +1 -1
  622. package/dist/utils/format.d.ts +14 -1
  623. package/dist/utils/format.d.ts.map +1 -0
  624. package/dist/utils/format.js +19 -5
  625. package/dist/utils/format.js.map +1 -1
  626. package/dist/utils/index.d.ts +4 -0
  627. package/dist/utils/index.d.ts.map +1 -0
  628. package/dist/utils/index.js +3 -0
  629. package/dist/utils/index.js.map +1 -1
  630. package/dist/utils/logEmitter.d.ts +1 -0
  631. package/dist/utils/logEmitter.d.ts.map +1 -0
  632. package/dist/utils/logger.d.ts +1 -0
  633. package/dist/utils/logger.d.ts.map +1 -0
  634. package/dist/utils/logger.js +10 -6
  635. package/dist/utils/logger.js.map +1 -1
  636. package/dist/utils/m3u.d.ts +3 -2
  637. package/dist/utils/m3u.d.ts.map +1 -0
  638. package/dist/utils/m3u.js +4 -3
  639. package/dist/utils/m3u.js.map +1 -1
  640. package/dist/utils/markup.d.ts +13 -1
  641. package/dist/utils/markup.d.ts.map +1 -0
  642. package/dist/utils/markup.js +28 -4
  643. package/dist/utils/markup.js.map +1 -1
  644. package/dist/utils/memo.d.ts +3 -2
  645. package/dist/utils/memo.d.ts.map +1 -0
  646. package/dist/utils/memo.js +3 -3
  647. package/dist/utils/morganStream.d.ts +1 -0
  648. package/dist/utils/morganStream.d.ts.map +1 -0
  649. package/dist/utils/network.d.ts +1 -0
  650. package/dist/utils/network.d.ts.map +1 -0
  651. package/dist/utils/network.js +2 -2
  652. package/dist/utils/pid.d.ts +3 -25
  653. package/dist/utils/pid.d.ts.map +1 -0
  654. package/dist/utils/pid.js +10 -49
  655. package/dist/utils/pid.js.map +1 -1
  656. package/dist/utils/platform.d.ts +3 -1
  657. package/dist/utils/platform.d.ts.map +1 -0
  658. package/dist/utils/platform.js +2 -1
  659. package/dist/utils/platform.js.map +1 -1
  660. package/dist/utils/processInspector.context.d.ts +8 -0
  661. package/dist/utils/processInspector.context.d.ts.map +1 -0
  662. package/dist/utils/processInspector.context.js +106 -0
  663. package/dist/utils/processInspector.context.js.map +1 -0
  664. package/dist/utils/processInspector.d.ts +55 -0
  665. package/dist/utils/processInspector.d.ts.map +1 -0
  666. package/dist/utils/processInspector.js +127 -0
  667. package/dist/utils/processInspector.js.map +1 -0
  668. package/dist/utils/retry.d.ts +25 -2
  669. package/dist/utils/retry.d.ts.map +1 -0
  670. package/dist/utils/retry.js +33 -6
  671. package/dist/utils/retry.js.map +1 -1
  672. package/dist/utils/runtimeIdentity.context.d.ts +7 -0
  673. package/dist/utils/runtimeIdentity.context.d.ts.map +1 -0
  674. package/dist/utils/runtimeIdentity.context.js +43 -0
  675. package/dist/utils/runtimeIdentity.context.js.map +1 -0
  676. package/dist/utils/runtimeIdentity.d.ts +98 -0
  677. package/dist/utils/runtimeIdentity.d.ts.map +1 -0
  678. package/dist/utils/runtimeIdentity.js +181 -0
  679. package/dist/utils/runtimeIdentity.js.map +1 -0
  680. package/dist/utils/sanitize.d.ts +1 -0
  681. package/dist/utils/sanitize.d.ts.map +1 -0
  682. package/dist/utils/streamContext.d.ts +2 -1
  683. package/dist/utils/streamContext.d.ts.map +1 -0
  684. package/dist/utils/streamContext.js +1 -1
  685. package/dist/utils/timing.d.ts +2 -1
  686. package/dist/utils/timing.d.ts.map +1 -0
  687. package/dist/utils/timing.js +0 -5
  688. package/dist/utils/timing.js.map +1 -1
  689. package/dist/utils/version.d.ts +1 -0
  690. package/dist/utils/version.d.ts.map +1 -0
  691. package/dist/utils/version.js +47 -13
  692. package/dist/utils/version.js.map +1 -1
  693. package/package.json +26 -20
@@ -1,8 +1,9 @@
1
- import { EvaluateTimeoutError, LOG, capitalize, formatError, getAbortSignal, isSessionClosedError, runWithStreamContext, startTimer } from "../utils/index.js";
2
- import { RECOVERY_METHODS, checkCircuitBreaker, createRecoveryMetrics, formatIssueType, formatRecoveryDuration, getIssueCategory, getIssueDescription, getRecoveryMethod, recordRecoveryAttempt, recordRecoverySuccess, resetCircuitBreaker } from "./recovery.js";
1
+ import { EvaluateTimeoutError, LOG, capitalize, formatError, getAbortSignal, isPageDeathError, isSessionClosedError, runWithStreamContext, startTimer } from "../utils/index.js";
2
+ import { RECOVERY_METHODS, checkCircuitBreaker, classifyNativeSegmentHealth, computeNextRecoveryLevel, createRecoveryMetrics, deriveStreamHealth, formatIssueType, formatRecoveryDuration, getIssueCategory, getIssueDescription, getRecoveryMethod, recordRecoveryAttempt, recordRecoverySuccess, resetCircuitBreaker, shouldTriggerRecovery } from "./recovery.js";
3
3
  import { applyVideoStyles, buildVideoSelectorType, checkVideoPresence, enforceVideoVolume, ensurePlayback, findVideoContext, getVideoState, tuneToChannel, validateVideoElement, verifyFullscreen } from "../browser/video.js";
4
4
  import { getLastSegmentHasVideo, getLastSegmentSize, getStream, getStreamMemoryUsage } from "./registry.js";
5
5
  import { CONFIG } from "../config/index.js";
6
+ import { clearNativeInitState } from "./hlsSegments.js";
6
7
  import { clearProbeCache } from "../native/probe.js";
7
8
  import { emitStreamHealthChanged } from "./statusEmitter.js";
8
9
  import { getChannelLogo } from "../config/userChannels.js";
@@ -33,8 +34,9 @@ import { resizeAndMinimizeWindow } from "../browser/cdp.js";
33
34
  * @param streamInfo - Stream metadata for status updates.
34
35
  * @param onCircuitBreak - Callback function called when circuit breaker trips.
35
36
  * @param onTabReplacement - Optional callback for tab replacement recovery. When provided and 3+ consecutive timeouts occur, this is called to replace the hung tab.
36
- * If null/undefined, tab replacement is not available and timeouts will eventually trip the circuit breaker.
37
- * @returns A cleanup function to stop the monitor.
37
+ * If null/undefined, tab replacement is not available; sustained evaluate timeouts are only surfaced via status (lastIssueType) with no
38
+ * automatic recovery escalation.
39
+ * @returns A MonitorHandle exposing the live recovery metrics (getMetrics) and a self-contained dispose that stops the monitor's polling interval.
38
40
  */
39
41
  export function monitorPlaybackHealth(page, context, profile, url, streamId, streamInfo, onCircuitBreak, onTabReplacement) {
40
42
  /* Monitor state. These track the video's behavior over time and control recovery decisions. Mutable variables are organized into typed state objects by subsystem
@@ -63,6 +65,15 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
63
65
  let fullscreenReapplyCount = 0;
64
66
  // Flag indicating the cleanup function was called. When true, the next interval check will clear itself.
65
67
  let intervalCleared = false;
68
+ /* Serialization flag for the tick body. The interval fires on its schedule whether or not the previous tick's async body has settled, so without this a single
69
+ * slow evaluate - bounded only by the evaluate timeout, seven and a half intervals long at the default cadence - stacks concurrent bodies that each mutate the
70
+ * shared counters and can each independently reach a recovery decision. With it, one dispatched tick body is the most that can be in flight.
71
+ *
72
+ * The scope is exactly that: the dispatched body. A native-mode recovery is launched fire-and-forget from inside a tick and outlives the body that started it,
73
+ * and recoveryState.inProgress is the guard covering that window - which is why every native recovery entry point raises that flag synchronously before its
74
+ * first await, and why a future native recovery path must do the same or it runs under neither guard.
75
+ */
76
+ let tickInProgress = false;
66
77
  // The current video context (page or frame). This can change after a page navigation recovery, when we need to find the new video context.
67
78
  let currentContext = context;
68
79
  // Circuit breaker state. Tracks total failures within a time window and trips (terminates the stream) when too many failures occur.
@@ -70,9 +81,20 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
70
81
  // Counter for consecutive "video not found" occurrences. We apply a grace period before triggering recovery to handle momentary context invalidation or readyState
71
82
  // fluctuations. Reset to 0 when video is found.
72
83
  let videoNotFoundCount = 0;
73
- // Counter for consecutive evaluate timeouts. When the browser tab becomes unresponsive, evaluate() calls will timeout instead of returning data. After 3
74
- // consecutive timeouts, we trigger tab replacement recovery (if the callback is provided). Reset to 0 on successful getVideoState().
84
+ /* Counter for consecutive evaluate timeouts. When the browser tab becomes unresponsive, evaluate() calls time out instead of returning data. The counter drives
85
+ * two things: the three-strike threshold that triggers tab replacement recovery (when the callback is provided), and the timeout the next state read is issued
86
+ * with - any non-zero value arms the short confirmation probe below. Reset to 0 on a successful getVideoState() and on every other piece of direct evidence
87
+ * that the tab answers evaluates.
88
+ */
75
89
  let consecutiveTimeouts = 0;
90
+ /* Evaluate bound for a state read issued while a timeout streak is open. One full-length timeout is what detects a hung tab; the reads that follow only have
91
+ * to tell a still-hung tab from a responsive one, and a tab that answers at all answers well inside this. At the default cadence three strikes then accumulate
92
+ * in roughly twenty seconds - one full timeout to detect, two short probes to confirm - rather than three full timeouts.
93
+ *
94
+ * The bound is fixed rather than derived from the operator's monitor interval: how quickly a tab must answer to count as alive is a property of the tab, not of
95
+ * how often we poll it, and the wall-clock to replacement scales with the chosen interval either way.
96
+ */
97
+ const UNRESPONSIVE_PROBE_TIMEOUT = 2000;
76
98
  // Last known video state for status reporting.
77
99
  let lastVideoState = null;
78
100
  // Recovery metrics tracked throughout the stream's lifetime.
@@ -83,15 +105,16 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
83
105
  // Recovery grace periods in milliseconds, indexed by recovery level (L0 = no recovery, L1 = play/unmute, L2 = source reload, L3 = page reload). After a recovery
84
106
  // action we wait this long before checking for new issues to give the action time to take effect. L1 is a quick action. L2 and L3 need more time for
85
107
  // rebuffering or navigation to complete.
86
- const recoveryGracePeriods = [0, 3_000, 10_000, 10_000];
108
+ const recoveryGracePeriods = [0, 3000, 10000, 10000];
87
109
  // Segment stall timeout (10 seconds). After L2/L3 recovery completes, if no new segments are produced within this window, the capture pipeline is considered
88
110
  // dead and we escalate directly to tab replacement. This catches the case where recovery reports success but the MediaRecorder/FFmpeg pipeline has silently died.
89
- const SEGMENT_STALL_TIMEOUT = 10_000;
111
+ const SEGMENT_STALL_TIMEOUT = 10000;
90
112
  // Tiny segment detection thresholds. Used for continuous segment size monitoring to detect dead capture pipelines. When video capture dies but audio continues,
91
- // segments contain only audio data. Audio is transcoded at a controlled bitrate (max 512Kbps), so audio-only segments are at most ~192KB for 3-second segments.
92
- // The 500KB threshold catches both dead captures (18 bytes) and audio-only captures while staying well below the smallest video preset (480p/3Mbps ~ 750KB/segment).
113
+ // segments contain only audio data. Audio is transcoded at a controlled bitrate (max 512Kbps), so audio-only segments are at most ~128KB for 2-second segments (the
114
+ // default hls.segmentDuration). The 500KB threshold catches both dead captures (18 bytes) and audio-only captures while staying well below the smallest video preset
115
+ // (480p/3Mbps ~ 750KB/segment, also a 2-second basis).
93
116
  // The default count trigger (10) requires roughly 20 seconds of consecutive tiny segments before action is taken, balancing responsiveness against false positives.
94
- const TINY_SEGMENT_THRESHOLD = 512_000;
117
+ const TINY_SEGMENT_THRESHOLD = 512000;
95
118
  const TINY_SEGMENT_COUNT_TRIGGER = 10;
96
119
  // Resolve the service-specific tiny segment count threshold once at monitor startup. Services with extended static content (e.g., Xfinity commercial
97
120
  // placeholders) set a higher value to tolerate longer periods of small segments without false positive tab replacements. Dead capture pipelines (segments with
@@ -101,7 +124,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
101
124
  // Segment staleness timeout. When no new segments have been produced for this duration, the capture pipeline is considered dead even though the video element may
102
125
  // appear healthy. This catches the case where Chrome's MediaRecorder silently stops emitting data without raising an error - the input stream stays "open" but no
103
126
  // data events fire. The 20-second threshold is 4x the maximum expected moof delivery interval (5 seconds) to avoid false positives during normal bursty delivery.
104
- const SEGMENT_STALENESS_TIMEOUT = 20_000;
127
+ const SEGMENT_STALENESS_TIMEOUT = 20000;
105
128
  // Resolution degradation detection. When the video element's intrinsic resolution is significantly below the configured viewport, the service's ABR is delivering
106
129
  // low-quality content. The threshold is expressed as a ratio - if either dimension is below this fraction of the viewport, the resolution is considered degraded.
107
130
  // 50% catches clear ABR degradation (768x432 on 1080p = 40%) while allowing legitimate 720p content on 1080p (67% > 50%).
@@ -115,7 +138,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
115
138
  // navigation and video reinitialization to complete before the site enforces its cutoff.
116
139
  const PROACTIVE_RELOAD_MARGIN_MS = 120000;
117
140
  // Timestamp of the most recent full page navigation. Used to calculate elapsed continuous playback for proactive reload when maxContinuousPlayback is configured.
118
- // Initialized to Date.now() because the monitor starts immediately after tuneToChannel() succeeds in stream setup, meaning a page load just completed. Reset
141
+ // Initialized to Date.now() because the monitor starts immediately after stream setup establishes playback, meaning a page load just completed. Reset
119
142
  // after any successful page navigation recovery or tab replacement, but NOT after source reloads (L2) which preserve the page's JavaScript context.
120
143
  let lastPageNavigationTime = Date.now();
121
144
  // Pre-compute the selector type string for video element selection. This is passed to evaluate() calls.
@@ -161,8 +184,8 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
161
184
  lastSegmentAdvanceTime: Date.now(),
162
185
  recoveryAttempts: 0
163
186
  };
164
- // Resolution degradation monitoring. Separate from the recovery escalation (L1-L4) which handles broken playback. Resolution degradation is a quality issue - the
165
- // stream works but at lower-than-expected resolution. Uses its own tracking and two-step escalation: page reload, then tab replacement.
187
+ // Resolution degradation monitoring. Separate from the recovery escalation (L1-L3 plus tab replacement) which handles broken playback. Resolution degradation is a
188
+ // quality issue - the stream works but at lower-than-expected resolution. Uses its own tracking and two-step escalation: page reload, then tab replacement.
166
189
  const resolutionState = {
167
190
  consecutiveDegradedReadings: 0,
168
191
  graceEnd: Date.now() + RESOLUTION_GRACE_PERIOD,
@@ -199,9 +222,8 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
199
222
  LOG.debug("native:monitor", "Native proxy errored for %s. Initiating capture fallback.", storeKey);
200
223
  nativeHealthState.issueType = "proxy error";
201
224
  nativeHealthState.issueTime = now;
202
- void runWithStreamContext(streamContext, async () => {
203
- await executeNativeL3Fallback(entry);
204
- });
225
+ // The caller establishes the stream context for this interval tick, so the fire-and-forget recovery promise inherits it across its async continuations.
226
+ void executeNativeL3Fallback(entry);
205
227
  return;
206
228
  }
207
229
  // Check if new segments have been produced since the last tick.
@@ -218,46 +240,45 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
218
240
  }
219
241
  // Calculate staleness: time since the last new segment was produced.
220
242
  const stalenessMs = now - nativeHealthState.lastSegmentAdvanceTime;
221
- const stalenessThreshold = targetDuration * 2 * 1000;
222
- // Classify health based on segment delivery metrics.
223
- let nativeHealth = "healthy";
224
- if (consecutiveErrors > 0) {
225
- nativeHealth = "recovering";
226
- if (!nativeHealthState.issueType) {
227
- nativeHealthState.issueType = "fetch errors";
228
- nativeHealthState.issueTime = now;
229
- }
243
+ // Classify health and the warranted escalation via the pure classifyNativeSegmentHealth core in recovery.ts. The shell below owns the side effects the core
244
+ // leaves out: issue-timestamp recording, diagnostic logging, and firing the returned recovery action.
245
+ const decision = classifyNativeSegmentHealth({
246
+ consecutiveErrors,
247
+ lastSegmentTime,
248
+ recoveryAttempts: nativeHealthState.recoveryAttempts,
249
+ stalenessMs,
250
+ targetDurationMs: targetDuration * 1000
251
+ });
252
+ // Record the issue label the decision named when none is already tracked, so an earlier issue keeps its original timestamp across ticks.
253
+ if (decision.issueType && !nativeHealthState.issueType) {
254
+ nativeHealthState.issueType = decision.issueType;
255
+ nativeHealthState.issueTime = now;
256
+ }
257
+ const staleSec = Math.round(stalenessMs / 1000);
258
+ // Diagnostic logging mirrors the classification. The staleness threshold reported is 2x the target duration in seconds.
259
+ if (decision.health === "recovering") {
230
260
  LOG.debug("native:monitor", "Native stream recovering for %s. Consecutive errors: %s.", storeKey, consecutiveErrors);
231
261
  }
232
- else if ((stalenessMs > stalenessThreshold) && (lastSegmentTime > 0)) {
233
- // Only flag staleness after at least one segment has been produced (lastSegmentTime > 0) and the staleness exceeds the threshold.
234
- nativeHealth = "stalled";
235
- if (!nativeHealthState.issueType) {
236
- nativeHealthState.issueType = "segment stall";
237
- nativeHealthState.issueTime = now;
238
- }
239
- const staleSec = Math.round(stalenessMs / 1000);
240
- LOG.debug("native:monitor", "Native stream stalled for %s. No new segments in %ss (threshold: %ss).", storeKey, staleSec, Math.round(stalenessThreshold / 1000));
241
- // L2: At 4x target duration, attempt a page reload for fresh tokens. This recovers from auth expiry where the manifest URL is still valid but segment URLs
242
- // are rejected. The proxy continues serving cached segments during the reload.
243
- if ((stalenessMs > (targetDuration * 4 * 1000)) && (nativeHealthState.recoveryAttempts === 0)) {
244
- LOG.warn("Native stream stalled for %s. No new segments in %ss. Attempting recovery.", storeKey, staleSec);
245
- nativeHealthState.recoveryAttempts++;
246
- void runWithStreamContext(streamContext, async () => {
247
- await executeNativeL2Recovery(entry);
248
- });
249
- return;
250
- }
251
- // L3: At 6x target duration (or if L2 was already attempted), fall back to capture mode via tab replacement.
252
- if ((stalenessMs > (targetDuration * 6 * 1000)) || ((stalenessMs > (targetDuration * 4 * 1000)) && (nativeHealthState.recoveryAttempts > 0))) {
253
- LOG.warn("Falling back to capture mode for %s: native streaming stalled after recovery attempt.", storeKey);
254
- void runWithStreamContext(streamContext, async () => {
255
- await executeNativeL3Fallback(entry);
256
- });
257
- return;
258
- }
262
+ else if (decision.health === "stalled") {
263
+ LOG.debug("native:monitor", "Native stream stalled for %s. No new segments in %ss (threshold: %ss).", storeKey, staleSec, Math.round(targetDuration * 2));
259
264
  }
260
- emitNativeStatus(entry, nativeHealth);
265
+ // L2: at 4x target duration on the first attempt, reload the page for fresh tokens - recovering from auth expiry where the manifest URL is still valid but
266
+ // segment URLs are rejected. The proxy continues serving cached segments during the reload.
267
+ if (decision.action === "l2") {
268
+ LOG.warn("Native stream stalled for %s. No new segments in %ss. Attempting recovery.", storeKey, staleSec);
269
+ nativeHealthState.recoveryAttempts++;
270
+ // The caller establishes the stream context for this interval tick, so the fire-and-forget recovery promise inherits it across its async continuations.
271
+ void executeNativeL2Recovery(entry);
272
+ return;
273
+ }
274
+ // L3: at 4x target duration once an L2 attempt has already been made and the stall persists, fall back to capture mode via tab replacement.
275
+ if (decision.action === "l3") {
276
+ LOG.warn("Falling back to capture mode for %s: native streaming stalled after recovery attempt.", storeKey);
277
+ // The caller establishes the stream context for this interval tick, so the fire-and-forget recovery promise inherits it across its async continuations.
278
+ void executeNativeL3Fallback(entry);
279
+ return;
280
+ }
281
+ emitNativeStatus(entry, decision.health);
261
282
  }
262
283
  /**
263
284
  * Emits a status update with native-specific health classification. Populates meaningful fields (health, issue tracking, memory, clients) and zeroes video-specific
@@ -274,6 +295,9 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
274
295
  const memoryBytes = getStreamMemoryUsage(entry).total;
275
296
  const channelKey = entry.info.storeKey;
276
297
  const clientSummary = getClientSummary(streamInfo.numericStreamId);
298
+ // Native streams have no recovery ladder, so we reuse escalationLevel purely as a UI severity encoding rather than as a recovery-level index: healthy maps to 0,
299
+ // stalled to 1, and recovering to 2. The intended ordering is stalled < recovering because an actively-recovering stream warrants a stronger visual signal than one
300
+ // that has merely stalled.
277
301
  const escalation = (health === "stalled") ? 1 : ((health === "recovering") ? 2 : 0);
278
302
  const status = {
279
303
  bufferingDuration: null,
@@ -317,13 +341,40 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
317
341
  if (!proxy || proxy.isStopped()) {
318
342
  return;
319
343
  }
344
+ /* The refresh probes under the identity the tune established, which the entry holds. It is null only for a pending entry whose setup has not filled it in,
345
+ * and a stream with a running native proxy is past that point - so this answers a window that recovery does not reach rather than papering over one.
346
+ * Declining is the honest response either way: an identity assembled here would stamp a binding this frame cannot see, and the stall escalates to L3.
347
+ */
348
+ const probeIdentity = entry.probeIdentity;
349
+ if (!probeIdentity) {
350
+ LOG.debug("native:monitor", "Skipping L2 recovery for %s: the stream has no probe-cache identity yet.", entry.info.storeKey);
351
+ return;
352
+ }
353
+ /* The refresh's page-reload strategy re-establishes the channel through the capability the stream's setup built. A pending entry that never completed
354
+ * setup has none, and a recovery frame cannot assemble one - the profile closure belongs to the setup path - so the honest response is the same
355
+ * decline the missing probe identity takes: skip L2 and let the stall escalate on the existing ladder.
356
+ */
357
+ const reestablishManifest = entry.reestablishManifest;
358
+ if (!reestablishManifest) {
359
+ LOG.debug("native:monitor", "Skipping L2 recovery for %s: the stream has no re-establishment capability.", entry.info.storeKey);
360
+ return;
361
+ }
320
362
  recoveryState.inProgress = true;
321
363
  LOG.debug("native:monitor", "Starting L2 recovery (page reload) for %s.", entry.info.storeKey);
322
364
  try {
323
365
  const success = await refreshNativeManifest({
324
366
  channelName: entry.info.storeKey,
367
+ onFeedApplied: (metadata) => {
368
+ // The registry write belongs to this layer, not to the native one: recovery already holds the entry, so a refresh that binds a different rung of the
369
+ // service's ladder is recorded here as the stream's current quality.
370
+ entry.captureCodec = metadata.codec;
371
+ entry.nativeBandwidth = metadata.bandwidth;
372
+ entry.nativeResolution = metadata.resolution;
373
+ },
325
374
  page: currentPage,
375
+ probeIdentity,
326
376
  proxy,
377
+ reestablishManifest,
327
378
  streamIdStr: streamId,
328
379
  url
329
380
  });
@@ -358,11 +409,18 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
358
409
  entry.nativeProxy = null;
359
410
  }
360
411
  // Use the existing tab replacement infrastructure. It sets recoveryState.inProgress = true internally and clears it in finalizeTabReplacement. It creates a new
361
- // page with capture, navigates, sets up playback, creates a segmenter, and updates the registry entry (page, rawCaptureStream, ffmpegProcess, segmenter).
412
+ // page with capture, navigates, sets up playback, creates a segmenter, and updates the registry entry (the page and the new capture session, with the segmenter
413
+ // attached to it).
362
414
  const outcome = await executeTabReplacement("native fallback to capture");
415
+ // The monitor stopped while tab replacement was in flight (the stream terminated). Nothing was applied and there is nothing to switch to, so return without
416
+ // mutating the entry or clearing the probe cache. This branch is explicit because the chain below ends in a catch-all else that would otherwise log a false
417
+ // "fallback failed" for a stop that is not a failure.
418
+ if (outcome.outcome === "stopped") {
419
+ return;
420
+ }
363
421
  if (outcome.outcome === "success") {
364
- // Tab replacement succeeded. Update the registry to reflect capture mode. The tab replacement handler already set page, rawCaptureStream, ffmpegProcess,
365
- // and segmenter on the registry entry. We just need to update the streaming mode and clear audio state.
422
+ // Tab replacement succeeded. Update the registry to reflect capture mode. The tab replacement handler already set the page and the new capture session (with
423
+ // its segmenter attached) on the registry entry. We just need to update the streaming mode and clear audio state.
366
424
  entry.streamingMode = "capture";
367
425
  // Clear separate audio state from the native proxy. Without this, hasAudio remains true and the HLS handler continues serving the master playlist (referencing
368
426
  // video.m3u8 and audio.m3u8) instead of the capture segmenter's variant playlist. Clients that cached the master playlist structure would request stale audio
@@ -372,6 +430,10 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
372
430
  entry.hls.audioSegments.clear();
373
431
  entry.hls.audioSegmentBytes = 0;
374
432
  entry.hls.videoPlaylist = "";
433
+ // Release the relay's initialization segments. They belong to a source this stream no longer consumes, and the memory report reads their byte counter with
434
+ // no mode gate, so state left here would be counted for the rest of the stream's life. The nativeContainer label needs no matching reset: every consumer
435
+ // reads it behind the streaming-mode check the line above just flipped, exactly as the sibling native quality fields work.
436
+ clearNativeInitState(entry.id);
375
437
  // Clear the probe cache so subsequent tunes to this channel don't re-attempt native streaming.
376
438
  clearProbeCache(entry.info.storeKey);
377
439
  LOG.info("Switched to capture mode for %s: native streaming failed.", entry.info.storeKey);
@@ -390,42 +452,28 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
390
452
  // Helper to mark a discontinuity in the HLS playlist after recovery events that disrupt the video source. The segmenter flushes its current fragment buffer and sets
391
453
  // a pending discontinuity flag so the next segment boundary includes an #EXT-X-DISCONTINUITY tag. This tells HLS clients to flush their decoder state.
392
454
  const markStreamDiscontinuity = () => {
393
- getStream(streamInfo.numericStreamId)?.segmenter?.markDiscontinuity();
455
+ getStream(streamInfo.numericStreamId)?.captureSession?.segmenter?.markDiscontinuity();
394
456
  };
395
457
  /**
396
458
  * Computes the health status classification based on current monitor state.
397
459
  * @returns The health status classification.
398
460
  */
399
461
  function computeHealthStatus() {
400
- // Error state takes precedence.
401
- if (lastVideoState?.error) {
402
- return "error";
403
- }
404
- // If we're at escalation level 3 (page reload), we're in serious trouble.
405
- if (recoveryState.escalationLevel >= 3) {
406
- return "error";
407
- }
408
- // If we're actively recovering (levels 1-2).
409
- if (recoveryState.escalationLevel > 0) {
410
- return "recovering";
411
- }
412
- // If we're buffering (within grace period).
413
- if (bufferingStartTime !== null) {
414
- return "buffering";
415
- }
416
- // If we have consecutive stalls but not yet triggering recovery.
417
- if (recoveryState.stallCount > 0) {
418
- return "stalled";
419
- }
420
- return "healthy";
462
+ // The health precedence ladder is the pure deriveStreamHealth core in recovery.ts; the monitor only supplies its current recovery and playback state.
463
+ return deriveStreamHealth({
464
+ escalationLevel: recoveryState.escalationLevel,
465
+ hasError: lastVideoState?.error ?? false,
466
+ isBuffering: bufferingStartTime !== null,
467
+ stallCount: recoveryState.stallCount
468
+ });
421
469
  }
422
470
  /**
423
471
  * Emits a status update for this stream.
424
472
  */
425
473
  function emitStatusUpdate() {
426
- // Do not emit after the monitor has been stopped. An in-flight async tick can resume from an await after terminateStream() has called stopMonitor() and
427
- // emitStreamRemoved(). Without this guard, the emitStreamHealthChanged() call below would re-add the dead stream to the streamStatuses Map, creating a zombie
428
- // entry that persists in SSE snapshots indefinitely.
474
+ // Do not emit after the monitor has been stopped. An in-flight async tick can resume from an await after terminateStream() has disposed the monitor handle
475
+ // (clearing this interval) and called emitStreamRemoved(). Without this guard, the emitStreamHealthChanged() call below would re-add the dead stream to the
476
+ // streamStatuses Map, creating a zombie entry that persists in SSE snapshots indefinitely.
429
477
  if (intervalCleared) {
430
478
  return;
431
479
  }
@@ -486,7 +534,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
486
534
  segmentState.productionStalled = false;
487
535
  segmentState.consecutiveTinySegments = 0;
488
536
  segmentState.wasInTinyState = false;
489
- segmentState.lastCheckedIndex = getStream(streamInfo.numericStreamId)?.segmenter?.getSegmentIndex() ?? 0;
537
+ segmentState.lastCheckedIndex = getStream(streamInfo.numericStreamId)?.captureSession?.segmenter?.getSegmentIndex() ?? 0;
490
538
  segmentState.lastSegmentAdvanceTime = Date.now();
491
539
  }
492
540
  /**
@@ -538,16 +586,49 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
538
586
  pendingReMinimize = true;
539
587
  recoveryState.graceUntil = Date.now() + (recoveryGracePeriods[level] ?? 0);
540
588
  }
589
+ /**
590
+ * Stops the monitoring interval. Pairs the two operations that must always happen together: setting intervalCleared so any in-flight async tick short-circuits at
591
+ * its next stop check, and clearing the interval so no further ticks fire. Every path that stops the monitor - the tick's own guards, the circuit-breaker
592
+ * terminations, and dispose() - routes through here, so a stopped monitor can never look un-stopped to a resuming await, which would go on to act on a stream
593
+ * that has already been terminated.
594
+ */
595
+ function stopMonitoring() {
596
+ intervalCleared = true;
597
+ clearInterval(interval);
598
+ }
599
+ /**
600
+ * Reports whether the monitor stopped while a recovery action was awaiting, and releases the recovery flag when it did. A recovery await can resume after
601
+ * terminateStream has already disposed this monitor; the resumption must then apply nothing at all - no grace window, no discontinuity mark, no context
602
+ * adoption, no counter reset, no navigation-failure tally, no status emission - because each of those would describe a stream that is already gone.
603
+ *
604
+ * Releasing recoveryState.inProgress is this helper's own job for exactly that reason: all five callers raise the flag before their await and lower it on the
605
+ * way out, and the resumption they skip is where that lowering would otherwise have happened. executeTabReplacement is not one of them - its finally already
606
+ * owns the release.
607
+ * @returns True if the monitor stopped and the caller must apply nothing.
608
+ */
609
+ const abandonRecoveryIfStopped = () => {
610
+ if (!intervalCleared) {
611
+ return false;
612
+ }
613
+ recoveryState.inProgress = false;
614
+ return true;
615
+ };
541
616
  /**
542
617
  * Handles tab replacement failure by checking the circuit breaker. If the breaker trips, terminates the stream. Returns the appropriate outcome for the caller.
543
618
  * @param context - Description of the failure for logging.
544
619
  * @returns The tab replacement outcome (failed or terminated).
545
620
  */
546
621
  function handleTabReplacementFailure(context) {
622
+ // Entry stop check, mirroring the tick's intervalCleared-first ordering (see the main interval's check-order note): a settlement that resumed after the monitor
623
+ // stopped must return the stopped outcome and trip nothing, because the breaker accounting, the logs, and the termination would all describe a stream that
624
+ // has already ended.
625
+ if (intervalCleared) {
626
+ return { outcome: "stopped" };
627
+ }
547
628
  const cbResult = checkCircuitBreaker(circuitBreaker, Date.now());
548
629
  if (cbResult.shouldTrip) {
549
630
  LOG.error("Recovery exhausted (%s) - terminating stream.", context);
550
- clearInterval(interval);
631
+ stopMonitoring();
551
632
  onCircuitBreak();
552
633
  return { outcome: "terminated" };
553
634
  }
@@ -580,6 +661,12 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
580
661
  * @returns The tab replacement outcome (failed or terminated).
581
662
  */
582
663
  function handleExhaustedTabReplacement(context) {
664
+ // Entry stop check, mirroring the tick's intervalCleared-first ordering (see the main interval's check-order note): a settlement that resumed after the monitor
665
+ // stopped must return the stopped outcome without clearing metrics or tripping the breaker, because both would be bookkeeping for a stream that has already
666
+ // ended.
667
+ if (intervalCleared) {
668
+ return { outcome: "stopped" };
669
+ }
583
670
  // Clear stale recovery metrics so the deferred-success check does not falsely log "Recovered" from leftover state set by recordRecoveryAttempt.
584
671
  metrics.currentRecoveryStartTime = null;
585
672
  metrics.currentRecoveryMethod = null;
@@ -590,7 +677,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
590
677
  // zombie entry in the registry. Terminate explicitly instead.
591
678
  if ((failureOutcome.outcome === "failed") && currentPage.isClosed()) {
592
679
  LOG.error("Tab replacement failed and the original page is no longer available - terminating stream.");
593
- clearInterval(interval);
680
+ stopMonitoring();
594
681
  onCircuitBreak();
595
682
  return { outcome: "terminated" };
596
683
  }
@@ -601,9 +688,9 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
601
688
  * tab) consistently, including metrics recording, success/failure logging, circuit breaker checks, and state resets.
602
689
  *
603
690
  * On failure, retries onTabReplacement once before giving up. The handler destroys old resources (capture, segmenter, FFmpeg, page) before calling
604
- * createPageWithCapture, so a retry is the only chance to save the stream when the first attempt fails. All handler cleanup steps are idempotent on retry:
605
- * rawCaptureStream.destroyed guard, segmenter stop() checks state.stopped, FFmpeg kill() checks ffmpeg.killed, page close checks !oldPage.isClosed(), and
606
- * unregisterManagedPage is idempotent.
691
+ * createPageWithCapture, so a retry is the only chance to save the stream when the first attempt fails. All handler cleanup steps are safe to call more than
692
+ * once on retry: rawCaptureStream.destroyed guard, segmenter stop() checks state.stopped, FFmpeg kill() checks ffmpeg.killed, page close checks
693
+ * !oldPage.isClosed(), and unregisterManagedPage is a no-op on repeat.
607
694
  * @param issueType - Description of what triggered the replacement (for logging and UI display).
608
695
  * @returns The tab replacement outcome.
609
696
  */
@@ -621,7 +708,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
621
708
  recordRecoveryAttempt(metrics, RECOVERY_METHODS.tabReplacement);
622
709
  try {
623
710
  let result = await onTabReplacement();
624
- // First attempt failed - retry once. See idempotency notes in the JSDoc above.
711
+ // First attempt failed - retry once. See the safe-to-retry notes in the JSDoc above.
625
712
  if (!result) {
626
713
  LOG.debug("recovery:tab", "Tab replacement attempt 1/2 failed. Retrying...");
627
714
  try {
@@ -632,6 +719,12 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
632
719
  }
633
720
  }
634
721
  if (result) {
722
+ // The stream can terminate during the awaited onTabReplacement above; a settlement that resumed after the monitor stopped must not adopt the fresh capture
723
+ // resources or reset recovery state. Report the stop and apply nothing - the fresh page and capture session were already torn down at the source by the
724
+ // hls.ts handler's own post-await re-check, so there is nothing to clean up here.
725
+ if (intervalCleared) {
726
+ return { outcome: "stopped" };
727
+ }
635
728
  applyTabReplacementSuccess(result);
636
729
  return { outcome: "success" };
637
730
  }
@@ -644,6 +737,10 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
644
737
  try {
645
738
  const retryResult = await onTabReplacement();
646
739
  if (retryResult) {
740
+ // Same post-await stop check as the try path: a settlement that resumed after the monitor stopped applies nothing and reports the stop.
741
+ if (intervalCleared) {
742
+ return { outcome: "stopped" };
743
+ }
647
744
  applyTabReplacementSuccess(retryResult);
648
745
  return { outcome: "success" };
649
746
  }
@@ -659,8 +756,8 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
659
756
  }
660
757
  }
661
758
  /**
662
- * Performs page navigation recovery with validation. This is the single recovery function used by both the "video not found" and "escalation level 4" code paths,
663
- * ensuring consistent behavior. The function:
759
+ * Performs page navigation recovery with validation. This is the single recovery function used by both the "video not found" and "escalation level 3" (page
760
+ * navigation) code paths, ensuring consistent behavior. The function:
664
761
  * 1. Calls tuneToChannel to reinitialize playback
665
762
  * 2. Checks for unexpected new tabs
666
763
  * 3. Validates the page URL
@@ -674,8 +771,8 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
674
771
  const browser = currentPage.browser();
675
772
  const pageCountBefore = (await browser.pages()).length;
676
773
  try {
677
- // Use tuneToChannel to reinitialize playback. This is the single source of truth for channel initialization, ensuring recovery uses the exact same sequence
678
- // as initial setup (navigation, channel selection, video detection, click-to-play, playback).
774
+ // Use tuneToChannel to reinitialize playback. It is the tune this recovery path owns, and it runs the phases stream setup ran - navigation, channel
775
+ // selection, video detection, click-to-play, playback - so a recovered stream comes up the way a fresh one did.
679
776
  const { context: newContext } = await tuneToChannel(currentPage, url, profile);
680
777
  // Check for unexpected new tabs created during tuning.
681
778
  const pageCountAfter = (await browser.pages()).length;
@@ -731,6 +828,9 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
731
828
  LOG.debug("recovery:general", "Video is buffering (readyState=%s, elements=%s).", presence.maxReadyState, presence.videoCount);
732
829
  // Reset video not found counter since video actually exists.
733
830
  videoNotFoundCount = 0;
831
+ // The presence check answering is direct evidence the tab runs evaluates, and the timeout streak both drives the tab-replacement threshold and arms the
832
+ // short confirmation probe, so it must not outlive that evidence.
833
+ consecutiveTimeouts = 0;
734
834
  emitStatusUpdate();
735
835
  return;
736
836
  }
@@ -751,6 +851,9 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
751
851
  LOG.info("Video found in different context after re-search. readyState=%s.", validationState.readyState);
752
852
  currentContext = newContext;
753
853
  videoNotFoundCount = 0;
854
+ // A validated video means the tab answered the re-search evaluates, and the timeout streak both drives the tab-replacement threshold and arms the
855
+ // short confirmation probe, so it must not outlive that evidence.
856
+ consecutiveTimeouts = 0;
754
857
  emitStatusUpdate();
755
858
  return;
756
859
  }
@@ -764,11 +867,17 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
764
867
  }
765
868
  // After 3+ consecutive failures, escalate to full page navigation recovery.
766
869
  LOG.warn("Video element not found - recovering via %s.", RECOVERY_METHODS.pageNavigation);
870
+ // Post-await stop check guarding both breaker branches below. The only await on the path to here is checkVideoPresence above; a tick that resumed after the
871
+ // monitor stopped must neither trip the breaker nor navigate, because the stream both would act on has already been terminated. No await separates the two
872
+ // breaker branches, so this single check makes both inert.
873
+ if (intervalCleared) {
874
+ return;
875
+ }
767
876
  // Check circuit breaker for too many failures.
768
877
  const cbResult = checkCircuitBreaker(circuitBreaker, now);
769
878
  if (cbResult.shouldTrip) {
770
879
  LOG.error("Recovery failed after %s attempts - terminating stream.", cbResult.totalCount);
771
- clearInterval(interval);
880
+ stopMonitoring();
772
881
  onCircuitBreak();
773
882
  return;
774
883
  }
@@ -783,15 +892,21 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
783
892
  // Check page reload limit before attempting recovery.
784
893
  if (!isPageReloadAllowed()) {
785
894
  LOG.error("Page navigation rate limit reached (%s in %s minutes) - cannot recover without video element.", CONFIG.playback.maxPageReloads, Math.round(CONFIG.playback.pageReloadWindow / 60000));
786
- clearInterval(interval);
895
+ stopMonitoring();
787
896
  onCircuitBreak();
788
897
  return;
789
898
  }
790
899
  const recoveryResult = await performPageNavigationRecovery();
900
+ // The stream can terminate while the navigation above runs. A resumption that lands after the monitor stopped applies nothing - the discontinuity mark, the
901
+ // grace window, the context adoption, and the failure tally below would all describe a stream that was torn down at the source.
902
+ if (abandonRecoveryIfStopped()) {
903
+ return;
904
+ }
791
905
  // Page navigation disrupted the video stream. Mark a discontinuity regardless of navigation success so HLS clients resynchronize their decoders.
792
906
  markStreamDiscontinuity();
793
- // Set grace period to give page navigation time to take effect (L3 = 10 seconds).
794
- recoveryState.graceUntil = now + recoveryGracePeriods[3];
907
+ // Open the grace window that gives page navigation time to take effect (L3 = 10 seconds). The helper reads the clock here, so the window is measured from
908
+ // the completed recovery action rather than from the tick that began it.
909
+ setRecoveryGracePeriod(3);
795
910
  if (recoveryResult.success && recoveryResult.newContext) {
796
911
  currentContext = recoveryResult.newContext;
797
912
  const duration = formatRecoveryDuration(metrics.currentRecoveryStartTime ?? now);
@@ -830,7 +945,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
830
945
  segmentState.waitStartTime ??= now;
831
946
  // Check if segments are flowing by comparing current index to pre-recovery index.
832
947
  const entry = getStream(streamInfo.numericStreamId);
833
- const currentIndex = entry?.segmenter?.getSegmentIndex() ?? null;
948
+ const currentIndex = entry?.captureSession?.segmenter?.getSegmentIndex() ?? null;
834
949
  if ((currentIndex !== null) && (currentIndex > segmentState.preRecoveryIndex)) {
835
950
  // Segments are flowing - recovery actually succeeded. Clear tracking state.
836
951
  segmentState.preRecoveryIndex = null;
@@ -845,7 +960,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
845
960
  }
846
961
  // Continuous segment size monitoring. Runs on every healthy interval to detect spontaneous capture pipeline death.
847
962
  const sizeCheckEntry = getStream(streamInfo.numericStreamId);
848
- const currentSegmentIndex = sizeCheckEntry?.segmenter?.getSegmentIndex() ?? 0;
963
+ const currentSegmentIndex = sizeCheckEntry?.captureSession?.segmenter?.getSegmentIndex() ?? 0;
849
964
  if ((currentSegmentIndex > segmentState.lastCheckedIndex) && sizeCheckEntry) {
850
965
  // A new segment was produced. Update the staleness tracker and check its size.
851
966
  segmentState.lastSegmentAdvanceTime = now;
@@ -947,13 +1062,30 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
947
1062
  }
948
1063
  pendingReMinimize = true;
949
1064
  const recoveryResult = await performPageNavigationRecovery();
1065
+ // Same post-await stop check as the other navigation sites: a resumption after the monitor stopped applies neither the grace windows nor the ratchet step
1066
+ // below, since both would be bookkeeping for a stream that is gone.
1067
+ if (abandonRecoveryIfStopped()) {
1068
+ return true;
1069
+ }
950
1070
  markStreamDiscontinuity();
1071
+ // Arm the shared recovery grace window unconditionally, mirroring the proactive-reload sibling. This is the one navigation path that would otherwise leave it
1072
+ // unarmed, letting the post-reload settle-in feed the general stall ladder and fire a colliding second recovery a couple of ticks later. Unconditional because
1073
+ // even a failed reload has a settle window that must not read as a fresh stall. This is the shared recoveryState.graceUntil, distinct from the
1074
+ // resolutionState.graceEnd cooldown set just below.
1075
+ setRecoveryGracePeriod(3);
951
1076
  resolutionState.consecutiveDegradedReadings = 0;
952
1077
  resolutionState.recoveryAttempt = 1;
953
1078
  resolutionState.graceEnd = now + RESOLUTION_GRACE_PERIOD;
954
1079
  if (recoveryResult.success && recoveryResult.newContext) {
955
1080
  currentContext = recoveryResult.newContext;
956
1081
  lastPageNavigationTime = Date.now();
1082
+ // Reset the general-recovery state a successful reload made stale, composed for this site. Deliberately NOT resetResolutionState() (which the proactive-reload
1083
+ // sibling calls): that zeroes the recoveryAttempt ratchet set to 1 just above, collapsing the two-step resolution ladder into an endless step-1 loop. And
1084
+ // deliberately WITH resetEscalationState() (which the sibling omits): the sibling's caller guarantees a zero escalation level, but this path can run mid-cooldown
1085
+ // with a stale general-ladder level, and every ungated navigation sibling clears it.
1086
+ resetRecoveryCounters();
1087
+ resetSegmentMonitoringState();
1088
+ resetEscalationState();
957
1089
  }
958
1090
  else {
959
1091
  LOG.warn("Resolution recovery via page reload unsuccessful.");
@@ -1033,6 +1165,11 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
1033
1165
  return true;
1034
1166
  }
1035
1167
  const recoveryResult = await performPageNavigationRecovery();
1168
+ // Same post-await stop check as the other navigation sites: a resumption after the monitor stopped applies nothing, and the reload it was maintaining is
1169
+ // moot now that the stream is gone.
1170
+ if (abandonRecoveryIfStopped()) {
1171
+ return true;
1172
+ }
1036
1173
  markStreamDiscontinuity();
1037
1174
  setRecoveryGracePeriod(3);
1038
1175
  if (recoveryResult.success && recoveryResult.newContext) {
@@ -1073,30 +1210,29 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
1073
1210
  await executeTabReplacement("capture pipeline stalled");
1074
1211
  return true;
1075
1212
  }
1213
+ // Post-await stop check for the breaker branch below. The tick awaited getVideoState before calling this; a tick that resumed after the monitor stopped must not
1214
+ // trip the breaker, because the accounting, the log, and the termination would all describe a stream that has already ended. Return terminal so the tick exits
1215
+ // without further work.
1216
+ if (intervalCleared) {
1217
+ return true;
1218
+ }
1076
1219
  // Check circuit breaker for too many failures.
1077
1220
  const cbResult = checkCircuitBreaker(circuitBreaker, now);
1078
1221
  if (cbResult.shouldTrip) {
1079
1222
  const elapsedSeconds = circuitBreaker.firstFailureTime ? Math.round((now - circuitBreaker.firstFailureTime) / 1000) : 0;
1080
1223
  LOG.error("Recovery failed after %s attempts in %ss - terminating stream.", cbResult.totalCount, elapsedSeconds);
1081
- clearInterval(interval);
1224
+ stopMonitoring();
1082
1225
  onCircuitBreak();
1083
1226
  return true;
1084
1227
  }
1085
- // Issue-aware escalation. Determine the appropriate level based on the type of issue and whether source reload has already been attempted.
1228
+ // Issue-aware escalation. The escalation ladder is the pure computeNextRecoveryLevel core in recovery.ts; the later L3-to-L2 fallback under a navigation rate
1229
+ // limit stays here in the executor because it is entangled with the async navigation attempt.
1086
1230
  const issueCategory = getIssueCategory(state, !isProgressing, isBuffering);
1087
- let nextLevel;
1088
- if ((issueCategory === "paused") && (recoveryState.escalationLevel === 0)) {
1089
- // Paused issues: try L1 first (play/unmute works ~50% for paused).
1090
- nextLevel = 1;
1091
- }
1092
- else if (!recoveryState.sourceReloadAttempted) {
1093
- // First recovery attempt for buffering/other, or L1 didn't fix paused: try L2 (source reload).
1094
- nextLevel = 2;
1095
- }
1096
- else {
1097
- // Source reload already attempted: go to L3 (page reload).
1098
- nextLevel = 3;
1099
- }
1231
+ const nextLevel = computeNextRecoveryLevel({
1232
+ currentEscalationLevel: recoveryState.escalationLevel,
1233
+ issueCategory,
1234
+ sourceReloadAttempted: recoveryState.sourceReloadAttempted
1235
+ });
1100
1236
  // Note: Keep state updates in sync with the video-not-found recovery path in handleVideoNotFound above.
1101
1237
  recoveryState.escalationLevel = nextLevel;
1102
1238
  recoveryState.lastRecoveryTime = now;
@@ -1120,7 +1256,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
1120
1256
  // For L2/L3 recovery, record the current segment index so we can verify segments are flowing after recovery completes.
1121
1257
  if (recoveryState.escalationLevel >= 2) {
1122
1258
  const entry = getStream(streamInfo.numericStreamId);
1123
- segmentState.preRecoveryIndex = entry?.segmenter?.getSegmentIndex() ?? null;
1259
+ segmentState.preRecoveryIndex = entry?.captureSession?.segmenter?.getSegmentIndex() ?? null;
1124
1260
  segmentState.waitStartTime = null;
1125
1261
  segmentState.productionStalled = false;
1126
1262
  }
@@ -1129,11 +1265,16 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
1129
1265
  // Levels 1-2: In-page recovery via ensurePlayback().
1130
1266
  if (recoveryState.escalationLevel <= 2) {
1131
1267
  await ensurePlayback(currentPage, currentContext, profile, { recoveryLevel: recoveryState.escalationLevel, skipNativeFullscreen: true });
1268
+ // The stream can terminate while ensurePlayback runs. A resumption after the monitor stopped applies none of the settlement below - the source-reload
1269
+ // mark, the discontinuity, the grace window, the resolution reset. Returning true is what makes the caller exit the tick immediately.
1270
+ if (abandonRecoveryIfStopped()) {
1271
+ return true;
1272
+ }
1132
1273
  if (recoveryState.escalationLevel === 2) {
1133
1274
  recoveryState.sourceReloadAttempted = true;
1134
1275
  markStreamDiscontinuity();
1135
1276
  }
1136
- recoveryState.graceUntil = now + (recoveryGracePeriods[recoveryState.escalationLevel] ?? 0);
1277
+ setRecoveryGracePeriod(recoveryState.escalationLevel);
1137
1278
  resetResolutionState();
1138
1279
  }
1139
1280
  else {
@@ -1153,8 +1294,13 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
1153
1294
  }
1154
1295
  else {
1155
1296
  const recoveryResult = await performPageNavigationRecovery();
1297
+ // Same post-await stop check as the other navigation sites. The exit value here is true, not the false this function returns on normal completion:
1298
+ // false means "continue the tick", and a tick running against a terminated stream has nothing left to do.
1299
+ if (abandonRecoveryIfStopped()) {
1300
+ return true;
1301
+ }
1156
1302
  markStreamDiscontinuity();
1157
- recoveryState.graceUntil = now + recoveryGracePeriods[3];
1303
+ setRecoveryGracePeriod(3);
1158
1304
  if (recoveryResult.success && recoveryResult.newContext) {
1159
1305
  currentContext = recoveryResult.newContext;
1160
1306
  const duration = formatRecoveryDuration(metrics.currentRecoveryStartTime ?? now);
@@ -1180,21 +1326,49 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
1180
1326
  recoveryState.inProgress = false;
1181
1327
  return false;
1182
1328
  }
1329
+ /**
1330
+ * Dispatches one tick body and holds the serialization flag for that body's whole lifetime. This is the only writer of tickInProgress: the flag is raised here
1331
+ * before the body starts and released when its promise settles, so no other site can leave it stuck raised and starve the monitor.
1332
+ *
1333
+ * The catch is the backstop for both dispatch branches. The capture branch's body ends in its own escape handler, but the native branch's does not, so a throw
1334
+ * out of the native health check would otherwise surface as an unhandled rejection. Both branches hand back the promise of an async call, which is what
1335
+ * guarantees the settlement handlers attach at all - a body that could throw synchronously would skip them and wedge the flag.
1336
+ * @param body - The tick body to run. Its promise settling is what releases the flag.
1337
+ */
1338
+ const dispatchSerializedTick = (body) => {
1339
+ tickInProgress = true;
1340
+ void body().catch((dispatchError) => {
1341
+ LOG.warn("Monitor tick dispatch failed: %s.", formatError(dispatchError));
1342
+ }).finally(() => {
1343
+ tickInProgress = false;
1344
+ });
1345
+ };
1183
1346
  /* Main monitoring interval. This runs every MONITOR_INTERVAL milliseconds to check video state and trigger recovery when needed.
1184
1347
  *
1185
1348
  * Every early return must call emitStatusUpdate() before returning (except when the stream is terminating, e.g., page closed or circuit breaker tripped). This
1186
1349
  * ensures SSE clients always have current status data (duration, memory, health) even during recovery, buffering, or video search periods. Without this, the
1187
1350
  * streamStatuses map becomes stale and new SSE connections receive outdated snapshots.
1188
1351
  *
1189
- * Check ordering: the recoveryState.inProgress check must come before the currentPage.isClosed() check. During tab replacement, the old page is intentionally
1190
- * closed while the handler creates a new page; if we checked isClosed() first, we would terminate the interval while recovery is still in progress, causing
1191
- * status updates to stop permanently. The required sequence is: (1) intervalCleared for explicit cleanup, (2) recoveryState.inProgress to continue during
1192
- * recovery, (3) isClosed() for unexpected page termination outside of recovery.
1352
+ * Check ordering, in the sequence the callback applies them:
1353
+ *
1354
+ * 1. intervalCleared, for explicit cleanup.
1355
+ * 2. tickInProgress, so a firing that lands while the previous body is still running skips instead of stacking a second body on the same counters.
1356
+ * 3. recoveryState.inProgress. On the capture path this covers the window a recovery await holds open inside a tick body; on the native path it is the only
1357
+ * guard there is, since a native recovery is launched fire-and-forget and outlives the tick that started it.
1358
+ * 4. currentPage.isClosed(), for page termination outside of recovery. It must come after the recovery check: during tab replacement the old page is
1359
+ * deliberately closed while the handler creates a new one, so testing isClosed() first would clear the interval mid-recovery and stop status updates
1360
+ * permanently.
1193
1361
  */
1194
1362
  const interval = setInterval(() => {
1195
1363
  // Stop monitoring if cleanup was requested.
1196
1364
  if (intervalCleared) {
1197
- clearInterval(interval);
1365
+ stopMonitoring();
1366
+ return;
1367
+ }
1368
+ // Skip this firing if the previous tick body has not settled. Emit status so SSE clients keep seeing current duration, memory, and health while a long read
1369
+ // is outstanding.
1370
+ if (tickInProgress) {
1371
+ emitStatusUpdate();
1198
1372
  return;
1199
1373
  }
1200
1374
  // Skip health checks if a recovery operation is in progress. During tab replacement, the old page will be closed but we must keep the interval running until the
@@ -1205,7 +1379,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
1205
1379
  }
1206
1380
  // Stop monitoring if the page was closed outside of recovery. This handles cases like browser disconnect or explicit stream termination.
1207
1381
  if (currentPage.isClosed()) {
1208
- clearInterval(interval);
1382
+ stopMonitoring();
1209
1383
  return;
1210
1384
  }
1211
1385
  // For static capture profiles (e.g., staticPage), there is no video element to monitor. Skip all video health checks and just emit a status update.
@@ -1217,16 +1391,20 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
1217
1391
  // startup because the streaming mode is set after the monitor starts (native streaming is attempted after setupStream returns).
1218
1392
  const nativeEntry = getStream(streamInfo.numericStreamId);
1219
1393
  if (nativeEntry?.streamingMode === "native") {
1220
- checkNativeStreamHealth(nativeEntry);
1394
+ // Re-establish stream context for this interval tick before running the native health check. AsyncLocalStorage context is lost when entering setInterval
1395
+ // callbacks, so without this wrapper the native path's non-debug warnings would emit without the stream-ID prefix. This mirrors the capture-mode branch below.
1396
+ dispatchSerializedTick(() => runWithStreamContext(streamContext, async () => {
1397
+ checkNativeStreamHealth(nativeEntry);
1398
+ }));
1221
1399
  return;
1222
1400
  }
1223
1401
  // Re-establish stream context for this interval tick. AsyncLocalStorage context is lost when entering setInterval callbacks.
1224
- runWithStreamContext(streamContext, async () => {
1402
+ dispatchSerializedTick(() => runWithStreamContext(streamContext, async () => {
1225
1403
  try {
1226
1404
  // Early exit if the stream's abort signal has been triggered. This prevents wasted work when the stream is being terminated.
1227
1405
  const abortSignal = getAbortSignal(streamId);
1228
1406
  if (abortSignal?.aborted) {
1229
- clearInterval(interval);
1407
+ stopMonitoring();
1230
1408
  return;
1231
1409
  }
1232
1410
  // Capture current timestamp for all timing calculations in this check cycle.
@@ -1236,13 +1414,13 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
1236
1414
  let stateInfo = null;
1237
1415
  let contextInvalidated = false;
1238
1416
  try {
1239
- stateInfo = await getVideoState(currentContext, selectorType);
1417
+ // A read issued while a timeout streak is open is a confirmation probe, so it carries the short bound. With no streak open the read takes the evaluate
1418
+ // wrapper's default, which is the bound that detects a hung tab in the first place.
1419
+ stateInfo = await getVideoState(currentContext, selectorType, (consecutiveTimeouts > 0) ? UNRESPONSIVE_PROBE_TIMEOUT : undefined);
1240
1420
  }
1241
1421
  catch (stateError) {
1242
- // Check for execution context destroyed errors, which indicate the frame was detached.
1243
- const errorMessage = formatError(stateError);
1244
- const isContextDestroyed = ["context", "destroyed", "detached", "target closed"].some((term) => errorMessage.toLowerCase().includes(term));
1245
- if (isContextDestroyed) {
1422
+ // Classify through the shared page-death predicate: the world the read ran in is gone, so the video may simply live in a context we no longer hold.
1423
+ if (isPageDeathError(stateError)) {
1246
1424
  LOG.debug("recovery:context", "Video context was invalidated (frame detached). Will re-search for video.");
1247
1425
  contextInvalidated = true;
1248
1426
  }
@@ -1263,6 +1441,9 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
1263
1441
  LOG.info("Video found in new context after detachment. readyState=%s.", validationState.readyState);
1264
1442
  currentContext = newContext;
1265
1443
  videoNotFoundCount = 0;
1444
+ // A validated video means the tab answered the re-search evaluates, and the timeout streak both drives the tab-replacement threshold and arms the
1445
+ // short confirmation probe, so it must not outlive that evidence.
1446
+ consecutiveTimeouts = 0;
1266
1447
  // Emit status so SSE clients stay current even when returning early after context re-search.
1267
1448
  emitStatusUpdate();
1268
1449
  return;
@@ -1381,10 +1562,18 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
1381
1562
  * - Video is stalled for too long (stallCount exceeds threshold and not in buffering grace)
1382
1563
  * - Segment production has stalled after recovery (capture pipeline dead)
1383
1564
  */
1384
- const needsRecovery = !withinRecoveryGrace && (state.error || state.ended ||
1385
- (state.paused && !withinBufferingGrace && (recoveryState.pauseCount > CONFIG.playback.stallCountThreshold)) ||
1386
- (!isProgressing && !withinBufferingGrace && (recoveryState.stallCount > CONFIG.playback.stallCountThreshold)) ||
1387
- segmentState.productionStalled);
1565
+ const needsRecovery = shouldTriggerRecovery({
1566
+ hasEnded: state.ended,
1567
+ hasError: state.error,
1568
+ isPaused: state.paused,
1569
+ isProgressing,
1570
+ pauseCount: recoveryState.pauseCount,
1571
+ productionStalled: segmentState.productionStalled,
1572
+ stallCount: recoveryState.stallCount,
1573
+ stallCountThreshold: CONFIG.playback.stallCountThreshold,
1574
+ withinBufferingGrace: Boolean(withinBufferingGrace),
1575
+ withinRecoveryGrace
1576
+ });
1388
1577
  /* Escalation reset. After sustained healthy playback (SUSTAINED_PLAYBACK_REQUIRED, default 60 seconds), we reset the escalation level and circuit breaker.
1389
1578
  * This allows a stream that recovered to start fresh, rather than immediately escalating to aggressive recovery on the next issue.
1390
1579
  */
@@ -1400,8 +1589,11 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
1400
1589
  // Clear buffering state. The bufferingStartTime may persist through recovery cycles due to networkState === 2 (NETWORK_LOADING) being true for live streams
1401
1590
  // even during healthy playback. Since we have confirmed 60 seconds of progression, the stream is definitively not buffering.
1402
1591
  bufferingStartTime = null;
1403
- // Reset escalation, segment tracking, and circuit breaker. Sustained healthy playback confirms the stream works.
1592
+ // Reset escalation, failure counters, segment tracking, and circuit breaker. Sustained healthy playback confirms the stream works, so we clear every
1593
+ // failure tally for a true fresh start. Without resetting the counters here, a stale consecutiveNavigationFailures count could carry across the healthy
1594
+ // window and prematurely escalate a future L3 episode, defeating the purpose of the 60-second reset.
1404
1595
  resetEscalationState();
1596
+ resetRecoveryCounters();
1405
1597
  resetSegmentMonitoringState();
1406
1598
  resetCircuitBreaker(circuitBreaker);
1407
1599
  }
@@ -1425,7 +1617,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
1425
1617
  recoveryState.inProgress = false;
1426
1618
  // If the session or page was closed, stop monitoring gracefully.
1427
1619
  if (isSessionClosedError(error) || currentPage.isClosed()) {
1428
- clearInterval(interval);
1620
+ stopMonitoring();
1429
1621
  return;
1430
1622
  }
1431
1623
  // Check for evaluate timeout errors, which indicate the browser tab may be unresponsive.
@@ -1462,15 +1654,21 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
1462
1654
  }).catch((outerError) => {
1463
1655
  // Log errors that escape the inner try/catch. In normal operation we should not reach here - if we do, there's a bug to investigate.
1464
1656
  LOG.warn("Monitor tick error escaped inner try/catch: %s.", formatError(outerError));
1465
- });
1657
+ }));
1466
1658
  }, CONFIG.playback.monitorInterval);
1467
- /* Return the cleanup function. The caller (stream handler) should call this when the stream ends to stop monitoring. Returns the recovery metrics for the
1468
- * termination summary log.
1659
+ /* The monitor's teardown: mark the interval cleared (so any in-flight async tick short-circuits) and clear it. Self-contained - it owns only the interval. Defined
1660
+ * as a const so it is exposed as both dispose() and [Symbol.dispose].
1469
1661
  */
1470
- return function () {
1471
- intervalCleared = true;
1472
- clearInterval(interval);
1473
- return metrics;
1662
+ const dispose = () => {
1663
+ stopMonitoring();
1664
+ };
1665
+ /* Return the monitor handle. getMetrics() exposes the live recovery metrics (read in the termination prologue for the summary log); dispose() / [Symbol.dispose]
1666
+ * stops the monitor. The metrics object remains valid after disposal because it is a retained counter snapshot, not derived at stop time.
1667
+ */
1668
+ return {
1669
+ dispose,
1670
+ getMetrics: () => metrics,
1671
+ [Symbol.dispose]: dispose
1474
1672
  };
1475
1673
  }
1476
1674
  //# sourceMappingURL=monitor.js.map