prismcast 1.10.2 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (693) hide show
  1. package/dist/app.d.ts +6 -3
  2. package/dist/app.d.ts.map +1 -0
  3. package/dist/app.js +140 -86
  4. package/dist/app.js.map +1 -1
  5. package/dist/browser/blockedPage.d.ts +80 -0
  6. package/dist/browser/blockedPage.d.ts.map +1 -0
  7. package/dist/browser/blockedPage.js +209 -0
  8. package/dist/browser/blockedPage.js.map +1 -0
  9. package/dist/browser/browserSupervisor.d.ts +80 -0
  10. package/dist/browser/browserSupervisor.d.ts.map +1 -0
  11. package/dist/browser/browserSupervisor.js +186 -0
  12. package/dist/browser/browserSupervisor.js.map +1 -0
  13. package/dist/browser/cdp.d.ts +1 -0
  14. package/dist/browser/cdp.d.ts.map +1 -0
  15. package/dist/browser/cdp.js +6 -5
  16. package/dist/browser/cdp.js.map +1 -1
  17. package/dist/browser/channelSelection.d.ts +45 -14
  18. package/dist/browser/channelSelection.d.ts.map +1 -0
  19. package/dist/browser/channelSelection.js +81 -18
  20. package/dist/browser/channelSelection.js.map +1 -1
  21. package/dist/browser/consent.d.ts +152 -0
  22. package/dist/browser/consent.d.ts.map +1 -0
  23. package/dist/browser/consent.js +418 -0
  24. package/dist/browser/consent.js.map +1 -0
  25. package/dist/browser/display.d.ts +1 -0
  26. package/dist/browser/display.d.ts.map +1 -0
  27. package/dist/browser/hlsPlaylistObserver.d.ts +55 -0
  28. package/dist/browser/hlsPlaylistObserver.d.ts.map +1 -0
  29. package/dist/browser/hlsPlaylistObserver.js +132 -0
  30. package/dist/browser/hlsPlaylistObserver.js.map +1 -0
  31. package/dist/browser/index.d.ts +67 -40
  32. package/dist/browser/index.d.ts.map +1 -0
  33. package/dist/browser/index.js +508 -300
  34. package/dist/browser/index.js.map +1 -1
  35. package/dist/browser/launchGovernor.d.ts +89 -0
  36. package/dist/browser/launchGovernor.d.ts.map +1 -0
  37. package/dist/browser/launchGovernor.js +96 -0
  38. package/dist/browser/launchGovernor.js.map +1 -0
  39. package/dist/browser/login.d.ts +10 -2
  40. package/dist/browser/login.d.ts.map +1 -0
  41. package/dist/browser/login.js +36 -5
  42. package/dist/browser/login.js.map +1 -1
  43. package/dist/browser/manifestInterceptor.d.ts +102 -49
  44. package/dist/browser/manifestInterceptor.d.ts.map +1 -0
  45. package/dist/browser/manifestInterceptor.js +219 -237
  46. package/dist/browser/manifestInterceptor.js.map +1 -1
  47. package/dist/browser/pageStaleness.d.ts +30 -0
  48. package/dist/browser/pageStaleness.d.ts.map +1 -0
  49. package/dist/browser/pageStaleness.js +76 -0
  50. package/dist/browser/pageStaleness.js.map +1 -0
  51. package/dist/browser/precaching.d.ts +95 -3
  52. package/dist/browser/precaching.d.ts.map +1 -0
  53. package/dist/browser/precaching.js +499 -65
  54. package/dist/browser/precaching.js.map +1 -1
  55. package/dist/browser/tabNetworkObserver.d.ts +49 -0
  56. package/dist/browser/tabNetworkObserver.d.ts.map +1 -0
  57. package/dist/browser/tabNetworkObserver.js +144 -0
  58. package/dist/browser/tabNetworkObserver.js.map +1 -0
  59. package/dist/browser/tuning/cache.d.ts +61 -0
  60. package/dist/browser/tuning/cache.d.ts.map +1 -0
  61. package/dist/browser/tuning/cache.js +62 -0
  62. package/dist/browser/tuning/cache.js.map +1 -0
  63. package/dist/browser/tuning/comcastPolymer.d.ts +3 -2
  64. package/dist/browser/tuning/comcastPolymer.d.ts.map +1 -0
  65. package/dist/browser/tuning/comcastPolymer.js +79 -102
  66. package/dist/browser/tuning/comcastPolymer.js.map +1 -1
  67. package/dist/browser/tuning/cox.d.ts +1 -0
  68. package/dist/browser/tuning/cox.d.ts.map +1 -0
  69. package/dist/browser/tuning/directv.d.ts +1 -0
  70. package/dist/browser/tuning/directv.d.ts.map +1 -0
  71. package/dist/browser/tuning/directv.js +52 -46
  72. package/dist/browser/tuning/directv.js.map +1 -1
  73. package/dist/browser/tuning/fox.d.ts +1 -0
  74. package/dist/browser/tuning/fox.d.ts.map +1 -0
  75. package/dist/browser/tuning/fox.js +13 -12
  76. package/dist/browser/tuning/fox.js.map +1 -1
  77. package/dist/browser/tuning/gridSearch.d.ts +68 -0
  78. package/dist/browser/tuning/gridSearch.d.ts.map +1 -0
  79. package/dist/browser/tuning/gridSearch.js +85 -0
  80. package/dist/browser/tuning/gridSearch.js.map +1 -0
  81. package/dist/browser/tuning/hbo.d.ts +1 -0
  82. package/dist/browser/tuning/hbo.d.ts.map +1 -0
  83. package/dist/browser/tuning/hbo.js +73 -45
  84. package/dist/browser/tuning/hbo.js.map +1 -1
  85. package/dist/browser/tuning/hulu.d.ts +1 -0
  86. package/dist/browser/tuning/hulu.d.ts.map +1 -0
  87. package/dist/browser/tuning/hulu.js +128 -173
  88. package/dist/browser/tuning/hulu.js.map +1 -1
  89. package/dist/browser/tuning/shared.d.ts +75 -5
  90. package/dist/browser/tuning/shared.d.ts.map +1 -0
  91. package/dist/browser/tuning/shared.js +216 -6
  92. package/dist/browser/tuning/shared.js.map +1 -1
  93. package/dist/browser/tuning/sling.d.ts +1 -0
  94. package/dist/browser/tuning/sling.d.ts.map +1 -0
  95. package/dist/browser/tuning/sling.js +105 -126
  96. package/dist/browser/tuning/sling.js.map +1 -1
  97. package/dist/browser/tuning/spectrum.d.ts +1 -0
  98. package/dist/browser/tuning/spectrum.d.ts.map +1 -0
  99. package/dist/browser/tuning/spectrum.js +80 -125
  100. package/dist/browser/tuning/spectrum.js.map +1 -1
  101. package/dist/browser/tuning/thumbnailRow.d.ts +1 -0
  102. package/dist/browser/tuning/thumbnailRow.d.ts.map +1 -0
  103. package/dist/browser/tuning/thumbnailRow.js +8 -2
  104. package/dist/browser/tuning/thumbnailRow.js.map +1 -1
  105. package/dist/browser/tuning/tileClick.d.ts +1 -0
  106. package/dist/browser/tuning/tileClick.d.ts.map +1 -0
  107. package/dist/browser/tuning/tileClick.js +3 -2
  108. package/dist/browser/tuning/tileClick.js.map +1 -1
  109. package/dist/browser/tuning/xfinity.d.ts +1 -0
  110. package/dist/browser/tuning/xfinity.d.ts.map +1 -0
  111. package/dist/browser/tuning/youtubeTv.d.ts +1 -0
  112. package/dist/browser/tuning/youtubeTv.d.ts.map +1 -0
  113. package/dist/browser/tuning/youtubeTv.js +74 -127
  114. package/dist/browser/tuning/youtubeTv.js.map +1 -1
  115. package/dist/browser/video.d.ts +54 -14
  116. package/dist/browser/video.d.ts.map +1 -0
  117. package/dist/browser/video.js +296 -138
  118. package/dist/browser/video.js.map +1 -1
  119. package/dist/channels/index.d.ts +1 -0
  120. package/dist/channels/index.d.ts.map +1 -0
  121. package/dist/channels/index.js +12 -9
  122. package/dist/channels/index.js.map +1 -1
  123. package/dist/config/channelForm.d.ts +15 -1
  124. package/dist/config/channelForm.d.ts.map +1 -0
  125. package/dist/config/channelForm.js +28 -12
  126. package/dist/config/channelForm.js.map +1 -1
  127. package/dist/config/consistencyProbe.d.ts +1 -0
  128. package/dist/config/consistencyProbe.d.ts.map +1 -0
  129. package/dist/config/consistencyProbe.js +27 -21
  130. package/dist/config/consistencyProbe.js.map +1 -1
  131. package/dist/config/health.d.ts +41 -8
  132. package/dist/config/health.d.ts.map +1 -0
  133. package/dist/config/health.js +189 -38
  134. package/dist/config/health.js.map +1 -1
  135. package/dist/config/index.d.ts +55 -3
  136. package/dist/config/index.d.ts.map +1 -0
  137. package/dist/config/index.js +320 -69
  138. package/dist/config/index.js.map +1 -1
  139. package/dist/config/paths.d.ts +10 -9
  140. package/dist/config/paths.d.ts.map +1 -0
  141. package/dist/config/paths.js +11 -11
  142. package/dist/config/paths.js.map +1 -1
  143. package/dist/config/persistence.context.d.ts +1 -0
  144. package/dist/config/persistence.context.d.ts.map +1 -0
  145. package/dist/config/persistence.d.ts +13 -10
  146. package/dist/config/persistence.d.ts.map +1 -0
  147. package/dist/config/persistence.js +40 -21
  148. package/dist/config/persistence.js.map +1 -1
  149. package/dist/config/presets.d.ts +1 -0
  150. package/dist/config/presets.d.ts.map +1 -0
  151. package/dist/config/profiles.d.ts +22 -15
  152. package/dist/config/profiles.d.ts.map +1 -0
  153. package/dist/config/profiles.js +81 -47
  154. package/dist/config/profiles.js.map +1 -1
  155. package/dist/config/providerLineups.d.ts +47 -0
  156. package/dist/config/providerLineups.d.ts.map +1 -0
  157. package/dist/config/providerLineups.js +137 -0
  158. package/dist/config/providerLineups.js.map +1 -0
  159. package/dist/config/reactivity.d.ts +84 -0
  160. package/dist/config/reactivity.d.ts.map +1 -0
  161. package/dist/config/reactivity.js +226 -0
  162. package/dist/config/reactivity.js.map +1 -0
  163. package/dist/config/servicePacks.d.ts +18 -2
  164. package/dist/config/servicePacks.d.ts.map +1 -0
  165. package/dist/config/servicePacks.js +53 -11
  166. package/dist/config/servicePacks.js.map +1 -1
  167. package/dist/config/services.d.ts +21 -15
  168. package/dist/config/services.d.ts.map +1 -0
  169. package/dist/config/services.js +35 -24
  170. package/dist/config/services.js.map +1 -1
  171. package/dist/config/sites.d.ts +6 -4
  172. package/dist/config/sites.d.ts.map +1 -0
  173. package/dist/config/sites.js +15 -11
  174. package/dist/config/sites.js.map +1 -1
  175. package/dist/config/userChannels.d.ts +129 -30
  176. package/dist/config/userChannels.d.ts.map +1 -0
  177. package/dist/config/userChannels.js +333 -68
  178. package/dist/config/userChannels.js.map +1 -1
  179. package/dist/config/userConfig.d.ts +14 -9
  180. package/dist/config/userConfig.d.ts.map +1 -0
  181. package/dist/config/userConfig.js +87 -25
  182. package/dist/config/userConfig.js.map +1 -1
  183. package/dist/config/userProfiles.d.ts +10 -6
  184. package/dist/config/userProfiles.d.ts.map +1 -0
  185. package/dist/config/userProfiles.js +64 -39
  186. package/dist/config/userProfiles.js.map +1 -1
  187. package/dist/hdhr/channelMap.d.ts +1 -0
  188. package/dist/hdhr/channelMap.d.ts.map +1 -0
  189. package/dist/hdhr/deviceId.d.ts +1 -0
  190. package/dist/hdhr/deviceId.d.ts.map +1 -0
  191. package/dist/hdhr/discover.d.ts +1 -0
  192. package/dist/hdhr/discover.d.ts.map +1 -0
  193. package/dist/hdhr/discover.js +48 -47
  194. package/dist/hdhr/discover.js.map +1 -1
  195. package/dist/hdhr/getHandlers.d.ts +29 -0
  196. package/dist/hdhr/getHandlers.d.ts.map +1 -0
  197. package/dist/hdhr/getHandlers.js +196 -0
  198. package/dist/hdhr/getHandlers.js.map +1 -0
  199. package/dist/hdhr/identity.d.ts +36 -0
  200. package/dist/hdhr/identity.d.ts.map +1 -0
  201. package/dist/hdhr/identity.js +51 -0
  202. package/dist/hdhr/identity.js.map +1 -0
  203. package/dist/hdhr/index.d.ts +22 -4
  204. package/dist/hdhr/index.d.ts.map +1 -0
  205. package/dist/hdhr/index.js +229 -54
  206. package/dist/hdhr/index.js.map +1 -1
  207. package/dist/hdhr/protocol.d.ts +75 -0
  208. package/dist/hdhr/protocol.d.ts.map +1 -0
  209. package/dist/hdhr/protocol.js +264 -0
  210. package/dist/hdhr/protocol.js.map +1 -0
  211. package/dist/hdhr/tunerState.d.ts +24 -0
  212. package/dist/hdhr/tunerState.d.ts.map +1 -0
  213. package/dist/hdhr/tunerState.js +67 -0
  214. package/dist/hdhr/tunerState.js.map +1 -0
  215. package/dist/hdhr/udp.d.ts +43 -0
  216. package/dist/hdhr/udp.d.ts.map +1 -0
  217. package/dist/hdhr/udp.js +277 -0
  218. package/dist/hdhr/udp.js.map +1 -0
  219. package/dist/identity.d.ts +1 -0
  220. package/dist/identity.d.ts.map +1 -0
  221. package/dist/identity.js +13 -11
  222. package/dist/identity.js.map +1 -1
  223. package/dist/index.d.ts +1 -0
  224. package/dist/index.d.ts.map +1 -0
  225. package/dist/index.js +11 -12
  226. package/dist/index.js.map +1 -1
  227. package/dist/native/codecInference.d.ts +1 -0
  228. package/dist/native/codecInference.d.ts.map +1 -0
  229. package/dist/native/codecInference.js +50 -5
  230. package/dist/native/codecInference.js.map +1 -1
  231. package/dist/native/decrypt.d.ts +6 -2
  232. package/dist/native/decrypt.d.ts.map +1 -0
  233. package/dist/native/decrypt.js +15 -8
  234. package/dist/native/decrypt.js.map +1 -1
  235. package/dist/native/index.d.ts +40 -17
  236. package/dist/native/index.d.ts.map +1 -0
  237. package/dist/native/index.js +281 -120
  238. package/dist/native/index.js.map +1 -1
  239. package/dist/native/probe.d.ts +114 -18
  240. package/dist/native/probe.d.ts.map +1 -0
  241. package/dist/native/probe.js +452 -115
  242. package/dist/native/probe.js.map +1 -1
  243. package/dist/native/proxy.d.ts +57 -5
  244. package/dist/native/proxy.d.ts.map +1 -0
  245. package/dist/native/proxy.js +604 -170
  246. package/dist/native/proxy.js.map +1 -1
  247. package/dist/native/tokenExpiry.d.ts +1 -0
  248. package/dist/native/tokenExpiry.d.ts.map +1 -0
  249. package/dist/native/tokenExpiry.js +1 -1
  250. package/dist/routes/assets.d.ts +1 -0
  251. package/dist/routes/assets.d.ts.map +1 -0
  252. package/dist/routes/assets.js +4 -2
  253. package/dist/routes/assets.js.map +1 -1
  254. package/dist/routes/auth.d.ts +1 -0
  255. package/dist/routes/auth.d.ts.map +1 -0
  256. package/dist/routes/auth.js +0 -1
  257. package/dist/routes/auth.js.map +1 -1
  258. package/dist/routes/cdp.d.ts +5 -5
  259. package/dist/routes/cdp.d.ts.map +1 -0
  260. package/dist/routes/cdp.js +59 -20
  261. package/dist/routes/cdp.js.map +1 -1
  262. package/dist/routes/channels.d.ts +1 -0
  263. package/dist/routes/channels.d.ts.map +1 -0
  264. package/dist/routes/channels.js +2 -1
  265. package/dist/routes/channels.js.map +1 -1
  266. package/dist/routes/clientActions.d.ts +98 -0
  267. package/dist/routes/clientActions.d.ts.map +1 -0
  268. package/dist/routes/clientActions.js +113 -0
  269. package/dist/routes/clientActions.js.map +1 -0
  270. package/dist/routes/components.d.ts +5 -3
  271. package/dist/routes/components.d.ts.map +1 -0
  272. package/dist/routes/components.js +36 -39
  273. package/dist/routes/components.js.map +1 -1
  274. package/dist/routes/config/channels/endpoints/browse.d.ts +1 -0
  275. package/dist/routes/config/channels/endpoints/browse.d.ts.map +1 -0
  276. package/dist/routes/config/channels/endpoints/browse.js +23 -10
  277. package/dist/routes/config/channels/endpoints/browse.js.map +1 -1
  278. package/dist/routes/config/channels/endpoints/bulk.d.ts +1 -0
  279. package/dist/routes/config/channels/endpoints/bulk.d.ts.map +1 -0
  280. package/dist/routes/config/channels/endpoints/bulk.js +5 -3
  281. package/dist/routes/config/channels/endpoints/bulk.js.map +1 -1
  282. package/dist/routes/config/channels/endpoints/crud.d.ts +1 -0
  283. package/dist/routes/config/channels/endpoints/crud.d.ts.map +1 -0
  284. package/dist/routes/config/channels/endpoints/crud.js +85 -93
  285. package/dist/routes/config/channels/endpoints/crud.js.map +1 -1
  286. package/dist/routes/config/channels/endpoints/importExport.d.ts +1 -0
  287. package/dist/routes/config/channels/endpoints/importExport.d.ts.map +1 -0
  288. package/dist/routes/config/channels/endpoints/importExport.js +6 -3
  289. package/dist/routes/config/channels/endpoints/importExport.js.map +1 -1
  290. package/dist/routes/config/channels/endpoints/predefined.d.ts +1 -0
  291. package/dist/routes/config/channels/endpoints/predefined.d.ts.map +1 -0
  292. package/dist/routes/config/channels/endpoints/prefs.d.ts +1 -0
  293. package/dist/routes/config/channels/endpoints/prefs.d.ts.map +1 -0
  294. package/dist/routes/config/channels/endpoints/prefs.js +2 -1
  295. package/dist/routes/config/channels/endpoints/prefs.js.map +1 -1
  296. package/dist/routes/config/channels/endpoints/service.d.ts +2 -1
  297. package/dist/routes/config/channels/endpoints/service.d.ts.map +1 -0
  298. package/dist/routes/config/channels/endpoints/service.js +3 -1
  299. package/dist/routes/config/channels/endpoints/service.js.map +1 -1
  300. package/dist/routes/config/channels/endpoints/tags.d.ts +1 -0
  301. package/dist/routes/config/channels/endpoints/tags.d.ts.map +1 -0
  302. package/dist/routes/config/channels/endpoints/tags.js +13 -7
  303. package/dist/routes/config/channels/endpoints/tags.js.map +1 -1
  304. package/dist/routes/config/channels/healthBridge.d.ts +24 -0
  305. package/dist/routes/config/channels/healthBridge.d.ts.map +1 -0
  306. package/dist/routes/config/channels/healthBridge.js +71 -0
  307. package/dist/routes/config/channels/healthBridge.js.map +1 -0
  308. package/dist/routes/config/channels/http/handler.d.ts +1 -0
  309. package/dist/routes/config/channels/http/handler.d.ts.map +1 -0
  310. package/dist/routes/config/channels/http/playlistHint.d.ts +2 -1
  311. package/dist/routes/config/channels/http/playlistHint.d.ts.map +1 -0
  312. package/dist/routes/config/channels/http/playlistHint.js +16 -2
  313. package/dist/routes/config/channels/http/playlistHint.js.map +1 -1
  314. package/dist/routes/config/channels/http/serviceWarning.d.ts +4 -2
  315. package/dist/routes/config/channels/http/serviceWarning.d.ts.map +1 -0
  316. package/dist/routes/config/channels/http/serviceWarning.js +3 -2
  317. package/dist/routes/config/channels/http/serviceWarning.js.map +1 -1
  318. package/dist/routes/config/channels/index.d.ts +1 -0
  319. package/dist/routes/config/channels/index.d.ts.map +1 -0
  320. package/dist/routes/config/channels/setup.d.ts +1 -0
  321. package/dist/routes/config/channels/setup.d.ts.map +1 -0
  322. package/dist/routes/config/channels/table.d.ts +3 -3
  323. package/dist/routes/config/channels/table.d.ts.map +1 -0
  324. package/dist/routes/config/channels/table.js +148 -107
  325. package/dist/routes/config/channels/table.js.map +1 -1
  326. package/dist/routes/config/http/envelope.d.ts +1 -0
  327. package/dist/routes/config/http/envelope.d.ts.map +1 -0
  328. package/dist/routes/config/index.d.ts +35 -3
  329. package/dist/routes/config/index.d.ts.map +1 -0
  330. package/dist/routes/config/index.js +51 -2
  331. package/dist/routes/config/index.js.map +1 -1
  332. package/dist/routes/config/services.d.ts +5 -4
  333. package/dist/routes/config/services.d.ts.map +1 -0
  334. package/dist/routes/config/services.js +57 -43
  335. package/dist/routes/config/services.js.map +1 -1
  336. package/dist/routes/config/settings.d.ts +1 -0
  337. package/dist/routes/config/settings.d.ts.map +1 -0
  338. package/dist/routes/config/settings.js +98 -49
  339. package/dist/routes/config/settings.js.map +1 -1
  340. package/dist/routes/debug.d.ts +1 -0
  341. package/dist/routes/debug.d.ts.map +1 -0
  342. package/dist/routes/debug.js +277 -139
  343. package/dist/routes/debug.js.map +1 -1
  344. package/dist/routes/health.d.ts +39 -1
  345. package/dist/routes/health.d.ts.map +1 -0
  346. package/dist/routes/health.js +33 -27
  347. package/dist/routes/health.js.map +1 -1
  348. package/dist/routes/hls.d.ts +1 -0
  349. package/dist/routes/hls.d.ts.map +1 -0
  350. package/dist/routes/icons.d.ts +1 -0
  351. package/dist/routes/icons.d.ts.map +1 -0
  352. package/dist/routes/icons.js +1 -1
  353. package/dist/routes/index.d.ts +1 -0
  354. package/dist/routes/index.d.ts.map +1 -0
  355. package/dist/routes/logs.d.ts +1 -0
  356. package/dist/routes/logs.d.ts.map +1 -0
  357. package/dist/routes/logs.js +14 -4
  358. package/dist/routes/logs.js.map +1 -1
  359. package/dist/routes/mpegts.d.ts +1 -0
  360. package/dist/routes/mpegts.d.ts.map +1 -0
  361. package/dist/routes/play.d.ts +1 -0
  362. package/dist/routes/play.d.ts.map +1 -0
  363. package/dist/routes/playlist.d.ts +1 -0
  364. package/dist/routes/playlist.d.ts.map +1 -0
  365. package/dist/routes/playlist.js +7 -2
  366. package/dist/routes/playlist.js.map +1 -1
  367. package/dist/routes/root/content.d.ts +8 -5
  368. package/dist/routes/root/content.d.ts.map +1 -0
  369. package/dist/routes/root/content.js +29 -26
  370. package/dist/routes/root/content.js.map +1 -1
  371. package/dist/routes/root/index.d.ts +2 -1
  372. package/dist/routes/root/index.d.ts.map +1 -0
  373. package/dist/routes/root/index.js +26 -21
  374. package/dist/routes/root/index.js.map +1 -1
  375. package/dist/routes/root/scripts/channels.d.ts +6 -0
  376. package/dist/routes/root/scripts/channels.d.ts.map +1 -0
  377. package/dist/routes/root/scripts/channels.js +197 -118
  378. package/dist/routes/root/scripts/channels.js.map +1 -1
  379. package/dist/routes/root/scripts/clientEscape.d.ts +22 -0
  380. package/dist/routes/root/scripts/clientEscape.d.ts.map +1 -0
  381. package/dist/routes/root/scripts/clientEscape.js +41 -0
  382. package/dist/routes/root/scripts/clientEscape.js.map +1 -0
  383. package/dist/routes/root/scripts/clientUrl.d.ts +23 -0
  384. package/dist/routes/root/scripts/clientUrl.d.ts.map +1 -0
  385. package/dist/routes/root/scripts/clientUrl.js +44 -0
  386. package/dist/routes/root/scripts/clientUrl.js.map +1 -0
  387. package/dist/routes/root/scripts/config.d.ts +6 -0
  388. package/dist/routes/root/scripts/config.d.ts.map +1 -0
  389. package/dist/routes/root/scripts/config.js +110 -24
  390. package/dist/routes/root/scripts/config.js.map +1 -1
  391. package/dist/routes/root/scripts/index.d.ts +1 -0
  392. package/dist/routes/root/scripts/index.d.ts.map +1 -0
  393. package/dist/routes/root/scripts/shared.d.ts +1 -0
  394. package/dist/routes/root/scripts/shared.d.ts.map +1 -0
  395. package/dist/routes/root/scripts/shared.js +112 -26
  396. package/dist/routes/root/scripts/shared.js.map +1 -1
  397. package/dist/routes/root/scripts/status.d.ts +1 -0
  398. package/dist/routes/root/scripts/status.d.ts.map +1 -0
  399. package/dist/routes/root/scripts/status.handlers.d.ts +23 -33
  400. package/dist/routes/root/scripts/status.handlers.d.ts.map +1 -0
  401. package/dist/routes/root/scripts/status.handlers.js +46 -92
  402. package/dist/routes/root/scripts/status.handlers.js.map +1 -1
  403. package/dist/routes/root/scripts/status.js +21 -8
  404. package/dist/routes/root/scripts/status.js.map +1 -1
  405. package/dist/routes/root/styles.d.ts +1 -0
  406. package/dist/routes/root/styles.d.ts.map +1 -0
  407. package/dist/routes/root/styles.js +7 -4
  408. package/dist/routes/root/styles.js.map +1 -1
  409. package/dist/routes/services.d.ts +13 -1
  410. package/dist/routes/services.d.ts.map +1 -0
  411. package/dist/routes/services.js +81 -68
  412. package/dist/routes/services.js.map +1 -1
  413. package/dist/routes/sse.d.ts +1 -0
  414. package/dist/routes/sse.d.ts.map +1 -0
  415. package/dist/routes/sse.js +1 -1
  416. package/dist/routes/sse.js.map +1 -1
  417. package/dist/routes/streams.d.ts +2 -1
  418. package/dist/routes/streams.d.ts.map +1 -0
  419. package/dist/routes/streams.js +6 -4
  420. package/dist/routes/streams.js.map +1 -1
  421. package/dist/routes/theme.d.ts +3 -2
  422. package/dist/routes/theme.d.ts.map +1 -0
  423. package/dist/routes/theme.js +6 -5
  424. package/dist/routes/theme.js.map +1 -1
  425. package/dist/routes/ui.d.ts +3 -1
  426. package/dist/routes/ui.d.ts.map +1 -0
  427. package/dist/routes/ui.js +16 -4
  428. package/dist/routes/ui.js.map +1 -1
  429. package/dist/routes/upgrade.d.ts +1 -0
  430. package/dist/routes/upgrade.d.ts.map +1 -0
  431. package/dist/routes/upgrade.js +6 -2
  432. package/dist/routes/upgrade.js.map +1 -1
  433. package/dist/service/commands.context.d.ts +1 -0
  434. package/dist/service/commands.context.d.ts.map +1 -0
  435. package/dist/service/commands.context.js +4 -3
  436. package/dist/service/commands.context.js.map +1 -1
  437. package/dist/service/commands.d.ts +1 -0
  438. package/dist/service/commands.d.ts.map +1 -0
  439. package/dist/service/commands.js +10 -13
  440. package/dist/service/commands.js.map +1 -1
  441. package/dist/service/generators.context.d.ts +1 -0
  442. package/dist/service/generators.context.d.ts.map +1 -0
  443. package/dist/service/generators.context.js +1 -1
  444. package/dist/service/generators.context.js.map +1 -1
  445. package/dist/service/generators.d.ts +4 -3
  446. package/dist/service/generators.d.ts.map +1 -0
  447. package/dist/service/generators.js +111 -31
  448. package/dist/service/generators.js.map +1 -1
  449. package/dist/service/index.d.ts +1 -0
  450. package/dist/service/index.d.ts.map +1 -0
  451. package/dist/streaming/captureLock.d.ts +44 -0
  452. package/dist/streaming/captureLock.d.ts.map +1 -0
  453. package/dist/streaming/captureLock.js +95 -0
  454. package/dist/streaming/captureLock.js.map +1 -0
  455. package/dist/streaming/captureSession.d.ts +31 -0
  456. package/dist/streaming/captureSession.d.ts.map +1 -0
  457. package/dist/streaming/captureSession.js +54 -0
  458. package/dist/streaming/captureSession.js.map +1 -0
  459. package/dist/streaming/clients.d.ts +1 -0
  460. package/dist/streaming/clients.d.ts.map +1 -0
  461. package/dist/streaming/codec.d.ts +1 -0
  462. package/dist/streaming/codec.d.ts.map +1 -0
  463. package/dist/streaming/fmp4Segmenter.d.ts +29 -2
  464. package/dist/streaming/fmp4Segmenter.d.ts.map +1 -0
  465. package/dist/streaming/fmp4Segmenter.js +79 -26
  466. package/dist/streaming/fmp4Segmenter.js.map +1 -1
  467. package/dist/streaming/hls.d.ts +18 -7
  468. package/dist/streaming/hls.d.ts.map +1 -0
  469. package/dist/streaming/hls.js +220 -120
  470. package/dist/streaming/hls.js.map +1 -1
  471. package/dist/streaming/hlsResume.d.ts +1 -0
  472. package/dist/streaming/hlsResume.d.ts.map +1 -0
  473. package/dist/streaming/hlsResume.js +1 -1
  474. package/dist/streaming/hlsResume.js.map +1 -1
  475. package/dist/streaming/hlsSegments.d.ts +59 -2
  476. package/dist/streaming/hlsSegments.d.ts.map +1 -0
  477. package/dist/streaming/hlsSegments.js +147 -17
  478. package/dist/streaming/hlsSegments.js.map +1 -1
  479. package/dist/streaming/lifecycle.d.ts +4 -1
  480. package/dist/streaming/lifecycle.d.ts.map +1 -0
  481. package/dist/streaming/lifecycle.js +65 -85
  482. package/dist/streaming/lifecycle.js.map +1 -1
  483. package/dist/streaming/monitor.d.ts +6 -4
  484. package/dist/streaming/monitor.d.ts.map +1 -0
  485. package/dist/streaming/monitor.js +343 -145
  486. package/dist/streaming/monitor.js.map +1 -1
  487. package/dist/streaming/mp4Parser.d.ts +5 -4
  488. package/dist/streaming/mp4Parser.d.ts.map +1 -0
  489. package/dist/streaming/mp4Parser.js +36 -13
  490. package/dist/streaming/mp4Parser.js.map +1 -1
  491. package/dist/streaming/mpegts.d.ts +16 -0
  492. package/dist/streaming/mpegts.d.ts.map +1 -0
  493. package/dist/streaming/mpegts.js +61 -17
  494. package/dist/streaming/mpegts.js.map +1 -1
  495. package/dist/streaming/playlistBuilder.d.ts +1 -0
  496. package/dist/streaming/playlistBuilder.d.ts.map +1 -0
  497. package/dist/streaming/preroll.d.ts +32 -5
  498. package/dist/streaming/preroll.d.ts.map +1 -0
  499. package/dist/streaming/preroll.js +70 -32
  500. package/dist/streaming/preroll.js.map +1 -1
  501. package/dist/streaming/pretune.d.ts +26 -1
  502. package/dist/streaming/pretune.d.ts.map +1 -0
  503. package/dist/streaming/pretune.js +22 -24
  504. package/dist/streaming/pretune.js.map +1 -1
  505. package/dist/streaming/pretuneTimers.d.ts +24 -0
  506. package/dist/streaming/pretuneTimers.d.ts.map +1 -0
  507. package/dist/streaming/pretuneTimers.js +56 -0
  508. package/dist/streaming/pretuneTimers.js.map +1 -0
  509. package/dist/streaming/recovery.d.ts +139 -5
  510. package/dist/streaming/recovery.d.ts.map +1 -0
  511. package/dist/streaming/recovery.js +160 -16
  512. package/dist/streaming/recovery.js.map +1 -1
  513. package/dist/streaming/registry.d.ts +27 -9
  514. package/dist/streaming/registry.d.ts.map +1 -0
  515. package/dist/streaming/registry.js +21 -3
  516. package/dist/streaming/registry.js.map +1 -1
  517. package/dist/streaming/setup.d.ts +157 -29
  518. package/dist/streaming/setup.d.ts.map +1 -0
  519. package/dist/streaming/setup.js +1034 -458
  520. package/dist/streaming/setup.js.map +1 -1
  521. package/dist/streaming/showInfo.d.ts +21 -1
  522. package/dist/streaming/showInfo.d.ts.map +1 -0
  523. package/dist/streaming/showInfo.js +31 -16
  524. package/dist/streaming/showInfo.js.map +1 -1
  525. package/dist/streaming/statusEmitter.d.ts +10 -10
  526. package/dist/streaming/statusEmitter.d.ts.map +1 -0
  527. package/dist/streaming/statusEmitter.js +5 -8
  528. package/dist/streaming/statusEmitter.js.map +1 -1
  529. package/dist/types/channels.d.ts +16 -4
  530. package/dist/types/channels.d.ts.map +1 -0
  531. package/dist/types/channels.js +4 -2
  532. package/dist/types/channels.js.map +1 -1
  533. package/dist/types/config.d.ts +16 -5
  534. package/dist/types/config.d.ts.map +1 -0
  535. package/dist/types/index.d.ts +3 -2
  536. package/dist/types/index.d.ts.map +1 -0
  537. package/dist/types/profiles.d.ts +6 -4
  538. package/dist/types/profiles.d.ts.map +1 -0
  539. package/dist/types/selection.d.ts +31 -3
  540. package/dist/types/selection.d.ts.map +1 -0
  541. package/dist/types/selection.js +4 -2
  542. package/dist/types/selection.js.map +1 -1
  543. package/dist/types/shared.d.ts +1 -0
  544. package/dist/types/shared.d.ts.map +1 -0
  545. package/dist/types/streaming.d.ts +8 -0
  546. package/dist/types/streaming.d.ts.map +1 -0
  547. package/dist/upgrade/commands.context.d.ts +1 -0
  548. package/dist/upgrade/commands.context.d.ts.map +1 -0
  549. package/dist/upgrade/commands.context.js +14 -10
  550. package/dist/upgrade/commands.context.js.map +1 -1
  551. package/dist/upgrade/commands.d.ts +8 -15
  552. package/dist/upgrade/commands.d.ts.map +1 -0
  553. package/dist/upgrade/commands.js +27 -28
  554. package/dist/upgrade/commands.js.map +1 -1
  555. package/dist/upgrade/detection.context.d.ts +1 -0
  556. package/dist/upgrade/detection.context.d.ts.map +1 -0
  557. package/dist/upgrade/detection.context.js +4 -1
  558. package/dist/upgrade/detection.context.js.map +1 -1
  559. package/dist/upgrade/detection.d.ts +9 -7
  560. package/dist/upgrade/detection.d.ts.map +1 -0
  561. package/dist/upgrade/detection.js +23 -54
  562. package/dist/upgrade/detection.js.map +1 -1
  563. package/dist/upgrade/index.d.ts +2 -0
  564. package/dist/upgrade/index.d.ts.map +1 -0
  565. package/dist/upgrade/index.js +1 -0
  566. package/dist/upgrade/index.js.map +1 -1
  567. package/dist/upgrade/lifecycle.context.d.ts +20 -0
  568. package/dist/upgrade/lifecycle.context.d.ts.map +1 -0
  569. package/dist/upgrade/lifecycle.context.js +47 -0
  570. package/dist/upgrade/lifecycle.context.js.map +1 -0
  571. package/dist/upgrade/lifecycle.d.ts +72 -0
  572. package/dist/upgrade/lifecycle.d.ts.map +1 -0
  573. package/dist/upgrade/lifecycle.js +175 -0
  574. package/dist/upgrade/lifecycle.js.map +1 -0
  575. package/dist/upgrade/pathHandle.d.ts +30 -0
  576. package/dist/upgrade/pathHandle.d.ts.map +1 -0
  577. package/dist/upgrade/pathHandle.js +102 -0
  578. package/dist/upgrade/pathHandle.js.map +1 -0
  579. package/dist/utils/bootSession.context.d.ts +16 -0
  580. package/dist/utils/bootSession.context.d.ts.map +1 -0
  581. package/dist/utils/bootSession.context.js +60 -0
  582. package/dist/utils/bootSession.context.js.map +1 -0
  583. package/dist/utils/bootSession.d.ts +17 -0
  584. package/dist/utils/bootSession.d.ts.map +1 -0
  585. package/dist/utils/bootSession.js +20 -0
  586. package/dist/utils/bootSession.js.map +1 -0
  587. package/dist/utils/chromeFetch.d.ts +1 -0
  588. package/dist/utils/chromeFetch.d.ts.map +1 -0
  589. package/dist/utils/cliOutput.d.ts +1 -0
  590. package/dist/utils/cliOutput.d.ts.map +1 -0
  591. package/dist/utils/clock.d.ts +5 -4
  592. package/dist/utils/clock.d.ts.map +1 -0
  593. package/dist/utils/clock.js +19 -16
  594. package/dist/utils/clock.js.map +1 -1
  595. package/dist/utils/debugFilter.d.ts +9 -1
  596. package/dist/utils/debugFilter.d.ts.map +1 -0
  597. package/dist/utils/debugFilter.js +60 -33
  598. package/dist/utils/debugFilter.js.map +1 -1
  599. package/dist/utils/delay.d.ts +48 -15
  600. package/dist/utils/delay.d.ts.map +1 -0
  601. package/dist/utils/delay.js +73 -20
  602. package/dist/utils/delay.js.map +1 -1
  603. package/dist/utils/errors.d.ts +32 -0
  604. package/dist/utils/errors.d.ts.map +1 -0
  605. package/dist/utils/errors.js +45 -4
  606. package/dist/utils/errors.js.map +1 -1
  607. package/dist/utils/evaluate.d.ts +1 -0
  608. package/dist/utils/evaluate.d.ts.map +1 -0
  609. package/dist/utils/evaluate.js +29 -23
  610. package/dist/utils/evaluate.js.map +1 -1
  611. package/dist/utils/ffmpeg.context.d.ts +1 -0
  612. package/dist/utils/ffmpeg.context.d.ts.map +1 -0
  613. package/dist/utils/ffmpeg.context.js.map +1 -1
  614. package/dist/utils/ffmpeg.d.ts +29 -7
  615. package/dist/utils/ffmpeg.d.ts.map +1 -0
  616. package/dist/utils/ffmpeg.js +32 -14
  617. package/dist/utils/ffmpeg.js.map +1 -1
  618. package/dist/utils/fileLogger.d.ts +3 -1
  619. package/dist/utils/fileLogger.d.ts.map +1 -0
  620. package/dist/utils/fileLogger.js +64 -29
  621. package/dist/utils/fileLogger.js.map +1 -1
  622. package/dist/utils/format.d.ts +14 -1
  623. package/dist/utils/format.d.ts.map +1 -0
  624. package/dist/utils/format.js +19 -5
  625. package/dist/utils/format.js.map +1 -1
  626. package/dist/utils/index.d.ts +4 -0
  627. package/dist/utils/index.d.ts.map +1 -0
  628. package/dist/utils/index.js +3 -0
  629. package/dist/utils/index.js.map +1 -1
  630. package/dist/utils/logEmitter.d.ts +1 -0
  631. package/dist/utils/logEmitter.d.ts.map +1 -0
  632. package/dist/utils/logger.d.ts +1 -0
  633. package/dist/utils/logger.d.ts.map +1 -0
  634. package/dist/utils/logger.js +10 -6
  635. package/dist/utils/logger.js.map +1 -1
  636. package/dist/utils/m3u.d.ts +3 -2
  637. package/dist/utils/m3u.d.ts.map +1 -0
  638. package/dist/utils/m3u.js +4 -3
  639. package/dist/utils/m3u.js.map +1 -1
  640. package/dist/utils/markup.d.ts +13 -1
  641. package/dist/utils/markup.d.ts.map +1 -0
  642. package/dist/utils/markup.js +28 -4
  643. package/dist/utils/markup.js.map +1 -1
  644. package/dist/utils/memo.d.ts +3 -2
  645. package/dist/utils/memo.d.ts.map +1 -0
  646. package/dist/utils/memo.js +3 -3
  647. package/dist/utils/morganStream.d.ts +1 -0
  648. package/dist/utils/morganStream.d.ts.map +1 -0
  649. package/dist/utils/network.d.ts +1 -0
  650. package/dist/utils/network.d.ts.map +1 -0
  651. package/dist/utils/network.js +2 -2
  652. package/dist/utils/pid.d.ts +3 -25
  653. package/dist/utils/pid.d.ts.map +1 -0
  654. package/dist/utils/pid.js +10 -49
  655. package/dist/utils/pid.js.map +1 -1
  656. package/dist/utils/platform.d.ts +3 -1
  657. package/dist/utils/platform.d.ts.map +1 -0
  658. package/dist/utils/platform.js +2 -1
  659. package/dist/utils/platform.js.map +1 -1
  660. package/dist/utils/processInspector.context.d.ts +8 -0
  661. package/dist/utils/processInspector.context.d.ts.map +1 -0
  662. package/dist/utils/processInspector.context.js +106 -0
  663. package/dist/utils/processInspector.context.js.map +1 -0
  664. package/dist/utils/processInspector.d.ts +55 -0
  665. package/dist/utils/processInspector.d.ts.map +1 -0
  666. package/dist/utils/processInspector.js +127 -0
  667. package/dist/utils/processInspector.js.map +1 -0
  668. package/dist/utils/retry.d.ts +25 -2
  669. package/dist/utils/retry.d.ts.map +1 -0
  670. package/dist/utils/retry.js +33 -6
  671. package/dist/utils/retry.js.map +1 -1
  672. package/dist/utils/runtimeIdentity.context.d.ts +7 -0
  673. package/dist/utils/runtimeIdentity.context.d.ts.map +1 -0
  674. package/dist/utils/runtimeIdentity.context.js +43 -0
  675. package/dist/utils/runtimeIdentity.context.js.map +1 -0
  676. package/dist/utils/runtimeIdentity.d.ts +98 -0
  677. package/dist/utils/runtimeIdentity.d.ts.map +1 -0
  678. package/dist/utils/runtimeIdentity.js +181 -0
  679. package/dist/utils/runtimeIdentity.js.map +1 -0
  680. package/dist/utils/sanitize.d.ts +1 -0
  681. package/dist/utils/sanitize.d.ts.map +1 -0
  682. package/dist/utils/streamContext.d.ts +2 -1
  683. package/dist/utils/streamContext.d.ts.map +1 -0
  684. package/dist/utils/streamContext.js +1 -1
  685. package/dist/utils/timing.d.ts +2 -1
  686. package/dist/utils/timing.d.ts.map +1 -0
  687. package/dist/utils/timing.js +0 -5
  688. package/dist/utils/timing.js.map +1 -1
  689. package/dist/utils/version.d.ts +1 -0
  690. package/dist/utils/version.d.ts.map +1 -0
  691. package/dist/utils/version.js +47 -13
  692. package/dist/utils/version.js.map +1 -1
  693. package/package.json +26 -20
