prismcast 1.5.2 → 1.7.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 (239) hide show
  1. package/README.md +44 -28
  2. package/dist/app.d.ts +5 -0
  3. package/dist/app.js +63 -5
  4. package/dist/app.js.map +1 -1
  5. package/dist/browser/cdp.js +1 -1
  6. package/dist/browser/cdp.js.map +1 -1
  7. package/dist/browser/channelSelection.d.ts +10 -34
  8. package/dist/browser/channelSelection.js +31 -96
  9. package/dist/browser/channelSelection.js.map +1 -1
  10. package/dist/browser/display.d.ts +19 -0
  11. package/dist/browser/display.js +16 -11
  12. package/dist/browser/display.js.map +1 -1
  13. package/dist/browser/index.d.ts +31 -54
  14. package/dist/browser/index.js +334 -272
  15. package/dist/browser/index.js.map +1 -1
  16. package/dist/browser/login.d.ts +65 -0
  17. package/dist/browser/login.js +193 -0
  18. package/dist/browser/login.js.map +1 -0
  19. package/dist/browser/precaching.js +15 -3
  20. package/dist/browser/precaching.js.map +1 -1
  21. package/dist/browser/tuning/comcastPolymer.d.ts +23 -0
  22. package/dist/browser/tuning/comcastPolymer.js +655 -0
  23. package/dist/browser/tuning/comcastPolymer.js.map +1 -0
  24. package/dist/browser/tuning/cox.d.ts +2 -0
  25. package/dist/browser/tuning/cox.js +14 -0
  26. package/dist/browser/tuning/cox.js.map +1 -0
  27. package/dist/browser/tuning/directv.js +14 -1
  28. package/dist/browser/tuning/directv.js.map +1 -1
  29. package/dist/browser/tuning/fox.js +14 -2
  30. package/dist/browser/tuning/fox.js.map +1 -1
  31. package/dist/browser/tuning/hbo.js +12 -1
  32. package/dist/browser/tuning/hbo.js.map +1 -1
  33. package/dist/browser/tuning/hulu.js +24 -1
  34. package/dist/browser/tuning/hulu.js.map +1 -1
  35. package/dist/browser/tuning/shared.d.ts +46 -0
  36. package/dist/browser/tuning/shared.js +100 -0
  37. package/dist/browser/tuning/shared.js.map +1 -0
  38. package/dist/browser/tuning/sling.js +35 -5
  39. package/dist/browser/tuning/sling.js.map +1 -1
  40. package/dist/browser/tuning/spectrum.js +13 -1
  41. package/dist/browser/tuning/spectrum.js.map +1 -1
  42. package/dist/browser/tuning/thumbnailRow.js +1 -1
  43. package/dist/browser/tuning/thumbnailRow.js.map +1 -1
  44. package/dist/browser/tuning/tileClick.js +1 -1
  45. package/dist/browser/tuning/tileClick.js.map +1 -1
  46. package/dist/browser/tuning/xfinity.d.ts +2 -0
  47. package/dist/browser/tuning/xfinity.js +14 -0
  48. package/dist/browser/tuning/xfinity.js.map +1 -0
  49. package/dist/browser/tuning/youtubeTv.js +15 -1
  50. package/dist/browser/tuning/youtubeTv.js.map +1 -1
  51. package/dist/browser/video.d.ts +20 -0
  52. package/dist/browser/video.js +168 -124
  53. package/dist/browser/video.js.map +1 -1
  54. package/dist/channels/index.d.ts +2 -2
  55. package/dist/channels/index.js +2075 -683
  56. package/dist/channels/index.js.map +1 -1
  57. package/dist/config/health.d.ts +28 -28
  58. package/dist/config/health.js +72 -54
  59. package/dist/config/health.js.map +1 -1
  60. package/dist/config/index.js +29 -64
  61. package/dist/config/index.js.map +1 -1
  62. package/dist/config/paths.d.ts +12 -0
  63. package/dist/config/paths.js +16 -0
  64. package/dist/config/paths.js.map +1 -1
  65. package/dist/config/profiles.d.ts +3 -2
  66. package/dist/config/profiles.js +45 -34
  67. package/dist/config/profiles.js.map +1 -1
  68. package/dist/config/providerPacks.js +4 -1
  69. package/dist/config/providerPacks.js.map +1 -1
  70. package/dist/config/providers.d.ts +19 -5
  71. package/dist/config/providers.js +123 -93
  72. package/dist/config/providers.js.map +1 -1
  73. package/dist/config/sites.d.ts +28 -0
  74. package/dist/config/sites.js +104 -127
  75. package/dist/config/sites.js.map +1 -1
  76. package/dist/config/userChannels.d.ts +53 -2
  77. package/dist/config/userChannels.js +222 -13
  78. package/dist/config/userChannels.js.map +1 -1
  79. package/dist/config/userConfig.d.ts +2 -1
  80. package/dist/config/userConfig.js +6 -15
  81. package/dist/config/userConfig.js.map +1 -1
  82. package/dist/config/userProfiles.d.ts +0 -1
  83. package/dist/config/userProfiles.js +18 -10
  84. package/dist/config/userProfiles.js.map +1 -1
  85. package/dist/hdhr/channelMap.d.ts +1 -1
  86. package/dist/hdhr/channelMap.js +4 -4
  87. package/dist/hdhr/channelMap.js.map +1 -1
  88. package/dist/index.js +25 -22
  89. package/dist/index.js.map +1 -1
  90. package/dist/native/decrypt.d.ts +32 -0
  91. package/dist/native/decrypt.js +85 -0
  92. package/dist/native/decrypt.js.map +1 -0
  93. package/dist/native/index.d.ts +61 -0
  94. package/dist/native/index.js +342 -0
  95. package/dist/native/index.js.map +1 -0
  96. package/dist/native/intercept.d.ts +35 -0
  97. package/dist/native/intercept.js +184 -0
  98. package/dist/native/intercept.js.map +1 -0
  99. package/dist/native/probe.d.ts +49 -0
  100. package/dist/native/probe.js +294 -0
  101. package/dist/native/probe.js.map +1 -0
  102. package/dist/native/proxy.d.ts +54 -0
  103. package/dist/native/proxy.js +947 -0
  104. package/dist/native/proxy.js.map +1 -0
  105. package/dist/routes/components.d.ts +43 -0
  106. package/dist/routes/components.js +122 -0
  107. package/dist/routes/components.js.map +1 -1
  108. package/dist/routes/config/channels/index.d.ts +2 -2
  109. package/dist/routes/config/channels/index.js +1 -1
  110. package/dist/routes/config/channels/index.js.map +1 -1
  111. package/dist/routes/config/channels/routes.js +443 -65
  112. package/dist/routes/config/channels/routes.js.map +1 -1
  113. package/dist/routes/config/channels/table.d.ts +76 -0
  114. package/dist/routes/config/channels/table.js +369 -89
  115. package/dist/routes/config/channels/table.js.map +1 -1
  116. package/dist/routes/config/index.d.ts +1 -1
  117. package/dist/routes/config/index.js +1 -1
  118. package/dist/routes/config/index.js.map +1 -1
  119. package/dist/routes/config/providers.d.ts +6 -6
  120. package/dist/routes/config/providers.js +73 -99
  121. package/dist/routes/config/providers.js.map +1 -1
  122. package/dist/routes/debug.js +5 -5
  123. package/dist/routes/debug.js.map +1 -1
  124. package/dist/routes/hls.js +12 -2
  125. package/dist/routes/hls.js.map +1 -1
  126. package/dist/routes/index.d.ts +0 -15
  127. package/dist/routes/index.js +2 -16
  128. package/dist/routes/index.js.map +1 -1
  129. package/dist/routes/providers.js +106 -3
  130. package/dist/routes/providers.js.map +1 -1
  131. package/dist/routes/root/content.js +124 -47
  132. package/dist/routes/root/content.js.map +1 -1
  133. package/dist/routes/root/index.js +2 -1
  134. package/dist/routes/root/index.js.map +1 -1
  135. package/dist/routes/root/scripts/channels.js +772 -312
  136. package/dist/routes/root/scripts/channels.js.map +1 -1
  137. package/dist/routes/root/scripts/config.js +596 -544
  138. package/dist/routes/root/scripts/config.js.map +1 -1
  139. package/dist/routes/root/scripts/index.d.ts +1 -0
  140. package/dist/routes/root/scripts/index.js +1 -0
  141. package/dist/routes/root/scripts/index.js.map +1 -1
  142. package/dist/routes/root/scripts/shared.d.ts +6 -0
  143. package/dist/routes/root/scripts/shared.js +391 -0
  144. package/dist/routes/root/scripts/shared.js.map +1 -0
  145. package/dist/routes/root/scripts/status.js +49 -33
  146. package/dist/routes/root/scripts/status.js.map +1 -1
  147. package/dist/routes/root/styles.js +117 -10
  148. package/dist/routes/root/styles.js.map +1 -1
  149. package/dist/service/commands.js +2 -1
  150. package/dist/service/commands.js.map +1 -1
  151. package/dist/streaming/fmp4Segmenter.d.ts +5 -0
  152. package/dist/streaming/fmp4Segmenter.js +146 -42
  153. package/dist/streaming/fmp4Segmenter.js.map +1 -1
  154. package/dist/streaming/hls.d.ts +24 -19
  155. package/dist/streaming/hls.js +603 -297
  156. package/dist/streaming/hls.js.map +1 -1
  157. package/dist/streaming/hlsResume.d.ts +16 -3
  158. package/dist/streaming/hlsResume.js +35 -8
  159. package/dist/streaming/hlsResume.js.map +1 -1
  160. package/dist/streaming/hlsSegments.d.ts +41 -2
  161. package/dist/streaming/hlsSegments.js +131 -28
  162. package/dist/streaming/hlsSegments.js.map +1 -1
  163. package/dist/streaming/lifecycle.js +27 -13
  164. package/dist/streaming/lifecycle.js.map +1 -1
  165. package/dist/streaming/monitor.d.ts +1 -0
  166. package/dist/streaming/monitor.js +862 -474
  167. package/dist/streaming/monitor.js.map +1 -1
  168. package/dist/streaming/mp4Parser.d.ts +52 -6
  169. package/dist/streaming/mp4Parser.js +156 -30
  170. package/dist/streaming/mp4Parser.js.map +1 -1
  171. package/dist/streaming/mpegts.d.ts +2 -2
  172. package/dist/streaming/mpegts.js +156 -109
  173. package/dist/streaming/mpegts.js.map +1 -1
  174. package/dist/streaming/playlistBuilder.d.ts +38 -0
  175. package/dist/streaming/playlistBuilder.js +66 -0
  176. package/dist/streaming/playlistBuilder.js.map +1 -0
  177. package/dist/streaming/preroll.d.ts +126 -0
  178. package/dist/streaming/preroll.js +434 -0
  179. package/dist/streaming/preroll.js.map +1 -0
  180. package/dist/streaming/pretune.d.ts +8 -0
  181. package/dist/streaming/pretune.js +226 -0
  182. package/dist/streaming/pretune.js.map +1 -0
  183. package/dist/streaming/recovery.d.ts +0 -6
  184. package/dist/streaming/recovery.js +0 -8
  185. package/dist/streaming/recovery.js.map +1 -1
  186. package/dist/streaming/registry.d.ts +57 -9
  187. package/dist/streaming/registry.js +26 -7
  188. package/dist/streaming/registry.js.map +1 -1
  189. package/dist/streaming/setup.d.ts +8 -1
  190. package/dist/streaming/setup.js +71 -27
  191. package/dist/streaming/setup.js.map +1 -1
  192. package/dist/streaming/showInfo.d.ts +29 -4
  193. package/dist/streaming/showInfo.js +210 -38
  194. package/dist/streaming/showInfo.js.map +1 -1
  195. package/dist/streaming/statusEmitter.d.ts +20 -6
  196. package/dist/streaming/statusEmitter.js +31 -7
  197. package/dist/streaming/statusEmitter.js.map +1 -1
  198. package/dist/types/channels.d.ts +32 -0
  199. package/dist/types/config.d.ts +2 -2
  200. package/dist/types/index.d.ts +2 -2
  201. package/dist/types/index.js.map +1 -1
  202. package/dist/types/profiles.d.ts +13 -1
  203. package/dist/types/selection.d.ts +5 -1
  204. package/dist/types/shared.d.ts +1 -1
  205. package/dist/types/streaming.d.ts +7 -0
  206. package/dist/utils/chromeFetch.d.ts +20 -0
  207. package/dist/utils/chromeFetch.js +43 -0
  208. package/dist/utils/chromeFetch.js.map +1 -0
  209. package/dist/utils/debugFilter.d.ts +0 -5
  210. package/dist/utils/debugFilter.js +20 -11
  211. package/dist/utils/debugFilter.js.map +1 -1
  212. package/dist/utils/delay.d.ts +28 -0
  213. package/dist/utils/delay.js +35 -1
  214. package/dist/utils/delay.js.map +1 -1
  215. package/dist/utils/evaluate.js +6 -11
  216. package/dist/utils/evaluate.js.map +1 -1
  217. package/dist/utils/ffmpeg.d.ts +14 -12
  218. package/dist/utils/ffmpeg.js +114 -134
  219. package/dist/utils/ffmpeg.js.map +1 -1
  220. package/dist/utils/format.d.ts +6 -0
  221. package/dist/utils/format.js +8 -0
  222. package/dist/utils/format.js.map +1 -1
  223. package/dist/utils/index.d.ts +3 -0
  224. package/dist/utils/index.js +3 -0
  225. package/dist/utils/index.js.map +1 -1
  226. package/dist/utils/logEmitter.js +1 -1
  227. package/dist/utils/logEmitter.js.map +1 -1
  228. package/dist/utils/pid.d.ts +40 -0
  229. package/dist/utils/pid.js +83 -0
  230. package/dist/utils/pid.js.map +1 -0
  231. package/dist/utils/retry.d.ts +15 -9
  232. package/dist/utils/retry.js +7 -23
  233. package/dist/utils/retry.js.map +1 -1
  234. package/dist/utils/timing.d.ts +5 -0
  235. package/dist/utils/timing.js +13 -0
  236. package/dist/utils/timing.js.map +1 -0
  237. package/dist/utils/version.js +2 -2
  238. package/dist/utils/version.js.map +1 -1
  239. package/package.json +7 -7
