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
@@ -5,11 +5,10 @@
5
5
  import { LOG, chromeFetch, realClock, startTimer } from "../utils/index.js";
6
6
  import { buildPrerollEntries, computePrerollWindow } from "../streaming/preroll.js";
7
7
  import { decryptSegment, deriveIvFromSequence, fetchDecryptionKey, parseExplicitIv } from "./decrypt.js";
8
- import { storeAudioSegment, storeSegment, updateAudioPlaylist, updatePlaylist, updateVideoPlaylist } from "../streaming/hlsSegments.js";
8
+ import { findNamedInitSegment, pruneNamedInitSegments, storeAudioSegment, storeNamedInitSegment, storeSegment, updateAudioPlaylist, updatePlaylist, updateVideoPlaylist } from "../streaming/hlsSegments.js";
9
9
  import { CONFIG } from "../config/index.js";
10
10
  import { buildPlaylist } from "../streaming/playlistBuilder.js";
11
11
  import { getStream } from "../streaming/registry.js";
12
- import { removeManifestInterceptor } from "../browser/manifestInterceptor.js";
13
12
  import { resolveUrl } from "./probe.js";
14
13
  /* This module implements the native HLS proxy that replaces Chrome screen capture for viable streams. It polls the service's variant manifest at regular intervals,
15
14
  * detects new segments by tracking #EXT-X-MEDIA-SEQUENCE, fetches each segment (optionally decrypting AES-128), stores them in the existing HLS segment system, and
@@ -20,20 +19,64 @@ import { resolveUrl } from "./probe.js";
20
19
  * it preserves all playback-critical tags from the upstream manifest so that downstream consumers (Channels DVR) can correctly handle PTS resets at ad boundaries,
21
20
  * synchronize wall-clock time, and detect commercial breaks.
22
21
  *
23
- * Video segments are stored as "segment0.ts", "segment1.ts", etc. For streams with separate audio renditions, audio segments are stored as "audio0.ts",
24
- * "audio1.ts", etc.
22
+ * Segment names carry the container. An MPEG-TS source stores "segment0.ts", "segment1.ts", and so on, with "audio0.ts" upward for a separate audio rendition.
23
+ * An fMP4/CMAF source stores ".m4s" fragments under the same numbering, and the proxy additionally fetches the initialization segments its #EXT-X-MAP tags
24
+ * reference, serves them under names it mints per track, and emits the matching MAP references in every playlist it generates.
25
25
  */
26
26
  // Timeout for segment fetches.
27
27
  const SEGMENT_FETCH_TIMEOUT = 10000;
28
28
  // Maximum consecutive manifest poll failures before reporting an error. Client errors (4xx) use this threshold directly. Server errors (5xx) and network timeouts
29
29
  // use double the threshold to tolerate transient CDN issues that typically self-resolve within a few retry cycles.
30
30
  const MAX_MANIFEST_FAILURES = 3;
31
- // Maximum consecutive segment fetch failures before reporting an error.
31
+ // Maximum consecutive tracked fetch failures before reporting an error. This governs media segments and the initialization segments an fMP4 source's #EXT-X-MAP
32
+ // tags reference, since both ride the same tracker. Those fetches are smaller and far more numerous than manifest polls, so isolated transient failures (a
33
+ // dropped connection on one segment) are more common here; this higher tolerance than MAX_MANIFEST_FAILURES avoids escalating on blips that would otherwise
34
+ // self-resolve on the next fetch.
32
35
  const MAX_SEGMENT_FAILURES = 5;
33
- // Manifest poll backoff base delay and cap. On success, the poll interval returns to the base delay (half the target duration, typically ~3s). On failure, the delay
36
+ // Number of segment byte-fetches one track keeps in flight while its commit walk consumes the results in broadcast order. Three concurrent connections lift a
37
+ // track's download throughput to roughly three times the per-connection pacing a CDN edge grants, which is what keeps a high-bitrate channel ahead of its own
38
+ // production rate rather than accumulating a backlog that delays the next manifest poll. The bound is what keeps the gain cheap: at most three segments per
39
+ // track are resident at once - six for a stream whose audio is a separate rendition, since each track runs its own window.
40
+ const PARALLEL_SEGMENT_FETCHES = 3;
41
+ // Manifest poll backoff base delay and cap. On success, the poll interval returns to the base delay (a fixed 3000ms, ~half a typical 6s segment). On failure, the delay
34
42
  // doubles on each consecutive failure up to the cap. Jitter of +/-20% prevents multiple streams from retrying in lockstep after a shared CDN outage.
35
43
  const MANIFEST_BACKOFF_BASE = 3000;
36
44
  const MANIFEST_BACKOFF_CAP = 15000;
45
+ /**
46
+ * Computes the consecutive-failure threshold for a manifest poll based on the HTTP status of the most recent failure. Client errors (4xx) typically indicate
47
+ * permanent issues (auth expiry, content removed) that won't self-resolve, so they use the base threshold directly. Server errors (5xx), network errors, and
48
+ * timeouts are transient CDN conditions that usually recover within a few retry cycles, so they get double the attempts. A missing status (network error, DNS
49
+ * failure, connection reset, timeout) is treated as transient. This is the single source of truth for the 4xx-versus-else threshold decision across every
50
+ * manifest-poll call site - video and audio, success-path and catch-path failure handlers alike - so audio and video share identical escalation logic.
51
+ *
52
+ * @param status - The HTTP status code of the failed response, or undefined for network/timeout errors that never produced a response.
53
+ * @returns The consecutive-failure threshold to compare against before reporting an error.
54
+ */
55
+ export function manifestFailureThreshold(status) {
56
+ const isClientError = (status !== undefined) && (status >= 400) && (status < 500);
57
+ return isClientError ? MAX_MANIFEST_FAILURES : (MAX_MANIFEST_FAILURES * 2);
58
+ }
59
+ /**
60
+ * Resolves the initialization vector for a segment from the manifest's explicit IV (when present) or the media sequence number (when absent), per RFC 8216
61
+ * Section 5.2. When the manifest provides an explicit IV in the #EXT-X-KEY tag, it is authoritative and must parse cleanly: a malformed explicit IV is rejected
62
+ * rather than silently substituted with the sequence-derived IV. Substituting would decrypt the segment with the wrong IV, yielding corrupted CBC output on the
63
+ * first block - plausible-looking but garbage video served silently. Rejecting lets the caller count a fetch failure so recovery can escalate. The genuine absence
64
+ * of an explicit IV (ivHex === null) is the only case that derives the IV from the sequence number.
65
+ *
66
+ * @param ivHex - The explicit IV hex string from the manifest, or null when the manifest declares no explicit IV.
67
+ * @param sequence - The media sequence number used to derive the IV when no explicit IV is provided.
68
+ * @returns An "ok" result carrying the resolved IV, or a "reject" result when an explicit IV was provided but failed to parse.
69
+ */
70
+ export function resolveSegmentIv(ivHex, sequence) {
71
+ if (ivHex !== null) {
72
+ const explicitIv = parseExplicitIv(ivHex);
73
+ if (!explicitIv) {
74
+ return { status: "reject" };
75
+ }
76
+ return { iv: explicitIv, status: "ok" };
77
+ }
78
+ return { iv: deriveIvFromSequence(sequence), status: "ok" };
79
+ }
37
80
  // Manifest Parsing Helpers.
38
81
  /**
39
82
  * Parses a variant manifest into its metadata and segment list. Handles #EXT-X-MEDIA-SEQUENCE, #EXT-X-TARGETDURATION, #EXT-X-KEY (AES-128 with key rotation),
@@ -67,6 +110,10 @@ function parseVariantManifest(body, baseUrl) {
67
110
  let currentSequence = mediaSequence;
68
111
  let currentManifestKeyUrl = null;
69
112
  let currentIvHex = null;
113
+ // The initialization segment currently in force, tracked exactly like the key above: an #EXT-X-MAP applies to every following segment until another replaces
114
+ // it. This is per-document by design - the caller carries the cross-poll state, because a document that omits MAP entirely must leave the track's existing
115
+ // initialization untouched rather than reset it.
116
+ let currentMapUri = null;
70
117
  // Pending metadata flags - accumulated from tags between segments and attached to the next #EXTINF.
71
118
  let pendingCueIn = false;
72
119
  let pendingCueOut = null;
@@ -82,7 +129,13 @@ function parseVariantManifest(body, baseUrl) {
82
129
  if (uri) {
83
130
  currentManifestKeyUrl = resolveUrl(uri, baseUrl);
84
131
  }
85
- currentIvHex = /IV=([0-9a-fA-Fx]+)/.exec(trimmed)?.[1] ?? null;
132
+ // A quoted attribute value - most often the URI - may legally contain commas and arbitrary text, including a literal "IV=" sequence, so no attribute regex
133
+ // may scan across one. We blank every quoted span to an empty "" before scanning for the IV attribute, while the URI extraction above still reads the
134
+ // original line. The [:,] boundary ties IV= to the start of an attribute, right after the tag colon or a separating comma, so a coincidental attribute-name
135
+ // suffix such as FOOIV= cannot match, and the \s* tolerates the whitespace-after-comma layout some services emit. The capture takes the raw attribute value
136
+ // and defers every format judgment - the 0x or 0X prefix and the 32-hex length - to parseExplicitIv, the single owner of IV format rules.
137
+ const attributeResidue = trimmed.replace(/"[^"]*"/g, "\"\"");
138
+ currentIvHex = /[:,]\s*IV=([^,\s]+)/.exec(attributeResidue)?.[1] ?? null;
86
139
  }
87
140
  else if (method === "NONE") {
88
141
  currentManifestKeyUrl = null;
@@ -90,6 +143,15 @@ function parseVariantManifest(body, baseUrl) {
90
143
  }
91
144
  continue;
92
145
  }
146
+ if (trimmed.startsWith("#EXT-X-MAP:")) {
147
+ // The URI attribute is extracted with the same quoted-attribute pattern the key handler uses. A MAP tag without a parseable URI leaves the current
148
+ // initialization in force rather than clearing it, matching how the key handler treats a URI-less AES-128 tag.
149
+ const uri = /URI="([^"]+)"/.exec(trimmed)?.[1];
150
+ if (uri) {
151
+ currentMapUri = resolveUrl(uri, baseUrl);
152
+ }
153
+ continue;
154
+ }
93
155
  if (trimmed === "#EXT-X-DISCONTINUITY") {
94
156
  pendingDiscontinuity = true;
95
157
  continue;
@@ -131,6 +193,7 @@ function parseVariantManifest(body, baseUrl) {
131
193
  duration,
132
194
  ivHex: currentIvHex,
133
195
  keyUrl: currentManifestKeyUrl,
196
+ mapUri: currentMapUri,
134
197
  programDateTime: pendingProgramDateTime,
135
198
  sequence: currentSequence,
136
199
  url: resolveUrl(segUrl, baseUrl)
@@ -156,11 +219,13 @@ function parseVariantManifest(body, baseUrl) {
156
219
  * @returns The formatted m3u8 playlist string.
157
220
  */