@@ -1,14 +1,12 @@
1
- /* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
- *
3
- * probe.ts: HLS manifest probe and media-feed normalizer.
4
- */
5
- import { LOG, chromeFetch, startTimer } from "../utils/index.js";
1
+ import { LOG, chromeFetch, startTimer, stringifySorted } from "../utils/index.js";
6
2
  import { inferMediaCodec } from "./codecInference.js";
7
3
  /* This module probes an intercepted HLS playlist URL and produces a fully described MediaFeed - the canonical input to the native proxy. The HLS spec defines
8
4
  * exactly two playlist kinds, and this module normalizes both to the same shape so downstream code does not branch on which kind arrived:
9
5
  *
10
- * - Master (multivariant) playlists declare variant streams via #EXT-X-STREAM-INF and reference media playlist URLs. We select the highest-bandwidth variant,
11
- * resolve any separate audio rendition declared via #EXT-X-MEDIA:TYPE=AUDIO, fetch the chosen variant body, and classify its encryption.
6
+ * - Master (multivariant) playlists declare variant streams via #EXT-X-STREAM-INF and reference media playlist URLs. We rank the variants by descending bandwidth
7
+ * and take the first whose body fetches, resolve any separate audio rendition declared via #EXT-X-MEDIA:TYPE=AUDIO for that variant's own audio group, and
8
+ * classify the chosen body's encryption. A caller that is already relaying a stream supplies the shape of that pipeline, which narrows the same ranked walk to
9
+ * the candidates the running relay can absorb.
12
10
  * - Media playlists declare segments directly via #EXTINF and #EXT-X-TARGETDURATION. The input URL is itself the media feed; we fetch the body once and
13
11
  * classify its encryption.
14
12
  *
@@ -25,6 +23,14 @@ import { inferMediaCodec } from "./codecInference.js";
25
23
  */