@@ -0,0 +1,342 @@
1
+ /* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * index.ts: Coordinator for native HLS streaming — manifest interception, DRM probe, and proxy lifecycle.
4
+ */
5
+ import { LOG, formatError, startTimer } from "../utils/index.js";
6
+ import { clearProbeCache, probeManifest } from "./probe.js";
7
+ import { installManifestInterceptor, removeManifestInterceptor } from "./intercept.js";
8
+ import { createNativeProxy } from "./proxy.js";
9
+ import { fetchDecryptionKey } from "./decrypt.js";
10
+ /* This module orchestrates the native streaming decision. After the browser navigates to a channel and video playback begins, we check whether the provider's HLS
11
+ * stream can be consumed directly in Node (bypassing screen capture). The decision flow is:
12
+ *
13
+ * 1. Await the manifest interception promise (CDP listener was installed before navigation)
14
+ * 2. If no manifest was intercepted: return null (fall back to capture)
15
+ * 3. Probe the manifest for encryption type
16
+ * 4. If DRM: log the reason, return null
17
+ * 5. If clear or AES-128: create and return the native proxy
18
+ *
19
+ * Token refresh: When the intercepted URL contains expiration tokens, we schedule a timer to refresh the manifest before the tokens expire. The refresh first
20
+ * attempts a direct Node.js fetch of the master manifest URL (no browser involvement), falling back to a browser page reload when the master URL itself has expired.
21
+ * The proxy continues serving cached segments during the refresh.
22
+ */
23
+ // Time in milliseconds before token expiry to trigger a refresh. We refresh 5 minutes early to ensure the new manifest is ready before the old one expires.
24
+ const TOKEN_REFRESH_MARGIN = 300000;
25
+ // Minimum delay in milliseconds before a token refresh fires. Prevents a refresh loop when the token lifetime is shorter than TOKEN_REFRESH_MARGIN (e.g., Fox Sports
26
+ // tokens expire in ~118 seconds). Without this floor, the refresh delay would be Math.max(0, 118000 - 300000) = 0ms, triggering an infinite loop of page reloads.
27
+ const MIN_REFRESH_DELAY = 30000;
28
+ // Minimum remaining token lifetime (in milliseconds) for a direct-fetched variant URL to be considered usable. If the variant URL's token expires sooner than this,
29
+ // the direct fetch result is discarded and we fall back to a page reload to get a genuinely fresh token. This prevents handing the proxy a variant URL that expires
30
+ // almost immediately. Set low (5s) because the proxy only needs the variant URL to survive one poll cycle (~3s). A higher threshold (e.g., 30s) would cause Fox.com
31
+ // channels to reject perfectly usable variant URLs — Fox.com tokens have ~57s total lifetime, leaving ~27s at the 30s refresh point.
32
+ const MIN_USABLE_TOKEN_LIFETIME = 5000;
33
+ // Timeout for awaiting the manifest interception promise after playback init.
34
+ const INTERCEPTION_AWAIT_TIMEOUT = 5000;
35
+ /**
36
+ * Attempts to upgrade a stream from screen capture to native HLS streaming. Returns the native proxy on success, or null if the stream is not viable for native
37
+ * consumption (DRM, interception timeout, or probe failure). The proxy owns the CDP session from interception and manages its lifecycle.
38
+ *
39
+ * @param options - Options for the native streaming attempt.
40
+ * @returns The native stream result on success, or null to fall back to capture.
41
+ */
42
+ export async function attemptNativeStreaming(options) {
43
+ const { channelName, interceptionPromise, mpegTsClient, onError, page, streamId, streamIdStr, url } = options;
44
+ const elapsed = startTimer();
45
+ LOG.debug("native:coordinator", "Attempting native streaming for %s.", channelName);
46
+ // Await the manifest interception with a short timeout. The CDP listener was installed before navigation, so by the time we get here the manifest should
47
+ // already be captured or close to it.
48
+ let interception;
49
+ try {
50
+ interception = await Promise.race([
51
+ interceptionPromise,
52
+ new Promise((resolve) => {
53
+ setTimeout(() => { resolve(null); }, INTERCEPTION_AWAIT_TIMEOUT);
54
+ })
55
+ ]);
56
+ }
57
+ catch (error) {
58
+ LOG.debug("native:coordinator", "Manifest interception error for %s: %s.", channelName, formatError(error));
59
+ return null;
60
+ }
61
+ if (!interception) {
62
+ LOG.debug("native:coordinator", "No manifest intercepted for %s in %sms. Falling back to capture.", channelName, elapsed());
63
+ return null;
64
+ }
65
+ LOG.debug("native:coordinator", "Manifest intercepted for %s in %sms.", channelName, elapsed());
66
+ // Probe the manifest for encryption type.
67
+ const probeResult = await probeManifest(interception.masterManifestUrl, channelName);
68
+ if (!probeResult) {
69
+ LOG.debug("native:coordinator", "Probe failed for %s. Falling back to capture.", channelName);
70
+ removeManifestInterceptor(interception.cdpSession);
71
+ return null;
72
+ }
73
+ if (probeResult.encryption === "drm") {
74
+ LOG.debug("native:coordinator", "Native streaming not viable for %s: DRM-protected stream.", channelName);
75
+ removeManifestInterceptor(interception.cdpSession);
76
+ return null;
77
+ }
78
+ // Separate audio renditions (e.g., Google DAI on BET/VH1) cannot be served to MPEG-TS clients because the independent video and audio MPEG-TS segments have
79
+ // incompatible PAT/PMT tables and variable packet sizes from ad splicing. HLS clients (Channels DVR) handle separate audio renditions natively via master playlist.
80
+ if (mpegTsClient && probeResult.audioVariantUrl) {
81
+ LOG.debug("native:coordinator", "Native streaming not viable for %s: separate audio rendition incompatible with MPEG-TS clients.", channelName);
82
+ removeManifestInterceptor(interception.cdpSession);
83
+ return null;
84
+ }
85
+ LOG.debug("native:coordinator", "Native streaming viable for %s (%s, variant: %s).", channelName, probeResult.encryption, probeResult.bestVariantUrl.slice(0, 80));
86
+ // For AES-128 streams, pre-fetch the decryption key before committing to native mode. This validates key accessibility while the capture pipeline is still intact,
87
+ // allowing a seamless fallback to capture if the key is inaccessible. Without this, the proxy would discover the problem on its first segment fetch — after the
88
+ // capture pipeline has already been torn down and no fallback is possible.
89
+ let prefetchedKey = null;
90
+ if ((probeResult.encryption === "aes128") && probeResult.keyUrl) {
91
+ prefetchedKey = await fetchDecryptionKey(probeResult.keyUrl);
92
+ if (!prefetchedKey) {
93
+ LOG.debug("native:coordinator", "Native streaming not viable for %s: decryption key inaccessible.", channelName);
94
+ removeManifestInterceptor(interception.cdpSession);
95
+ return null;
96
+ }
97
+ LOG.debug("native:coordinator", "AES-128 decryption key pre-fetched for %s.", channelName);
98
+ }
99
+ // Create the native proxy. The CDP session is passed so the proxy can clean it up on stop(), preventing session leaks when the stream terminates before a token
100
+ // refresh occurs. For AES-128 streams, the pre-fetched key is passed so the proxy does not need to fetch it again on the first segment. The preroll segment count
101
+ // determines the segment index offset (reserving index space for preroll). Streams with separate audio cannot use preroll because the preroll content is muxed
102
+ // video+audio and can't be split into separate renditions.
103
+ const hasSeparateAudio = probeResult.audioVariantUrl !== null;
104
+ const proxyPrerollSegmentCount = (!hasSeparateAudio && options.prerollSegmentCount) ? options.prerollSegmentCount : 0;
105
+ const proxy = createNativeProxy({
106
+ audioVariantUrl: probeResult.audioVariantUrl,
107
+ cdpSession: interception.cdpSession,
108
+ channelName,
109
+ encryption: probeResult.encryption,
110
+ keyUrl: probeResult.keyUrl,
111
+ onError,
112
+ prefetchedKey,
113
+ prerollCodec: options.prerollCodec,
114
+ prerollSegmentCount: proxyPrerollSegmentCount,
115
+ streamId,
116
+ streamIdStr,
117
+ variantUrl: probeResult.bestVariantUrl
118
+ });
119
+ // Schedule token refresh if the URL contains expiration tokens.
120
+ scheduleTokenRefresh({
121
+ channelName,
122
+ masterUrl: interception.masterManifestUrl,
123
+ page,
124
+ proxy,
125
+ streamIdStr,
126
+ url
127
+ });
128
+ LOG.debug("timing:native", "Native streaming setup completed for %s in %sms.", channelName, elapsed());
129
+ return { bandwidth: probeResult.bandwidth, codec: probeResult.codec, hasAudio: probeResult.audioVariantUrl !== null, proxy, resolution: probeResult.resolution };
130
+ }
131
+ /**
132
+ * Parses token expiration from an HLS manifest or variant URL. Common patterns include `exp=N`, `hdnea=...~exp=N~...`, path-style `/exp=N~`, and `hdnts=exp%3DN`.
133
+ * Returns the expiration timestamp in milliseconds, or null if no expiration is found.
134
+ *
135
+ * @param url - The URL to parse for token expiration.
136
+ * @returns The expiration timestamp in milliseconds, or null.
137
+ */
138
+ function parseTokenExpiry(url) {
139
+ // Pattern 1: exp=N (plain query parameter).
140
+ const expMatch = /[?&]exp=(\d{10,13})/.exec(url);
141
+ if (expMatch) {
142
+ const value = Number(expMatch[1]);
143
+ // If the value is 10 digits, it's seconds; if 13, it's milliseconds.
144
+ return value < 1e12 ? value * 1000 : value;
145
+ }
146
+ // Pattern 2: hdnea=...~exp=N~... (Akamai token in query parameter).
147
+ const hdneaExpMatch = /~exp=(\d{10,13})~/.exec(url);
148
+ if (hdneaExpMatch) {
149
+ const value = Number(hdneaExpMatch[1]);
150
+ return value < 1e12 ? value * 1000 : value;
151
+ }
152
+ // Pattern 3: /exp=N~ (Akamai token in URL path, e.g., foxvideo-sports.akamaized.net/exp=N~acl=...).
153
+ const pathExpMatch = /\/exp=(\d{10,13})~/.exec(url);
154
+ if (pathExpMatch) {
155
+ const value = Number(pathExpMatch[1]);
156
+ return value < 1e12 ? value * 1000 : value;
157
+ }
158
+ // Pattern 4: URL-encoded exp%3D (hdnts parameter).
159
+ const encodedExpMatch = /exp%3D(\d{10,13})/.exec(url);
160
+ if (encodedExpMatch) {
161
+ const value = Number(encodedExpMatch[1]);
162
+ return value < 1e12 ? value * 1000 : value;
163
+ }
164
+ return null;
165
+ }
166
+ /**
167
+ * Schedules a token refresh timer if the manifest URL contains an expiration token. The refresh occurs TOKEN_REFRESH_MARGIN milliseconds before the token expires.
168
+ * On timer fire, the refresh first attempts a direct Node.js fetch of the master manifest URL, falling back to a browser page reload if the direct fetch fails.
169
+ *
170
+ * @param options - Token refresh options.
171
+ */
172
+ function scheduleTokenRefresh(options) {
173
+ const { channelName, masterUrl, page, proxy, streamIdStr, url } = options;
174
+ const expiry = parseTokenExpiry(masterUrl);
175
+ if (!expiry) {
176
+ LOG.debug("native:token", "No token expiry found in manifest URL for %s.", channelName);
177
+ return;
178
+ }
179
+ const timeUntilExpiry = expiry - Date.now();
180
+ const refreshIn = Math.max(MIN_REFRESH_DELAY, timeUntilExpiry - TOKEN_REFRESH_MARGIN);
181
+ LOG.debug("native:token", "Token expires in %ss for %s. Refresh scheduled in %ss.", Math.round(timeUntilExpiry / 1000), channelName, Math.round(refreshIn / 1000));
182
+ // Store the timer handle on the proxy so it can be cancelled if the proxy is stopped before the timer fires. Pass the master URL so the refresh can attempt a
183
+ // direct fetch before falling back to a page reload.
184
+ const timer = setTimeout(() => {
185
+ void refreshNativeManifest({ channelName, masterUrl, page, proxy, streamIdStr, url });
186
+ }, refreshIn);
187
+ proxy.setTokenRefreshTimer(timer);
188
+ }
189
+ /**
190
+ * Refreshes a native stream's manifest to obtain fresh auth tokens. Two strategies are tried in order:
191
+ *
192
+ * 1. **Direct fetch** (if masterUrl is provided): Re-fetches the master manifest URL from Node.js without involving the browser. This avoids the visible page reload
193
+ * that disrupts the browser tab. The CDN returns the manifest with current token values as long as the URL's own auth token hasn't expired. When the master URL
194
+ * expires, the direct fetch returns 403 and we fall through to strategy 2.
195
+ *
196
+ * 2. **Page reload**: Navigates the browser page back to the channel URL, triggering fresh authentication via cookies. A CDP interceptor captures the new master
197
+ * manifest URL with fresh tokens. This is the only path that generates genuinely new tokens and is required when the master URL itself has expired.
198
+ *
199
+ * L2 recovery (failure-triggered) from the monitor omits masterUrl, going straight to page reload since the stream is already failing and needs a full refresh.
200
+ *
201
+ * @param options - Refresh options. masterUrl is optional — when provided, direct fetch is attempted first.
202
+ * @returns True if the refresh succeeded (proxy updated with new manifest), false otherwise.
203
+ */
204
+ export async function refreshNativeManifest(options) {
205
+ const { channelName, masterUrl, page, proxy, streamIdStr, url } = options;
206
+ const streamLog = LOG.withStreamId(streamIdStr);
207
+ if (proxy.isStopped()) {
208
+ return false;
209
+ }
210
+ const refreshElapsed = startTimer();
211
+ streamLog.debug("native:token", "Starting manifest refresh for %s.", channelName);
212
+ // Strategy 1: Direct fetch. Re-fetch the master manifest URL from Node.js to get fresh variant URLs without reloading the browser page. This works as long as the
213
+ // master URL's own CDN auth token hasn't expired. When it does expire, probeManifest returns null (403) and we fall through to the page reload strategy.
214
+ if (masterUrl) {
215
+ const directResult = await tryDirectManifestRefresh(masterUrl, channelName, streamLog);
216
+ if (directResult) {
217
+ // Check if the proxy was stopped during the async probe (e.g., stream terminated while probing).
218
+ if (proxy.isStopped()) {
219
+ return false;
220
+ }
221
+ // Direct fetch succeeded. Update the proxy with the fresh variant URL(s).
222
+ proxy.updateVariantUrl(directResult.bestVariantUrl);
223
+ if (directResult.audioVariantUrl) {
224
+ proxy.updateAudioVariantUrl(directResult.audioVariantUrl);
225
+ }
226
+ streamLog.debug("native:token", "Manifest refresh completed for %s via direct fetch in %sms.", channelName, refreshElapsed());
227
+ // Schedule the next refresh with the same master URL. It may still be valid for subsequent direct fetches. Once it expires, the direct fetch will fail and
228
+ // the page reload fallback will generate a new master URL.
229
+ scheduleTokenRefresh({
230
+ channelName,
231
+ masterUrl,
232
+ page,
233
+ proxy,
234
+ streamIdStr,
235
+ url
236
+ });
237
+ return true;
238
+ }
239
+ // Direct fetch failed. Fall through to page reload.
240
+ streamLog.debug("native:token", "Direct manifest fetch failed for %s. Falling back to page reload.", channelName);
241
+ }
242
+ // Strategy 2: Page reload. Navigate the browser page to trigger fresh authentication and intercept a new master manifest via CDP.
243
+ if (page.isClosed()) {
244
+ streamLog.debug("native:token", "Manifest refresh failed for %s: page is closed.", channelName);
245
+ return false;
246
+ }
247
+ try {
248
+ // Install a fresh interceptor on the page. For token refresh, the page navigates directly to the channel URL (no guide grid), so the first manifest captured is
249
+ // the correct one. We call finalize() after navigation to resolve immediately with whatever was captured.
250
+ const handle = await installManifestInterceptor(page, 20000);
251
+ if (!handle) {
252
+ streamLog.debug("native:token", "Manifest refresh failed for %s: could not install interceptor.", channelName);
253
+ return false;
254
+ }
255
+ // Navigate the page back to the channel URL to trigger fresh authentication. The interceptor captures .m3u8 requests generated by this navigation. Note: this
256
+ // uses a bare page.goto without the provider's site profile (no waitForNetworkIdle, scroll options, etc.). This is acceptable because native streaming only
257
+ // activates for providers whose video players load the HLS manifest during basic page load. Providers requiring profile-aware navigation for manifest delivery
258
+ // would have failed the initial interception and would not be in native mode.
259
+ await page.goto(url, { timeout: 30000, waitUntil: "domcontentloaded" });
260
+ // Check if the proxy was stopped while we were navigating (e.g., stream terminated during the page load).
261
+ if (proxy.isStopped()) {
262
+ return false;
263
+ }
264
+ // Signal finalize and await the interception result. Token refresh always navigates directly (no guide grid), so the first manifest is correct.
265
+ handle.finalize(true);
266
+ const newInterception = await handle.promise;
267
+ if (!newInterception) {
268
+ streamLog.debug("native:token", "Manifest refresh failed for %s: no manifest intercepted after reload.", channelName);
269
+ return false;
270
+ }
271
+ // Check if the proxy was stopped while we were waiting for the interception. Clean up the new session since the proxy will not take ownership of it.
272
+ if (proxy.isStopped()) {
273
+ removeManifestInterceptor(newInterception.cdpSession);
274
+ return false;
275
+ }
276
+ // Probe the new manifest to get the updated variant URL. We probe before handing the new CDP session to the proxy so that a probe failure does not leave the
277
+ // proxy holding a reference to a session we are about to clean up.
278
+ const probeResult = await probeManifest(newInterception.masterManifestUrl, channelName);
279
+ if (!probeResult) {
280
+ streamLog.debug("native:token", "Manifest refresh failed for %s: probe failed on new manifest.", channelName);
281
+ removeManifestInterceptor(newInterception.cdpSession);
282
+ return false;
283
+ }
284
+ // Check if the proxy was stopped during the probe. Clean up the new session since the proxy will not take ownership of it.
285
+ if (proxy.isStopped()) {
286
+ removeManifestInterceptor(newInterception.cdpSession);
287
+ return false;
288
+ }
289
+ // Probe succeeded. Hand the new CDP session to the proxy. updateCdpSession cleans up the old session internally before replacing it.
290
+ proxy.updateCdpSession(newInterception.cdpSession);
291
+ // Update the proxy with the new variant URL(s).
292
+ proxy.updateVariantUrl(probeResult.bestVariantUrl);
293
+ if (probeResult.audioVariantUrl) {
294
+ proxy.updateAudioVariantUrl(probeResult.audioVariantUrl);
295
+ }
296
+ streamLog.debug("native:token", "Manifest refresh completed for %s via page reload in %sms.", channelName, refreshElapsed());
297
+ // Schedule the next proactive token refresh with the NEW master URL from the fresh interception. Subsequent direct fetches will use this URL until it expires.
298
+ scheduleTokenRefresh({
299
+ channelName,
300
+ masterUrl: newInterception.masterManifestUrl,
301
+ page,
302
+ proxy,
303
+ streamIdStr,
304
+ url
305
+ });
306
+ return true;
307
+ }
308
+ catch (error) {
309
+ streamLog.debug("native:token", "Manifest refresh failed for %s: %s.", channelName, formatError(error));
310
+ // Clear the probe cache so a subsequent attempt re-probes.
311
+ clearProbeCache(channelName);
312
+ return false;
313
+ }
314
+ }
315
+ /**
316
+ * Attempts to refresh the manifest by directly fetching the master manifest URL from Node.js. Returns the probe result if the fetch succeeds and the variant URL
317
+ * has sufficient token lifetime remaining, or null if the direct fetch should be abandoned in favor of a page reload.
318
+ *
319
+ * @param masterUrl - The master manifest URL to re-fetch.
320
+ * @param channelName - The channel name for logging and cache keys.
321
+ * @param streamLog - The stream-scoped logger.
322
+ * @returns The probe result with a fresh variant URL, or null on failure.
323
+ */
324
+ async function tryDirectManifestRefresh(masterUrl, channelName, streamLog) {
325
+ const probeResult = await probeManifest(masterUrl, channelName);
326
+ if (!probeResult || (probeResult.encryption === "drm")) {
327
+ return null;
328
+ }
329
+ // Verify the variant URL's token hasn't already expired or is about to expire. Parse the expiry from the variant URL (not the master URL) since that's what the
330
+ // proxy will actually poll. If the token expires within MIN_USABLE_TOKEN_LIFETIME, the direct fetch result is stale — the page reload path will generate a
331
+ // genuinely fresh token.
332
+ const variantExpiry = parseTokenExpiry(probeResult.bestVariantUrl);
333
+ if (variantExpiry) {
334
+ const remaining = variantExpiry - Date.now();
335
+ if (remaining < MIN_USABLE_TOKEN_LIFETIME) {
336
+ streamLog.debug("native:token", "Direct fetch returned near-expired variant for %s (%ss remaining). Discarding.", channelName, Math.round(remaining / 1000));
337
+ return null;
338
+ }
339
+ }
340
+ return probeResult;
341
+ }
342
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/native/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAOvF,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD;;;;;;;;;;;;GAYG;AAEH,4JAA4J;AAC5J,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAEpC,qKAAqK;AACrK,kKAAkK;AAClK,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEhC,oKAAoK;AACpK,oKAAoK;AACpK,oKAAoK;AACpK,qIAAqI;AACrI,MAAM,yBAAyB,GAAG,IAAI,CAAC;AAEvC,8EAA8E;AAC9E,MAAM,0BAA0B,GAAG,IAAI,CAAC;AA8DxC;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,OAAsC;IAEjF,MAAM,EAAE,WAAW,EAAE,mBAAmB,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;IAE9G,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAE7B,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,qCAAqC,EAAE,WAAW,CAAC,CAAC;IAEpF,yJAAyJ;IACzJ,sCAAsC;IACtC,IAAI,YAAkD,CAAC;IAEvD,IAAI,CAAC;QAEH,YAAY,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;YAChC,mBAAmB;YACnB,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;gBAE5B,UAAU,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,0BAA0B,CAAC,CAAC;YACnE,CAAC,CAAC;SACH,CAAC,CAAC;IACL,CAAC;IAAC,OAAM,KAAK,EAAE,CAAC;QAEd,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,yCAAyC,EAAE,WAAW,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;QAE5G,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAG,CAAC,YAAY,EAAE,CAAC;QAEjB,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,kEAAkE,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC;QAE5H,OAAO,IAAI,CAAC;IACd,CAAC;IAED,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,sCAAsC,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC;IAEhG,0CAA0C;IAC1C,MAAM,WAAW,GAAG,MAAM,aAAa,CAAC,YAAY,CAAC,iBAAiB,EAAE,WAAW,CAAC,CAAC;IAErF,IAAG,CAAC,WAAW,EAAE,CAAC;QAEhB,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,+CAA+C,EAAE,WAAW,CAAC,CAAC;QAC9F,yBAAyB,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;QAEnD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAG,WAAW,CAAC,UAAU,KAAK,KAAK,EAAE,CAAC;QAEpC,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,2DAA2D,EAAE,WAAW,CAAC,CAAC;QAC1G,yBAAyB,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;QAEnD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,4JAA4J;IAC5J,oKAAoK;IACpK,IAAG,YAAY,IAAI,WAAW,CAAC,eAAe,EAAE,CAAC;QAE/C,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,iGAAiG,EAAE,WAAW,CAAC,CAAC;QAChJ,yBAAyB,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;QAEnD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,mDAAmD,EAAE,WAAW,EAAE,WAAW,CAAC,UAAU,EAAE,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAEnK,mKAAmK;IACnK,gKAAgK;IAChK,2EAA2E;IAC3E,IAAI,aAAa,GAAqB,IAAI,CAAC;IAE3C,IAAG,CAAC,WAAW,CAAC,UAAU,KAAK,QAAQ,CAAC,IAAI,WAAW,CAAC,MAAM,EAAE,CAAC;QAE/D,aAAa,GAAG,MAAM,kBAAkB,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAE7D,IAAG,CAAC,aAAa,EAAE,CAAC;YAElB,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,kEAAkE,EAAE,WAAW,CAAC,CAAC;YACjH,yBAAyB,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;YAEnD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,4CAA4C,EAAE,WAAW,CAAC,CAAC;IAC7F,CAAC;IAED,gKAAgK;IAChK,kKAAkK;IAClK,+JAA+J;IAC/J,2DAA2D;IAC3D,MAAM,gBAAgB,GAAG,WAAW,CAAC,eAAe,KAAK,IAAI,CAAC;IAC9D,MAAM,wBAAwB,GAAG,CAAC,CAAC,gBAAgB,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;IAEtH,MAAM,KAAK,GAAG,iBAAiB,CAAC;QAE9B,eAAe,EAAE,WAAW,CAAC,eAAe;QAC5C,UAAU,EAAE,YAAY,CAAC,UAAU;QACnC,WAAW;QACX,UAAU,EAAE,WAAW,CAAC,UAAU;QAClC,MAAM,EAAE,WAAW,CAAC,MAAM;QAC1B,OAAO;QACP,aAAa;QACb,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,mBAAmB,EAAE,wBAAwB;QAC7C,QAAQ;QACR,WAAW;QACX,UAAU,EAAE,WAAW,CAAC,cAAc;KACvC,CAAC,CAAC;IAEH,gEAAgE;IAChE,oBAAoB,CAAC;QAEnB,WAAW;QACX,SAAS,EAAE,YAAY,CAAC,iBAAiB;QACzC,IAAI;QACJ,KAAK;QACL,WAAW;QACX,GAAG;KACJ,CAAC,CAAC;IAEH,GAAG,CAAC,KAAK,CAAC,eAAe,EAAE,kDAAkD,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC;IAEvG,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,SAAS,EAAE,KAAK,EAAE,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,WAAW,CAAC,eAAe,KAAK,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,CAAC,UAAU,EAAE,CAAC;AACnK,CAAC;AAiBD;;;;;;GAMG;AACH,SAAS,gBAAgB,CAAC,GAAW;IAEnC,4CAA4C;IAC5C,MAAM,QAAQ,GAAG,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEjD,IAAG,QAAQ,EAAE,CAAC;QAEZ,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAElC,qEAAqE;QACrE,OAAO,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;IAC7C,CAAC;IAED,oEAAoE;IACpE,MAAM,aAAa,GAAG,mBAAmB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEpD,IAAG,aAAa,EAAE,CAAC;QAEjB,MAAM,KAAK,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;QAEvC,OAAO,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;IAC7C,CAAC;IAED,oGAAoG;IACpG,MAAM,YAAY,GAAG,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEpD,IAAG,YAAY,EAAE,CAAC;QAEhB,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;QAEtC,OAAO,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;IAC7C,CAAC;IAED,mDAAmD;IACnD,MAAM,eAAe,GAAG,mBAAmB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEtD,IAAG,eAAe,EAAE,CAAC;QAEnB,MAAM,KAAK,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;QAEzC,OAAO,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;IAC7C,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,SAAS,oBAAoB,CAAC,OAA4B;IAExD,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;IAE1E,MAAM,MAAM,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAE3C,IAAG,CAAC,MAAM,EAAE,CAAC;QAEX,GAAG,CAAC,KAAK,CAAC,cAAc,EAAE,+CAA+C,EAAE,WAAW,CAAC,CAAC;QAExF,OAAO;IACT,CAAC;IAED,MAAM,eAAe,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,eAAe,GAAG,oBAAoB,CAAC,CAAC;IAEtF,GAAG,CAAC,KAAK,CAAC,cAAc,EAAE,wDAAwD,EAChF,IAAI,CAAC,KAAK,CAAC,eAAe,GAAG,IAAI,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC;IAEjF,8JAA8J;IAC9J,qDAAqD;IACrD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;QAE5B,KAAK,qBAAqB,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC;IACxF,CAAC,EAAE,SAAS,CAAC,CAAC;IAEd,KAAK,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC;AACpC,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,OAO3C;IAEC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;IAC1E,MAAM,SAAS,GAAG,GAAG,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;IAEhD,IAAG,KAAK,CAAC,SAAS,EAAE,EAAE,CAAC;QAErB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,cAAc,GAAG,UAAU,EAAE,CAAC;IAEpC,SAAS,CAAC,KAAK,CAAC,cAAc,EAAE,mCAAmC,EAAE,WAAW,CAAC,CAAC;IAElF,kKAAkK;IAClK,yJAAyJ;IACzJ,IAAG,SAAS,EAAE,CAAC;QAEb,MAAM,YAAY,GAAG,MAAM,wBAAwB,CAAC,SAAS,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;QAEvF,IAAG,YAAY,EAAE,CAAC;YAEhB,iGAAiG;YACjG,IAAG,KAAK,CAAC,SAAS,EAAE,EAAE,CAAC;gBAErB,OAAO,KAAK,CAAC;YACf,CAAC;YAED,0EAA0E;YAC1E,KAAK,CAAC,gBAAgB,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC;YAEpD,IAAG,YAAY,CAAC,eAAe,EAAE,CAAC;gBAEhC,KAAK,CAAC,qBAAqB,CAAC,YAAY,CAAC,eAAe,CAAC,CAAC;YAC5D,CAAC;YAED,SAAS,CAAC,KAAK,CAAC,cAAc,EAAE,6DAA6D,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,CAAC;YAE9H,2JAA2J;YAC3J,2DAA2D;YAC3D,oBAAoB,CAAC;gBAEnB,WAAW;gBACX,SAAS;gBACT,IAAI;gBACJ,KAAK;gBACL,WAAW;gBACX,GAAG;aACJ,CAAC,CAAC;YAEH,OAAO,IAAI,CAAC;QACd,CAAC;QAED,oDAAoD;QACpD,SAAS,CAAC,KAAK,CAAC,cAAc,EAAE,mEAAmE,EAAE,WAAW,CAAC,CAAC;IACpH,CAAC;IAED,kIAAkI;IAClI,IAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;QAEnB,SAAS,CAAC,KAAK,CAAC,cAAc,EAAE,iDAAiD,EAAE,WAAW,CAAC,CAAC;QAEhG,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,CAAC;QAEH,gKAAgK;QAChK,0GAA0G;QAC1G,MAAM,MAAM,GAAG,MAAM,0BAA0B,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAE7D,IAAG,CAAC,MAAM,EAAE,CAAC;YAEX,SAAS,CAAC,KAAK,CAAC,cAAc,EAAE,gEAAgE,EAAE,WAAW,CAAC,CAAC;YAE/G,OAAO,KAAK,CAAC;QACf,CAAC;QAED,8JAA8J;QAC9J,4JAA4J;QAC5J,+JAA+J;QAC/J,8EAA8E;QAC9E,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC,CAAC;QAExE,0GAA0G;QAC1G,IAAG,KAAK,CAAC,SAAS,EAAE,EAAE,CAAC;YAErB,OAAO,KAAK,CAAC;QACf,CAAC;QAED,gJAAgJ;QAChJ,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAEtB,MAAM,eAAe,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC;QAE7C,IAAG,CAAC,eAAe,EAAE,CAAC;YAEpB,SAAS,CAAC,KAAK,CAAC,cAAc,EAAE,uEAAuE,EAAE,WAAW,CAAC,CAAC;YAEtH,OAAO,KAAK,CAAC;QACf,CAAC;QAED,qJAAqJ;QACrJ,IAAG,KAAK,CAAC,SAAS,EAAE,EAAE,CAAC;YAErB,yBAAyB,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;YAEtD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,6JAA6J;QAC7J,mEAAmE;QACnE,MAAM,WAAW,GAAG,MAAM,aAAa,CAAC,eAAe,CAAC,iBAAiB,EAAE,WAAW,CAAC,CAAC;QAExF,IAAG,CAAC,WAAW,EAAE,CAAC;YAEhB,SAAS,CAAC,KAAK,CAAC,cAAc,EAAE,+DAA+D,EAAE,WAAW,CAAC,CAAC;YAC9G,yBAAyB,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;YAEtD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,2HAA2H;QAC3H,IAAG,KAAK,CAAC,SAAS,EAAE,EAAE,CAAC;YAErB,yBAAyB,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;YAEtD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,qIAAqI;QACrI,KAAK,CAAC,gBAAgB,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;QAEnD,gDAAgD;QAChD,KAAK,CAAC,gBAAgB,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC;QAEnD,IAAG,WAAW,CAAC,eAAe,EAAE,CAAC;YAE/B,KAAK,CAAC,qBAAqB,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC;QAC3D,CAAC;QAED,SAAS,CAAC,KAAK,CAAC,cAAc,EAAE,4DAA4D,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,CAAC;QAE7H,+JAA+J;QAC/J,oBAAoB,CAAC;YAEnB,WAAW;YACX,SAAS,EAAE,eAAe,CAAC,iBAAiB;YAC5C,IAAI;YACJ,KAAK;YACL,WAAW;YACX,GAAG;SACJ,CAAC,CAAC;QAEH,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAM,KAAK,EAAE,CAAC;QAEd,SAAS,CAAC,KAAK,CAAC,cAAc,EAAE,qCAAqC,EAAE,WAAW,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;QAExG,2DAA2D;QAC3D,eAAe,CAAC,WAAW,CAAC,CAAC;QAE7B,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,wBAAwB,CAAC,SAAiB,EAAE,WAAmB,EAC5E,SAA8C;IAE9C,MAAM,WAAW,GAAG,MAAM,aAAa,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IAEhE,IAAG,CAAC,WAAW,IAAI,CAAC,WAAW,CAAC,UAAU,KAAK,KAAK,CAAC,EAAE,CAAC;QAEtD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,gKAAgK;IAChK,2JAA2J;IAC3J,yBAAyB;IACzB,MAAM,aAAa,GAAG,gBAAgB,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC;IAEnE,IAAG,aAAa,EAAE,CAAC;QAEjB,MAAM,SAAS,GAAG,aAAa,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAE7C,IAAG,SAAS,GAAG,yBAAyB,EAAE,CAAC;YAEzC,SAAS,CAAC,KAAK,CAAC,cAAc,EAAE,gFAAgF,EAAE,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC;YAE7J,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,WAAW,CAAC;AACrB,CAAC"}
@@ -0,0 +1,35 @@
1
+ import type { CDPSession, Page } from "puppeteer-core";
2
+ import type { Nullable } from "../types/index.js";
3
+ /**
4
+ * Result of manifest interception containing the CDP session for reuse during token refresh and the master manifest URL.
5
+ */
6
+ export interface ManifestInterceptionResult {
7
+ cdpSession: CDPSession;
8
+ masterManifestUrl: string;
9
+ }
10
+ /**
11
+ * Handle returned by installManifestInterceptor. Provides the interception promise and a finalize function to signal that channel selection is complete.
12
+ */
13
+ export interface ManifestInterceptorHandle {
14
+ finalize: (directTune: boolean) => void;
15
+ promise: Promise<Nullable<ManifestInterceptionResult>>;
16
+ }
17
+ /**
18
+ * Installs a CDP Network.responseReceived listener on the given page to capture master HLS manifest URLs. The listener tracks both the first and latest master
19
+ * manifest URLs as they arrive. The returned handle provides a promise that resolves when finalize() is called — the caller invokes finalize() after channel
20
+ * selection is complete, passing directTune to control which manifest is selected.
21
+ *
22
+ * For direct-navigation sites (directTune=true), the first master manifest is the correct one — it was loaded for the navigated URL. Background prefetches for
23
+ * other channels are ignored. For guide-based sites (directTune=false), the latest manifest is selected — it corresponds to the channel the user clicked.
24
+ *
25
+ * @param page - The Puppeteer page to monitor.
26
+ * @param timeout - Maximum time in milliseconds to wait for a manifest (default: 15 seconds). Acts as a safety net if finalize() is never called.
27
+ * @returns The interceptor handle, or null if the CDP session could not be created.
28
+ */
29
+ export declare function installManifestInterceptor(page: Page, timeout?: number): Promise<Nullable<ManifestInterceptorHandle>>;
30
+ /**
31
+ * Removes the Network domain listener and detaches the CDP session. Safe to call multiple times or on an already-detached session.
32
+ *
33
+ * @param cdpSession - The CDP session to clean up.
34
+ */
35
+ export declare function removeManifestInterceptor(cdpSession: CDPSession): void;
@@ -0,0 +1,184 @@
1
+ import { LOG, chromeFetch, startTimer } from "../utils/index.js";
2
+ /* This module installs a Chrome DevTools Protocol (CDP) listener on the Network domain to capture HLS manifest URLs as the browser's video player fetches them. Unlike
3
+ * the per-call CDP sessions created by withCDPSession() in cdp.ts, we create a long-lived session here because the listener must remain active across multiple network
4
+ * requests until channel selection is complete.
5
+ *
6
+ * The listener filters for URLs ending in .m3u8 and fetches the manifest body directly via Node.js to inspect the content. Master manifests contain #EXT-X-STREAM-INF
7
+ * directives (variant playlist references), which distinguishes them from variant/media playlists. We use a direct fetch rather than CDP's Network.getResponseBody
8
+ * because Chrome's network cache can evict response bodies before we read them, causing spurious "No data found for resource" failures.
9
+ *
10
+ * For multi-channel sites, the video player may load manifests for channels other than the one requested. The interceptor tracks both the first and latest master
11
+ * manifest URLs to handle two distinct scenarios:
12
+ *
13
+ * - Direct-navigation sites (Fox Sports): The first manifest is the correct one — the player loaded it for the navigated URL. Background prefetches for other channels
14
+ * may arrive later but should be ignored. finalize(directTune=true) selects the first manifest.
15
+ *
16
+ * - Guide-based sites (Fox.com, Hulu, Sling, YouTube TV): The player loads a default channel's manifest during page navigation, then the guide grid strategy clicks the
17
+ * correct entry, triggering a new manifest fetch. finalize(directTune=false) selects the latest manifest.
18
+ */
19
+ // Default timeout for manifest interception after installation.
20
+ const INTERCEPTION_TIMEOUT = 15000;
21
+ // Brief delay after finalize() is called to allow any in-flight manifest response to arrive. Applied only for guide-based tunes (directTune=false) where the
22
+ // channel switch may trigger a manifest fetch that arrives milliseconds after the click handler returns. Direct tunes skip this delay and resolve immediately.
23
+ const FINALIZE_SETTLE_DELAY = 1500;
24
+ /**
25
+ * Installs a CDP Network.responseReceived listener on the given page to capture master HLS manifest URLs. The listener tracks both the first and latest master
26
+ * manifest URLs as they arrive. The returned handle provides a promise that resolves when finalize() is called — the caller invokes finalize() after channel
27
+ * selection is complete, passing directTune to control which manifest is selected.
28
+ *
29
+ * For direct-navigation sites (directTune=true), the first master manifest is the correct one — it was loaded for the navigated URL. Background prefetches for
30
+ * other channels are ignored. For guide-based sites (directTune=false), the latest manifest is selected — it corresponds to the channel the user clicked.
31
+ *
32
+ * @param page - The Puppeteer page to monitor.
33
+ * @param timeout - Maximum time in milliseconds to wait for a manifest (default: 15 seconds). Acts as a safety net if finalize() is never called.
34
+ * @returns The interceptor handle, or null if the CDP session could not be created.
35
+ */
36
+ export async function installManifestInterceptor(page, timeout = INTERCEPTION_TIMEOUT) {
37
+ if (page.isClosed()) {
38
+ return null;
39
+ }
40
+ const elapsed = startTimer();
41
+ let cdpSession;
42
+ try {
43
+ cdpSession = await page.createCDPSession();
44
+ }
45
+ catch (error) {
46
+ LOG.debug("native:intercept", "Failed to create CDP session: %s.", String(error));
47
+ return null;
48
+ }
49
+ // Enable the Network domain to receive request and response events.
50
+ try {
51
+ await cdpSession.send("Network.enable");
52
+ }
53
+ catch (error) {
54
+ LOG.debug("native:intercept", "Failed to enable Network domain: %s.", String(error));
55
+ return null;
56
+ }
57
+ LOG.debug("native:intercept", "CDP manifest interceptor installed.");
58
+ // Track both the first and most recently observed master manifest URLs. For direct-navigation sites (directTune=true), the first manifest is the correct one — the
59
+ // player loaded it for the navigated URL. Background prefetches for other channels may arrive later and must not overwrite the selection. For guide-based sites
60
+ // (directTune=false), the last manifest is correct — the guide click triggers a new manifest fetch that replaces whatever the player initially loaded.
61
+ let firstManifestUrl = null;
62
+ let latestManifestUrl = null;
63
+ let resolved = false;
64
+ let manifestCount = 0;
65
+ // Finalize function, assigned inside the promise constructor and exposed on the returned handle.
66
+ let finalize = () => { };
67
+ const promise = new Promise((resolve) => {
68
+ // Timeout guard. If finalize() is never called (defensive), resolve with whatever we have after the timeout.
69
+ const timer = setTimeout(() => {
70
+ if (!resolved) {
71
+ resolved = true;
72
+ cdpSession.off("Network.responseReceived", wrappedHandler);
73
+ if (latestManifestUrl) {
74
+ LOG.debug("native:intercept", "Manifest interception timed out after %sms. Resolving with latest URL (%s captured).", elapsed(), manifestCount);
75
+ resolve({ cdpSession, masterManifestUrl: latestManifestUrl });
76
+ }
77
+ else {
78
+ LOG.debug("native:intercept", "Manifest interception timed out after %sms. No master manifest captured.", elapsed());
79
+ removeManifestInterceptor(cdpSession);
80
+ resolve(null);
81
+ }
82
+ }
83
+ }, timeout);
84
+ // Listen for completed responses. We use Network.responseReceived to capture .m3u8 URLs, then fetch the manifest body directly from Node.js to verify it is
85
+ // a master manifest. This avoids CDP's Network.getResponseBody which is unreliable — Chrome's network cache can evict response bodies before we read them.
86
+ const onResponseReceived = async (params) => {
87
+ if (resolved) {
88
+ return;
89
+ }
90
+ const url = params.response.url;
91
+ // Filter for .m3u8 URLs. Strip query parameters before checking the extension.
92
+ const urlPath = url.split("?")[0];
93
+ if (!urlPath.endsWith(".m3u8")) {
94
+ return;
95
+ }
96
+ LOG.debug("native:intercept", "Observed .m3u8 response: %s.", url.slice(0, 120));
97
+ // Fetch the manifest body directly to inspect whether this is a master manifest. The URL contains embedded CDN auth tokens, so no cookies or special
98
+ // headers are needed beyond the Chrome User-Agent injected by chromeFetch().
99
+ try {
100
+ const response = await chromeFetch(url, { signal: AbortSignal.timeout(5000) });
101
+ if (!response.ok) {
102
+ LOG.debug("native:intercept", "Manifest fetch returned HTTP %s for %s.", response.status, url.slice(0, 120));
103
+ return;
104
+ }
105
+ const body = await response.text();
106
+ // Master manifests contain #EXT-X-STREAM-INF directives that reference variant playlists. Media/variant playlists contain #EXTINF or #EXT-X-TARGETDURATION
107
+ // but not #EXT-X-STREAM-INF.
108
+ if (body.includes("#EXT-X-STREAM-INF")) {
109
+ manifestCount++;
110
+ firstManifestUrl ??= url;
111
+ latestManifestUrl = url;
112
+ LOG.debug("native:intercept", "Master manifest captured (#%s) in %sms: %s.", manifestCount, elapsed(), url.slice(0, 120));
113
+ }
114
+ else {
115
+ LOG.debug("native:intercept", "Skipping non-master .m3u8 (no #EXT-X-STREAM-INF).");
116
+ }
117
+ }
118
+ catch (error) {
119
+ LOG.debug("native:intercept", "Could not fetch .m3u8 body: %s.", String(error));
120
+ }
121
+ };
122
+ // Wrap the async handler to avoid no-misused-promises — EventEmitter.on() does not handle returned promises.
123
+ const wrappedHandler = (...args) => { void onResponseReceived(...args); };
124
+ cdpSession.on("Network.responseReceived", wrappedHandler);
125
+ // Assign the finalize function. Called by the stream setup code after channel selection is complete. The resolution strategy depends on two factors: whether a
126
+ // manifest has already been captured, and whether the tune is direct or guide-based.
127
+ //
128
+ // - Manifest captured + direct tune: resolve immediately (A&E, most TVE sites — manifest arrived during page load).
129
+ // - Manifest captured + guide tune: wait FINALIZE_SETTLE_DELAY (Fox guide, Hulu — a newer manifest from the channel switch may still arrive).
130
+ // - No manifest captured + either: wait FINALIZE_SETTLE_DELAY (Fox Sports — manifest fetch starts after video element appears, hasn't arrived yet).
131
+ finalize = (directTune) => {
132
+ if (resolved) {
133
+ return;
134
+ }
135
+ // Helper that resolves the promise with the current state. For direct tunes, the first manifest is the correct one (loaded for the navigated URL — background
136
+ // prefetches for other channels may have overwritten latestManifestUrl). For guide tunes, the last manifest is correct (from the channel switch click).
137
+ const resolveNow = () => {
138
+ if (resolved) {
139
+ return;
140
+ }
141
+ resolved = true;
142
+ clearTimeout(timer);
143
+ cdpSession.off("Network.responseReceived", wrappedHandler);
144
+ const selectedUrl = directTune ? firstManifestUrl : latestManifestUrl;
145
+ if (selectedUrl) {
146
+ LOG.debug("native:intercept", "Interception finalized in %sms with %s manifest(s). Using %s: %s.", elapsed(), manifestCount, directTune ? "first" : "latest", selectedUrl.slice(0, 120));
147
+ resolve({ cdpSession, masterManifestUrl: selectedUrl });
148
+ }
149
+ else {
150
+ LOG.debug("native:intercept", "Interception finalized in %sms but no master manifest was captured.", elapsed());
151
+ removeManifestInterceptor(cdpSession);
152
+ resolve(null);
153
+ }
154
+ };
155
+ if (directTune && firstManifestUrl) {
156
+ // Direct tune with manifest already captured: resolve immediately with zero delay. The first manifest arrived during page load and is the correct one.
157
+ resolveNow();
158
+ }
159
+ else {
160
+ // Either no manifest captured yet (some providers fetch the manifest after the video element appears) or a guide-based tune where the channel switch
161
+ // may produce a newer manifest. Wait briefly for in-flight responses.
162
+ setTimeout(resolveNow, FINALIZE_SETTLE_DELAY);
163
+ }
164
+ };
165
+ });
166
+ return { finalize, promise };
167
+ }
168
+ /**
169
+ * Removes the Network domain listener and detaches the CDP session. Safe to call multiple times or on an already-detached session.
170
+ *
171
+ * @param cdpSession - The CDP session to clean up.
172
+ */
173
+ export function removeManifestInterceptor(cdpSession) {
174
+ try {
175
+ cdpSession.removeAllListeners("Network.responseReceived");
176
+ void cdpSession.send("Network.disable").catch(() => { });
177
+ void cdpSession.detach().catch(() => { });
178
+ }
179
+ catch (_error) {
180
+ // Ignore errors during cleanup — the session may already be detached.
181
+ }
182
+ LOG.debug("native:intercept", "CDP manifest interceptor removed.");
183
+ }
184
+ //# sourceMappingURL=intercept.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intercept.js","sourceRoot":"","sources":["../../src/native/intercept.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import type { Nullable } from "../types/index.js";
2
+ /**
3
+ * Encryption classification result from probing an HLS manifest.
4
+ */
5
+ export type EncryptionType = "aes128" | "clear" | "drm";
6
+ /**
7
+ * Result of probing an HLS master manifest for encryption type and best variant.
8
+ */
9
+ export interface ProbeResult {
10
+ audioVariantUrl: Nullable<string>;
11
+ bandwidth: number;
12
+ bestVariantUrl: string;
13
+ codec: Nullable<string>;
14
+ encryption: EncryptionType;
15
+ keyUrl: Nullable<string>;
16
+ resolution: Nullable<string>;
17
+ }
18
+ /**
19
+ * 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
20
+ * CDP interceptor installation for channels already known to use DRM.
21
+ *
22
+ * @param channelName - The channel name to look up.
23
+ * @returns The cached encryption type, or null if not probed or expired.
24
+ */
25
+ export declare function getCachedEncryption(channelName: string): Nullable<EncryptionType>;
26
+ /**
27
+ * Clears the probe cache for a specific channel. Called when a native stream fails, forcing a fresh probe on the next attempt.
28
+ *
29
+ * @param channelName - The channel name to clear from the cache.
30
+ */
31
+ export declare function clearProbeCache(channelName: string): void;
32
+ /**
33
+ * Probes an HLS master manifest to determine encryption type and select the best variant. The probe cache is checked for DRM channels only — if a previous probe
34
+ * classified the channel as DRM, we return the cached result immediately since the caller will bail out regardless of URLs. For viable channels (clear or aes128),
35
+ * we always run the full probe because the variant URL and key URL contain auth tokens that expire between browser sessions.
36
+ *
37
+ * @param masterUrl - The master manifest URL (contains auth tokens from the browser's original request).
38
+ * @param channelName - The channel name for cache lookup.
39
+ * @returns The probe result, or null if probing fails.
40
+ */
41
+ export declare function probeManifest(masterUrl: string, channelName: string): Promise<Nullable<ProbeResult>>;
42
+ /**
43
+ * Resolves a potentially relative URL against a base URL. Handles both absolute and relative URLs. Exported for reuse by the proxy module.
44
+ *
45
+ * @param url - The URL to resolve (may be relative or absolute).
46
+ * @param baseUrl - The base URL for resolving relative references.
47
+ * @returns The resolved absolute URL.
48
+ */
49
+ export declare function resolveUrl(url: string, baseUrl: string): string;