158
221
  function buildVariantPlaylist(segmentEntries, metadata, prefix, targetDuration) {
159
- // Compute MEDIA-SEQUENCE from the first filename in the window. The filename encodes the local segment index (e.g., "segment5.ts" -> 5).
222
+ // Compute MEDIA-SEQUENCE from the first filename in the window. The filename encodes the local segment index between the prefix and the extension (e.g.,
223
+ // "segment5.ts" or "segment5.m4s" -> 5). The index is read by slicing to the final dot rather than stripping a specific extension, so the derivation holds for
224
+ // every container the relay names - stripping a hardcoded ".ts" would leave "5.m4s" and yield NaN for an fMP4 source.
160
225
  let mediaSequence = 0;
161
226
  const firstEntry = segmentEntries[0];
162
227
  if (firstEntry) {
163
- mediaSequence = Number(firstEntry.replace(prefix, "").replace(".ts", ""));
228
+ mediaSequence = Number(firstEntry.slice(prefix.length, firstEntry.lastIndexOf(".")));
164
229
  }
165
230
  // Compute DISCONTINUITY-SEQUENCE: total discontinuities ever observed minus those still visible in the current window. Only provided when > 0 to keep playlists
166
231
  // clean for streams that never have discontinuities.
@@ -173,7 +238,12 @@ function buildVariantPlaylist(segmentEntries, metadata, prefix, targetDuration)
173
238
  const discSeq = metadata.totalDiscontinuities - windowDiscontinuities;
174
239
  const discontinuitySequence = (discSeq > 0) ? discSeq : undefined;
175
240
  const entries = segmentEntries.map((filename) => buildEntryFromMetadata(filename, metadata, targetDuration));
176
- return buildPlaylist({ discontinuitySequence, mediaSequence, targetDuration, version: 3 }, entries);
241
+ // Place the MAP references through the shared chokepoint. Version 7 is required whenever a MAP appears anywhere in the window - as the opening reference or as
242
+ // a mid-window transition - because EXT-X-MAP is a version 6+ tag; an MPEG-TS window has neither and stays at version 3.
243
+ const initialMapUri = annotateMapTransitions(entries, metadata);
244
+ const hasTransition = entries.some((entry) => entry.mapUri !== undefined);
245
+ const version = ((initialMapUri !== null) || hasTransition) ? 7 : 3;
246
+ return buildPlaylist({ discontinuitySequence, initialMapUri: initialMapUri ?? undefined, mediaSequence, targetDuration, version }, entries);
177
247
  }
178
248
  /**
179
249
  * Creates an empty SegmentMetadata instance with fresh Maps and a zeroed discontinuity counter.
@@ -186,6 +256,7 @@ function createSegmentMetadata() {
186
256
  cueOuts: new Map(),
187
257
  discontinuities: new Map(),
188
258
  durations: new Map(),
259
+ mapUris: new Map(),
189
260
  timestamps: new Map(),
190
261
  totalDiscontinuities: 0
191
262
  };
@@ -197,9 +268,13 @@ function createSegmentMetadata() {
197
268
  * @param meta - The metadata instance to update.
198
269
  * @param filename - The local segment filename (e.g., "segment0.ts").
199
270
  * @param seg - The parsed segment with upstream metadata.
271
+ * @param servedInitName - The served initialization segment name this segment requires, or null for a self-describing MPEG-TS segment.
200
272
  */