26
24
  // Timeout for individual manifest/key fetches.
27
25
  const FETCH_TIMEOUT = 10000;
26
+ // How many variants a master-playlist probe tries before giving up. Fallback covers the observed failure shape - a broken top variant with healthy siblings below
27
+ // it - while capping the tune-time worst case at three sequential fetch timeouts. The bound matters because the probe runs inside the preroll bridge window:
28
+ // capture segments are produced only once the native attempt resolves, so an unbounded crawl could outlast the preroll a client is playing while it waits. The
29
+ // cap governs both moments a stream selects a variant, the tune and every token refresh after it, so one ladder-walking policy serves the system.
30
+ const MAX_VARIANT_FALLBACK_ATTEMPTS = 3;
31
+ // The smallest segment count a tune-time probe will admit as a channel. A window holding a single segment has nothing to advance to, so it describes a fixed
32
+ // asset - a session bumper, a slate, a trailer - rather than a stream the relay can follow. Two is the floor at which a window can be observed to move at all.
33
+ const MIN_ADMISSIBLE_SEGMENT_COUNT = 2;
28
34
  /**
29
35
  * Classifies an HLS playlist body by inspecting its directives. Master detection short-circuits on the first #EXT-X-STREAM-INF tag because that directive only
30
36
  * appears in master playlists; media detection accumulates positive signals (#EXTINF, #EXT-X-TARGETDURATION) across the body. Bodies with neither signal are
@@ -50,56 +56,224 @@ export function classifyHlsPlaylist(body) {
50
56
  }
51
57
  return mediaSignal ? "media" : "unknown";
52
58
  }
53
- // Cache of encryption types keyed by channel name. Stores the classification (clear/aes128/drm) with a timestamp for TTL expiration. Variant URLs and key URLs
54
- // contain session-bound auth tokens that expire between tunes, so they must never be cached - only the stable encryption type is safe to persist across sessions.
55
- // The DRM skip optimization in setup.ts uses this cache to avoid installing the CDP interceptor for channels known to use DRM.
59
+ /**
60
+ * Extracts the value of a URI="..." attribute from a single tag line. This is the one home for the URI attribute pattern in this module: the #EXT-X-KEY key
61
+ * location, #EXT-X-MEDIA rendition URIs, and the master child walk all read a quoted URI the same way, so the pattern lives in exactly one place. Returns null when
62
+ * the line carries no URI attribute.
63
+ *
64
+ * @param line - A single trimmed HLS tag line.
65
+ * @returns The URI attribute value, or null when the line has none.
66
+ */
67
+ function uriAttribute(line) {
68
+ return /URI="([^"]+)"/.exec(line)?.[1] ?? null;
69
+ }
70
+ /**
71
+ * Resolves a single child URI against the master URL, returning null when resolution throws (a malformed relative reference). Keeps extractChildPlaylistUrls total:
72
+ * one bad child reference drops only itself rather than aborting the whole walk.
73
+ *
74
+ * @param uri - The child playlist URI (relative or absolute).
75
+ * @param masterUrl - The master manifest URL for resolving relative references.
76
+ * @returns The resolved absolute URL, or null when resolution throws.
77
+ */
78
+ function resolveChildUri(uri, masterUrl) {
79
+ try {
80
+ return resolveUrl(uri, masterUrl);
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ /**
87
+ * Enumerates every child playlist URI a master manifest declares, resolved to absolute URLs. Two child sources are walked: each #EXT-X-STREAM-INF variant, whose
88
+ * URI is the immediately following non-tag line (the same single-line walk selectVariants uses, so membership sees exactly the children variant selection would),
89
+ * and each #EXT-X-MEDIA rendition of any type that carries a URI attribute (descriptive-only renditions without a URI are skipped). probe is the single home of
90
+ * manifest-format knowledge, so this extraction lives here and feeds the HLS playlist observer's master observations; membership judgment against these children
91
+ * lives in the interceptor's selection policy, not here. Each URI resolves inside a per-entry guard so a single malformed reference skips only that entry and the
92
+ * helper never throws; the returned list is deduplicated.
93
+ *
94
+ * @param masterBody - The master manifest text.
95
+ * @param masterUrl - The master manifest URL for resolving relative child URIs.
96
+ * @returns The deduplicated absolute child playlist URLs.
97
+ */
98
+ export function extractChildPlaylistUrls(masterBody, masterUrl) {
99
+ const lines = masterBody.split("\n");
100
+ const childUrls = new Set();
101
+ for (let i = 0; i < lines.length; i++) {
102
+ const line = lines[i]?.trim() ?? "";
103
+ // A variant stream's URI is the next non-tag line, matching the walk in selectVariants exactly so membership and variant selection agree on the child set.
104
+ if (line.startsWith("#EXT-X-STREAM-INF:")) {
105
+ const variantLine = lines[i + 1]?.trim() ?? "";
106
+ if (variantLine && !variantLine.startsWith("#")) {
107
+ const resolved = resolveChildUri(variantLine, masterUrl);
108
+ if (resolved !== null) {
109
+ childUrls.add(resolved);
110
+ }
111
+ }
112
+ continue;
113
+ }
114
+ // A rendition's URI is a same-line attribute; renditions of every type participate, and descriptive-only entries without a URI are skipped.
115
+ if (line.startsWith("#EXT-X-MEDIA:")) {
116
+ const uri = uriAttribute(line);
117
+ if (uri) {
118
+ const resolved = resolveChildUri(uri, masterUrl);
119
+ if (resolved !== null) {
120
+ childUrls.add(resolved);
121
+ }
122
+ }
123
+ }
124
+ }
125
+ return Array.from(childUrls);
126
+ }
127
+ /**
128
+ * Reports whether a media playlist is live - one that may still grow. A playlist is live unless it carries an #EXT-X-ENDLIST marker (the producer has declared the
129
+ * playlist complete) or an #EXT-X-PLAYLIST-TYPE:VOD tag (a fixed, fully-available asset); an EVENT-typed playlist counts as live because a live-event playlist only
130
+ * ever appends segments. The scan is line-anchored (trim then startsWith) rather than a substring test so an ENDLIST or VOD token appearing inside a segment URI or
131
+ * comment does not flip the classification.
132
+ *
133
+ * @param mediaBody - The media playlist text.
134
+ * @returns True when the playlist is live (no ENDLIST marker and not VOD-typed).
135
+ */
136
+ export function isLiveMediaPlaylist(mediaBody) {
137
+ for (const rawLine of mediaBody.split("\n")) {
138
+ const line = rawLine.trim();
139
+ if (line.startsWith("#EXT-X-ENDLIST")) {
140
+ return false;
141
+ }
142
+ if (line.startsWith("#EXT-X-PLAYLIST-TYPE:VOD")) {
143
+ return false;
144
+ }
145
+ }
146
+ return true;
147
+ }
148
+ /**
149
+ * Counts the segments a media playlist declares. Each segment is introduced by exactly one #EXTINF duration line, so counting those lines counts the window. The
150
+ * scan is line-anchored - trim then startsWith - and tests the colon-suffixed form that a duration line always carries, so an #EXTINF token riding inside a segment
151
+ * URI, a comment, or another tag's quoted attribute value cannot inflate the count.
152
+ *
153
+ * @param mediaBody - The media playlist text.
154
+ * @returns The number of segments the playlist declares.
155
+ */
156
+ function countMediaSegments(mediaBody) {
157
+ let segments = 0;
158
+ for (const rawLine of mediaBody.split("\n")) {
159
+ if (rawLine.trim().startsWith("#EXTINF:")) {
160
+ segments++;
161
+ }
162
+ }
163
+ return segments;
164
+ }
165
+ /**
166
+ * Reports whether a pipeline shape admits a candidate feed, comparing the three axes as equals. This is the one home of the compatibility rule: the variant walk
167
+ * consults it to drop a fetched candidate cheaply, the media-only resolver consults it before paying for codec inference, and probeManifest consults it on the
168
+ * fully classified feed to make the constrained probe's guarantee.
169
+ *
170
+ * @param shape - The running consumer's compatibility envelope.
171
+ * @param candidate - The candidate's classification: its container (null when nothing has classified it), its encryption kind, and its audio topology.
172
+ * @returns True when the candidate matches the shape on every axis.
173
+ */
174
+ function shapeAdmits(shape, candidate) {
175
+ return (candidate.container === shape.container) && (candidate.encryption === shape.encryption) && (candidate.separateAudio === shape.separateAudio);
176
+ }
177
+ /**
178
+ * Builds the validity stamp for a probe-cache entry from the binding a tune resolves under. Serialization runs through stringifySorted, the house canonical
179
+ * serializer, so equal bindings produce byte-equal stamps whatever order their properties were written in...and a member whose value is undefined drops out of
180
+ * JSON entirely, which is the normalization we want: a channel that never carried a selector and one whose selector was cleared describe the same binding and
181
+ * must stamp identically.
182
+ *
183
+ * Every input is a stored configuration value, never the per-tune manifest URL, whose session tokens rotate on every tune and would defeat the cache outright.
184
+ *
185
+ * @param binding - The user-editable binding projection the classification is derived from.
186
+ * @returns The canonical stamp string for that binding.
187
+ */
188
+ export function buildProbeCacheStamp(binding) {
189
+ return stringifySorted(binding, 0);
190
+ }
191
+ // Cache of encryption types keyed by channel key. Each entry is bound by its stamp to the user-editable binding it was derived from - the
192
+ // DELTA_ELIGIBLE_BINDING_KEYS projection in types/channels.ts - so a reconfigured channel never reads a prior binding's classification. Entries hold only the
193
+ // stable classification (clear/aes128/drm) plus a timestamp for TTL expiration: variant URLs and key URLs contain session-bound auth tokens that expire between
194
+ // tunes, so they must never be cached. The DRM skip optimization in setup.ts uses this cache to avoid installing the CDP interceptor for channels known to use DRM.
56
195
  const probeCache = new Map();
57
196
  // Cache entries older than this are considered stale and re-probed. 24 hours covers the case where a service changes a channel's encryption profile (e.g., free ->
58
197
  // premium DRM). The DRM short-circuit in probeManifest() still applies within the TTL, so frequently-tuned DRM channels avoid repeated probe overhead.
59
198
  const PROBE_CACHE_TTL = 24 * 60 * 60 * 1000;
60
199
  /**
61
- * Returns the cached encryption type for a channel, or null if the channel has not been probed or the cache entry has expired. Used by the stream setup path to skip
62
- * CDP interceptor installation for channels already known to use DRM.
200
+ * Returns the cached encryption type for a channel, or null if the channel has not been probed under this binding or the cache entry has expired. Used by the
201
+ * stream setup path to skip CDP interceptor installation for channels already known to use DRM.
63
202
  *
64
- * @param channelName - The channel name to look up.
65
- * @returns The cached encryption type, or null if not probed or expired.
203
+ * @param identity - The probe-cache identity to look up: the channel key that locates the entry, and the binding stamp the entry must match.
204
+ * @returns The cached encryption type, or null if not probed, probed under a different binding, or expired.
66
205
  */
67
- export function getCachedEncryption(channelName) {
68
- const entry = probeCache.get(channelName);
206
+ export function getCachedEncryption(identity) {
207
+ const entry = probeCache.get(identity.key);
69
208
  if (!entry) {
70
209
  return null;
71
210
  }
211
+ /* The stamp test runs ahead of the age test because a mismatch asks a different question than staleness does. A classification describes the stream one
212
+ * binding reached, and a different binding reaches a different stream, so an entry whose stamp does not match is not old - it is about something else. We
213
+ * delete it on the way out so the slot is free for the classification the current binding produces.
214
+ */
215
+ if (entry.stamp !== identity.stamp) {
216
+ probeCache.delete(identity.key);
217
+ return null;
218
+ }
72
219
  if ((Date.now() - entry.timestamp) > PROBE_CACHE_TTL) {
73
- probeCache.delete(channelName);
220
+ probeCache.delete(identity.key);
74
221
  return null;
75
222
  }
76
223
  return entry.encryption;
77
224
  }
78
225
  /**
79
- * Clears the probe cache for a specific channel. Called when a native stream fails, forcing a fresh probe on the next attempt.
226
+ * Clears the probe cache for a specific channel. Called when a native stream fails, forcing a fresh probe on the next attempt. The clear is by key alone: a
227
+ * failure invalidates whatever classification the channel holds, whichever binding produced it.
80
228
  *
81
- * @param channelName - The channel name to clear from the cache.
229
+ * @param channelKey - The channel key to clear from the cache.
82
230
  */
83
- export function clearProbeCache(channelName) {
84
- probeCache.delete(channelName);
231
+ export function clearProbeCache(channelKey) {
232
+ probeCache.delete(channelKey);
85
233
  }
86
234
  /**
87
235
  * Probes an HLS playlist URL and returns a fully described MediaFeed. The input may be either a master playlist or a media playlist; classifyHlsPlaylist()
88
- * decides at runtime and the resolver dispatches accordingly. The probe cache is checked for DRM channels only - if a previous probe classified the channel as
89
- * DRM, we return the cached result immediately since the caller will bail out regardless of URLs. For viable channels (clear or aes128), we always run the full
90
- * probe because the variant URL and key URL contain auth tokens that expire between browser sessions.
236
+ * decides at runtime and the resolver dispatches accordingly. The probe cache is checked for DRM channels only: an unconstrained probe returns the cached
237
+ * classification immediately, since its caller abandons native streaming without reading any URL, while a probe constrained to a running pipeline declines
238
+ * instead, because no running pipeline is built around DRM. For viable channels (clear or aes128), we always run the full probe because the variant URL and key
239
+ * URL contain auth tokens that expire between browser sessions.
240
+ *
241
+ * A constrained probe carries one further guarantee: every feed it returns matches the supplied pipeline shape on container, encryption kind, and audio
242
+ * topology. A master whose candidates all mismatch resolves to null exactly as one whose candidates all fail to fetch does, because a feed the asking pipeline
243
+ * cannot absorb is not an error - it is a feed for some other pipeline.
91
244
  *
92
245
  * @param playlistUrl - The HLS playlist URL (master or media; contains auth tokens from the browser's original request).
93
- * @param channelName - The channel name for cache lookup.
94
- * @returns The MediaFeed, or null if probing fails.
246
+ * @param identity - The probe-cache identity this stream resolves under: the channel key for lookup, and the binding stamp any entry read or written must carry.
247
+ * @param options - Probe options.
248
+ * @param options.maxVariantAttempts - How many ranked variants a master playlist may fetch before giving up. Callers omit it and take the capped
249
+ * descending-bandwidth walk that is the selection policy for both tune and refresh; a caller naming a smaller number takes
250
+ * a narrower walk.
251
+ * @param options.pipelineShape - The compatibility envelope of a consumer that is already running, supplied by the token-refresh path. Selection is constrained
252
+ * to the candidates that shape admits. A tune omits it, having no pipeline yet to be compatible with.
253
+ * @param options.rejectStaticPlaylists - Whether a resolved window of at most one segment is refused. The tune path sets it so a session bumper falls back to
254
+ * capture instead of becoming the stream. The refresh path leaves it off: its probe re-describes a feed the proxy is
255
+ * already relaying, so admitting the channel is not its decision to make.
256
+ * @returns The MediaFeed, or null when the probe fails or resolves a feed the supplied pipeline shape cannot absorb.
95
257
  */
96
- export async function probeManifest(playlistUrl, channelName) {
258
+ export async function probeManifest(playlistUrl, identity, options = {}) {
259
+ const { maxVariantAttempts = MAX_VARIANT_FALLBACK_ATTEMPTS, pipelineShape, rejectStaticPlaylists = false } = options;
260
+ // Normalize to a floor of one whole attempt. Array.prototype.slice reads a negative count from the end of the list, so an out-of-range value from a caller
261
+ // would otherwise become a surprising selection rather than a single top-ranked try.
262
+ const variantAttempts = Math.max(1, Math.trunc(maxVariantAttempts));
97
263
  // Short-circuit for DRM channels only. The cached DRM classification is stable within the TTL window (services rarely change DRM type), and the caller returns
98
264
  // null immediately on DRM without using any URLs. For clear/aes128 channels, we must re-probe to get fresh variant and key URLs with current auth tokens.
99
- const cached = getCachedEncryption(channelName);
265
+ const cached = getCachedEncryption(identity);
100
266
  if (cached === "drm") {
101
- LOG.debug("native:probe", "Probe cache hit for %s: drm.", channelName);
102
- return { audioVariantUrl: null, bandwidth: 0, bestVariantUrl: "", codec: null, encryption: "drm", keyUrl: null, resolution: null };
267
+ /* A constrained probe declines the sentinel instead of returning it. The entry stays where it is - the classification remains a true fact about this channel
268
+ * for the next tune to read - and this probe simply cannot serve it, since no running pipeline is built around DRM. Handing back the sentinel would give the
269
+ * caller a feed carrying an empty variant URL, which reads as a successful refresh right up until the relay polls nothing.
270
+ */
271
+ if (pipelineShape) {
272
+ LOG.debug("native:probe", "Declining the probe for %s: the cached classification is drm, which no running pipeline can absorb.", identity.key);
273
+ return null;
274
+ }
275
+ LOG.debug("native:probe", "Probe cache hit for %s: drm.", identity.key);
276
+ return { audioVariantUrl: null, bandwidth: 0, bestVariantUrl: "", codec: null, container: null, encryption: "drm", keyUrl: null, resolution: null };
103
277
  }
104
278
  const elapsed = startTimer();
105
279
  try {
@@ -108,26 +282,57 @@ export async function probeManifest(playlistUrl, channelName) {
108
282
  // live, mutating playlist, and (b) probeManifest() is also invoked directly by the token-refresh path which has no interceptor classification to inherit.
109
283
  const body = await fetchManifestText(playlistUrl);
110
284
  if (!body) {
111
- LOG.debug("native:probe", "Failed to fetch playlist for %s.", channelName);
285
+ LOG.debug("native:probe", "Failed to fetch playlist for %s.", identity.key);
112
286
  return null;
113
287
  }
114
288
  const kind = classifyHlsPlaylist(body);
115
- const resolved = (kind === "master") ? await resolveMasterPlaylist(body, playlistUrl) :
116
- (kind === "media") ? await resolveMediaPlaylist(body, playlistUrl) :
289
+ const resolved = (kind === "master") ? await resolveMasterPlaylist(body, playlistUrl, variantAttempts, pipelineShape) :
290
+ (kind === "media") ? await resolveMediaPlaylist(body, playlistUrl, pipelineShape) :
117
291
  null;
118
292
  if (!resolved) {
119
- LOG.debug("native:probe", "Could not resolve %s playlist for %s.", kind, channelName);
293
+ LOG.debug("native:probe", "Could not resolve %s playlist for %s.", kind, identity.key);
120
294
  return null;
121
295
  }
296
+ /* Tune admission, opted into by the caller. A window holding at most one segment is not a channel: services front their live player with a per-session
297
+ * bumper - a single-segment playlist, often tagged live, whose sequence never moves - and the interceptor latches whichever master reaches the wire first.
298
+ * Relaying that playlist delivers one slate segment and then stalls forever, so the tune declines here and the coordinator falls back to capture, which
299
+ * shows whatever the page itself is playing. Liveness tagging is deliberately not consulted, because a one-segment VOD window is no more consumable than a
300
+ * one-segment live one. This runs ahead of both the encryption classification and the cache write, so a playlist we refuse never contributes a
301
+ * channel-level fact describing itself.
302
+ */
303
+ if (rejectStaticPlaylists) {
304
+ const segments = countMediaSegments(resolved.mediaBody);
305
+ if (segments < MIN_ADMISSIBLE_SEGMENT_COUNT) {
306
+ LOG.debug("native:probe", "Declining native streaming for %s: the playlist window holds %s segment(s), which is a fixed asset rather than a channel.", identity.key, segments);
307
+ return null;
308
+ }
309
+ }
122
310
  // Classify encryption from the media body. This branch is identical for master-derived and media-only feeds because #EXT-X-KEY tags live on the media
123
311
  // playlist regardless of which playlist kind originally arrived.
124
- const result = await classifyEncryption(resolved, channelName);
125
- probeCache.set(channelName, { encryption: result.encryption, timestamp: Date.now() });
126
- LOG.debug("native:probe", "Probe completed for %s in %sms: %s (%s).", channelName, elapsed(), result.encryption, kind);
312
+ const result = await classifyEncryption(resolved, identity.key);
313
+ /* Record the classification as the channel's fact only when the top-ranked variant produced it. The master walk falls back to a lower variant when the one
314
+ * above it fails to fetch, and a fallback-derived classification describes a variant the next tune may not select - while the top variant's own encryption
315
+ * is exactly what stays unknown, since its fetch is what failed. Writing it down would let one variant's encryption stand in for another's for the length
316
+ * of the TTL, including the DRM short-circuit that skips interception outright. The returned MediaFeed is unaffected either way: the caller streams the
317
+ * variant that answered, and only the persisted claim is withheld.
318
+ */
319
+ if (resolved.topRankedVariant) {
320
+ probeCache.set(identity.key, { encryption: result.encryption, stamp: identity.stamp, timestamp: Date.now() });
321
+ }
322
+ /* The constrained probe's guarantee is made here, once, against the finished classification. The walk compares each candidate as it reads the body, which is
323
+ * ahead of the key fetch that can still turn an AES-128 declaration into drm, so this is the only point at which every axis is settled. A mismatch declines
324
+ * the probe, after the classification has been recorded: what this pipeline can absorb has no bearing on what the channel is.
325
+ */
326
+ if (pipelineShape && !shapeAdmits(pipelineShape, { container: result.container, encryption: result.encryption,
327
+ separateAudio: result.audioVariantUrl !== null })) {
328
+ LOG.debug("native:probe", "Declining the resolved feed for %s: it serves %s/%s, which the running pipeline cannot absorb.", identity.key, result.container, result.encryption);
329
+ return null;
330
+ }
331
+ LOG.debug("native:probe", "Probe completed for %s in %sms: %s (%s).", identity.key, elapsed(), result.encryption, kind);
127
332
  return result;
128
333
  }
129
334
  catch (error) {
130
- LOG.debug("native:probe", "Probe failed for %s: %s.", channelName, String(error));
335
+ LOG.debug("native:probe", "Probe failed for %s: %s.", identity.key, String(error));
131
336
  return null;
132
337
  }
133
338
  }
@@ -152,78 +357,142 @@ async function fetchManifestText(url) {
152
357
  }
153
358
  }
154
359
  /**
155
- * Resolves a master playlist into a ResolvedMedia by selecting the highest-bandwidth variant, finding the optional separate-audio rendition, and fetching the
156
- * chosen variant body.
360
+ * Resolves a master playlist into a ResolvedMedia. The declared variants are walked in descending-bandwidth order and the first one that answers usably becomes
361
+ * the feed - so a master whose top variant is broken still yields a stream through a healthy sibling beneath it. The audio rendition resolves from the chosen
362
+ * variant's own audio group, which keeps the feed's audio bound to the video it accompanies.
363
+ *
364
+ * Under a pipeline shape the same ranked walk runs, narrowed to the candidates a running consumer can absorb. Eligibility is decided lazily, as the descent
365
+ * reaches each variant, so the cost of the constraint is proportional to how far the walk actually goes. Only a fetch counts against the attempt budget: a
366
+ * candidate ruled out by pure string work costs nothing and spends nothing.
157
367
  *
158
368
  * @param masterBody - The master manifest text.
159
369
  * @param masterUrl - The master manifest URL for resolving relative variant URLs.
160
- * @returns The resolved media feed metadata, or null when the master cannot be resolved.
370
+ * @param maxVariantAttempts - How many candidate fetches to spend before giving up.
371
+ * @param pipelineShape - The running consumer's compatibility envelope, or undefined for an unconstrained walk.
372
+ * @returns The resolved media feed metadata, or null when the master yields no candidate this walk can use.
161
373
  */
162
- async function resolveMasterPlaylist(masterBody, masterUrl) {
163
- // Parse variant streams and select the highest bandwidth.
164
- const bestVariant = selectBestVariant(masterBody, masterUrl);
165
- if (!bestVariant) {
374
+ async function resolveMasterPlaylist(masterBody, masterUrl, maxVariantAttempts, pipelineShape) {
375
+ const variants = selectVariants(masterBody, masterUrl);
376
+ if (!variants.length) {
166
377
  LOG.debug("native:probe", "No variant streams found in master manifest.");
167
378
  return null;
168
379
  }
169
- LOG.debug("native:probe", "Best variant selected: %s.", bestVariant.url.slice(0, 120));
170
- // Check for a separate audio rendition in the master manifest.
171
- const audioVariantUrl = parseAudioRendition(masterBody, masterUrl);
172
- if (audioVariantUrl) {
173
- LOG.debug("native:probe", "Separate audio rendition found: %s.", audioVariantUrl.slice(0, 120));
380
+ if (maxVariantAttempts < variants.length) {
381
+ LOG.debug("native:probe", "Attempting at most %s of %s advertised variant(s).", maxVariantAttempts, variants.length);
174
382
  }
175
- // Fetch the chosen variant manifest. The variant is what classifyEncryption() will inspect for #EXT-X-KEY tags and what the proxy will poll for segments.
176
- const variantBody = await fetchManifestText(bestVariant.url);
177
- if (!variantBody) {
178
- LOG.debug("native:probe", "Failed to fetch variant manifest for %s.", bestVariant.url);
179
- return null;
383
+ let attempts = 0;
384
+ for (const variant of variants) {
385
+ if (attempts >= maxVariantAttempts) {
386
+ break;
387
+ }
388
+ /* The audio topology axis is settled before the fetch because it can be: the rendition resolves from the master body already in hand, with no network work
389
+ * at all. A candidate whose topology differs from the running consumer's is therefore dropped without spending an attempt, and the URL this resolution
390
+ * produces is carried to the feed below so the winner never derives it twice. An unconstrained walk resolves nothing here, keeping that work on the one
391
+ * candidate it selects.
392
+ */
393
+ let audioVariantUrl = pipelineShape ? resolveAudioRendition(masterBody, masterUrl, variant.audioGroupId) : null;
394
+ if (pipelineShape && ((audioVariantUrl !== null) !== pipelineShape.separateAudio)) {
395
+ LOG.debug("native:probe", "Skipping the variant at %s bps: its audio topology differs from the running pipeline's.", variant.bandwidth);
396
+ continue;
397
+ }
398
+ attempts++;
399
+ LOG.debug("native:probe", "Attempting variant at %s bps: %s.", variant.bandwidth, variant.url.slice(0, 120));
400
+ /* Fetch this candidate's manifest. The candidate that answers is what classifyEncryption() inspects for #EXT-X-KEY tags and what the proxy polls for segments.
401
+ * The fetches run one after another because the ranking IS the selection policy: a lower variant is tried only once the one above it has actually failed, and
402
+ * a healthy top variant therefore costs exactly one fetch.
403
+ */
404
+ // eslint-disable-next-line no-await-in-loop
405
+ const variantBody = await fetchManifestText(variant.url);
406
+ if (!variantBody) {
407
+ LOG.debug("native:probe", "Failed to fetch variant manifest for %s.", variant.url);
408
+ continue;
409
+ }
410
+ /* The remaining two axes read the body that just arrived: the container tag scan, and the encryption tag scan without the key fetch that settles an aes128
411
+ * declaration. A candidate that mismatches on either is treated exactly as a failed fetch - it is a feed for some other pipeline - so the walk moves on to
412
+ * the next one rather than abandoning the master. The container this scan produces travels with the feed, which is what spares the converge site a second
413
+ * pass over the same body.
414
+ */
415
+ let container = null;
416
+ if (pipelineShape) {
417
+ container = classifyContainer(variantBody);
418
+ const declaredEncryption = scanEncryptionDeclaration(variantBody).kind;
419
+ if (!shapeAdmits(pipelineShape, { container, encryption: declaredEncryption, separateAudio: audioVariantUrl !== null })) {
420
+ LOG.debug("native:probe", "Skipping the variant at %s bps: it serves %s/%s, which the running pipeline cannot absorb.", variant.bandwidth, container, declaredEncryption);
421
+ continue;
422
+ }
423
+ }
424
+ else {
425
+ audioVariantUrl = resolveAudioRendition(masterBody, masterUrl, variant.audioGroupId);
426
+ }
427
+ if (audioVariantUrl) {
428
+ LOG.debug("native:probe", "Separate audio rendition found: %s.", audioVariantUrl.slice(0, 120));
429
+ }
430
+ return {
431
+ audioVariantUrl,
432
+ bandwidth: variant.bandwidth,
433
+ codec: variant.codec,
434
+ container,
435
+ mediaBody: variantBody,
436
+ mediaUrl: variant.url,
437
+ resolution: variant.resolution,
438
+ // The ranking's own first variant is what the cache write is conditioned on, so a constrained winner reached further down the ladder is measured against
439
+ // the full ranking rather than against the narrowed walk that selected it.
440
+ topRankedVariant: variant === variants[0]
441
+ };
180
442
  }
181
- return {
182
- audioVariantUrl,
183
- bandwidth: bestVariant.bandwidth,
184
- codec: bestVariant.codec,
185
- mediaBody: variantBody,
186
- mediaUrl: bestVariant.url,
187
- resolution: bestVariant.resolution
188
- };
443
+ LOG.debug("native:probe", "None of the %s attempted variant(s) produced a usable feed.", attempts);
444
+ return null;
189
445
  }
190
446
  /**
191
447
  * Resolves a media playlist into a ResolvedMedia. The input URL is itself the media feed - there is no master to traverse - so the resolver wraps the body and
192
448
  * URL verbatim, infers the codec from the first segment via codecInference.ts, and returns. Resolution stays null because TS PMT does not carry resolution and
193
449
  * SPS-level inference is out of scope.
194
450
  *
451
+ * A media playlist is its own single candidate, so under a pipeline shape this is where it is admitted or declined: its audio is muxed by definition, and its
452
+ * container and encryption kind read from the body already in hand. The comparison runs ahead of the codec inference because that inference fetches a segment,
453
+ * and a feed about to be declined must not cost a network round trip.
454
+ *
195
455
  * @param mediaBody - The media playlist text.
196
456
  * @param mediaUrl - The media playlist URL (the proxy will poll this).
197
- * @returns The resolved media feed metadata.
457
+ * @param pipelineShape - The running consumer's compatibility envelope, or undefined for an unconstrained resolution.
458
+ * @returns The resolved media feed metadata, or null when the shape cannot absorb this feed.
198
459
  */
199
- async function resolveMediaPlaylist(mediaBody, mediaUrl) {
460
+ async function resolveMediaPlaylist(mediaBody, mediaUrl, pipelineShape) {
461
+ let container = null;
462
+ if (pipelineShape) {
463
+ container = classifyContainer(mediaBody);
464
+ const declaredEncryption = scanEncryptionDeclaration(mediaBody).kind;
465
+ if (!shapeAdmits(pipelineShape, { container, encryption: declaredEncryption, separateAudio: false })) {
466
+ LOG.debug("native:probe", "Declining the media playlist at %s: it serves %s/%s with muxed audio, which the running pipeline cannot absorb.", mediaUrl.slice(0, 120), container, declaredEncryption);
467
+ return null;
468
+ }
469
+ }
200
470
  // Best-effort codec inference. Returns codec=null on any failure (no segment, fetch error, unrecognized format) so the rest of the pipeline continues unimpaired.
201
471
  const inferred = await inferMediaCodec({ baseUrl: mediaUrl, playlistBody: mediaBody });
202
472
  return {
203
473
  audioVariantUrl: null,
204
474
  bandwidth: 0,
205
475
  codec: inferred.codec,
476
+ container,
206
477
  mediaBody,
207
478
  mediaUrl,
208
- resolution: null
479
+ resolution: null,
480
+ topRankedVariant: true
209
481
  };
210
482
  }
211
483
  /**
212
- * Parses #EXT-X-STREAM-INF lines from a master manifest and returns the highest-bandwidth variant with its metadata.
484
+ * Parses every #EXT-X-STREAM-INF entry in a master manifest and returns the declared variants ranked by descending bandwidth. An entry whose URI line is missing,
485
+ * or whose URI cannot be resolved against the master URL, drops out of the list so that one malformed declaration costs only itself.
213
486
  *
214
487
  * @param masterBody - The master manifest content.
215
488
  * @param masterUrl - The master manifest URL for resolving relative variant URLs.
216
- * @returns The selected variant metadata, or null if no variants are found.
489
+ * @returns The resolvable variants ordered by descending bandwidth, empty when the master declares none.
217
490
  */
218
- function selectBestVariant(masterBody, masterUrl) {
491
+ function selectVariants(masterBody, masterUrl) {
219
492
  // Map video codec prefixes from CODECS attribute to human-readable labels. Defined outside the variant iteration loop to avoid per-line allocation.
220
493
  const codecPrefixes = { "av01": "AV1", "avc1": "H264", "avc3": "H264", "hev1": "HEVC", "hvc1": "HEVC", "vp09": "VP9" };
221
494
  const lines = masterBody.split("\n");
222
- let bestBandwidth = 0;
223
- let bestCodec = null;
224
- let bestResolution = null;
225
- let bestUrl = null;
226
- const bandwidths = [];
495
+ const variants = [];
227
496
  for (let i = 0; i < lines.length; i++) {
228
497
  const line = lines[i]?.trim() ?? "";
229
498
  if (!line.startsWith("#EXT-X-STREAM-INF:")) {
@@ -233,6 +502,8 @@ function selectBestVariant(masterBody, masterUrl) {
233
502
  const bandwidth = Number(/BANDWIDTH=(\d+)/.exec(line)?.[1] ?? 0);
234
503
  // Parse RESOLUTION attribute (e.g., RESOLUTION=1920x1080).
235
504
  const resolution = /RESOLUTION=(\d+x\d+)/.exec(line)?.[1] ?? null;
505
+ // Parse the AUDIO attribute, which names the rendition group holding this variant's audio. Its absence means the audio is muxed into the variant's segments.
506
+ const audioGroupId = /AUDIO="([^"]+)"/.exec(line)?.[1] ?? null;
236
507
  // Parse CODECS attribute and map the video codec prefix to a human-readable label. The CODECS value contains comma-separated codec strings (e.g.,
237
508
  // "avc1.640028,mp4a.40.2"). The video codec is identified by its prefix: avc1/avc3 -> H264, hvc1/hev1 -> HEVC, av01 -> AV1, vp09 -> VP9.
238
509
  const codecsValue = /CODECS="([^"]+)"/.exec(line)?.[1];
@@ -246,34 +517,59 @@ function selectBestVariant(masterBody, masterUrl) {
246
517
  if (!variantLine || variantLine.startsWith("#")) {
247
518
  continue;
248
519
  }
249
- bandwidths.push(bandwidth);
250
- if (bandwidth > bestBandwidth) {
251
- bestBandwidth = bandwidth;
252
- bestCodec = codec;
253
- bestResolution = resolution;
254
- bestUrl = variantLine;
520
+ // Resolve each candidate inside the per-entry guard, the same way the membership walk does. Because every declared variant is resolved here rather than only
521
+ // the one that wins, a naked resolveUrl would let a single malformed URI throw the entire probe away through probeManifest's outer catch.
522
+ const url = resolveChildUri(variantLine, masterUrl);
523
+ if (url === null) {
524
+ continue;
255
525
  }
526
+ variants.push({ audioGroupId, bandwidth, codec, resolution, url });
256
527
  }
257
- LOG.debug("native:probe", "Found %s variant(s) with bandwidths: %s.", bandwidths.length, bandwidths.join(", "));
258
- if (!bestUrl) {
259
- return null;
260
- }
261
- // Resolve relative URLs against the master manifest URL.
262
- return { bandwidth: bestBandwidth, codec: bestCodec, resolution: bestResolution, url: resolveUrl(bestUrl, masterUrl) };
528
+ LOG.debug("native:probe", "Found %s variant(s) with bandwidths: %s.", variants.length, variants.map((variant) => variant.bandwidth).join(", "));
529
+ // Rank by descending bandwidth. Array.prototype.sort is stable, so variants sharing a bandwidth keep their document order - which is what lets a master that
530
+ // advertises no BANDWIDTH at all, and therefore parses entirely as zeroes, resolve through its first declared variant with no special-case handling.
531
+ variants.sort((first, second) => second.bandwidth - first.bandwidth);
532
+ return variants;
263
533
  }
264
534
  /**
265
535
  * Classifies the encryption type of a media playlist by parsing its #EXT-X-KEY tags. Operates uniformly on master-derived and media-only ResolvedMedia inputs
266
536
  * because #EXT-X-KEY tags are a media-playlist-level concept regardless of whether a master playlist sat above the media.
267
537
  *
268
538
  * @param resolved - The resolved media feed metadata.
269
- * @param channelName - The channel name for logging.
539
+ * @param channelKey - The channel key for logging.
270
540
  * @returns The MediaFeed with the classified encryption type and (when applicable) the AES-128 key URL.
271
541
  */
272
- async function classifyEncryption(resolved, channelName) {
273
- const lines = resolved.mediaBody.split("\n");
274
- let encryption = "clear";
275
- let keyUrl = null;
276
- for (const line of lines) {
542
+ /**
543
+ * Classifies a media playlist's container format by looking for an #EXT-X-MAP declaration. A playlist that references an initialization segment is fMP4/CMAF;
544
+ * one that does not is MPEG-TS, whose segments carry their own PAT/PMT and need no init. The scan is line-anchored - each line is trimmed and tested with
545
+ * startsWith - so a segment or key URI that happens to contain the literal "#EXT-X-MAP:" inside a query string cannot be mistaken for the tag. This mirrors the
546
+ * discipline classifyHlsPlaylist uses for the master/media decision.
547
+ *
548
+ * @param mediaBody - The media playlist body text.
549
+ * @returns "fmp4" when the body declares an initialization segment, "ts" otherwise.
550
+ */
551
+ function classifyContainer(mediaBody) {
552
+ for (const rawLine of mediaBody.split("\n")) {
553
+ if (rawLine.trim().startsWith("#EXT-X-MAP:")) {
554
+ return "fmp4";
555
+ }
556
+ }
557
+ return "ts";
558
+ }
559
+ /**
560
+ * Reads a media playlist's #EXT-X-KEY tags and reports what they declare. This is the tag scan alone - it issues no request, so it costs one pass over a body the
561
+ * caller already holds. A NONE tag does not settle the classification on its own: some manifests interleave a NONE tag with a later, more restrictive key tag
562
+ * (a clear lead-in segment followed by an AES-128 or DRM-protected one), so the scan keeps reading and the first method that is not NONE settles it, favoring the
563
+ * strongest encryption signal present over the order in which the tags happen to appear.
564
+ *
565
+ * Two callers read this, with different appetites: the encryption classifier settles an "aes128" declaration by fetching the key it names, while the constrained
566
+ * variant walk compares the declared kind against a running pipeline's and moves on, paying for no fetch it would only discard.
567
+ *
568
+ * @param mediaBody - The media playlist body text.
569
+ * @returns What the key tags declare.
570
+ */
571
+ function scanEncryptionDeclaration(mediaBody) {
572
+ for (const line of mediaBody.split("\n")) {
277
573
  const trimmed = line.trim();
278
574
  if (!trimmed.startsWith("#EXT-X-KEY:")) {
279
575
  continue;
@@ -284,37 +580,56 @@ async function classifyEncryption(resolved, channelName) {
284
580
  continue;
285
581
  }
286
582
  if (method === "AES-128") {
287
- // Parse URI attribute for the key URL.
288
- const uri = /URI="([^"]+)"/.exec(trimmed)?.[1];
289
- if (!uri) {
290
- LOG.debug("native:probe", "AES-128 key tag has no URI for %s.", channelName);
291
- encryption = "drm";
292
- break;
293
- }
294
- const rawKeyUrl = resolveUrl(uri, resolved.mediaUrl);
295
- // Test that the key is accessible and is exactly 16 bytes.
296
- // eslint-disable-next-line no-await-in-loop
583
+ // Parse URI attribute for the key URL. A key that names no location cannot be fetched, so the declaration is no more usable than an unsupported method.
584
+ const uri = uriAttribute(trimmed);
585
+ return (uri === null) ? { kind: "drm", method, reason: "no-key-uri" } : { keyUri: uri, kind: "aes128" };
586
+ }
587
+ // SAMPLE-AES, SAMPLE-AES-CTR, or any other method indicates DRM.
588
+ return { kind: "drm", method, reason: "unsupported-method" };
589
+ }
590
+ return { kind: "clear" };
591
+ }
592
+ async function classifyEncryption(resolved, channelKey) {
593
+ const declaration = scanEncryptionDeclaration(resolved.mediaBody);
594
+ let encryption = declaration.kind;
595
+ let keyUrl = null;
596
+ switch (declaration.kind) {
597
+ case "aes128": {
598
+ const rawKeyUrl = resolveUrl(declaration.keyUri, resolved.mediaUrl);
599
+ // Test that the key is accessible and is exactly 16 bytes. This is the classification's only request, and a body that named no key never reaches it.
297
600
  const keyAccessible = await testKeyAccessibility(rawKeyUrl);
298
601
  if (keyAccessible) {
299
- encryption = "aes128";
300
602
  keyUrl = rawKeyUrl;
301
603
  }
302
604
  else {
303
- LOG.debug("native:probe", "AES-128 key inaccessible or wrong size for %s.", channelName);
605
+ LOG.debug("native:probe", "AES-128 key inaccessible or wrong size for %s.", channelKey);
304
606
  encryption = "drm";
305
607
  }
306
608
  break;
307
609
  }
308
- // SAMPLE-AES, SAMPLE-AES-CTR, or any other method indicates DRM.
309
- LOG.debug("native:probe", "Unsupported encryption method '%s' for %s.", method, channelName);
310
- encryption = "drm";
311
- break;
610
+ case "drm": {
611
+ if (declaration.reason === "no-key-uri") {
612
+ LOG.debug("native:probe", "AES-128 key tag has no URI for %s.", channelKey);
613
+ }
614
+ else {
615
+ LOG.debug("native:probe", "Unsupported encryption method '%s' for %s.", declaration.method, channelKey);
616
+ }
617
+ break;
618
+ }
619
+ default: {
620
+ // A clear body names no key, so there is nothing to fetch and nothing to report.
621
+ break;
622
+ }
312
623
  }
313
624
  return {
314
625
  audioVariantUrl: resolved.audioVariantUrl,
315
626
  bandwidth: resolved.bandwidth,
316
627
  bestVariantUrl: resolved.mediaUrl,
317
628
  codec: resolved.codec,
629
+ // Both playlist-kind branches converge here, so this is the one place the container is decided. A DRM feed carries null because the caller abandons native
630
+ // streaming without reading it, and classifying a body we will never relay would be a fabricated value. A resolver that already classified the body under a
631
+ // pipeline shape hands its result across, so no body is scanned twice.
632
+ container: (encryption === "drm") ? null : (resolved.container ?? classifyContainer(resolved.mediaBody)),
318
633
  encryption,
319
634
  keyUrl,
320
635
  resolution: resolved.resolution
@@ -342,14 +657,22 @@ async function testKeyAccessibility(keyUrl) {
342
657
  }
343
658
  }
344
659
  /**
345
- * Parses the first #EXT-X-MEDIA:TYPE=AUDIO rendition with a URI from a master manifest. Returns the absolute audio variant URL, or null if no separate audio
346
- * rendition is declared (audio is muxed into the video variant).
660
+ * Resolves the audio rendition playlist belonging to the selected variant's audio group. RFC 8216 section 4.3.4.2.1 binds a variant to its renditions through the
661
+ * AUDIO attribute, so the renditions a master declares for other groups say nothing about this variant. Within the named group the first DEFAULT=YES rendition
662
+ * carrying a URI wins, and the first URI-bearing rendition of the group is the fallback when the group names no default. Null covers two outcomes - a variant
663
+ * whose audio is muxed by design, and a declared group in which no rendition carries a URI - and the debug log on the second tells them apart in the field.
347
664
  *
348
665
  * @param masterBody - The master manifest content.
349
666
  * @param masterUrl - The master manifest URL for resolving relative URIs.
667
+ * @param audioGroupId - The selected variant's AUDIO group, or null when the variant declares none.
350
668
  * @returns The absolute audio variant URL, or null.
351
669
  */
352
- function parseAudioRendition(masterBody, masterUrl) {
670
+ function resolveAudioRendition(masterBody, masterUrl, audioGroupId) {
671
+ // A variant that names no audio group carries its audio inside its own segments, so no rendition in the master applies to it.
672
+ if (audioGroupId === null) {
673
+ return null;
674
+ }
675
+ let candidateUri = null;
353
676
  for (const line of masterBody.split("\n")) {
354
677
  const trimmed = line.trim();
355
678
  if (!trimmed.startsWith("#EXT-X-MEDIA:")) {
@@ -359,13 +682,27 @@ function parseAudioRendition(masterBody, masterUrl) {
359
682
  if (!trimmed.includes("TYPE=AUDIO")) {
360
683
  continue;
361
684
  }
362
- // Extract the URI attribute. Not all #EXT-X-MEDIA:TYPE=AUDIO tags have a URI - some are descriptive-only when audio is muxed into the video variant.
363
- const uri = /URI="([^"]+)"/.exec(trimmed)?.[1];
364
- if (uri) {
685
+ // Match the group by its exact value. A substring test would accept a neighboring group whose id merely contains the selected one.
686
+ if (/GROUP-ID="([^"]+)"/.exec(trimmed)?.[1] !== audioGroupId) {
687
+ continue;
688
+ }
689
+ // Extract the URI attribute. Not all #EXT-X-MEDIA:TYPE=AUDIO tags have a URI - some are descriptive-only when audio is muxed into the video variant. Such a
690
+ // tag is skipped even when it is the group's default, so a descriptive default never ends the walk ahead of a sibling that can actually be played.
691
+ const uri = uriAttribute(trimmed);
692
+ if (!uri) {
693
+ continue;
694
+ }
695
+ // The group's default rendition wins outright; any other is held only until a default turns up.
696
+ if (trimmed.includes("DEFAULT=YES")) {
365
697
  return resolveUrl(uri, masterUrl);
366
698
  }
699
+ candidateUri ??= uri;
367
700
  }
368
- return null;
701
+ if (candidateUri === null) {
702
+ LOG.debug("native:probe", "No audio rendition carrying a URI was found in group '%s'.", audioGroupId);
703
+ return null;
704
+ }
705
+ return resolveUrl(candidateUri, masterUrl);
369
706
  }
370
707
  /**
371
708
  * Resolves a potentially relative URL against a base URL. Handles both absolute and relative URLs. Exported for reuse by the proxy module.