201
- function storeSegmentMetadata(meta, filename, seg) {
273
+ function storeSegmentMetadata(meta, filename, seg, servedInitName = null) {
202
274
  meta.durations.set(filename, seg.duration);
275
+ if (servedInitName !== null) {
276
+ meta.mapUris.set(filename, servedInitName);
277
+ }
203
278
  if (seg.programDateTime) {
204
279
  meta.timestamps.set(filename, seg.programDateTime);
205
280
  }
@@ -251,6 +326,112 @@ function buildEntryFromMetadata(filename, metadata, defaultDuration) {
251
326
  }
252
327
  return entry;
253
328
  }
329
+ /**
330
+ * Places #EXT-X-MAP transition markers across a window of playlist entries and reports the initialization segment the window opens with. Every playlist the
331
+ * relay emits routes its MAP placement through here, so the variant and composite builders cannot drift on where a MAP belongs.
332
+ *
333
+ * An entry carries its own filename as its url, so the metadata lookup needs no parallel array. Only transitions are marked: the first entry's initialization
334
+ * becomes the window-level reference the caller emits as initialMapUri, and thereafter an entry gets a per-entry mapUri only when its initialization differs
335
+ * from its predecessor's. Stamping every entry would be legal but would defeat a client's ability to see where the codec configuration actually changes, and an
336
+ * MPEG-TS window - where no entry has an initialization at all - correctly produces neither.
337
+ *
338
+ * @param entries - The window's playlist entries, mutated in place to carry transition markers.
339
+ * @param metadata - The metadata instance holding each stored segment's served initialization name.
340
+ * @returns The initialization segment the window opens with, or null when the window's first entry needs none.
341
+ */
342
+ function annotateMapTransitions(entries, metadata) {
343
+ let previousInit = null;
344
+ let initialMapUri = null;
345
+ for (const [index, entry] of entries.entries()) {
346
+ const servedInit = metadata.mapUris.get(entry.url) ?? null;
347
+ if (index === 0) {
348
+ initialMapUri = servedInit;
349
+ }
350
+ else if ((servedInit !== null) && (servedInit !== previousInit)) {
351
+ entry.mapUri = servedInit;
352
+ }
353
+ previousInit = servedInit;
354
+ }
355
+ return initialMapUri;
356
+ }
357
+ /**
358
+ * Replaces the contents of a key-URL Set with the distinct AES-128 key URLs referenced by the segments of a freshly-parsed manifest. The Set is rebuilt from
359
+ * scratch on each poll so it always reflects exactly the keys the current manifest window references - never a stale accumulation. The video and audio paths each
360
+ * own one such Set; their union is the live working set the per-URL key cache prunes against at token-refresh boundaries, which bounds the cache to the keys the
361
+ * stream is actually using rather than every key it has ever rotated through.
362
+ *
363
+ * @param target - The Set to rebuild in place.
364
+ * @param segments - The parsed segments whose key URLs (when present) define the new active set.
365
+ */
366
+ function refreshActiveKeyUrls(target, segments) {
367
+ target.clear();
368
+ for (const seg of segments) {
369
+ if (seg.keyUrl !== null) {
370
+ target.add(seg.keyUrl);
371
+ }
372
+ }
373
+ }
374
+ /**
375
+ * Evicts decryption keys whose URL is not in the active working set, mutating the cache in place. The cache is keyed by URL because each token rotation can
376
+ * reference a different key URL; without this prune, the cache accumulates one dead entry per rotation indefinitely. The active set is the union of the URLs the
377
+ * current video and audio manifests reference, so keys still in use survive while keys that rotated out of both manifests are released. Returns the number of
378
+ * entries evicted so the caller can decide whether to log. Extracted as a pure module-level function (no closure capture) so the eviction invariant is unit-testable
379
+ * in isolation from the polling loop.
380
+ *
381
+ * @param keysByUrl - The per-URL key cache to prune in place.
382
+ * @param activeKeyUrls - The union of key URLs referenced by the current video and audio manifests.
383
+ * @returns The number of cache entries evicted.
384
+ */
385
+ export function pruneKeyCache(keysByUrl, activeKeyUrls) {
386
+ let evicted = 0;
387
+ for (const cachedKeyUrl of keysByUrl.keys()) {
388
+ if (!activeKeyUrls.has(cachedKeyUrl)) {
389
+ keysByUrl.delete(cachedKeyUrl);
390
+ evicted++;
391
+ }
392
+ }
393
+ return evicted;
394
+ }
395
+ /**
396
+ * Fetches, identifies, and stores the initialization segment a track has just transitioned to. Both the video and audio loops call this, so init handling has one
397
+ * implementation regardless of track.
398
+ *
399
+ * The fetch goes through the caller's tracked-fetch function with a null key URL, which makes it a plain fetch (the decrypt path gates on the key, and the
400
+ * sequence argument only feeds keyed-IV derivation). Riding that machinery is deliberate: the initialization fetch inherits the timeout, the consecutive-failure
401
+ * accounting, the reset on any success, and the existing escalation threshold, rather than introducing a second failure counter whose bound-and-reset semantics
402
+ * would have to be re-derived.
403
+ *
404
+ * Identity is content-based against everything the track still holds. A service that rotates a token through its MAP URI serves identical bytes under a new URL,
405
+ * and an ad break can alternate between two initializations; comparing bytes lets both reuse the name already being served instead of minting one whose data a
406
+ * client has. Only genuinely new bytes take the next name.
407
+ *
408
+ * On failure the upstream URI is deliberately not recorded, so the next poll cycle retries it and the caller's gate keeps dependent segments out of the window
409
+ * meanwhile. After the fetch resolves the lifecycle is re-checked: the capture fallback stops the proxy and then clears the init store, and a write landing after
410
+ * that clear would orphan itself permanently, because init pruning only ever runs from this poll loop.
411
+ *
412
+ * @param options - The context, the track and its bookkeeping state, the tracked-fetch function, and the upstream MAP URI to resolve.
413
+ */
414
+ async function resolveTrackInit(options) {
415
+ const { ctx, fetchSegment, mapUri, track, tracking } = options;
416
+ const data = await fetchSegment(tracking.segmentTracker, mapUri, 0, null, null);
417
+ if (!data) {
418
+ LOG.debug("native:proxy", "Initialization segment fetch failed for %s (%s track, %s).", ctx.channelName, track, mapUri.slice(0, 80));
419
+ return;
420
+ }
421
+ if (ctx.lifecycle.stopped) {
422
+ return;
423
+ }
424
+ const existingName = findNamedInitSegment(ctx.streamId, track, data);
425
+ let servedName = existingName;
426
+ if (servedName === undefined) {
427
+ servedName = ((track === "video") ? "init-v" : "init-a") + String(tracking.initCounter) + ".mp4";
428
+ tracking.initCounter++;
429
+ }
430
+ storeNamedInitSegment(ctx.streamId, track, servedName, data);
431
+ tracking.lastUpstreamMapUri = mapUri;
432
+ LOG.debug("native:proxy", "Initialization segment %s for %s (%s track, %s bytes, %s).", (existingName === undefined) ? "stored as " + servedName :
433
+ "reused as " + servedName, ctx.channelName, track, data.length, mapUri.slice(0, 80));
434
+ }
254
435
  /**
255
436
  * Prunes stale entries from a SegmentMetadata instance. Removes entries for segments that are no longer in the active segment set (evicted from the sliding window).
256
437
  * This prevents unbounded growth over hours of streaming.
@@ -266,10 +447,203 @@ function pruneMetadata(meta, activeSegments) {
266
447
  meta.cueOuts.delete(key);
267
448
  meta.discontinuities.delete(key);
268
449
  meta.durations.delete(key);
450
+ meta.mapUris.delete(key);
269
451
  meta.timestamps.delete(key);
270
452
  }
271
453
  }
272
454
  }
455
+ /**
456
+ * Releases a track's initialization segments that nothing in its playlist window still references. The retain set is every initialization named by a stored
457
+ * segment plus the track's current one, so an initialization outlives its own transition exactly as long as a segment that needs it remains fetchable, and no
458
+ * longer. Pruning per track keeps the two tracks' lifetimes independent.
459
+ *
460
+ * @param streamId - The numeric stream ID.
461
+ * @param track - The track to prune.
462
+ * @param metadata - The track's segment metadata, holding each stored segment's served initialization name.
463
+ * @param windowFilenames - The segment filenames currently in the track's store.
464
+ */
465
+ function pruneTrackInits(streamId, track, metadata, windowFilenames) {
466
+ const retain = new Set();
467
+ for (const filename of windowFilenames) {
468
+ const servedInit = metadata.mapUris.get(filename);
469
+ if (servedInit !== undefined) {
470
+ retain.add(servedInit);
471
+ }
472
+ }
473
+ const currentName = getStream(streamId)?.hls.currentInitNames[track];
474
+ if (currentName) {
475
+ retain.add(currentName);
476
+ }
477
+ pruneNamedInitSegments(streamId, track, retain);
478
+ }
479
+ // Segment Fetch Pipeline.
480
+ /**
481
+ * Performs the terminal transition every escalation site shares: latch the error state the monitor reads, stop the poll loop, cancel every fetch the proxy
482
+ * still has open, and report the reason to the coordinator. Single-homed so no site can flip the flags while leaving fetches running on past the stop, and so
483
+ * the state the monitor reads has exactly one writer.
484
+ *
485
+ * @param options - The proxy context and the message the coordinator receives.
486
+ */
487
+ function escalateProxyFailure(options) {
488
+ const { ctx, message } = options;
489
+ ctx.lifecycle.errorThresholdReached = true;
490
+ ctx.lifecycle.stopped = true;
491
+ ctx.lifecycle.abortController.abort();
492
+ ctx.onError(message);
493
+ }
494
+ /**
495
+ * Applies a settled fetch outcome to a track's failure accounting: a success clears the consecutive count, a failure raises it along with the cumulative error
496
+ * total, and crossing MAX_SEGMENT_FAILURES escalates. A cancellation touches neither counter - the proxy cancels its own fetches at teardown and at escalation,
497
+ * and counting those would make the termination summary report the stop's own casualties as fetches that failed. Single-homed here so the tracked fetch wrapper
498
+ * and the commit walk apply identical accounting to identical outcomes.
499
+ *
500
+ * @param options - The proxy context, the settled outcome, and the track's failure tracker.
501
+ */
502
+ function recordFetchOutcome(options) {
503
+ const { ctx, outcome, tracker } = options;
504
+ if (outcome.status === "abort") {
505
+ return;
506
+ }
507
+ if (outcome.status === "success") {
508
+ tracker.consecutiveFailures = 0;
509
+ return;
510
+ }
511
+ // A thrown failure carries a cause worth narrating; the core's null failure has already logged whatever it knew at the point it gave up.
512
+ if (outcome.status === "failed-thrown") {
513
+ LOG.debug("native:proxy", "%s fetch failed for %s: %s.", tracker.debugLabel, ctx.channelName, String(outcome.error));
514
+ }
515
+ tracker.consecutiveFailures++;
516
+ ctx.stats.totalFetchErrors++;
517
+ if (tracker.consecutiveFailures >= MAX_SEGMENT_FAILURES) {
518
+ const detail = (outcome.status === "failed-thrown") ? " fetch error: " + String(outcome.error) :
519
+ " fetch failed " + String(tracker.consecutiveFailures) + " times";
520
+ escalateProxyFailure({ ctx, message: tracker.label + detail });
521
+ }
522
+ }
523
+ /**
524
+ * Starts one fetch and settles with its outcome, never rejecting. The cancellation signal's own state is the first thing read at settlement: when the proxy has
525
+ * aborted, the outcome is a cancellation whatever shape the settlement took, because a cancelled key fetch surfaces as a resolved null rather than as a
526
+ * rejection and the signal is the one place cancellation is unambiguous. With the signal unaborted, shape decides: a rejection is a failure, which is how a
527
+ * per-fetch timeout keeps counting as one, and a null return is the core's untyped failure. Every fetch the proxy starts settles through here, so no bare core
528
+ * promise is left for the process-level rejection handler to catch.
529
+ *
530
+ * @param options - The fetch to start and the proxy's cancellation signal.
531
+ * @returns The settled outcome.
532
+ */
533
+ async function captureFetchOutcome(options) {
534
+ const { fetchSegment, signal } = options;
535
+ try {
536
+ const data = await fetchSegment();
537
+ if (signal.aborted) {
538
+ return { status: "abort" };
539
+ }
540
+ return data ? { data, status: "success" } : { status: "failed-null" };
541
+ }
542
+ catch (error) {
543
+ return signal.aborted ? { status: "abort" } : { error, status: "failed-thrown" };
544
+ }
545
+ }
546
+ /**
547
+ * Fetches a poll cycle's new segments and commits them in broadcast order. The walk over the batch is the ordered one the relay performs for every track: the
548
+ * initialization attempt at its transition, the unadopted skip, the container-true naming, the metadata, and the accounting all happen at the same points
549
+ * against the same state. What the scheduler adds is where the bytes come from - it runs ahead of the walk and keeps up to PARALLEL_SEGMENT_FETCHES byte-fetches
550
+ * open for the track, so the walk consumes results that are already settled or already downloading instead of opening each connection when it arrives. Download
551
+ * throughput therefore scales with the window while the served window still advances one segment at a time, in upstream order.
552
+ *
553
+ * A slot is occupied from the moment a fetch starts until the item that owns it has committed: the window is topped up only at the head of an iteration, which
554
+ * is after the previous item's store. Segments at or beyond an initialization transition the track has not adopted are never prefetched - the walk's own inline
555
+ * attempt is what decides whether they are fetchable at all, and starting them early would spend connections on bytes the adoption outcome can make unusable.
556
+ *
557
+ * @param options - The proxy context, the cycle's new segments in upstream order, and the track's pipeline binding.
558
+ * @returns True if at least one segment was stored.
559
+ */
560
+ async function runSegmentPipeline(options) {
561
+ const { ctx, segments, track } = options;
562
+ const { lifecycle } = ctx;
563
+ const tracking = track.tracking;
564
+ const inFlight = new Map();
565
+ // The next batch item the scheduler may start. It advances as fetches begin, and the walk carries it past any item it skips, so a batch that returns to the
566
+ // adopted initialization after an unresolved transition still prefetches its remainder.
567
+ let dispatchIndex = 0;
568
+ // At most one initialization fetch per track per poll cycle. A function-scoped local is the whole mechanism: it cannot outlive the cycle, cannot be
569
+ // forgotten, and cannot be shared with the other track, whose own pipeline call declares its own. One failing upstream initialization therefore costs one
570
+ // fetch timeout per cycle rather than one per dependent segment.
571
+ let initAttempted = false;
572
+ let storedAny = false;
573
+ /* Tops the window up to PARALLEL_SEGMENT_FETCHES open fetches, each started against the proxy's cancellation signal and wrapped so it settles rather than
574
+ * rejects. Eligibility is read against the walk's adoption state as it stands at this moment, so the scheduler stops at the first segment declaring an
575
+ * initialization the track has not adopted and resumes from there once the walk's inline attempt has settled that transition.
576
+ */
577
+ const fillWindow = () => {
578
+ while ((inFlight.size < PARALLEL_SEGMENT_FETCHES) && (dispatchIndex < segments.length) && !lifecycle.stopped) {
579
+ const segment = segments[dispatchIndex];
580
+ if (!segment || ((segment.mapUri !== null) && (segment.mapUri !== tracking.lastUpstreamMapUri))) {
581
+ return;
582
+ }
583
+ inFlight.set(dispatchIndex, captureFetchOutcome({ fetchSegment: () => track.fetchSegment(segment), signal: lifecycle.abortController.signal }));
584
+ dispatchIndex++;
585
+ }
586
+ };
587
+ for (const [index, segment] of segments.entries()) {
588
+ if (lifecycle.stopped) {
589
+ break;
590
+ }
591
+ // Resolve this segment's initialization before its name is chosen, so the first segment of a fresh tune is named from the initialization its own document
592
+ // established rather than transiently mislabeled.
593
+ if ((segment.mapUri !== null) && (segment.mapUri !== tracking.lastUpstreamMapUri) && !initAttempted) {
594
+ initAttempted = true;
595
+ // The attempt is sequential by contract: its outcome decides whether the rest of the batch is fetchable at all, so the walk holds here rather than
596
+ // running ahead of an adoption that has not happened.
597
+ // eslint-disable-next-line no-await-in-loop
598
+ await resolveTrackInit({ ctx, fetchSegment: track.fetchInit, mapUri: segment.mapUri, track: track.initTrack, tracking });
599
+ }
600
+ /* Store only segments whose initialization is resolved. A segment declaring a MAP the track has not adopted is skipped whether this cycle's attempt at that
601
+ * URI failed or the cycle's one attempt went to a different transition earlier in the window. Naming never falls back to the track's current initialization
602
+ * for a segment whose own transition is unresolved, so a second distinct transition in one batch cannot be mislabeled with the first one's initialization.
603
+ * The cost is bounded: each additional distinct transition in a batch resolves one cycle later.
604
+ */
605
+ if ((segment.mapUri !== null) && (segment.mapUri !== tracking.lastUpstreamMapUri)) {
606
+ // Carry the scheduler past an item that will not be fetched at all, so a later segment back under the adopted initialization is still prefetched.
607
+ dispatchIndex = index + 1;
608
+ continue;
609
+ }
610
+ fillWindow();
611
+ const pending = inFlight.get(index);
612
+ if (!pending) {
613
+ // The window declines to start a fetch once the proxy is stopping, which is the only way the walk reaches an item with nothing open for it.
614
+ break;
615
+ }
616
+ // Commits are sequential by contract - the served window advances one segment at a time, in upstream order - while the fetches the scheduler started ahead
617
+ // of this point keep running concurrently. Waiting here is what the commit ordering costs, and it costs nothing in download time.
618
+ // eslint-disable-next-line no-await-in-loop
619
+ const outcome = await pending;
620
+ inFlight.delete(index);
621
+ // Every outcome the walk consumes goes through the one accounting helper, cancellations included - it is the helper that decides a cancellation counts as
622
+ // neither a success nor a failure, and that decision is made in exactly one place.
623
+ recordFetchOutcome({ ctx, outcome, tracker: tracking.segmentTracker });
624
+ if (outcome.status === "abort") {
625
+ // Only teardown and escalation cancel, and both have already stopped the loop, so the rest of the batch goes with this item.
626
+ break;
627
+ }
628
+ if (outcome.status !== "success") {
629
+ continue;
630
+ }
631
+ // Name by container. A track holding an initialization is fMP4 and its fragments are .m4s; a track with none is MPEG-TS and keeps .ts.
632
+ const servedInit = getStream(ctx.streamId)?.hls.currentInitNames[track.initTrack] ?? null;
633
+ const filename = track.filenamePrefix + String(tracking.segmentIndex) + ((servedInit !== null) ? ".m4s" : ".ts");
634
+ const storedBefore = track.countStored(ctx.streamId);
635
+ track.store(ctx.streamId, filename, outcome.data);
636
+ tracking.fetchedSequences.add(segment.sequence);
637
+ tracking.highWaterSequence = Math.max(tracking.highWaterSequence, segment.sequence);
638
+ storeSegmentMetadata(tracking.metadata, filename, segment, servedInit);
639
+ storedAny = true;
640
+ tracking.segmentIndex++;
641
+ ctx.stats.totalSegmentsFetched++;
642
+ track.afterCommit?.({ byteLength: outcome.data.length, filename, storedBefore });
643
+ LOG.debug("native:proxy", "Stored %s (%s bytes, seq %s) for %s.", filename, outcome.data.length, segment.sequence, ctx.channelName);
644
+ }
645
+ return storedAny;
646
+ }
273
647
  // Audio Stream Handler.
274
648
  /**
275
649
  * Polls the audio variant manifest and fetches new audio segments. Combines the audio manifest poll and segment processing into a single entry point that mirrors the
@@ -277,10 +651,10 @@ function pruneMetadata(meta, activeSegments) {
277
651
  *
278
652
  * @param ctx - Shared proxy context with lifecycle state, stats, channel name, and error callback.
279
653
  * @param audio - Audio-specific tracking state (sequence tracking, metadata, failure counters).
280
- * @param fetchSegment - Closure-bound segment fetch function that handles HTTP fetch, decryption, and error tracking.
654
+ * @param pipeline - The audio track's pipeline binding: its fetch callbacks, store, naming, and bookkeeping.
281
655
  * @returns True if new audio segments were stored, false otherwise.
282
656
  */
283
- async function pollAudioStream(ctx, audio, fetchSegment) {
657
+ async function pollAudioStream(ctx, audio, pipeline) {
284
658
  if (ctx.lifecycle.stopped || !audio.variantUrl) {
285
659
  return false;
286
660
  }
@@ -289,26 +663,28 @@ async function pollAudioStream(ctx, audio, fetchSegment) {
289
663
  if (!response.ok) {
290
664
  audio.consecutiveManifestFailures++;
291
665
  ctx.stats.totalFetchErrors++;
292
- LOG.debug("native:proxy", "Audio manifest poll failed for %s: HTTP %s.", ctx.channelName, response.status);
293
- if (audio.consecutiveManifestFailures >= MAX_MANIFEST_FAILURES) {
294
- ctx.lifecycle.errorThresholdReached = true;
295
- ctx.lifecycle.stopped = true;
296
- ctx.onError("audio manifest poll failed " + String(audio.consecutiveManifestFailures) + " times");
666
+ // Classify the failure via the shared threshold helper so the audio path escalates identically to the video path: 4xx uses the base threshold, 5xx and
667
+ // network conditions get double the attempts.
668
+ const effectiveThreshold = manifestFailureThreshold(response.status);
669
+ LOG.debug("native:proxy", "Audio manifest poll failed for %s: HTTP %s (%s/%s).", ctx.channelName, response.status, audio.consecutiveManifestFailures, effectiveThreshold);
670
+ if (audio.consecutiveManifestFailures >= effectiveThreshold) {
671
+ escalateProxyFailure({ ctx, message: "audio manifest poll failed " + String(audio.consecutiveManifestFailures) + " times" });
297
672
  }
298
673
  return false;
299
674
  }
300
675
  audio.consecutiveManifestFailures = 0;
301
676
  const body = await response.text();
302
- return await processAudioStream(ctx, audio, body, audio.variantUrl, fetchSegment);
677
+ return await processAudioStream(ctx, audio, body, audio.variantUrl, pipeline);
303
678
  }
304
679
  catch (error) {
680
+ // Network errors (timeouts, DNS failures, connection resets) are transient - the threshold helper returns the doubled value for a missing status, matching the
681
+ // video catch path so audio and video escalate identically.
305
682
  audio.consecutiveManifestFailures++;
306
683
  ctx.stats.totalFetchErrors++;
307
- LOG.debug("native:proxy", "Audio manifest poll failed for %s: %s.", ctx.channelName, String(error));
308
- if (audio.consecutiveManifestFailures >= MAX_MANIFEST_FAILURES) {
309
- ctx.lifecycle.errorThresholdReached = true;
310
- ctx.lifecycle.stopped = true;
311
- ctx.onError("audio manifest poll error: " + String(error));
684
+ const networkThreshold = manifestFailureThreshold();
685
+ LOG.debug("native:proxy", "Audio manifest poll failed for %s: %s (%s/%s).", ctx.channelName, String(error), audio.consecutiveManifestFailures, networkThreshold);
686
+ if (audio.consecutiveManifestFailures >= networkThreshold) {
687
+ escalateProxyFailure({ ctx, message: "audio manifest poll error: " + String(error) });
312
688
  }
313
689
  return false;
314
690
  }
@@ -321,12 +697,15 @@ async function pollAudioStream(ctx, audio, fetchSegment) {
321
697
  * @param audio - Audio-specific tracking state.
322
698
  * @param body - The audio variant manifest text content.
323
699
  * @param baseUrl - The audio variant URL for resolving relative segment URLs.
324
- * @param fetchSegment - Closure-bound segment fetch function.
700
+ * @param pipeline - The audio track's pipeline binding: its fetch callbacks, store, naming, and bookkeeping.
325
701
  * @returns True if new segments were stored, false if the media sequence hadn't advanced.
326
702
  */
327
- async function processAudioStream(ctx, audio, body, baseUrl, fetchSegment) {
703
+ async function processAudioStream(ctx, audio, body, baseUrl, pipeline) {
328
704
  const { mediaSequence, segments, targetDuration } = parseVariantManifest(body, baseUrl);
329
705
  audio.lastTargetDuration = targetDuration;
706
+ // Record the AES-128 key URLs the current audio manifest references so the per-URL key cache can prune against the live video+audio working set on the next
707
+ // token refresh, rather than retaining a key per rotation indefinitely.
708
+ refreshActiveKeyUrls(audio.activeKeyUrls, segments);
330
709
  // Prune old entries from audio fetchedSequences and audio metadata on each poll cycle.
331
710
  for (const seq of audio.fetchedSequences) {
332
711
  if (seq < mediaSequence) {
@@ -357,34 +736,15 @@ async function processAudioStream(ctx, audio, body, baseUrl, fetchSegment) {
357
736
  return false;
358
737
  }
359
738
  LOG.debug("native:proxy", "Fetching %s new audio segment(s) for %s (sequence %s).", newSegments.length, ctx.channelName, mediaSequence);
360
- let storedAny = false;
361
- for (const seg of newSegments) {
362
- if (ctx.lifecycle.stopped) {
363
- break;
364
- }
365
- // eslint-disable-next-line no-await-in-loop
366
- const segmentData = await fetchSegment(audio.segmentTracker, seg.url, seg.sequence, seg.ivHex, seg.keyUrl);
367
- if (!segmentData) {
368
- continue;
369
- }
370
- const filename = "audio" + String(audio.segmentIndex) + ".ts";
371
- storeAudioSegment(ctx.streamId, filename, segmentData);
372
- audio.fetchedSequences.add(seg.sequence);
373
- audio.highWaterSequence = Math.max(audio.highWaterSequence, seg.sequence);
374
- storeSegmentMetadata(audio.metadata, filename, seg);
375
- storedAny = true;
376
- audio.segmentIndex++;
377
- ctx.stats.totalSegmentsFetched++;
378
- LOG.debug("native:proxy", "Stored %s (%s bytes, seq %s) for %s.", filename, segmentData.length, seg.sequence, ctx.channelName);
379
- }
380
- return storedAny;
739
+ return await runSegmentPipeline({ ctx, segments: newSegments, track: pipeline });
381
740
  }
382
741
  // Composite Playlist Builder.
383
742
  /**
384
- * Builds a composite playlist with fMP4 preroll entries and MPEG-TS real entries. Uses the same compositor and builder as the capture path's generatePlaylist(),
385
- * ensuring identical windowing behavior (maxPrerollInWindow cap, progressive falloff). The DISCONTINUITY tag at the preroll-to-real boundary signals the container
386
- * format change (fMP4 -> MPEG-TS), which is spec-compliant per RFC 8216 Section 4.3.3.3. VERSION:7 is used to support EXT-X-MAP for the preroll init segment;
387
- * after preroll entries fall off the window, VERSION:7 remains but is backward-compatible with the MPEG-TS entries.
743
+ * Builds a composite playlist with fMP4 preroll entries and the relayed source's real entries. Uses the same compositor and builder as the capture path's
744
+ * generatePlaylist(), ensuring identical windowing behavior (maxPrerollInWindow cap, progressive falloff). The DISCONTINUITY tag at the preroll-to-real boundary
745
+ * signals the transition between two independently-initialized runs of content, which is spec-compliant per RFC 8216 Section 4.3.3.3 - and when the relayed
746
+ * source is itself fMP4, that boundary is also where its own initialization reference is re-emitted. VERSION:7 is used throughout to support EXT-X-MAP, and stays
747
+ * backward-compatible with MPEG-TS entries once preroll falls off the window.
388
748
  *
389
749
  * @param options - Composite playlist configuration with segment data, preroll settings, and composite tracking state.
390
750
  * @returns The formatted composite m3u8 playlist string.
@@ -438,10 +798,24 @@ function buildCompositePlaylist(options) {
438
798
  }
439
799
  const discSeq = (composite.baseOffset + composite.discontinuities.size) - windowDiscontinuities;
440
800
  const discontinuitySequence = (discSeq > 0) ? discSeq : undefined;
441
- // Determine the initial MAP URI. When the window starts with preroll entries, the preroll init segment (fMP4) is referenced. When the window has moved past all
442
- // preroll, no MAP is needed (MPEG-TS segments are self-describing). The DISCONTINUITY tag at the preroll-to-real boundary invalidates the MAP per RFC 8216
443
- // Section 4.3.3.3, so MPEG-TS entries after the boundary carry their codec config inline.
444
- const initialMapUri = (prerollEntries.length > 0) ? (prerollBaseUrl + "/preroll/" + prerollCodec + "/init.mp4") : undefined;
801
+ /* Determine the MAP references. Two initialization regimes can meet in this one window: the fMP4 preroll's own init, and whatever the relayed source needs.
802
+ *
803
+ * The real entries are annotated through the same chokepoint the variant playlist uses, which reports the initialization the real content opens with - null
804
+ * for an MPEG-TS tail, whose segments are self-describing. When preroll occupies the head of the window, the window-level reference must stay the preroll
805
+ * init, so a non-null real-content initialization is redirected onto the first real entry: the DISCONTINUITY already marked at that boundary invalidates the
806
+ * preroll MAP per RFC 8216 Section 4.3.3.3, and the redirected reference is what re-establishes codec configuration for the content that follows. Once preroll
807
+ * has fallen out of the window, the real content's own initialization becomes the window-level reference directly.
808
+ */
809
+ const realInitialMapUri = annotateMapTransitions(realEntries, videoMetadata);
810
+ let initialMapUri = (prerollEntries.length > 0) ? (prerollBaseUrl + "/preroll/" + prerollCodec + "/init.mp4") : undefined;
811
+ if (realInitialMapUri !== null) {
812
+ if ((prerollEntries.length > 0) && firstRealEntry) {
813
+ firstRealEntry.mapUri = realInitialMapUri;
814
+ }
815
+ else {
816
+ initialMapUri = realInitialMapUri;
817
+ }
818
+ }
445
819
  return buildPlaylist({
446
820
  discontinuitySequence,
447
821
  initialMapUri,
@@ -457,9 +831,8 @@ function buildCompositePlaylist(options) {
457
831
  * @returns The native proxy interface.
458
832
  */
459
833
  export function createNativeProxy(options) {
460
- const { channelName, clock = realClock, encryption, keyUrl, onError, streamId } = options;
834
+ const { channelName, clock = realClock, container, encryption, keyUrl, onError, streamId } = options;
461
835
  const hasAudio = options.audioVariantUrl !== null;
462
- let activeCdpSession = options.cdpSession;
463
836
  // Preroll segment index offset. When preroll is ready (prerollSegmentCount > 0), real segments start numbering after the preroll range (e.g., segmentN.ts where
464
837
  // N = prerollSegmentCount). This offset is unconditional - it reserves the index space for preroll regardless of whether the deferred preroll timer fires.
465
838
  // The composite playlist behavior (including preroll entries) is determined dynamically by checking stream.hls.prerollStartTime at playlist generation time.
@@ -471,22 +844,28 @@ export function createNativeProxy(options) {
471
844
  */
472
845
  // Proxy lifecycle state. Controls whether the proxy is running, tracks readiness signaling, and manages poll timing.
473
846
  const lifecycle = {
847
+ abortController: new AbortController(),
474
848
  errorThresholdReached: false,
475
849
  firstPollComplete: false,
476
850
  manifestBackoffMs: MANIFEST_BACKOFF_BASE,
851
+ pendingRefresh: null,
477
852
  readinessSignaled: false,
853
+ refreshFailureCount: 0,
478
854
  stopped: false,
479
855
  tokenRefreshTimer: null
480
856
  };
481
857
  // Video segment and manifest tracking state.
482
858
  const video = {
859
+ activeKeyUrls: new Set(),
483
860
  consecutiveManifestFailures: 0,
484
861
  fetchedSequences: new Set(),
485
862
  highWaterSequence: -1,
863
+ initCounter: 0,
486
864
  lastMediaSequence: -1,
487
865
  lastSegmentSize: null,
488
866
  lastSegmentTime: 0,
489
867
  lastTargetDuration: 6,
868
+ lastUpstreamMapUri: null,
490
869
  metadata: createSegmentMetadata(),
491
870
  segmentIndex: prerollSegmentCount,
492
871
  segmentTracker: { consecutiveFailures: 0, debugLabel: "Segment", label: "segment" },
@@ -495,10 +874,13 @@ export function createNativeProxy(options) {
495
874
  };
496
875
  // Audio segment and manifest tracking state for streams with separate audio renditions.
497
876
  const audio = {
877
+ activeKeyUrls: new Set(),
498
878
  consecutiveManifestFailures: 0,
499
879
  fetchedSequences: new Set(),
500
880
  highWaterSequence: -1,
881
+ initCounter: 0,
501
882
  lastTargetDuration: 6,
883
+ lastUpstreamMapUri: null,
502
884
  metadata: createSegmentMetadata(),
503
885
  segmentIndex: 0,
504
886
  segmentTracker: { consecutiveFailures: 0, debugLabel: "Audio segment", label: "audio segment" },
@@ -523,12 +905,60 @@ export function createNativeProxy(options) {
523
905
  };
524
906
  // Shared context for extracted module-level functions. Bundles immutable references with shared mutable state objects.
525
907
  const ctx = { channelName, lifecycle, onError, stats, streamId };
908
+ /* The two tracks' pipeline bindings. Construction happens here, at the proxy's composition root, so the pipeline that serves both loops carries no branch on
909
+ * which track it is running: everything that differs between them - where segments are stored, how many are stored, how they are named, which initialization
910
+ * slot they read, and the extra bookkeeping video alone keeps - is data on these two objects.
911
+ */
912
+ const videoPipeline = {
913
+ afterCommit: ({ byteLength, storedBefore }) => {
914
+ video.lastSegmentSize = byteLength;
915
+ video.lastSegmentTime = Date.now();
916
+ // Log the first segment fetch latency for timing diagnostics.
917
+ if (stats.totalSegmentsFetched === 1) {
918
+ LOG.debug("timing:native", "First segment fetched for %s (%s bytes).", channelName, byteLength);
919
+ }
920
+ // Detect segment rotation - if the count didn't increase, the oldest segment was evicted.
921
+ const storedAfter = getStream(streamId)?.hls.segments.size ?? 0;
922
+ if ((storedAfter <= storedBefore) && (storedBefore > 0)) {
923
+ LOG.debug("native:proxy", "Segment rotated for %s (oldest removed, %s segments retained).", channelName, storedAfter);
924
+ }
925
+ },
926
+ countStored: (id) => getStream(id)?.hls.segments.size ?? 0,
927
+ fetchInit: fetchTrackedSegment,
928
+ fetchSegment: prefetchSegment,
929
+ filenamePrefix: "segment",
930
+ initTrack: "video",
931
+ store: storeSegment,
932
+ tracking: video
933
+ };
934
+ const audioPipeline = {
935
+ countStored: (id) => getStream(id)?.hls.audioSegments.size ?? 0,
936
+ fetchInit: fetchTrackedSegment,
937
+ fetchSegment: prefetchSegment,
938
+ filenamePrefix: "audio",
939
+ initTrack: "audio",
940
+ store: storeAudioSegment,
941
+ tracking: audio
942
+ };
526
943
  // AES-128 decryption key cache. Maps key URLs to their fetched 16-byte keys. Each segment in a manifest can reference a different key URL (key rotation), so we
527
944
  // cache by URL rather than maintaining a single "current key". The coordinator pre-fetches the initial key, which is seeded into the cache here.
528
945
  const keysByUrl = new Map();
529
946
  if (options.prefetchedKey && keyUrl) {
530
947
  keysByUrl.set(keyUrl, options.prefetchedKey);
531
948
  }
949
+ /**
950
+ * Prunes the per-URL decryption key cache against this stream's live working set - the union of the key URLs the most recently polled video and audio manifests
951
+ * reference - and logs the eviction count. Called at each token-refresh boundary, this bounds the cache to the keys the stream is actually using (typically one or
952
+ * two) rather than retaining one dead entry per rotation. Refresh is the natural point at which the prior CDN session's keys become unreachable, so it runs here
953
+ * rather than on every poll. The eviction itself lives in the pure module-level pruneKeyCache so the invariant is unit-testable.
954
+ */
955
+ function pruneStreamKeyCache() {
956
+ const activeKeyUrls = new Set([...video.activeKeyUrls, ...audio.activeKeyUrls]);
957
+ const evicted = pruneKeyCache(keysByUrl, activeKeyUrls);
958
+ if (evicted > 0) {
959
+ LOG.debug("native:proxy", "Pruned %s stale decryption key(s) for %s (%s active).", evicted, channelName, keysByUrl.size);
960
+ }
961
+ }
532
962
  /**
533
963
  * Computes the next retry delay with exponential backoff and jitter. Doubles the current backoff (capped at MANIFEST_BACKOFF_CAP) and applies +/-20% jitter.
534
964
  * @returns The jittered delay in milliseconds.
@@ -551,15 +981,14 @@ export function createNativeProxy(options) {
551
981
  if (!response.ok) {
552
982
  video.consecutiveManifestFailures++;
553
983
  stats.totalFetchErrors++;
554
- // Classify the error. Client errors (4xx) typically indicate permanent issues (auth expiry, content removed) that won't self-resolve. Server errors (5xx)
555
- // are transient CDN issues that usually recover within a few seconds. Client errors use the base threshold; server errors get double the attempts.
984
+ // Classify the error via the shared threshold helper. Client errors (4xx) use the base threshold; server errors (5xx) and network conditions get double the
985
+ // attempts. This is the same logic the audio poll path uses, so video and audio escalate identically.
556
986
  const isClientError = (response.status >= 400) && (response.status < 500);
557
- const effectiveThreshold = isClientError ? MAX_MANIFEST_FAILURES : (MAX_MANIFEST_FAILURES * 2);
987
+ const effectiveThreshold = manifestFailureThreshold(response.status);
558
988
  LOG.debug("native:proxy", "Manifest poll failed for %s: HTTP %s (%s, %s/%s).", channelName, response.status, isClientError ? "client" : "server", video.consecutiveManifestFailures, effectiveThreshold);
559
989
  if (video.consecutiveManifestFailures >= effectiveThreshold) {
560
- lifecycle.errorThresholdReached = true;
561
- lifecycle.stopped = true;
562
- onError("manifest poll failed " + String(video.consecutiveManifestFailures) + " times (HTTP " + String(response.status) + ")");
990
+ escalateProxyFailure({ ctx, message: "manifest poll failed " + String(video.consecutiveManifestFailures) + " times (HTTP " +
991
+ String(response.status) + ")" });
563
992
  return;
564
993
  }
565
994
  schedulePoll(nextBackoffDelay());
@@ -575,7 +1004,7 @@ export function createNativeProxy(options) {
575
1004
  let hasNewVideoSegments;
576
1005
  let hasNewAudioSegments = false;
577
1006
  if (hasAudio) {
578
- [hasNewVideoSegments, hasNewAudioSegments] = await Promise.all([processManifest(body), pollAudioStream(ctx, audio, fetchTrackedSegment)]);
1007
+ [hasNewVideoSegments, hasNewAudioSegments] = await Promise.all([processManifest(body), pollAudioStream(ctx, audio, audioPipeline)]);
579
1008
  }
580
1009
  else {
581
1010
  hasNewVideoSegments = await processManifest(body);
@@ -591,8 +1020,9 @@ export function createNativeProxy(options) {
591
1020
  if (!lifecycle.firstPollComplete && (video.segmentIndex > 0) && (!hasAudio || (audio.segmentIndex > 0))) {
592
1021
  lifecycle.firstPollComplete = true;
593
1022
  }
594
- // Signal playlist readiness after the first poll completes. Note: initSegmentReady is signaled immediately when the proxy starts (in hls.ts) since native
595
- // MPEG-TS has no separate init segment.
1023
+ // Signal playlist readiness after the first poll completes. Init readiness is a separate signal owned elsewhere: an MPEG-TS source has no separate
1024
+ // initialization segment, so hls.ts signals it the moment the proxy starts, while an fMP4 source's signal comes from the store when this loop's first
1025
+ // video initialization lands.
596
1026
  if (lifecycle.firstPollComplete && !lifecycle.readinessSignaled) {
597
1027
  lifecycle.readinessSignaled = true;
598
1028
  const stream = getStream(streamId);
@@ -603,20 +1033,20 @@ export function createNativeProxy(options) {
603
1033
  }
604
1034
  }
605
1035
  catch (error) {
606
- // Network errors (timeouts, DNS failures, connection resets) are transient - use the extended threshold and backoff.
1036
+ // Network errors (timeouts, DNS failures, connection resets) are transient - use the extended threshold and backoff. The threshold helper returns the doubled
1037
+ // value for a missing status, which is exactly the network-error case here.
607
1038
  video.consecutiveManifestFailures++;
608
1039
  stats.totalFetchErrors++;
609
- LOG.debug("native:proxy", "Manifest poll failed for %s: %s (%s/%s).", channelName, String(error), video.consecutiveManifestFailures, MAX_MANIFEST_FAILURES * 2);
610
- if (video.consecutiveManifestFailures >= (MAX_MANIFEST_FAILURES * 2)) {
611
- lifecycle.errorThresholdReached = true;
612
- lifecycle.stopped = true;
613
- onError("manifest poll error: " + String(error));
1040
+ const networkThreshold = manifestFailureThreshold();
1041
+ LOG.debug("native:proxy", "Manifest poll failed for %s: %s (%s/%s).", channelName, String(error), video.consecutiveManifestFailures, networkThreshold);
1042
+ if (video.consecutiveManifestFailures >= networkThreshold) {
1043
+ escalateProxyFailure({ ctx, message: "manifest poll error: " + String(error) });
614
1044
  return;
615
1045
  }
616
1046
  schedulePoll(nextBackoffDelay());
617
1047
  return;
618
1048
  }
619
- // Schedule the next poll at roughly half the target segment duration for timely detection of new segments.
1049
+ // Schedule the next poll at MANIFEST_BACKOFF_BASE (3000ms, ~half a typical 6s segment) for timely detection of new segments.
620
1050
  lifecycle.manifestBackoffMs = MANIFEST_BACKOFF_BASE;
621
1051
  schedulePoll(MANIFEST_BACKOFF_BASE);
622
1052
  }
@@ -629,6 +1059,9 @@ export function createNativeProxy(options) {
629
1059
  async function processManifest(body) {
630
1060
  const { mediaSequence, segments, targetDuration } = parseVariantManifest(body, video.variantUrl);
631
1061
  video.lastTargetDuration = targetDuration;
1062
+ // Record the AES-128 key URLs the current video manifest references so the per-URL key cache can prune against the live video+audio working set on the next
1063
+ // token refresh, rather than retaining a key per rotation indefinitely.
1064
+ refreshActiveKeyUrls(video.activeKeyUrls, segments);
632
1065
  // Prune old entries from the fetchedSequences Set and segment metadata on each poll cycle. The service's media sequence window slides forward, so entries below
633
1066
  // the current base sequence will never be checked again. Without pruning, these structures grow unboundedly over hours of streaming.
634
1067
  for (const seq of video.fetchedSequences) {
@@ -675,55 +1108,26 @@ export function createNativeProxy(options) {
675
1108
  }
676
1109
  LOG.debug("native:proxy", "Fetching %s new segment(s) for %s (sequence %s).", newSegments.length, channelName, mediaSequence);
677
1110
  video.lastMediaSequence = mediaSequence;
678
- // Fetch and store each new segment sequentially. Track whether at least one segment was stored so the caller only generates playlists when data actually changed.
679
- let storedAny = false;
680
- for (const seg of newSegments) {
681
- if (lifecycle.stopped) {
682
- break;
683
- }
684
- // eslint-disable-next-line no-await-in-loop
685
- const segmentData = await fetchTrackedSegment(video.segmentTracker, seg.url, seg.sequence, seg.ivHex, seg.keyUrl);
686
- if (!segmentData) {
687
- continue;
688
- }
689
- const filename = "segment" + String(video.segmentIndex) + ".ts";
690
- // Check segment count before store to detect rotation (oldest segment evicted to enforce maxSegments limit).
691
- const stream = getStream(streamId);
692
- const countBefore = stream?.hls.segments.size ?? 0;
693
- storeSegment(streamId, filename, segmentData);
694
- video.fetchedSequences.add(seg.sequence);
695
- video.highWaterSequence = Math.max(video.highWaterSequence, seg.sequence);
696
- storeSegmentMetadata(video.metadata, filename, seg);
697
- storedAny = true;
698
- video.lastSegmentSize = segmentData.length;
699
- video.lastSegmentTime = Date.now();
700
- video.segmentIndex++;
701
- stats.totalSegmentsFetched++;
702
- // Log the first segment fetch latency for timing diagnostics.
703
- if (stats.totalSegmentsFetched === 1) {
704
- LOG.debug("timing:native", "First segment fetched for %s (%s bytes).", channelName, segmentData.length);
705
- }
706
- // Detect segment rotation - if the count didn't increase, the oldest segment was evicted.
707
- const countAfter = stream?.hls.segments.size ?? 0;
708
- if ((countAfter <= countBefore) && (countBefore > 0)) {
709
- LOG.debug("native:proxy", "Segment rotated for %s (oldest removed, %s segments retained).", channelName, countAfter);
710
- }
711
- LOG.debug("native:proxy", "Stored %s (%s bytes, seq %s) for %s.", filename, segmentData.length, seg.sequence, channelName);
712
- }
713
- return storedAny;
1111
+ return await runSegmentPipeline({ ctx, segments: newSegments, track: videoPipeline });
714
1112
  }
715
1113
  /**
716
- * Fetches and optionally decrypts a segment. This is the shared core used by both video and audio fetch paths. It handles the HTTP fetch, AES-128 decryption with
717
- * key caching, and IV derivation. Error tracking is the caller's responsibility since video and audio have independent failure thresholds.
1114
+ * Fetches and optionally decrypts a segment. This is the shared core used by both video and audio fetch paths, for media segments and for the initialization
1115
+ * segments an fMP4 source references. It handles the HTTP fetch, AES-128 decryption with key caching, and IV derivation. A caller with no key URL gets a plain
1116
+ * fetch, since the decryption block below gates on the key and the sequence argument only feeds keyed-IV derivation. Error tracking is the caller's
1117
+ * responsibility since video and audio have independent failure thresholds.
718
1118
  *
719
1119
  * @param url - The segment URL.
720
1120
  * @param sequence - The media sequence number (for IV derivation).
721
1121
  * @param ivHex - Explicit IV hex string from the manifest, or null.
722
1122
  * @param segKeyUrl - The key URL for this specific segment from the manifest's #EXT-X-KEY tag, or null for clear segments.
1123
+ * @param abortSignal - Optional cancellation signal, composed with this fetch's own timeout.
723
1124
  * @returns The segment data (decrypted if necessary), or null on failure.
724
1125
  */
725
- async function fetchAndDecryptSegment(url, sequence, ivHex, segKeyUrl) {
726
- const response = await chromeFetch(url, { signal: AbortSignal.timeout(SEGMENT_FETCH_TIMEOUT) });
1126
+ async function fetchAndDecryptSegment(url, sequence, ivHex, segKeyUrl, abortSignal) {
1127
+ // The caller's cancellation and this fetch's own timeout are composed, so the segment ends on whichever arrives first and a caller with nothing to cancel
1128
+ // gets the timeout alone.
1129
+ const timeout = AbortSignal.timeout(SEGMENT_FETCH_TIMEOUT);
1130
+ const response = await chromeFetch(url, { signal: abortSignal ? AbortSignal.any([abortSignal, timeout]) : timeout });
727
1131
  if (!response.ok) {
728
1132
  LOG.debug("native:proxy", "Segment fetch failed for %s: HTTP %s.", channelName, response.status);
729
1133
  return null;
@@ -736,7 +1140,9 @@ export function createNativeProxy(options) {
736
1140
  // Look up the key in the per-URL cache, or fetch it if not cached.
737
1141
  let key = keysByUrl.get(segKeyUrl);
738
1142
  if (!key) {
739
- key = await fetchDecryptionKey(segKeyUrl) ?? undefined;
1143
+ // The key fetch receives the raw cancellation signal rather than the composed one above, so it gets its own full timeout budget instead of inheriting
1144
+ // whatever this segment's fetch has already spent.
1145
+ key = await fetchDecryptionKey(segKeyUrl, abortSignal) ?? undefined;
740
1146
  if (!key) {
741
1147
  LOG.debug("native:proxy", "Decryption key unavailable for %s (key URL: %s).", channelName, segKeyUrl.slice(0, 80));
742
1148
  return null;
@@ -744,16 +1150,33 @@ export function createNativeProxy(options) {
744
1150
  keysByUrl.set(segKeyUrl, key);
745
1151
  LOG.debug("native:proxy", "New decryption key cached for %s.", channelName);
746
1152
  }
747
- // Determine the IV: use explicit IV from the manifest, or derive from the media sequence number.
748
- const iv = ivHex ? (parseExplicitIv(ivHex) ?? deriveIvFromSequence(sequence)) : deriveIvFromSequence(sequence);
749
- LOG.debug("native:decrypt", "IV source for sequence %s: %s.", sequence, ivHex ? "explicit" : "sequence");
750
- data = decryptSegment(data, key, iv);
1153
+ // Determine the IV via the shared resolver. A malformed explicit IV is rejected rather than silently substituted with the sequence-derived IV, which would
1154
+ // decrypt the segment with the wrong IV and serve plausible-looking but garbage video. Rejecting returns null here so the fetch tracker counts a failure and
1155
+ // recovery can escalate.
1156
+ const ivResult = resolveSegmentIv(ivHex, sequence);
1157
+ if (ivResult.status === "reject") {
1158
+ LOG.warn("Rejecting segment for %s: the manifest provided a malformed explicit IV.", channelName, { ivHex, sequence });
1159
+ return null;
1160
+ }
1161
+ LOG.debug("native:decrypt", "IV source for sequence %s: %s.", sequence, (ivHex !== null) ? "explicit" : "sequence");
1162
+ data = decryptSegment(data, key, ivResult.iv);
751
1163
  }
752
1164
  return data;
753
1165
  }
754
1166
  /**
755
- * Fetches a segment with error tracking. Wraps fetchAndDecryptSegment with per-tracker failure counting. Consecutive failures trigger the error threshold which
756
- * stops the proxy and signals the monitor for recovery. Used for both video and audio segments with independent trackers.
1167
+ * Starts one segment's byte fetch for the prefetch scheduler, which applies accounting when the segment commits rather than when its bytes arrive. The
1168
+ * proxy's cancellation signal is supplied here, so a fetch the scheduler started ahead of the commit walk ends the moment the proxy stops.
1169
+ *
1170
+ * @param segment - The parsed segment to fetch.
1171
+ * @returns The segment data (decrypted if necessary), or null on failure.
1172
+ */
1173
+ async function prefetchSegment(segment) {
1174
+ return fetchAndDecryptSegment(segment.url, segment.sequence, segment.ivHex, segment.keyUrl, lifecycle.abortController.signal);
1175
+ }
1176
+ /**
1177
+ * Fetches a segment with error tracking, for the callers that fetch outside the pipeline's window: the initialization segments an fMP4 source's #EXT-X-MAP
1178
+ * tags reference, which ride the track's own tracker so their failures escalate through the one segment threshold rather than a parallel counter. The fetch
1179
+ * carries the proxy's cancellation signal and settles through the shared outcome classifier, so a cancelled initialization is discarded rather than counted.
757
1180
  *
758
1181
  * @param tracker - Mutable error tracking state with labels for log and error messages.
759
1182
  * @param url - The segment URL.
@@ -763,38 +1186,17 @@ export function createNativeProxy(options) {
763
1186
  * @returns The segment data (decrypted if necessary), or null on failure.
764
1187
  */
765
1188
  async function fetchTrackedSegment(tracker, url, sequence, ivHex, segKeyUrl) {
766
- try {
767
- const data = await fetchAndDecryptSegment(url, sequence, ivHex, segKeyUrl);
768
- if (!data) {
769
- tracker.consecutiveFailures++;
770
- stats.totalFetchErrors++;
771
- if (tracker.consecutiveFailures >= MAX_SEGMENT_FAILURES) {
772
- lifecycle.errorThresholdReached = true;
773
- lifecycle.stopped = true;
774
- onError(tracker.label + " fetch failed " + String(tracker.consecutiveFailures) + " times");
775
- }
776
- return null;
777
- }
778
- tracker.consecutiveFailures = 0;
779
- return data;
780
- }
781
- catch (error) {
782
- tracker.consecutiveFailures++;
783
- stats.totalFetchErrors++;
784
- LOG.debug("native:proxy", "%s fetch failed for %s: %s.", tracker.debugLabel, channelName, String(error));
785
- if (tracker.consecutiveFailures >= MAX_SEGMENT_FAILURES) {
786
- lifecycle.errorThresholdReached = true;
787
- lifecycle.stopped = true;
788
- onError(tracker.label + " fetch error: " + String(error));
789
- }
790
- return null;
791
- }
1189
+ const signal = lifecycle.abortController.signal;
1190
+ const outcome = await captureFetchOutcome({ fetchSegment: () => fetchAndDecryptSegment(url, sequence, ivHex, segKeyUrl, signal), signal });
1191
+ recordFetchOutcome({ ctx, outcome, tracker });
1192
+ return (outcome.status === "success") ? outcome.data : null;
792
1193
  }
793
1194
  /**
794
1195
  * Generates playlists from the stored segments. For streams without separate audio, generates a single variant playlist. When preroll is active (muxed audio only),
795
1196
  * produces a composite playlist with fMP4 preroll entries and MPEG-TS real entries bridged by a DISCONTINUITY tag. The preroll entries use the same fMP4 segments
796
- * served during the standalone preroll phase, ensuring smooth MEDIA-SEQUENCE progression. For streams with separate audio, generates a master playlist referencing
797
- * video.m3u8 and audio.m3u8, plus individual variant playlists for each (no preroll - preroll is muxed and can't be split into separate renditions).
1197
+ * served during the standalone preroll phase, ensuring smooth MEDIA-SEQUENCE progression. For streams with separate audio, generates the video variant playlist
1198
+ * and the master playlist referencing video.m3u8 and audio.m3u8 (no preroll - preroll is muxed and can't be split into separate renditions); the audio variant
1199
+ * playlist is produced separately by generateAudioPlaylist(), which the poll loop calls alongside this function.
798
1200
  *
799
1201
  * @param targetDuration - The #EXT-X-TARGETDURATION value from the service's manifest.
800
1202
  */
@@ -852,6 +1254,9 @@ export function createNativeProxy(options) {
852
1254
  "video.m3u8\n";
853
1255
  updatePlaylist(streamId, masterPlaylist);
854
1256
  }
1257
+ // Release initialization segments the freshly-generated window no longer references. This runs after generation so the retain set reflects exactly the
1258
+ // segments the playlist just published.
1259
+ pruneTrackInits(streamId, "video", video.metadata, segmentEntries);
855
1260
  LOG.debug("native:proxy", "Playlist generated for %s with %s segment(s), target duration %ss.", channelName, segmentEntries.length, targetDuration);
856
1261
  }
857
1262
  /**
@@ -866,12 +1271,16 @@ export function createNativeProxy(options) {
866
1271
  }
867
1272
  const audioEntries = Array.from(stream.hls.audioSegments.keys());
868
1273
  updateAudioPlaylist(streamId, buildVariantPlaylist(audioEntries, audio.metadata, "audio", targetDuration));
1274
+ // Release initialization segments the freshly-generated audio window no longer references. The audio track's Map is pruned on its own, so this can never
1275
+ // evict a video initialization.
1276
+ pruneTrackInits(streamId, "audio", audio.metadata, audioEntries);
869
1277
  }
870
1278
  /**
871
1279
  * Schedules the next manifest poll after a delay. The sleep is awaited through the injected Clock port (defaulting to realClock) so tests can virtualize the
872
1280
  * polling cadence without depending on real timers. Cancellation semantics: stop() flips lifecycle.stopped, and the post-sleep guard catches that flip before
873
- * issuing the next poll. The in-flight sleep itself is not cancelled - in production a stopped proxy lingers for at most one MANIFEST_BACKOFF_BASE before the
874
- * awaiter wakes and exits cleanly. This is the same shape as retryOperation in utils/retry.ts which already adopted the Clock port for the same reason.
1281
+ * issuing the next poll. The in-flight sleep itself is not cancelled - in production a stopped proxy lingers for at most the currently scheduled poll delay,
1282
+ * which can be as long as MANIFEST_BACKOFF_CAP once jittered, before the awaiter wakes and exits cleanly. This is the same shape as retryOperation in
1283
+ * utils/retry.ts which already adopted the Clock port for the same reason.
875
1284
  *
876
1285
  * @param delayMs - Delay in milliseconds before the next poll.
877
1286
  */
@@ -887,11 +1296,37 @@ export function createNativeProxy(options) {
887
1296
  void pollManifest();
888
1297
  })();
889
1298
  }
1299
+ // The proxy's teardown: flip the lifecycle flag the polling awaiter checks, and cancel the pending token-refresh timer. Defined as a const so it can be exposed
1300
+ // both as stop() (the established domain verb) and as [Symbol.dispose] (the TC39 protocol), making the proxy a self-disposing node without duplicating the body.
1301
+ const stop = () => {
1302
+ lifecycle.stopped = true;
1303
+ // Cancel every fetch the proxy still has open - segments, initialization segments, and decryption keys - so teardown does not wait on a CDN and no
1304
+ // straggler write can land after the stream's state has been cleared.
1305
+ lifecycle.abortController.abort();
1306
+ // The polling cadence sleep is owned by schedulePoll's awaiter and is not cancelled here - the awaiter checks lifecycle.stopped after clock.sleep resolves
1307
+ // and exits before issuing the next poll. See schedulePoll's docblock for the cancellation contract.
1308
+ // Cancel the pending token refresh timer to prevent fire-after-termination. Without this, the timer fires on a stopped proxy and attempts to navigate a
1309
+ // potentially closed or reused page.
1310
+ if (lifecycle.tokenRefreshTimer) {
1311
+ clearTimeout(lifecycle.tokenRefreshTimer);
1312
+ lifecycle.tokenRefreshTimer = null;
1313
+ }
1314
+ LOG.debug("native:proxy", "Stopped native proxy for %s.", channelName);
1315
+ };
890
1316
  return {
1317
+ clearRefreshFailures: () => {
1318
+ lifecycle.refreshFailureCount = 0;
1319
+ },
891
1320
  getConsecutiveErrors: () => video.segmentTracker.consecutiveFailures + video.consecutiveManifestFailures +
892
1321
  audio.segmentTracker.consecutiveFailures + audio.consecutiveManifestFailures,
893
1322
  getLastSegmentSize: () => video.lastSegmentSize,
894
1323
  getLastSegmentTime: () => video.lastSegmentTime,
1324
+ getPendingRefresh: () => lifecycle.pendingRefresh,
1325
+ /* The envelope a refresh selects against, assembled from what construction fixed: the container the relay reads, the encryption kind it was built to handle,
1326
+ * and whether its audio arrives as a separate rendition. It lives here because the pipeline lives here - the coordinator that runs the refresh holds a proxy
1327
+ * reference and nothing else that could answer the question.
1328
+ */
1329
+ getPipelineShape: () => ({ container, encryption, separateAudio: hasAudio }),
895
1330
  getSegmentIndex: () => video.segmentIndex,
896
1331
  getStats: () => ({
897
1332
  fetchErrors: stats.totalFetchErrors,
@@ -901,38 +1336,31 @@ export function createNativeProxy(options) {
901
1336
  getTargetDuration: () => video.lastTargetDuration,
902
1337
  hasErrored: () => lifecycle.errorThresholdReached,
903
1338
  isStopped: () => lifecycle.stopped,
1339
+ noteRefreshFailure: () => ++lifecycle.refreshFailureCount,
1340
+ setPendingRefresh: (refresh) => {
1341
+ lifecycle.pendingRefresh = refresh;
1342
+ },
1343
+ /* The proxy holds at most one live refresh timer, and this is the single site that writes the slot. Arming a successor therefore retires its predecessor
1344
+ * here, so a reschedule cannot leave an orphaned handle firing against a proxy that a newer schedule already speaks for, and the stop path retires whichever
1345
+ * handle is live when the stream ends.
1346
+ */
904
1347
  setTokenRefreshTimer: (timer) => {
1348
+ if (lifecycle.tokenRefreshTimer) {
1349
+ clearTimeout(lifecycle.tokenRefreshTimer);
1350
+ }
905
1351
  lifecycle.tokenRefreshTimer = timer;
906
1352
  },
907
1353
  start: () => {
908
1354
  LOG.debug("native:proxy", "Starting native proxy for %s (%s).", channelName, encryption);
909
1355
  void pollManifest();
910
1356
  },
911
- stop: () => {
912
- lifecycle.stopped = true;
913
- // The polling cadence sleep is owned by schedulePoll's awaiter and is not cancelled here - the awaiter checks lifecycle.stopped after clock.sleep resolves
914
- // and exits before issuing the next poll. See schedulePoll's docblock for the cancellation contract.
915
- // Cancel the pending token refresh timer to prevent fire-after-termination. Without this, the timer fires on a stopped proxy and attempts to navigate a
916
- // potentially closed or reused page.
917
- if (lifecycle.tokenRefreshTimer) {
918
- clearTimeout(lifecycle.tokenRefreshTimer);
919
- lifecycle.tokenRefreshTimer = null;
920
- }
921
- // Clean up the CDP session from manifest interception to prevent session leaks.
922
- removeManifestInterceptor(activeCdpSession);
923
- LOG.debug("native:proxy", "Stopped native proxy for %s.", channelName);
924
- },
1357
+ stop,
925
1358
  updateAudioVariantUrl: (newUrl) => {
926
1359
  audio.variantUrl = newUrl;
927
1360
  audio.fetchedSequences.clear();
928
1361
  audio.tokenRefreshPending = true;
929
1362
  LOG.debug("native:proxy", "Audio variant URL updated for %s.", channelName);
930
1363
  },
931
- updateCdpSession: (session) => {
932
- // Clean up the old CDP session before replacing it. This prevents session leaks when the session is swapped during token refresh or L2 recovery.
933
- removeManifestInterceptor(activeCdpSession);
934
- activeCdpSession = session;
935
- },
936
1364
  updateVariantUrl: (newUrl) => {
937
1365
  video.variantUrl = newUrl;
938
1366
  stats.totalTokenRefreshes++;
@@ -943,8 +1371,14 @@ export function createNativeProxy(options) {
943
1371
  video.fetchedSequences.clear();
944
1372
  video.lastMediaSequence = -1;
945
1373
  video.tokenRefreshPending = true;
1374
+ // Evict decryption keys the prior CDN session referenced but the live manifests no longer do. A token rotation typically retires the old session's key URL,
1375
+ // so without this the per-URL key cache would grow by one dead entry per refresh over the stream's lifetime.
1376
+ pruneStreamKeyCache();
946
1377
  LOG.debug("native:proxy", "Variant URL updated for %s. Segment tracking reset.", channelName);
947
- }
1378
+ },
1379
+ // TC39 explicit resource management hook aliasing stop(), so the proxy is a self-disposing node consumable via the protocol (a DisposableStack or "using"). The
1380
+ // explicit teardown in disposeStreamResources calls stop() directly, following the codebase convention that explicit call sites use the readable verb.
1381
+ [Symbol.dispose]: stop
948
1382
  };
949
1383
  }
950
1384
  //# sourceMappingURL=proxy.js.map