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.
- package/dist/app.d.ts +6 -3
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +140 -86
- package/dist/app.js.map +1 -1
- package/dist/browser/blockedPage.d.ts +80 -0
- package/dist/browser/blockedPage.d.ts.map +1 -0
- package/dist/browser/blockedPage.js +209 -0
- package/dist/browser/blockedPage.js.map +1 -0
- package/dist/browser/browserSupervisor.d.ts +80 -0
- package/dist/browser/browserSupervisor.d.ts.map +1 -0
- package/dist/browser/browserSupervisor.js +186 -0
- package/dist/browser/browserSupervisor.js.map +1 -0
- package/dist/browser/cdp.d.ts +1 -0
- package/dist/browser/cdp.d.ts.map +1 -0
- package/dist/browser/cdp.js +6 -5
- package/dist/browser/cdp.js.map +1 -1
- package/dist/browser/channelSelection.d.ts +45 -14
- package/dist/browser/channelSelection.d.ts.map +1 -0
- package/dist/browser/channelSelection.js +81 -18
- package/dist/browser/channelSelection.js.map +1 -1
- package/dist/browser/consent.d.ts +152 -0
- package/dist/browser/consent.d.ts.map +1 -0
- package/dist/browser/consent.js +418 -0
- package/dist/browser/consent.js.map +1 -0
- package/dist/browser/display.d.ts +1 -0
- package/dist/browser/display.d.ts.map +1 -0
- package/dist/browser/hlsPlaylistObserver.d.ts +55 -0
- package/dist/browser/hlsPlaylistObserver.d.ts.map +1 -0
- package/dist/browser/hlsPlaylistObserver.js +132 -0
- package/dist/browser/hlsPlaylistObserver.js.map +1 -0
- package/dist/browser/index.d.ts +67 -40
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +508 -300
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/launchGovernor.d.ts +89 -0
- package/dist/browser/launchGovernor.d.ts.map +1 -0
- package/dist/browser/launchGovernor.js +96 -0
- package/dist/browser/launchGovernor.js.map +1 -0
- package/dist/browser/login.d.ts +10 -2
- package/dist/browser/login.d.ts.map +1 -0
- package/dist/browser/login.js +36 -5
- package/dist/browser/login.js.map +1 -1
- package/dist/browser/manifestInterceptor.d.ts +102 -49
- package/dist/browser/manifestInterceptor.d.ts.map +1 -0
- package/dist/browser/manifestInterceptor.js +219 -237
- package/dist/browser/manifestInterceptor.js.map +1 -1
- package/dist/browser/pageStaleness.d.ts +30 -0
- package/dist/browser/pageStaleness.d.ts.map +1 -0
- package/dist/browser/pageStaleness.js +76 -0
- package/dist/browser/pageStaleness.js.map +1 -0
- package/dist/browser/precaching.d.ts +95 -3
- package/dist/browser/precaching.d.ts.map +1 -0
- package/dist/browser/precaching.js +499 -65
- package/dist/browser/precaching.js.map +1 -1
- package/dist/browser/tabNetworkObserver.d.ts +49 -0
- package/dist/browser/tabNetworkObserver.d.ts.map +1 -0
- package/dist/browser/tabNetworkObserver.js +144 -0
- package/dist/browser/tabNetworkObserver.js.map +1 -0
- package/dist/browser/tuning/cache.d.ts +61 -0
- package/dist/browser/tuning/cache.d.ts.map +1 -0
- package/dist/browser/tuning/cache.js +62 -0
- package/dist/browser/tuning/cache.js.map +1 -0
- package/dist/browser/tuning/comcastPolymer.d.ts +3 -2
- package/dist/browser/tuning/comcastPolymer.d.ts.map +1 -0
- package/dist/browser/tuning/comcastPolymer.js +79 -102
- package/dist/browser/tuning/comcastPolymer.js.map +1 -1
- package/dist/browser/tuning/cox.d.ts +1 -0
- package/dist/browser/tuning/cox.d.ts.map +1 -0
- package/dist/browser/tuning/directv.d.ts +1 -0
- package/dist/browser/tuning/directv.d.ts.map +1 -0
- package/dist/browser/tuning/directv.js +52 -46
- package/dist/browser/tuning/directv.js.map +1 -1
- package/dist/browser/tuning/fox.d.ts +1 -0
- package/dist/browser/tuning/fox.d.ts.map +1 -0
- package/dist/browser/tuning/fox.js +13 -12
- package/dist/browser/tuning/fox.js.map +1 -1
- package/dist/browser/tuning/gridSearch.d.ts +68 -0
- package/dist/browser/tuning/gridSearch.d.ts.map +1 -0
- package/dist/browser/tuning/gridSearch.js +85 -0
- package/dist/browser/tuning/gridSearch.js.map +1 -0
- package/dist/browser/tuning/hbo.d.ts +1 -0
- package/dist/browser/tuning/hbo.d.ts.map +1 -0
- package/dist/browser/tuning/hbo.js +73 -45
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.d.ts +1 -0
- package/dist/browser/tuning/hulu.d.ts.map +1 -0
- package/dist/browser/tuning/hulu.js +128 -173
- package/dist/browser/tuning/hulu.js.map +1 -1
- package/dist/browser/tuning/shared.d.ts +75 -5
- package/dist/browser/tuning/shared.d.ts.map +1 -0
- package/dist/browser/tuning/shared.js +216 -6
- package/dist/browser/tuning/shared.js.map +1 -1
- package/dist/browser/tuning/sling.d.ts +1 -0
- package/dist/browser/tuning/sling.d.ts.map +1 -0
- package/dist/browser/tuning/sling.js +105 -126
- package/dist/browser/tuning/sling.js.map +1 -1
- package/dist/browser/tuning/spectrum.d.ts +1 -0
- package/dist/browser/tuning/spectrum.d.ts.map +1 -0
- package/dist/browser/tuning/spectrum.js +80 -125
- package/dist/browser/tuning/spectrum.js.map +1 -1
- package/dist/browser/tuning/thumbnailRow.d.ts +1 -0
- package/dist/browser/tuning/thumbnailRow.d.ts.map +1 -0
- package/dist/browser/tuning/thumbnailRow.js +8 -2
- package/dist/browser/tuning/thumbnailRow.js.map +1 -1
- package/dist/browser/tuning/tileClick.d.ts +1 -0
- package/dist/browser/tuning/tileClick.d.ts.map +1 -0
- package/dist/browser/tuning/tileClick.js +3 -2
- package/dist/browser/tuning/tileClick.js.map +1 -1
- package/dist/browser/tuning/xfinity.d.ts +1 -0
- package/dist/browser/tuning/xfinity.d.ts.map +1 -0
- package/dist/browser/tuning/youtubeTv.d.ts +1 -0
- package/dist/browser/tuning/youtubeTv.d.ts.map +1 -0
- package/dist/browser/tuning/youtubeTv.js +74 -127
- package/dist/browser/tuning/youtubeTv.js.map +1 -1
- package/dist/browser/video.d.ts +54 -14
- package/dist/browser/video.d.ts.map +1 -0
- package/dist/browser/video.js +296 -138
- package/dist/browser/video.js.map +1 -1
- package/dist/channels/index.d.ts +1 -0
- package/dist/channels/index.d.ts.map +1 -0
- package/dist/channels/index.js +12 -9
- package/dist/channels/index.js.map +1 -1
- package/dist/config/channelForm.d.ts +15 -1
- package/dist/config/channelForm.d.ts.map +1 -0
- package/dist/config/channelForm.js +28 -12
- package/dist/config/channelForm.js.map +1 -1
- package/dist/config/consistencyProbe.d.ts +1 -0
- package/dist/config/consistencyProbe.d.ts.map +1 -0
- package/dist/config/consistencyProbe.js +27 -21
- package/dist/config/consistencyProbe.js.map +1 -1
- package/dist/config/health.d.ts +41 -8
- package/dist/config/health.d.ts.map +1 -0
- package/dist/config/health.js +189 -38
- package/dist/config/health.js.map +1 -1
- package/dist/config/index.d.ts +55 -3
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +320 -69
- package/dist/config/index.js.map +1 -1
- package/dist/config/paths.d.ts +10 -9
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +11 -11
- package/dist/config/paths.js.map +1 -1
- package/dist/config/persistence.context.d.ts +1 -0
- package/dist/config/persistence.context.d.ts.map +1 -0
- package/dist/config/persistence.d.ts +13 -10
- package/dist/config/persistence.d.ts.map +1 -0
- package/dist/config/persistence.js +40 -21
- package/dist/config/persistence.js.map +1 -1
- package/dist/config/presets.d.ts +1 -0
- package/dist/config/presets.d.ts.map +1 -0
- package/dist/config/profiles.d.ts +22 -15
- package/dist/config/profiles.d.ts.map +1 -0
- package/dist/config/profiles.js +81 -47
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/providerLineups.d.ts +47 -0
- package/dist/config/providerLineups.d.ts.map +1 -0
- package/dist/config/providerLineups.js +137 -0
- package/dist/config/providerLineups.js.map +1 -0
- package/dist/config/reactivity.d.ts +84 -0
- package/dist/config/reactivity.d.ts.map +1 -0
- package/dist/config/reactivity.js +226 -0
- package/dist/config/reactivity.js.map +1 -0
- package/dist/config/servicePacks.d.ts +18 -2
- package/dist/config/servicePacks.d.ts.map +1 -0
- package/dist/config/servicePacks.js +53 -11
- package/dist/config/servicePacks.js.map +1 -1
- package/dist/config/services.d.ts +21 -15
- package/dist/config/services.d.ts.map +1 -0
- package/dist/config/services.js +35 -24
- package/dist/config/services.js.map +1 -1
- package/dist/config/sites.d.ts +6 -4
- package/dist/config/sites.d.ts.map +1 -0
- package/dist/config/sites.js +15 -11
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +129 -30
- package/dist/config/userChannels.d.ts.map +1 -0
- package/dist/config/userChannels.js +333 -68
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +14 -9
- package/dist/config/userConfig.d.ts.map +1 -0
- package/dist/config/userConfig.js +87 -25
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +10 -6
- package/dist/config/userProfiles.d.ts.map +1 -0
- package/dist/config/userProfiles.js +64 -39
- package/dist/config/userProfiles.js.map +1 -1
- package/dist/hdhr/channelMap.d.ts +1 -0
- package/dist/hdhr/channelMap.d.ts.map +1 -0
- package/dist/hdhr/deviceId.d.ts +1 -0
- package/dist/hdhr/deviceId.d.ts.map +1 -0
- package/dist/hdhr/discover.d.ts +1 -0
- package/dist/hdhr/discover.d.ts.map +1 -0
- package/dist/hdhr/discover.js +48 -47
- package/dist/hdhr/discover.js.map +1 -1
- package/dist/hdhr/getHandlers.d.ts +29 -0
- package/dist/hdhr/getHandlers.d.ts.map +1 -0
- package/dist/hdhr/getHandlers.js +196 -0
- package/dist/hdhr/getHandlers.js.map +1 -0
- package/dist/hdhr/identity.d.ts +36 -0
- package/dist/hdhr/identity.d.ts.map +1 -0
- package/dist/hdhr/identity.js +51 -0
- package/dist/hdhr/identity.js.map +1 -0
- package/dist/hdhr/index.d.ts +22 -4
- package/dist/hdhr/index.d.ts.map +1 -0
- package/dist/hdhr/index.js +229 -54
- package/dist/hdhr/index.js.map +1 -1
- package/dist/hdhr/protocol.d.ts +75 -0
- package/dist/hdhr/protocol.d.ts.map +1 -0
- package/dist/hdhr/protocol.js +264 -0
- package/dist/hdhr/protocol.js.map +1 -0
- package/dist/hdhr/tunerState.d.ts +24 -0
- package/dist/hdhr/tunerState.d.ts.map +1 -0
- package/dist/hdhr/tunerState.js +67 -0
- package/dist/hdhr/tunerState.js.map +1 -0
- package/dist/hdhr/udp.d.ts +43 -0
- package/dist/hdhr/udp.d.ts.map +1 -0
- package/dist/hdhr/udp.js +277 -0
- package/dist/hdhr/udp.js.map +1 -0
- package/dist/identity.d.ts +1 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +13 -11
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -12
- package/dist/index.js.map +1 -1
- package/dist/native/codecInference.d.ts +1 -0
- package/dist/native/codecInference.d.ts.map +1 -0
- package/dist/native/codecInference.js +50 -5
- package/dist/native/codecInference.js.map +1 -1
- package/dist/native/decrypt.d.ts +6 -2
- package/dist/native/decrypt.d.ts.map +1 -0
- package/dist/native/decrypt.js +15 -8
- package/dist/native/decrypt.js.map +1 -1
- package/dist/native/index.d.ts +40 -17
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +281 -120
- package/dist/native/index.js.map +1 -1
- package/dist/native/probe.d.ts +114 -18
- package/dist/native/probe.d.ts.map +1 -0
- package/dist/native/probe.js +452 -115
- package/dist/native/probe.js.map +1 -1
- package/dist/native/proxy.d.ts +57 -5
- package/dist/native/proxy.d.ts.map +1 -0
- package/dist/native/proxy.js +604 -170
- package/dist/native/proxy.js.map +1 -1
- package/dist/native/tokenExpiry.d.ts +1 -0
- package/dist/native/tokenExpiry.d.ts.map +1 -0
- package/dist/native/tokenExpiry.js +1 -1
- package/dist/routes/assets.d.ts +1 -0
- package/dist/routes/assets.d.ts.map +1 -0
- package/dist/routes/assets.js +4 -2
- package/dist/routes/assets.js.map +1 -1
- package/dist/routes/auth.d.ts +1 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +0 -1
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/cdp.d.ts +5 -5
- package/dist/routes/cdp.d.ts.map +1 -0
- package/dist/routes/cdp.js +59 -20
- package/dist/routes/cdp.js.map +1 -1
- package/dist/routes/channels.d.ts +1 -0
- package/dist/routes/channels.d.ts.map +1 -0
- package/dist/routes/channels.js +2 -1
- package/dist/routes/channels.js.map +1 -1
- package/dist/routes/clientActions.d.ts +98 -0
- package/dist/routes/clientActions.d.ts.map +1 -0
- package/dist/routes/clientActions.js +113 -0
- package/dist/routes/clientActions.js.map +1 -0
- package/dist/routes/components.d.ts +5 -3
- package/dist/routes/components.d.ts.map +1 -0
- package/dist/routes/components.js +36 -39
- package/dist/routes/components.js.map +1 -1
- package/dist/routes/config/channels/endpoints/browse.d.ts +1 -0
- package/dist/routes/config/channels/endpoints/browse.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/browse.js +23 -10
- package/dist/routes/config/channels/endpoints/browse.js.map +1 -1
- package/dist/routes/config/channels/endpoints/bulk.d.ts +1 -0
- package/dist/routes/config/channels/endpoints/bulk.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/bulk.js +5 -3
- package/dist/routes/config/channels/endpoints/bulk.js.map +1 -1
- package/dist/routes/config/channels/endpoints/crud.d.ts +1 -0
- package/dist/routes/config/channels/endpoints/crud.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/crud.js +85 -93
- package/dist/routes/config/channels/endpoints/crud.js.map +1 -1
- package/dist/routes/config/channels/endpoints/importExport.d.ts +1 -0
- package/dist/routes/config/channels/endpoints/importExport.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/importExport.js +6 -3
- package/dist/routes/config/channels/endpoints/importExport.js.map +1 -1
- package/dist/routes/config/channels/endpoints/predefined.d.ts +1 -0
- package/dist/routes/config/channels/endpoints/predefined.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/prefs.d.ts +1 -0
- package/dist/routes/config/channels/endpoints/prefs.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/prefs.js +2 -1
- package/dist/routes/config/channels/endpoints/prefs.js.map +1 -1
- package/dist/routes/config/channels/endpoints/service.d.ts +2 -1
- package/dist/routes/config/channels/endpoints/service.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/service.js +3 -1
- package/dist/routes/config/channels/endpoints/service.js.map +1 -1
- package/dist/routes/config/channels/endpoints/tags.d.ts +1 -0
- package/dist/routes/config/channels/endpoints/tags.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/tags.js +13 -7
- package/dist/routes/config/channels/endpoints/tags.js.map +1 -1
- package/dist/routes/config/channels/healthBridge.d.ts +24 -0
- package/dist/routes/config/channels/healthBridge.d.ts.map +1 -0
- package/dist/routes/config/channels/healthBridge.js +71 -0
- package/dist/routes/config/channels/healthBridge.js.map +1 -0
- package/dist/routes/config/channels/http/handler.d.ts +1 -0
- package/dist/routes/config/channels/http/handler.d.ts.map +1 -0
- package/dist/routes/config/channels/http/playlistHint.d.ts +2 -1
- package/dist/routes/config/channels/http/playlistHint.d.ts.map +1 -0
- package/dist/routes/config/channels/http/playlistHint.js +16 -2
- package/dist/routes/config/channels/http/playlistHint.js.map +1 -1
- package/dist/routes/config/channels/http/serviceWarning.d.ts +4 -2
- package/dist/routes/config/channels/http/serviceWarning.d.ts.map +1 -0
- package/dist/routes/config/channels/http/serviceWarning.js +3 -2
- package/dist/routes/config/channels/http/serviceWarning.js.map +1 -1
- package/dist/routes/config/channels/index.d.ts +1 -0
- package/dist/routes/config/channels/index.d.ts.map +1 -0
- package/dist/routes/config/channels/setup.d.ts +1 -0
- package/dist/routes/config/channels/setup.d.ts.map +1 -0
- package/dist/routes/config/channels/table.d.ts +3 -3
- package/dist/routes/config/channels/table.d.ts.map +1 -0
- package/dist/routes/config/channels/table.js +148 -107
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/http/envelope.d.ts +1 -0
- package/dist/routes/config/http/envelope.d.ts.map +1 -0
- package/dist/routes/config/index.d.ts +35 -3
- package/dist/routes/config/index.d.ts.map +1 -0
- package/dist/routes/config/index.js +51 -2
- package/dist/routes/config/index.js.map +1 -1
- package/dist/routes/config/services.d.ts +5 -4
- package/dist/routes/config/services.d.ts.map +1 -0
- package/dist/routes/config/services.js +57 -43
- package/dist/routes/config/services.js.map +1 -1
- package/dist/routes/config/settings.d.ts +1 -0
- package/dist/routes/config/settings.d.ts.map +1 -0
- package/dist/routes/config/settings.js +98 -49
- package/dist/routes/config/settings.js.map +1 -1
- package/dist/routes/debug.d.ts +1 -0
- package/dist/routes/debug.d.ts.map +1 -0
- package/dist/routes/debug.js +277 -139
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/health.d.ts +39 -1
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +33 -27
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/hls.d.ts +1 -0
- package/dist/routes/hls.d.ts.map +1 -0
- package/dist/routes/icons.d.ts +1 -0
- package/dist/routes/icons.d.ts.map +1 -0
- package/dist/routes/icons.js +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/logs.d.ts +1 -0
- package/dist/routes/logs.d.ts.map +1 -0
- package/dist/routes/logs.js +14 -4
- package/dist/routes/logs.js.map +1 -1
- package/dist/routes/mpegts.d.ts +1 -0
- package/dist/routes/mpegts.d.ts.map +1 -0
- package/dist/routes/play.d.ts +1 -0
- package/dist/routes/play.d.ts.map +1 -0
- package/dist/routes/playlist.d.ts +1 -0
- package/dist/routes/playlist.d.ts.map +1 -0
- package/dist/routes/playlist.js +7 -2
- package/dist/routes/playlist.js.map +1 -1
- package/dist/routes/root/content.d.ts +8 -5
- package/dist/routes/root/content.d.ts.map +1 -0
- package/dist/routes/root/content.js +29 -26
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/index.d.ts +2 -1
- package/dist/routes/root/index.d.ts.map +1 -0
- package/dist/routes/root/index.js +26 -21
- package/dist/routes/root/index.js.map +1 -1
- package/dist/routes/root/scripts/channels.d.ts +6 -0
- package/dist/routes/root/scripts/channels.d.ts.map +1 -0
- package/dist/routes/root/scripts/channels.js +197 -118
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/clientEscape.d.ts +22 -0
- package/dist/routes/root/scripts/clientEscape.d.ts.map +1 -0
- package/dist/routes/root/scripts/clientEscape.js +41 -0
- package/dist/routes/root/scripts/clientEscape.js.map +1 -0
- package/dist/routes/root/scripts/clientUrl.d.ts +23 -0
- package/dist/routes/root/scripts/clientUrl.d.ts.map +1 -0
- package/dist/routes/root/scripts/clientUrl.js +44 -0
- package/dist/routes/root/scripts/clientUrl.js.map +1 -0
- package/dist/routes/root/scripts/config.d.ts +6 -0
- package/dist/routes/root/scripts/config.d.ts.map +1 -0
- package/dist/routes/root/scripts/config.js +110 -24
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/index.d.ts +1 -0
- package/dist/routes/root/scripts/index.d.ts.map +1 -0
- package/dist/routes/root/scripts/shared.d.ts +1 -0
- package/dist/routes/root/scripts/shared.d.ts.map +1 -0
- package/dist/routes/root/scripts/shared.js +112 -26
- package/dist/routes/root/scripts/shared.js.map +1 -1
- package/dist/routes/root/scripts/status.d.ts +1 -0
- package/dist/routes/root/scripts/status.d.ts.map +1 -0
- package/dist/routes/root/scripts/status.handlers.d.ts +23 -33
- package/dist/routes/root/scripts/status.handlers.d.ts.map +1 -0
- package/dist/routes/root/scripts/status.handlers.js +46 -92
- package/dist/routes/root/scripts/status.handlers.js.map +1 -1
- package/dist/routes/root/scripts/status.js +21 -8
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/root/styles.d.ts +1 -0
- package/dist/routes/root/styles.d.ts.map +1 -0
- package/dist/routes/root/styles.js +7 -4
- package/dist/routes/root/styles.js.map +1 -1
- package/dist/routes/services.d.ts +13 -1
- package/dist/routes/services.d.ts.map +1 -0
- package/dist/routes/services.js +81 -68
- package/dist/routes/services.js.map +1 -1
- package/dist/routes/sse.d.ts +1 -0
- package/dist/routes/sse.d.ts.map +1 -0
- package/dist/routes/sse.js +1 -1
- package/dist/routes/sse.js.map +1 -1
- package/dist/routes/streams.d.ts +2 -1
- package/dist/routes/streams.d.ts.map +1 -0
- package/dist/routes/streams.js +6 -4
- package/dist/routes/streams.js.map +1 -1
- package/dist/routes/theme.d.ts +3 -2
- package/dist/routes/theme.d.ts.map +1 -0
- package/dist/routes/theme.js +6 -5
- package/dist/routes/theme.js.map +1 -1
- package/dist/routes/ui.d.ts +3 -1
- package/dist/routes/ui.d.ts.map +1 -0
- package/dist/routes/ui.js +16 -4
- package/dist/routes/ui.js.map +1 -1
- package/dist/routes/upgrade.d.ts +1 -0
- package/dist/routes/upgrade.d.ts.map +1 -0
- package/dist/routes/upgrade.js +6 -2
- package/dist/routes/upgrade.js.map +1 -1
- package/dist/service/commands.context.d.ts +1 -0
- package/dist/service/commands.context.d.ts.map +1 -0
- package/dist/service/commands.context.js +4 -3
- package/dist/service/commands.context.js.map +1 -1
- package/dist/service/commands.d.ts +1 -0
- package/dist/service/commands.d.ts.map +1 -0
- package/dist/service/commands.js +10 -13
- package/dist/service/commands.js.map +1 -1
- package/dist/service/generators.context.d.ts +1 -0
- package/dist/service/generators.context.d.ts.map +1 -0
- package/dist/service/generators.context.js +1 -1
- package/dist/service/generators.context.js.map +1 -1
- package/dist/service/generators.d.ts +4 -3
- package/dist/service/generators.d.ts.map +1 -0
- package/dist/service/generators.js +111 -31
- package/dist/service/generators.js.map +1 -1
- package/dist/service/index.d.ts +1 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/streaming/captureLock.d.ts +44 -0
- package/dist/streaming/captureLock.d.ts.map +1 -0
- package/dist/streaming/captureLock.js +95 -0
- package/dist/streaming/captureLock.js.map +1 -0
- package/dist/streaming/captureSession.d.ts +31 -0
- package/dist/streaming/captureSession.d.ts.map +1 -0
- package/dist/streaming/captureSession.js +54 -0
- package/dist/streaming/captureSession.js.map +1 -0
- package/dist/streaming/clients.d.ts +1 -0
- package/dist/streaming/clients.d.ts.map +1 -0
- package/dist/streaming/codec.d.ts +1 -0
- package/dist/streaming/codec.d.ts.map +1 -0
- package/dist/streaming/fmp4Segmenter.d.ts +29 -2
- package/dist/streaming/fmp4Segmenter.d.ts.map +1 -0
- package/dist/streaming/fmp4Segmenter.js +79 -26
- package/dist/streaming/fmp4Segmenter.js.map +1 -1
- package/dist/streaming/hls.d.ts +18 -7
- package/dist/streaming/hls.d.ts.map +1 -0
- package/dist/streaming/hls.js +220 -120
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +1 -0
- package/dist/streaming/hlsResume.d.ts.map +1 -0
- package/dist/streaming/hlsResume.js +1 -1
- package/dist/streaming/hlsResume.js.map +1 -1
- package/dist/streaming/hlsSegments.d.ts +59 -2
- package/dist/streaming/hlsSegments.d.ts.map +1 -0
- package/dist/streaming/hlsSegments.js +147 -17
- package/dist/streaming/hlsSegments.js.map +1 -1
- package/dist/streaming/lifecycle.d.ts +4 -1
- package/dist/streaming/lifecycle.d.ts.map +1 -0
- package/dist/streaming/lifecycle.js +65 -85
- package/dist/streaming/lifecycle.js.map +1 -1
- package/dist/streaming/monitor.d.ts +6 -4
- package/dist/streaming/monitor.d.ts.map +1 -0
- package/dist/streaming/monitor.js +343 -145
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/mp4Parser.d.ts +5 -4
- package/dist/streaming/mp4Parser.d.ts.map +1 -0
- package/dist/streaming/mp4Parser.js +36 -13
- package/dist/streaming/mp4Parser.js.map +1 -1
- package/dist/streaming/mpegts.d.ts +16 -0
- package/dist/streaming/mpegts.d.ts.map +1 -0
- package/dist/streaming/mpegts.js +61 -17
- package/dist/streaming/mpegts.js.map +1 -1
- package/dist/streaming/playlistBuilder.d.ts +1 -0
- package/dist/streaming/playlistBuilder.d.ts.map +1 -0
- package/dist/streaming/preroll.d.ts +32 -5
- package/dist/streaming/preroll.d.ts.map +1 -0
- package/dist/streaming/preroll.js +70 -32
- package/dist/streaming/preroll.js.map +1 -1
- package/dist/streaming/pretune.d.ts +26 -1
- package/dist/streaming/pretune.d.ts.map +1 -0
- package/dist/streaming/pretune.js +22 -24
- package/dist/streaming/pretune.js.map +1 -1
- package/dist/streaming/pretuneTimers.d.ts +24 -0
- package/dist/streaming/pretuneTimers.d.ts.map +1 -0
- package/dist/streaming/pretuneTimers.js +56 -0
- package/dist/streaming/pretuneTimers.js.map +1 -0
- package/dist/streaming/recovery.d.ts +139 -5
- package/dist/streaming/recovery.d.ts.map +1 -0
- package/dist/streaming/recovery.js +160 -16
- package/dist/streaming/recovery.js.map +1 -1
- package/dist/streaming/registry.d.ts +27 -9
- package/dist/streaming/registry.d.ts.map +1 -0
- package/dist/streaming/registry.js +21 -3
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +157 -29
- package/dist/streaming/setup.d.ts.map +1 -0
- package/dist/streaming/setup.js +1034 -458
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +21 -1
- package/dist/streaming/showInfo.d.ts.map +1 -0
- package/dist/streaming/showInfo.js +31 -16
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +10 -10
- package/dist/streaming/statusEmitter.d.ts.map +1 -0
- package/dist/streaming/statusEmitter.js +5 -8
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/channels.d.ts +16 -4
- package/dist/types/channels.d.ts.map +1 -0
- package/dist/types/channels.js +4 -2
- package/dist/types/channels.js.map +1 -1
- package/dist/types/config.d.ts +16 -5
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/profiles.d.ts +6 -4
- package/dist/types/profiles.d.ts.map +1 -0
- package/dist/types/selection.d.ts +31 -3
- package/dist/types/selection.d.ts.map +1 -0
- package/dist/types/selection.js +4 -2
- package/dist/types/selection.js.map +1 -1
- package/dist/types/shared.d.ts +1 -0
- package/dist/types/shared.d.ts.map +1 -0
- package/dist/types/streaming.d.ts +8 -0
- package/dist/types/streaming.d.ts.map +1 -0
- package/dist/upgrade/commands.context.d.ts +1 -0
- package/dist/upgrade/commands.context.d.ts.map +1 -0
- package/dist/upgrade/commands.context.js +14 -10
- package/dist/upgrade/commands.context.js.map +1 -1
- package/dist/upgrade/commands.d.ts +8 -15
- package/dist/upgrade/commands.d.ts.map +1 -0
- package/dist/upgrade/commands.js +27 -28
- package/dist/upgrade/commands.js.map +1 -1
- package/dist/upgrade/detection.context.d.ts +1 -0
- package/dist/upgrade/detection.context.d.ts.map +1 -0
- package/dist/upgrade/detection.context.js +4 -1
- package/dist/upgrade/detection.context.js.map +1 -1
- package/dist/upgrade/detection.d.ts +9 -7
- package/dist/upgrade/detection.d.ts.map +1 -0
- package/dist/upgrade/detection.js +23 -54
- package/dist/upgrade/detection.js.map +1 -1
- package/dist/upgrade/index.d.ts +2 -0
- package/dist/upgrade/index.d.ts.map +1 -0
- package/dist/upgrade/index.js +1 -0
- package/dist/upgrade/index.js.map +1 -1
- package/dist/upgrade/lifecycle.context.d.ts +20 -0
- package/dist/upgrade/lifecycle.context.d.ts.map +1 -0
- package/dist/upgrade/lifecycle.context.js +47 -0
- package/dist/upgrade/lifecycle.context.js.map +1 -0
- package/dist/upgrade/lifecycle.d.ts +72 -0
- package/dist/upgrade/lifecycle.d.ts.map +1 -0
- package/dist/upgrade/lifecycle.js +175 -0
- package/dist/upgrade/lifecycle.js.map +1 -0
- package/dist/upgrade/pathHandle.d.ts +30 -0
- package/dist/upgrade/pathHandle.d.ts.map +1 -0
- package/dist/upgrade/pathHandle.js +102 -0
- package/dist/upgrade/pathHandle.js.map +1 -0
- package/dist/utils/bootSession.context.d.ts +16 -0
- package/dist/utils/bootSession.context.d.ts.map +1 -0
- package/dist/utils/bootSession.context.js +60 -0
- package/dist/utils/bootSession.context.js.map +1 -0
- package/dist/utils/bootSession.d.ts +17 -0
- package/dist/utils/bootSession.d.ts.map +1 -0
- package/dist/utils/bootSession.js +20 -0
- package/dist/utils/bootSession.js.map +1 -0
- package/dist/utils/chromeFetch.d.ts +1 -0
- package/dist/utils/chromeFetch.d.ts.map +1 -0
- package/dist/utils/cliOutput.d.ts +1 -0
- package/dist/utils/cliOutput.d.ts.map +1 -0
- package/dist/utils/clock.d.ts +5 -4
- package/dist/utils/clock.d.ts.map +1 -0
- package/dist/utils/clock.js +19 -16
- package/dist/utils/clock.js.map +1 -1
- package/dist/utils/debugFilter.d.ts +9 -1
- package/dist/utils/debugFilter.d.ts.map +1 -0
- package/dist/utils/debugFilter.js +60 -33
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/utils/delay.d.ts +48 -15
- package/dist/utils/delay.d.ts.map +1 -0
- package/dist/utils/delay.js +73 -20
- package/dist/utils/delay.js.map +1 -1
- package/dist/utils/errors.d.ts +32 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +45 -4
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/evaluate.d.ts +1 -0
- package/dist/utils/evaluate.d.ts.map +1 -0
- package/dist/utils/evaluate.js +29 -23
- package/dist/utils/evaluate.js.map +1 -1
- package/dist/utils/ffmpeg.context.d.ts +1 -0
- package/dist/utils/ffmpeg.context.d.ts.map +1 -0
- package/dist/utils/ffmpeg.context.js.map +1 -1
- package/dist/utils/ffmpeg.d.ts +29 -7
- package/dist/utils/ffmpeg.d.ts.map +1 -0
- package/dist/utils/ffmpeg.js +32 -14
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/fileLogger.d.ts +3 -1
- package/dist/utils/fileLogger.d.ts.map +1 -0
- package/dist/utils/fileLogger.js +64 -29
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/format.d.ts +14 -1
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +19 -5
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logEmitter.d.ts +1 -0
- package/dist/utils/logEmitter.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +10 -6
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/m3u.d.ts +3 -2
- package/dist/utils/m3u.d.ts.map +1 -0
- package/dist/utils/m3u.js +4 -3
- package/dist/utils/m3u.js.map +1 -1
- package/dist/utils/markup.d.ts +13 -1
- package/dist/utils/markup.d.ts.map +1 -0
- package/dist/utils/markup.js +28 -4
- package/dist/utils/markup.js.map +1 -1
- package/dist/utils/memo.d.ts +3 -2
- package/dist/utils/memo.d.ts.map +1 -0
- package/dist/utils/memo.js +3 -3
- package/dist/utils/morganStream.d.ts +1 -0
- package/dist/utils/morganStream.d.ts.map +1 -0
- package/dist/utils/network.d.ts +1 -0
- package/dist/utils/network.d.ts.map +1 -0
- package/dist/utils/network.js +2 -2
- package/dist/utils/pid.d.ts +3 -25
- package/dist/utils/pid.d.ts.map +1 -0
- package/dist/utils/pid.js +10 -49
- package/dist/utils/pid.js.map +1 -1
- package/dist/utils/platform.d.ts +3 -1
- package/dist/utils/platform.d.ts.map +1 -0
- package/dist/utils/platform.js +2 -1
- package/dist/utils/platform.js.map +1 -1
- package/dist/utils/processInspector.context.d.ts +8 -0
- package/dist/utils/processInspector.context.d.ts.map +1 -0
- package/dist/utils/processInspector.context.js +106 -0
- package/dist/utils/processInspector.context.js.map +1 -0
- package/dist/utils/processInspector.d.ts +55 -0
- package/dist/utils/processInspector.d.ts.map +1 -0
- package/dist/utils/processInspector.js +127 -0
- package/dist/utils/processInspector.js.map +1 -0
- package/dist/utils/retry.d.ts +25 -2
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +33 -6
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/runtimeIdentity.context.d.ts +7 -0
- package/dist/utils/runtimeIdentity.context.d.ts.map +1 -0
- package/dist/utils/runtimeIdentity.context.js +43 -0
- package/dist/utils/runtimeIdentity.context.js.map +1 -0
- package/dist/utils/runtimeIdentity.d.ts +98 -0
- package/dist/utils/runtimeIdentity.d.ts.map +1 -0
- package/dist/utils/runtimeIdentity.js +181 -0
- package/dist/utils/runtimeIdentity.js.map +1 -0
- package/dist/utils/sanitize.d.ts +1 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/streamContext.d.ts +2 -1
- package/dist/utils/streamContext.d.ts.map +1 -0
- package/dist/utils/streamContext.js +1 -1
- package/dist/utils/timing.d.ts +2 -1
- package/dist/utils/timing.d.ts.map +1 -0
- package/dist/utils/timing.js +0 -5
- package/dist/utils/timing.js.map +1 -1
- package/dist/utils/version.d.ts +1 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +47 -13
- package/dist/utils/version.js.map +1 -1
- package/package.json +26 -20
package/dist/streaming/setup.js
CHANGED
|
@@ -1,28 +1,86 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
import { BrowserSupersededError, BrowserUnavailableError, getBrowserInstance, getCurrentBrowser, getStream, invalidateBrowser, minimizeBrowserWindow, registerManagedPage, setCaptureProbe, unregisterManagedPage } from "../browser/index.js";
|
|
54
|
+
import { CaptureAbandonedError, createCaptureLock } from "./captureLock.js";
|
|
55
|
+
import { FINALIZE_SETTLE_DELAY, installManifestInterceptor } from "../browser/manifestInterceptor.js";
|
|
56
|
+
import { LOG, delay, extractDomain, formatError, getStreamContext, isStaleCaptureMutexError, maxRetryDuration, realClock, registerAbortController, resolveFFmpegPath, retryOperation, runWithStreamContext, spawnFFmpeg, startTimer, waitWithTimeout } from "../utils/index.js";
|
|
57
|
+
import { getAllStreams, getNextStreamId } from "./registry.js";
|
|
58
|
+
import { getAuthDomainForChannel, getServiceDisplayName, resolveServiceKey } from "../config/services.js";
|
|
59
|
+
import { getBuiltinProfile, getProfileForChannel, getProfileForUrl, resolveProfile } from "../config/profiles.js";
|
|
5
60
|
import { getProviderByStrategy, invalidateDirectUrl, resolveDirectUrl } from "../browser/channelSelection.js";
|
|
6
|
-
import { initializePlayback, injectVideoSelector, navigateToPage } from "../browser/video.js";
|
|
61
|
+
import { initializePlayback, injectVideoSelector, muteExistingVideos, navigateToPage } from "../browser/video.js";
|
|
7
62
|
import { CONFIG } from "../config/index.js";
|
|
8
63
|
import { chromeFetch } from "../utils/index.js";
|
|
64
|
+
import { createCaptureSession } from "./captureSession.js";
|
|
9
65
|
import { getCachedEncryption } from "../native/probe.js";
|
|
10
66
|
import { getCaptureMimeType } from "./codec.js";
|
|
67
|
+
import { getDomainAuthState } from "../config/health.js";
|
|
11
68
|
import { getDomainConfig } from "../config/sites.js";
|
|
12
69
|
import { getEffectiveViewport } from "../config/presets.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
70
|
+
import { getUserProfiles } from "../config/userProfiles.js";
|
|
71
|
+
import { isCaptureInfrastructureError } from "./recovery.js";
|
|
15
72
|
import { isChannelSelectionProfile } from "../types/index.js";
|
|
16
73
|
import { monitorPlaybackHealth } from "./monitor.js";
|
|
17
74
|
import { mutateChannels } from "../config/userChannels.js";
|
|
18
75
|
import { pipeline } from "node:stream/promises";
|
|
19
76
|
import { resizeAndMinimizeWindow } from "../browser/cdp.js";
|
|
77
|
+
import { startOverlayHandling } from "../browser/consent.js";
|
|
20
78
|
/* This module contains the common stream setup logic for HLS streaming. The core logic is split into two functions:
|
|
21
79
|
*
|
|
22
80
|
* 1. createPageWithCapture(): Creates a browser page, starts media capture, navigates to the URL, and sets up video playback. This is the reusable core that both
|
|
23
81
|
* initial stream setup and tab replacement recovery use.
|
|
24
82
|
*
|
|
25
|
-
* 2. setupStream(): Orchestrates stream creation by calling createPageWithCapture(), then
|
|
83
|
+
* 2. setupStream(): Orchestrates stream creation by calling createPageWithCapture(), then starting the health monitor. This is the
|
|
26
84
|
* entry point for new stream requests.
|
|
27
85
|
*
|
|
28
86
|
* The separation allows tab replacement recovery (in monitor.ts) to reuse the capture setup logic without duplicating code. When a browser tab becomes unresponsive,
|
|
@@ -35,24 +93,105 @@ import { resizeAndMinimizeWindow } from "../browser/cdp.js";
|
|
|
35
93
|
* - Video element detection and playback setup
|
|
36
94
|
*
|
|
37
95
|
* setupStream() additionally handles:
|
|
38
|
-
* - Request validation (URL format
|
|
39
|
-
* - Stream registration
|
|
96
|
+
* - Request validation (URL format only)
|
|
40
97
|
* - Health monitor startup
|
|
41
98
|
* - Cleanup function creation
|
|
42
99
|
*/
|
|
43
100
|
// Native fMP4 capture uses MP4/AAC for direct HLS segmentation without transcoding.
|
|
44
101
|
const NATIVE_FMP4_MIME_TYPE = "video/mp4;codecs=avc1,mp4a.40.2";
|
|
45
|
-
// Capture initialization
|
|
46
|
-
// tab with an active stream
|
|
47
|
-
// concurrently with other captures without issue.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
//
|
|
54
|
-
//
|
|
102
|
+
// Capture initialization is serialized through a task-scoped lock. Chrome's tabCapture extension can only initialize one capture at a time - concurrent getStream()
|
|
103
|
+
// calls fail with "Cannot capture a tab with an active stream" - so every getStream initialization runs as a task on this one process-wide lock, which holds the
|
|
104
|
+
// turn until the task's promise settles. Once a capture is established it runs concurrently with other captures without issue.
|
|
105
|
+
// The wedge-derivation policy for the capture lock. A task held past max(CAPTURE_WEDGE_FLOOR_MS, deadline + CAPTURE_WEDGE_MARGIN_MS) without settling invokes its
|
|
106
|
+
// onWedge callback so the call site can route it into browser recovery. The floor is the wedge bound at the normal navigation timeout; the margin keeps the wedge
|
|
107
|
+
// strictly later than any larger caller deadline.
|
|
108
|
+
const CAPTURE_WEDGE_FLOOR_MS = 30000;
|
|
109
|
+
const CAPTURE_WEDGE_MARGIN_MS = 5000;
|
|
110
|
+
// The one production capture lock. It takes no config-derived values: setup.ts's module body runs before initializeConfiguration(), and streaming.* saves mutate the
|
|
111
|
+
// live CONFIG binding mid-process, so every timing bound is read per call at the call sites instead.
|
|
112
|
+
const captureLock = createCaptureLock({ clock: realClock, wedgeFloorMs: CAPTURE_WEDGE_FLOOR_MS, wedgeMarginMs: CAPTURE_WEDGE_MARGIN_MS });
|
|
113
|
+
// The delay puppeteer-stream's fire-and-forget STOP_RECORDING chain needs to settle after a raw capture stream is destroyed, before the owning page is closed.
|
|
114
|
+
// Destroying the stream triggers STOP_RECORDING via the close handler, but the async chain (STOP_RECORDING -> recorder.stop() -> onstop -> track.stop()) must finish
|
|
115
|
+
// while the browser is still connected, or Chrome's tabCapture state lingers and the next getStream() draws "Cannot capture a tab with an active stream".
|
|
116
|
+
const STOP_RECORDING_SETTLE_MS = 500;
|
|
117
|
+
// The teardown allowance the mid-life probe's outer lock deadline adds over its getStream criterion bound. The pass/fail criterion times getStream alone; this
|
|
118
|
+
// allowance lets the destroy-plus-STOP_RECORDING-settle teardown complete inside the turn without counting against that criterion. A teardown that hangs past the
|
|
119
|
+
// allowance trips the outer deadline and is reported as a capture-infrastructure failure.
|
|
120
|
+
const PROBE_TEARDOWN_ALLOWANCE_MS = 3000;
|
|
121
|
+
// The playback-initialization safety-net timeout. Channel selection plus video setup runs after navigation with no outer timeout racing its internal click
|
|
122
|
+
// retries; for guideGrid strategies a selection failure triggers an overlay dismiss and retry, which doubles the channel-selection budget. 45 seconds accommodates
|
|
123
|
+
// that retry while still preventing a pathological hang if multiple internal timeouts chain sequentially. Consumed by both the phase-2 race and the interception
|
|
124
|
+
// budget below.
|
|
125
|
+
const PLAYBACK_INIT_TIMEOUT = 45000;
|
|
126
|
+
// Margin folded into the interception budget beyond the phases it explicitly sizes: the small span between phase-2 finishing and finalize firing (the window
|
|
127
|
+
// resize and minimize, setupStream's pre-verification work) plus true slack. The interception window is a leak bound for a tune that dies without unwinding, not a
|
|
128
|
+
// latency bound - no healthy path waits on it - so its generosity costs nothing.
|
|
129
|
+
const INTERCEPTION_BUDGET_MARGIN_MS = 5000;
|
|
130
|
+
// The caller-visible capture-timeout messages, each defined once so the lock's deadline errors and any pin test read the exact text. Both match
|
|
131
|
+
// isCaptureInfrastructureError via its "timed out" substring, so they are exported for the classification pin that locks that contract.
|
|
132
|
+
export const STREAM_INIT_TIMEOUT_MESSAGE = "Stream initialization timed out.";
|
|
133
|
+
export const CAPTURE_PROBE_TIMEOUT_MESSAGE = "Capture probe timed out.";
|
|
134
|
+
/**
|
|
135
|
+
* Retires a raw capture stream: destroys it to fire STOP_RECORDING while the browser is still connected, then waits STOP_RECORDING_SETTLE_MS for that fire-and-forget
|
|
136
|
+
* chain to finish before the caller closes the page. This is the destroy-plus-settle core shared by the mid-life probe's success teardown and every path that must
|
|
137
|
+
* retire a capture stream produced after its caller had already abandoned the turn. It is the async sibling of captureSession.ts's disposer, which tears the composed
|
|
138
|
+
* FFmpeg pipeline down synchronously: this helper awaits the settle because a page close follows it directly, and unlike the composed disposer it is used on
|
|
139
|
+
* abandoned paths where the page may already be closed, so the STOP_RECORDING side is then best-effort.
|
|
140
|
+
* @param stream - The raw capture stream to destroy.
|
|
141
|
+
* @param clock - Clock used for the settle delay. Defaults to realClock; tests inject a fake.
|
|
142
|
+
*/
|
|
143
|
+
async function retireRawStream(stream, clock = realClock) {
|
|
144
|
+
stream.destroy();
|
|
145
|
+
await clock.sleep(STOP_RECORDING_SETTLE_MS);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Escalates the stale-capture-mutex condition from a single home. A "Cannot capture a tab with an active stream" rejection leaks puppeteer-stream's module-level
|
|
149
|
+
* mutex permanently - a Chrome restart cannot clear module state, so only a fresh process recovers. This logs the one canonical diagnostic with the failing site as
|
|
150
|
+
* structured context, then schedules a deferred process exit. The exit is deferred briefly so the error log flushes to disk first; an immediate exit can truncate the
|
|
151
|
+
* buffered file write and lose the diagnostic that explains the restart. Every caller throws or exits its own path right after, so no further capture work runs in the
|
|
152
|
+
* interim.
|
|
153
|
+
* @param siteContext - A short phrase naming where the stale mutex surfaced, attached as structured log context.
|
|
154
|
+
*/
|
|
155
|
+
function escalateStaleCaptureMutex(siteContext) {
|
|
156
|
+
LOG.error("Stale capture state detected. puppeteer-stream's internal capture mutex is now permanently locked and the capture system is unrecoverable. Exiting so " +
|
|
157
|
+
"the service manager can restart with a clean module state.", { site: siteContext });
|
|
158
|
+
setTimeout(() => process.exit(1), 100);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Thrown by the capture-lock task when it finds the page already closed at the instant its turn is granted (a browser crash during the turn-wait). createPageWithCapture
|
|
162
|
+
* catches it to drive the closed-page recursion outside the lock, so the turn releases at once rather than the recursion running while the turn is held. Module-private:
|
|
163
|
+
* it never crosses the function boundary.
|
|
164
|
+
*/
|
|
165
|
+
class PageClosedDuringTurnError extends Error {
|
|
166
|
+
constructor() {
|
|
167
|
+
super("Page closed while waiting for its capture turn.");
|
|
168
|
+
this.name = "PageClosedDuringTurnError";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Thrown by createPageWithCapture when an establishment that navigated to a resolved direct watch URL failed on evidence against that URL - the coordinator's own
|
|
173
|
+
* verdict, read from invalidateDirectUrl, never re-derived here. It says one thing to the caller: the hint has already been evicted, and a fresh attempt down the
|
|
174
|
+
* guide path is worth making. setupStream is the one caller that acts on it; every other caller sees an ordinary establishment failure carrying the original
|
|
175
|
+
* rejection as its cause.
|
|
176
|
+
*/
|
|
177
|
+
export class DirectUrlEstablishmentError extends Error {
|
|
178
|
+
constructor(cause) {
|
|
179
|
+
// The underlying failure travels in the message as well as in cause, because the callers that do not act on the type - tab replacement above all - log the
|
|
180
|
+
// message alone, and a wrapper that swallowed the reason would make this path harder to diagnose than an untyped throw.
|
|
181
|
+
super("Direct watch URL establishment failed: " + formatError(cause) + ".", { cause });
|
|
182
|
+
this.name = "DirectUrlEstablishmentError";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Maximum number of times createPageWithCapture() will retry when it detects that the page was closed while waiting for its turn on the capture lock (e.g., due to a
|
|
186
|
+
// browser crash). An explicit guard prevents unbounded recursion.
|
|
55
187
|
const MAX_PAGE_CLOSED_RETRIES = 3;
|
|
188
|
+
// Maximum time in milliseconds to wait for a single capture probe's getStream() to respond. Shared by the launch-gate verification (verifyCaptureSystem) and the
|
|
189
|
+
// mid-life re-verification, so both tiers exercise the capture path with the same bound.
|
|
190
|
+
const CAPTURE_PROBE_TIMEOUT_MS = 5000;
|
|
191
|
+
// Wire the capture-readiness probe into the browser launch gate. setup.ts owns getStream and the unrecoverable stale-mutex process.exit decision; browser/index.ts
|
|
192
|
+
// owns the launch lifecycle. Injecting verifyCaptureSystem here (setup.ts already depends on browser/index.ts) keeps the dependency one-directional and breaks the
|
|
193
|
+
// cycle, mirroring the browserAccessors boundary between login.ts and index.ts. It runs once at module load, before any browser launch.
|
|
194
|
+
setCaptureProbe(verifyCaptureSystem);
|
|
56
195
|
/**
|
|
57
196
|
* Error thrown when stream setup fails. Includes HTTP status code and user-friendly message for the response.
|
|
58
197
|
*/
|
|
@@ -66,6 +205,28 @@ export class StreamSetupError extends Error {
|
|
|
66
205
|
this.userMessage = userMessage;
|
|
67
206
|
}
|
|
68
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Prepends sign-in guidance to a user-facing failure message when the failing channel's service domain is currently marked needs-sign-in. A confirmed
|
|
210
|
+
* authentication wall on the service is the most likely cause of a failed tune there, and the channel table's login icon is the remedy - the underlying error
|
|
211
|
+
* stays in the message so the original failure remains identifiable. Returns the message unchanged for ad-hoc URL streams without a channel identity, for
|
|
212
|
+
* channels whose domain cannot be resolved, and for domains not marked needs-sign-in.
|
|
213
|
+
*
|
|
214
|
+
* Exported for unit-test coverage of the guidance composition. Production callers reach this only through setupStream's failure paths, never directly.
|
|
215
|
+
* @param userMessage - The user-facing failure message being composed.
|
|
216
|
+
* @param channelKey - The failing channel's key, or null/undefined for ad-hoc URL streams.
|
|
217
|
+
* @param serviceName - The service's display name, used to name who needs the sign-in.
|
|
218
|
+
* @returns The user message, led by sign-in guidance when the channel's domain is marked needs-sign-in.
|
|
219
|
+
*/
|
|
220
|
+
export function withSignInGuidance(userMessage, channelKey, serviceName) {
|
|
221
|
+
if (!channelKey) {
|
|
222
|
+
return userMessage;
|
|
223
|
+
}
|
|
224
|
+
const domain = getAuthDomainForChannel(resolveServiceKey(channelKey));
|
|
225
|
+
if (!domain || (getDomainAuthState(domain)?.status !== "needsLogin")) {
|
|
226
|
+
return userMessage;
|
|
227
|
+
}
|
|
228
|
+
return serviceName + " needs sign-in. Open PrismCast's channel table and click this channel's login icon to sign in. " + userMessage;
|
|
229
|
+
}
|
|
69
230
|
// Request ID Generation.
|
|
70
231
|
/**
|
|
71
232
|
* Generates a short alphanumeric request ID for log correlation. The ID is 6 characters to keep log messages readable while providing enough uniqueness for
|
|
@@ -130,6 +291,21 @@ export function validateStreamUrl(url) {
|
|
|
130
291
|
}
|
|
131
292
|
}
|
|
132
293
|
// Page and Capture Creation.
|
|
294
|
+
/**
|
|
295
|
+
* Disposes a browser page acquired during stream setup: unregisters it from managed-page tracking and closes it if still open. The close is fire-and-forget with a
|
|
296
|
+
* debug log on error, matching the long-standing setup-failure cleanup behavior. Used as the DisposableStack disposer for pages in createPageWithCapture and
|
|
297
|
+
* setupStream, and by the setup-result cleanup closure, so every setup-phase page teardown flows through one definition.
|
|
298
|
+
* @param page - The page to dispose.
|
|
299
|
+
*/
|
|
300
|
+
function disposePage(page) {
|
|
301
|
+
unregisterManagedPage(page);
|
|
302
|
+
if (!page.isClosed()) {
|
|
303
|
+
page.close().catch((error) => {
|
|
304
|
+
LOG.debug("streaming:setup", "Page close error during setup cleanup: %s.", formatError(error));
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const defaultCreatePageWithCaptureDeps = { getCurrentBrowser, getStream, startOverlayHandling };
|
|
133
309
|
/**
|
|
134
310
|
* Creates a browser page with media capture and navigates to the URL. This is the reusable core function used by both initial stream setup and tab replacement
|
|
135
311
|
* recovery. It handles:
|
|
@@ -139,260 +315,300 @@ export function validateStreamUrl(url) {
|
|
|
139
315
|
* - Setting up video playback via navigateToPage() + initializePlayback()
|
|
140
316
|
*
|
|
141
317
|
* The caller is responsible for:
|
|
142
|
-
* - Creating the segmenter and
|
|
318
|
+
* - Creating the segmenter and attaching it to the capture session via captureSession.attachSegmenter()
|
|
143
319
|
* - Registering/updating the stream in the registry
|
|
144
320
|
* - Starting/updating the health monitor
|
|
145
321
|
* - Handling cleanup on failure
|
|
146
322
|
*
|
|
147
323
|
* @param options - Options for page and capture creation.
|
|
148
|
-
* @
|
|
324
|
+
* @param deps - The injected browser and overlay-poll collaborators; defaults to defaultCreatePageWithCaptureDeps. Threaded so a test drives this function without a
|
|
325
|
+
* live Chrome by substituting the shared-browser accessor, the capture launcher, and the static-capture overlay poll.
|
|
326
|
+
* @returns The page, context, and capture session (which owns the raw capture stream and any FFmpeg child).
|
|
149
327
|
* @throws Error if page creation, capture initialization, or navigation fails.
|
|
150
328
|
*/
|
|
151
|
-
export async function createPageWithCapture(options) {
|
|
152
|
-
const
|
|
153
|
-
const { comment, onFFmpegError, profile, streamId, url } = options;
|
|
154
|
-
// Create browser page.
|
|
155
|
-
const browser = await getCurrentBrowser();
|
|
156
|
-
const page = await browser.newPage();
|
|
157
|
-
registerManagedPage(page);
|
|
158
|
-
await page.setBypassCSP(true);
|
|
159
|
-
// Inject the shared video selector helper into the browser context. This must happen before navigation so the helper is available when evaluate calls run during
|
|
160
|
-
// initializePlayback (startVideoPlayback, applyVideoStyles, verifyFullscreen, lockVolumeProperties) and subsequent health monitoring (getVideoState).
|
|
161
|
-
await injectVideoSelector(page);
|
|
162
|
-
// Install CDP manifest interceptor before navigation. This listener captures .m3u8 URLs from the browser's network requests, enabling native HLS streaming for
|
|
163
|
-
// services that use clear or AES-128 encrypted streams. Skipped for tab replacements (native proxy is independent of capture) and for channels already known to
|
|
164
|
-
// use DRM (avoids 15 seconds of wasted CDP overhead per tune). The await ensures the CDP session and Network domain are ready before navigation begins.
|
|
165
|
-
const manifestInterception = (!options.tabReplacement && !options.skipManifestInterception) ? await installManifestInterceptor(page) : null;
|
|
166
|
-
// Select MIME type based on capture mode. FFmpeg mode is more stable for long recordings because Chrome's native fMP4 MediaRecorder can become unstable. The
|
|
167
|
-
// codec decision (H.264 vs HEVC) is delegated to the codec module, which considers the user's allowlist and GPU hardware capabilities.
|
|
168
|
-
const useFFmpeg = CONFIG.streaming.captureMode === "ffmpeg";
|
|
169
|
-
const captureMimeType = useFFmpeg ? getCaptureMimeType() : NATIVE_FMP4_MIME_TYPE;
|
|
170
|
-
// Track the output stream that will be sent to the segmenter and FFmpeg process if used. Also track the raw capture stream separately - it must be destroyed
|
|
171
|
-
// before closing the page to ensure chrome.tabCapture releases the capture.
|
|
172
|
-
let outputStream;
|
|
173
|
-
let rawCaptureStream = null;
|
|
174
|
-
let ffmpegProcess = null;
|
|
175
|
-
// Capture queue release function, hoisted here so both the try and catch blocks can access it. The promise and resolver are created together via
|
|
176
|
-
// withResolvers when the queue entry is registered below. The once-guard prevents double-releasing from multiple code paths (success, catch, timeout).
|
|
177
|
-
let captureQueueReleased = false;
|
|
178
|
-
let releaseCaptureQueue;
|
|
179
|
-
const releaseCaptureOnce = () => {
|
|
180
|
-
if (!captureQueueReleased) {
|
|
181
|
-
captureQueueReleased = true;
|
|
182
|
-
captureQueueDepth--;
|
|
183
|
-
releaseCaptureQueue();
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
// Initialize media stream capture.
|
|
329
|
+
export async function createPageWithCapture(options, deps = defaultCreatePageWithCaptureDeps) {
|
|
330
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
187
331
|
try {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
//
|
|
332
|
+
const captureElapsed = startTimer();
|
|
333
|
+
const { comment, numericStreamId, onFFmpegError, profile, streamId, url } = options;
|
|
334
|
+
// Acquire every resource on a DisposableStack so that any throw - in capture initialization, navigation, or playback setup - disposes them structurally as the
|
|
335
|
+
// function unwinds, in last-acquired-first order (capture session before page, so the capture stream is destroyed and STOP_RECORDING fires while the browser is
|
|
336
|
+
// still connected, before the page closes). On success we move() the stack to disarm it and transfer ownership to the caller. This centralizes teardown that
|
|
337
|
+
// would otherwise be repeated in each failure path, and closes the navigation-path leak of the manifest interceptor.
|
|
338
|
+
const resources = __addDisposableResource(env_1, new DisposableStack(), false);
|
|
339
|
+
// Create browser page.
|
|
340
|
+
const browser = await deps.getCurrentBrowser();
|
|
341
|
+
const page = await browser.newPage();
|
|
342
|
+
// Register in-flight: the registry does not record this page against the stream until setup finishes, so the mark is what keeps stale page cleanup from closing
|
|
343
|
+
// it mid-tune.
|
|
344
|
+
registerManagedPage(page, { inFlightSetup: true });
|
|
345
|
+
resources.adopt(page, disposePage);
|
|
346
|
+
await page.setBypassCSP(true);
|
|
347
|
+
// Inject the shared video selector helper into the browser context. This must happen before navigation so the helper is available when evaluate calls run during
|
|
348
|
+
// initializePlayback (startVideoPlayback, applyVideoStyles, verifyFullscreen, lockVolumeProperties) and subsequent health monitoring (getVideoState).
|
|
349
|
+
await injectVideoSelector(page);
|
|
350
|
+
// Select MIME type based on capture mode. FFmpeg mode is more stable for long recordings because Chrome's native fMP4 MediaRecorder can become unstable. The
|
|
351
|
+
// codec decision (H.264 vs HEVC) is delegated to the codec module, which considers the user's allowlist and GPU hardware capabilities.
|
|
352
|
+
const useFFmpeg = CONFIG.streaming.captureMode === "ffmpeg";
|
|
353
|
+
const captureMimeType = useFFmpeg ? getCaptureMimeType() : NATIVE_FMP4_MIME_TYPE;
|
|
354
|
+
// Resolve the FFmpeg binary path up front, before the raw capture stream is acquired below. resolveFFmpegPath is a memoized resolver that can sticky-reject; if its
|
|
355
|
+
// await sat between acquiring the raw capture stream and wrapping it in a CaptureSession, a rejection would strand the stream undestroyed (STOP_RECORDING never
|
|
356
|
+
// fires, leaving chrome.tabCapture active). Resolving it here keeps any rejection on a path with no capture resource yet acquired, so the CaptureSession remains
|
|
357
|
+
// the single owner from the instant the stream exists. Falls back to "ffmpeg" so spawn() defers to PATH lookup; only meaningful in FFmpeg mode.
|
|
358
|
+
const ffmpegBin = useFFmpeg ? ((await resolveFFmpegPath()) ?? "ffmpeg") : "ffmpeg";
|
|
359
|
+
// The capture-pipeline composite, assigned once the raw capture stream and optional FFmpeg child exist and registered on the DisposableStack the moment it is built.
|
|
360
|
+
let captureSession;
|
|
361
|
+
// Initialize media stream capture. The whole capture-init phase runs inside one try so a browser crash detected at turn grant drives the closed-page recursion,
|
|
362
|
+
// while every other failure unwinds through the DisposableStack.
|
|
219
363
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// Release the queue on success only. On failure, the catch block handles the release. The rejection handler is a no-op to suppress unhandled rejection
|
|
240
|
-
// warnings; the actual error handling happens in the catch block below.
|
|
241
|
-
void streamPromise.then(() => { releaseCaptureOnce(); }, () => { });
|
|
242
|
-
const stream = await raceWithTimeout(streamPromise, CONFIG.streaming.navigationTimeout, new Error("Stream initialization timed out."));
|
|
243
|
-
// Store the raw capture stream. This must be destroyed before closing the page.
|
|
244
|
-
rawCaptureStream = stream;
|
|
245
|
-
// For FFmpeg mode, spawn FFmpeg to transcode the Matroska stream to fMP4. FFmpeg copies the H264 video and transcodes Opus audio to AAC. The resolved binary
|
|
246
|
-
// path comes from the production-cached resolver - first call probes; subsequent calls return the memoized result. Falls back to "ffmpeg" so spawn() defers
|
|
247
|
-
// to PATH lookup if the resolver couldn't find a path (matching the previous behavior; the spawn will then fail with ENOENT if PATH is also empty).
|
|
248
|
-
if (useFFmpeg) {
|
|
249
|
-
const ffmpegBin = (await resolveFFmpegPath()) ?? "ffmpeg";
|
|
250
|
-
const ffmpeg = spawnFFmpeg(ffmpegBin, CONFIG.streaming.audioBitsPerSecond, (error) => {
|
|
251
|
-
LOG.error("FFmpeg process error: %s.", formatError(error));
|
|
252
|
-
if (onFFmpegError) {
|
|
253
|
-
onFFmpegError(error);
|
|
364
|
+
const streamOptions = {
|
|
365
|
+
audio: true,
|
|
366
|
+
audioBitsPerSecond: CONFIG.streaming.audioBitsPerSecond,
|
|
367
|
+
mimeType: captureMimeType,
|
|
368
|
+
video: true,
|
|
369
|
+
videoBitsPerSecond: CONFIG.streaming.videoBitsPerSecond,
|
|
370
|
+
// Constrain capture frame rate to a 30-60 fps band: 60 is the live-TV ceiling, and a 30 floor keeps motion smooth even when the user configures a lower rate.
|
|
371
|
+
// The ceiling is fixed at 60 while the floor follows the user's configured rate (clamped into the band), so the encoder favours the requested rate but never
|
|
372
|
+
// drops below 30. The readiness probe (attemptCaptureProbe) instead pins both bounds to a flat 30 because its getStream() fails or succeeds at the tabCapture
|
|
373
|
+
// API level before encoding matters, so a representative-but-minimal constraint set suffices there.
|
|
374
|
+
videoConstraints: {
|
|
375
|
+
mandatory: {
|
|
376
|
+
maxFrameRate: 60,
|
|
377
|
+
maxHeight: getEffectiveViewport(CONFIG).height,
|
|
378
|
+
maxWidth: getEffectiveViewport(CONFIG).width,
|
|
379
|
+
minFrameRate: Math.max(30, Math.min(60, CONFIG.streaming.frameRate)),
|
|
380
|
+
minHeight: getEffectiveViewport(CONFIG).height,
|
|
381
|
+
minWidth: getEffectiveViewport(CONFIG).width
|
|
382
|
+
}
|
|
254
383
|
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
384
|
+
};
|
|
385
|
+
// Acquire the raw capture stream through the capture lock. The lock serializes getStream against every other capture init process-wide and holds the turn until
|
|
386
|
+
// this task's promise settles, so no two inits collide. Every timing bound is read here, at the call, because the module-scope lock captured nothing from CONFIG.
|
|
387
|
+
const stream = await captureLock.run(async (signal) => {
|
|
388
|
+
// If Chrome crashed while this task waited for its turn, the page is dead. Throw a typed error so the turn releases at once and the closed-page recursion runs
|
|
389
|
+
// OUTSIDE the lock, on a fresh page.
|
|
390
|
+
if (page.isClosed()) {
|
|
391
|
+
throw new PageClosedDuringTurnError();
|
|
262
392
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
393
|
+
// Initialize capture. On a stale-mutex rejection - including one that arrives after this task was abandoned at the caller deadline - escalate from the single
|
|
394
|
+
// home before rethrowing, so the process-exit safety net fires even on the abandoned path, where a late stale-mutex rejection would otherwise go unescalated.
|
|
395
|
+
let raw;
|
|
396
|
+
try {
|
|
397
|
+
raw = await deps.getStream(page, streamOptions);
|
|
269
398
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
// EPIPE, "write after end", and "Premature close" errors are expected during cleanup when FFmpeg is killed or the capture stream is destroyed.
|
|
276
|
-
if (errorMessage.includes("EPIPE") || errorMessage.includes("write after end") || errorMessage.includes("Premature close")) {
|
|
277
|
-
return;
|
|
399
|
+
catch (error) {
|
|
400
|
+
if (isStaleCaptureMutexError(error)) {
|
|
401
|
+
escalateStaleCaptureMutex("stream initialization");
|
|
402
|
+
}
|
|
403
|
+
throw error;
|
|
278
404
|
}
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
405
|
+
// The caller deadline fired while getStream was still running: retire the stream this task just produced - destroy plus the STOP_RECORDING settle - inside the
|
|
406
|
+
// turn, then reject, so no path strands a live capture on a closing page or mistakes a retired stream for a usable one.
|
|
407
|
+
if (signal.aborted) {
|
|
408
|
+
await retireRawStream(raw, realClock);
|
|
409
|
+
throw new CaptureAbandonedError();
|
|
284
410
|
}
|
|
411
|
+
return raw;
|
|
412
|
+
}, {
|
|
413
|
+
deadlineMessage: STREAM_INIT_TIMEOUT_MESSAGE,
|
|
414
|
+
deadlineMs: CONFIG.streaming.navigationTimeout,
|
|
415
|
+
// A wedged capture init is decisive only when no OTHER stream is active - the same judgment, from the same predicate, as the mid-life detector: any other
|
|
416
|
+
// active stream means the browser is demonstrably capturing, so tearing it down would violate per-stream failure isolation. reverificationInProgress is passed
|
|
417
|
+
// as the literal false because an in-flight mid-life probe is itself queued BEHIND this wedged task on the same lock, so deferring to it would double the
|
|
418
|
+
// recovery bound, and a redundant invalidate is identity-guarded and converges harmlessly.
|
|
419
|
+
onWedge: () => {
|
|
420
|
+
if (shouldReverifyCapture({ activeStreamIds: getAllStreams().map((entry) => entry.id), failingStreamId: numericStreamId, hasBrowser: true,
|
|
421
|
+
reverificationInProgress: false })) {
|
|
422
|
+
// Route the wedge into the existing browser-recovery ladder, targeting the exact instance this task ran against; invalidateBrowser no-ops if it was already
|
|
423
|
+
// superseded. A rejection is warn-logged rather than rethrown out of the wedge callback.
|
|
424
|
+
void invalidateBrowser(browser, "capture initialization wedged past the recovery bound").catch((error) => {
|
|
425
|
+
LOG.warn("Invalidating a wedged capture browser failed: %s.", formatError(error));
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
// Other streams are active, so hold rather than tear the browser down: waiters fail fast on their own turn-wait bounds, the wedged operation settles at
|
|
430
|
+
// worst at Puppeteer's protocolTimeout, and once the other streams drain a later wedge or setup failure reaches the zero-other case.
|
|
431
|
+
LOG.warn("Capture initialization has wedged past the recovery bound, but other streams are active; holding rather than invalidating the browser.", { failingStreamId: numericStreamId });
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
turnWaitMs: CONFIG.streaming.navigationTimeout
|
|
285
435
|
});
|
|
286
|
-
//
|
|
287
|
-
|
|
436
|
+
// For FFmpeg mode, spawn FFmpeg to transcode the Matroska stream to fMP4. FFmpeg copies the H264 video and transcodes Opus audio to AAC. The binary path was
|
|
437
|
+
// resolved up front (above) so no throwable await sits between acquiring the raw capture stream and wrapping it in the CaptureSession that owns it. The spawn and
|
|
438
|
+
// pipeline wiring below are synchronous, so the stream is owned the instant it exists.
|
|
439
|
+
let ffmpegProcess = null;
|
|
440
|
+
if (useFFmpeg) {
|
|
441
|
+
const ffmpeg = spawnFFmpeg(ffmpegBin, CONFIG.streaming.audioBitsPerSecond, (error) => {
|
|
442
|
+
LOG.error("FFmpeg process error: %s.", formatError(error));
|
|
443
|
+
if (onFFmpegError) {
|
|
444
|
+
onFFmpegError(error);
|
|
445
|
+
}
|
|
446
|
+
}, streamId, comment);
|
|
447
|
+
ffmpegProcess = ffmpeg;
|
|
448
|
+
// Handle pipe errors on stdout. Stdin errors are handled by pipeline() below.
|
|
449
|
+
ffmpeg.stdout.on("error", (error) => {
|
|
450
|
+
const errorMessage = formatError(error);
|
|
451
|
+
if (errorMessage.includes("EPIPE")) {
|
|
452
|
+
LOG.debug("streaming:ffmpeg", "FFmpeg stdout pipe closed: %s.", errorMessage);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
LOG.error("FFmpeg stdout pipe error: %s.", errorMessage);
|
|
456
|
+
ffmpeg.kill();
|
|
457
|
+
if (onFFmpegError) {
|
|
458
|
+
onFFmpegError(error);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
// Pipe the Matroska capture stream to FFmpeg's stdin using pipeline() for proper cleanup. When FFmpeg is killed during tab replacement, pipeline() automatically
|
|
463
|
+
// destroys the source stream, preventing "write after end" errors that would occur with .pipe().
|
|
464
|
+
pipeline(stream, ffmpeg.stdin).catch((error) => {
|
|
465
|
+
const errorMessage = formatError(error);
|
|
466
|
+
// EPIPE, "write after end", and "Premature close" errors are expected during cleanup when FFmpeg is killed or the capture stream is destroyed.
|
|
467
|
+
if (errorMessage.includes("EPIPE") || errorMessage.includes("write after end") || errorMessage.includes("Premature close")) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
// Unexpected pipeline errors require cleanup.
|
|
471
|
+
LOG.error("Capture pipeline error: %s.", errorMessage);
|
|
472
|
+
ffmpeg.kill();
|
|
473
|
+
if (onFFmpegError) {
|
|
474
|
+
onFFmpegError(error instanceof Error ? error : new Error(String(error)));
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
// Wrap the raw capture stream and optional FFmpeg child as one self-disposing pipeline unit, and register it for structural teardown. The session derives the
|
|
479
|
+
// segmenter input internally (FFmpeg's stdout in FFmpeg mode, the raw stream in native-fMP4 mode); the caller attaches the segmenter once it is created.
|
|
480
|
+
captureSession = createCaptureSession({ ffmpegProcess, rawCaptureStream: stream });
|
|
481
|
+
resources.use(captureSession);
|
|
288
482
|
}
|
|
289
|
-
|
|
290
|
-
//
|
|
291
|
-
|
|
483
|
+
catch (error) {
|
|
484
|
+
// A browser crash detected at turn grant surfaces as PageClosedDuringTurnError: the page is dead, so unregister it, bump the retry counter, and start over on a
|
|
485
|
+
// fresh page on the new browser. The recursion runs here, outside the lock, so the turn was already released the instant the task threw. The retry cap prevents
|
|
486
|
+
// unbounded recursion during a crash loop.
|
|
487
|
+
if (error instanceof PageClosedDuringTurnError) {
|
|
488
|
+
unregisterManagedPage(page);
|
|
489
|
+
const retryCount = options._pageClosedRetries ?? 0;
|
|
490
|
+
if (retryCount >= MAX_PAGE_CLOSED_RETRIES) {
|
|
491
|
+
throw new Error("Browser crashed too many times during capture initialization.");
|
|
492
|
+
}
|
|
493
|
+
return await createPageWithCapture({ ...options, _pageClosedRetries: retryCount + 1 }, deps);
|
|
494
|
+
}
|
|
495
|
+
// Every other rejection - a stale-mutex escalation already fired inside the task, a caller-deadline CaptureDeadlineError, or any other capture-init failure - just
|
|
496
|
+
// unwinds. Resource teardown (page, interceptor, and the capture session once built) is handled by the DisposableStack as this throw unwinds the function scope.
|
|
497
|
+
throw error;
|
|
292
498
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
499
|
+
// Navigate and set up playback. For static capture profiles, just navigate without video setup.
|
|
500
|
+
let context;
|
|
501
|
+
let strategyDirectTune = false;
|
|
502
|
+
let usedDirectUrl = false;
|
|
503
|
+
// Install the CDP manifest interceptor immediately before the navigate-and-tune fork, so its observation window opens after the capture-lock and getStream phase
|
|
504
|
+
// (which the observer would otherwise idle through) and spans exactly the phases that produce manifests: navigation with retry, channel selection, and video
|
|
505
|
+
// setup. The install decision is gated on tab replacement and the skip flag only - static-capture profiles install too, since they stay native-HLS eligible
|
|
506
|
+
// through the interception's presence, so the guard sits upstream of the fork and both branches inherit it. The navigation allowance handed to the budget is
|
|
507
|
+
// this path's worst-case retry duration, backoff sleeps included; establishmentBudgetMs owns why the window has to outlive that allowance and every phase
|
|
508
|
+
// after it, direct tunes included.
|
|
509
|
+
const interceptionBudgetMs = establishmentBudgetMs(maxRetryDuration({
|
|
510
|
+
backoffJitter: CONFIG.recovery.backoffJitter,
|
|
511
|
+
maxAttempts: CONFIG.streaming.maxNavigationRetries,
|
|
512
|
+
maxBackoffDelay: CONFIG.recovery.maxBackoffDelay,
|
|
513
|
+
timeoutMs: CONFIG.streaming.navigationTimeout
|
|
514
|
+
}));
|
|
515
|
+
const manifestInterception = (!options.tabReplacement && !options.skipManifestInterception) ? await installManifestInterceptor(page, interceptionBudgetMs) : null;
|
|
516
|
+
// Register the interception on the resource stack after the capture session, so on an unwind it disposes first. Its disposal is a CDP observer detach with no
|
|
517
|
+
// ordering dependency on the capture session or the page; the only ordered teardown pair is capture-session-before-page (STOP_RECORDING while the browser is
|
|
518
|
+
// still connected), which both this stack and the later owned stack preserve.
|
|
519
|
+
if (manifestInterception) {
|
|
520
|
+
resources.use(manifestInterception);
|
|
298
521
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
522
|
+
try {
|
|
523
|
+
if (!profile.staticCapture) {
|
|
524
|
+
// Check for a direct watch URL, unless the caller has asked for the guide path outright. When one is available, navigate directly to it and skip channel
|
|
525
|
+
// selection, avoiding guide page navigation entirely. On a failure the coordinator blames the URL for, the catch block below evicts the hint and leaves this
|
|
526
|
+
// function typed, and setupStream re-invokes once with the resolution skipped so the tune still gets its guide attempt.
|
|
527
|
+
const directUrl = options.skipDirectUrl ? null : await resolveDirectUrl(profile, page);
|
|
528
|
+
usedDirectUrl = !!directUrl;
|
|
529
|
+
const navigationUrl = directUrl ?? url;
|
|
530
|
+
/* Phase 1 (navigation) and phase 2 (channel selection + video setup) run through the shared establishment composition, so the step order this path uses -
|
|
531
|
+
* navigate, stamp the observation epoch, initialize playback under the module-scope PLAYBACK_INIT_TIMEOUT safety net - is the same order the
|
|
532
|
+
* re-establishment path runs. When navigating to a cached direct URL, channel selection is skipped because the URL already targets the correct channel.
|
|
533
|
+
* The composition places no outer timeout around channel selection: each sub-step (selectChannel, waitForVideoReady, etc.) carries its own internal
|
|
534
|
+
* timeout via videoTimeout and the click retry constants.
|
|
535
|
+
*/
|
|
536
|
+
const tuneResult = await establishChannelPlayback(page, profile, manifestInterception, {
|
|
537
|
+
initOptions: { persistResolution: options.persistResolution, requestedUrl: navigationUrl, skipChannelSelection: usedDirectUrl },
|
|
538
|
+
// Navigate with retry. The 10-second navigationTimeout is appropriate for page loads, and retryOperation correctly reloads the page on genuine navigation
|
|
539
|
+
// failures. The retry ladder is this path's own policy, kept separate from channel selection so its timeout does not race the internal click retry loops
|
|
540
|
+
// in channel selection strategies (guideGrid can take 15-20 seconds for binary search + click retries).
|
|
541
|
+
navigate: async () => {
|
|
542
|
+
await retryOperation({
|
|
543
|
+
backoffJitter: CONFIG.recovery.backoffJitter,
|
|
544
|
+
description: "page navigation for " + navigationUrl,
|
|
545
|
+
maxAttempts: CONFIG.streaming.maxNavigationRetries,
|
|
546
|
+
maxBackoffDelay: CONFIG.recovery.maxBackoffDelay,
|
|
547
|
+
operation: async () => {
|
|
548
|
+
await navigateToPage(page, navigationUrl, profile);
|
|
549
|
+
},
|
|
550
|
+
shouldAbort: () => page.isClosed(),
|
|
551
|
+
timeoutMs: CONFIG.streaming.navigationTimeout
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
strategyDirectTune = tuneResult.directTune ?? false;
|
|
556
|
+
context = tuneResult.context;
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
await page.goto(url);
|
|
560
|
+
// A static capture navigates once and takes the page as-is, with no channel selection or video wait, so it never reaches the tune path's overlay poll. Launch
|
|
561
|
+
// a bounded staticCapture poll so a cookie banner or per-site modal is dismissed on the captured page. There is no controller: the phase's window bounds it and
|
|
562
|
+
// a closed page stops it via the tick-error taxonomy. Any dismissal click lands in the captured pixels, which is exactly the intent for a static capture.
|
|
563
|
+
void deps.startOverlayHandling(page, profile, { phase: "staticCapture" });
|
|
564
|
+
context = page;
|
|
565
|
+
}
|
|
302
566
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
567
|
+
catch (error) {
|
|
568
|
+
// If a direct watch URL was used, offer the failure to the cache coordinator, which evicts the live and persisted hints when the failure is evidence against
|
|
569
|
+
// the URL and reports that verdict back. The retention policy lives there and is read from there - this file never re-derives which failures count.
|
|
570
|
+
const urlEvidence = usedDirectUrl && invalidateDirectUrl(profile, error);
|
|
571
|
+
// Re-minimize the browser window. Navigation may have un-minimized it (new tab activation on macOS), and without this the window stays visible after the failed
|
|
572
|
+
// attempt. Fire-and-forget since we're about to throw. Resource teardown (capture session, interceptor, page) is handled by the DisposableStack as this throw
|
|
573
|
+
// unwinds the function scope; the capture session disposes first, destroying the capture stream before the page closes, so STOP_RECORDING ordering is preserved.
|
|
574
|
+
minimizeBrowserWindow().catch(() => { });
|
|
575
|
+
/* A failure the coordinator blamed on the URL is worth one more attempt down the guide path, so it leaves this function typed for setupStream to act on. An
|
|
576
|
+
* establishment timeout qualifies: the retry is a whole fresh invocation, so there is no abandoned first attempt still driving a page, no one-shot manifest
|
|
577
|
+
* interception handle to reuse, and no direct-tune marker to reset - the retry gets its own page, handle, and markers, which is exactly what a fresh tune
|
|
578
|
+
* would get. Page-death and abort failures, the ones the retention policy keeps the URL for, rethrow raw: nothing about them says the URL is wrong.
|
|
579
|
+
*/
|
|
580
|
+
if (urlEvidence) {
|
|
581
|
+
throw new DirectUrlEstablishmentError(error);
|
|
582
|
+
}
|
|
312
583
|
throw error;
|
|
313
584
|
}
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
// Navigate and set up playback. For static capture profiles, just navigate without video setup.
|
|
319
|
-
let context;
|
|
320
|
-
let strategyDirectTune = false;
|
|
321
|
-
let usedDirectUrl = false;
|
|
322
|
-
try {
|
|
323
|
-
if (!profile.staticCapture) {
|
|
324
|
-
// Check for a direct watch URL. If available, navigate directly to it and skip channel selection, avoiding guide page navigation entirely. On failure,
|
|
325
|
-
// the cache entry is invalidated in the catch block so the outer retry loop (in streaming/hls.ts) re-invokes with the guide URL.
|
|
326
|
-
const directUrl = await resolveDirectUrl(profile, page);
|
|
327
|
-
usedDirectUrl = !!directUrl;
|
|
328
|
-
const navigationUrl = directUrl ?? url;
|
|
329
|
-
// Phase 1: Navigate to the page with retry. The 10-second navigationTimeout is appropriate for page loads, and retryOperation correctly reloads the page on
|
|
330
|
-
// genuine navigation failures. Navigation is wrapped in retryOperation separately from channel selection so the timeout does not race with the internal click
|
|
331
|
-
// retry loops in channel selection strategies (guideGrid can take 15-20 seconds for binary search + click retries).
|
|
332
|
-
await retryOperation({
|
|
333
|
-
backoffJitter: CONFIG.recovery.backoffJitter,
|
|
334
|
-
description: "page navigation for " + navigationUrl,
|
|
335
|
-
maxAttempts: CONFIG.streaming.maxNavigationRetries,
|
|
336
|
-
maxBackoffDelay: CONFIG.recovery.maxBackoffDelay,
|
|
337
|
-
operation: async () => {
|
|
338
|
-
await navigateToPage(page, navigationUrl, profile);
|
|
339
|
-
},
|
|
340
|
-
shouldAbort: () => page.isClosed(),
|
|
341
|
-
timeoutMs: CONFIG.streaming.navigationTimeout
|
|
342
|
-
});
|
|
343
|
-
// Phase 2: Channel selection + video setup. When navigating to a cached direct URL, skip channel selection since the URL already targets the correct
|
|
344
|
-
// channel. Runs after navigation succeeds with no outer timeout racing against internal click retries. Each sub-step (selectChannel, waitForVideoReady,
|
|
345
|
-
// etc.) has its own internal timeout via videoTimeout and click retry constants. For guideGrid strategies, a channel selection failure triggers an overlay
|
|
346
|
-
// dismiss and retry, which doubles the channel selection time budget. The 45-second safety-net timeout accommodates this retry while still preventing
|
|
347
|
-
// pathological hangs if multiple internal timeouts chain sequentially.
|
|
348
|
-
const PLAYBACK_INIT_TIMEOUT = 45000;
|
|
349
|
-
const tuneResult = await raceWithTimeout(initializePlayback(page, profile, { persistResolution: options.persistResolution, skipChannelSelection: usedDirectUrl }), PLAYBACK_INIT_TIMEOUT, new Error("Playback initialization timed out after " + String(PLAYBACK_INIT_TIMEOUT) + "ms."));
|
|
350
|
-
strategyDirectTune = tuneResult.directTune ?? false;
|
|
351
|
-
context = tuneResult.context;
|
|
352
|
-
}
|
|
353
|
-
else {
|
|
354
|
-
await page.goto(url);
|
|
355
|
-
context = page;
|
|
585
|
+
// During tab replacement, allow Chrome's compositor to fully stabilize the fullscreen video surface before minimizing. Without this delay, the compositor may
|
|
586
|
+
// snapshot an incorrect scaling state during the minimize transition, causing the captured content to appear zoomed into the top-left corner.
|
|
587
|
+
if (options.tabReplacement && !profile.staticCapture) {
|
|
588
|
+
await delay(500);
|
|
356
589
|
}
|
|
590
|
+
// Resize and minimize window.
|
|
591
|
+
await resizeAndMinimizeWindow(page);
|
|
592
|
+
LOG.debug("timing:startup", "Page with capture ready. Total: %sms.", captureElapsed());
|
|
593
|
+
// Success: transfer ownership of the page, interceptor, and capture session out of the scope guard. move() empties the stack so its scope-exit disposal is a no-op
|
|
594
|
+
// and the caller becomes responsible for disposing what it now holds.
|
|
595
|
+
resources.move();
|
|
596
|
+
return {
|
|
597
|
+
captureSession,
|
|
598
|
+
context,
|
|
599
|
+
// The kind the interception is finalized against, derived by the shared formula from this path's tune facts and the profile.
|
|
600
|
+
directTune: computeDirectTuneKind({ profile, strategyDirectTune, usedDirectUrl }),
|
|
601
|
+
manifestInterception,
|
|
602
|
+
page
|
|
603
|
+
};
|
|
357
604
|
}
|
|
358
|
-
catch (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
invalidateDirectUrl(profile);
|
|
362
|
-
}
|
|
363
|
-
// Clean up on navigation or playback initialization failure. Destroy the raw capture stream first to ensure chrome.tabCapture releases the capture.
|
|
364
|
-
if (!rawCaptureStream.destroyed) {
|
|
365
|
-
rawCaptureStream.destroy();
|
|
366
|
-
}
|
|
367
|
-
if (ffmpegProcess) {
|
|
368
|
-
ffmpegProcess.kill();
|
|
369
|
-
}
|
|
370
|
-
unregisterManagedPage(page);
|
|
371
|
-
if (!page.isClosed()) {
|
|
372
|
-
page.close().catch(() => { });
|
|
373
|
-
}
|
|
374
|
-
// Re-minimize the browser window. Navigation may have un-minimized it (new tab activation on macOS), and without this the window stays visible after the
|
|
375
|
-
// failed attempt. Fire-and-forget since we're about to throw.
|
|
376
|
-
minimizeBrowserWindow().catch(() => { });
|
|
377
|
-
throw error;
|
|
605
|
+
catch (e_1) {
|
|
606
|
+
env_1.error = e_1;
|
|
607
|
+
env_1.hasError = true;
|
|
378
608
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (options.tabReplacement && !profile.staticCapture) {
|
|
382
|
-
await delay(500);
|
|
609
|
+
finally {
|
|
610
|
+
__disposeResources(env_1);
|
|
383
611
|
}
|
|
384
|
-
// Resize and minimize window.
|
|
385
|
-
await resizeAndMinimizeWindow(page);
|
|
386
|
-
LOG.debug("timing:startup", "Page with capture ready. Total: %sms.", captureElapsed());
|
|
387
|
-
return {
|
|
388
|
-
captureStream: outputStream,
|
|
389
|
-
context,
|
|
390
|
-
directTune: usedDirectUrl || strategyDirectTune || !isChannelSelectionProfile(profile),
|
|
391
|
-
ffmpegProcess,
|
|
392
|
-
manifestInterception,
|
|
393
|
-
page,
|
|
394
|
-
rawCaptureStream
|
|
395
|
-
};
|
|
396
612
|
}
|
|
397
613
|
// URL Redirect Resolution.
|
|
398
614
|
/**
|
|
@@ -421,8 +637,8 @@ async function resolveRedirectUrl(url) {
|
|
|
421
637
|
* Promise that resolves once the write is committed; resolution-layer errors thrown from the underlying file store are surfaced for caller-side logging but do not
|
|
422
638
|
* abort the tune (selectChannel attaches a .catch on the returned promise).
|
|
423
639
|
*
|
|
424
|
-
*
|
|
425
|
-
* not pre-check whether the value differs - the underlying store handles that.
|
|
640
|
+
* Safe to call more than once at the storage layer: writing the same selector twice is a no-op (the file store deduplicates identical deltas via normalization).
|
|
641
|
+
* The closure does not pre-check whether the value differs - the underlying store handles that.
|
|
426
642
|
* @param canonicalKey - The canonical channel key (e.g., "fox").
|
|
427
643
|
* @param serviceTag - The active service tag (e.g., "foxone").
|
|
428
644
|
* @returns Async callback that persists the resolved selector to the channel store.
|
|
@@ -437,21 +653,116 @@ function buildPersistResolutionCallback(canonicalKey, serviceTag) {
|
|
|
437
653
|
LOG.debug("tuning", "Persisted resolved selector \"%s\" to channel store as \"%s\".", resolvedSelector, variantKey);
|
|
438
654
|
};
|
|
439
655
|
}
|
|
656
|
+
/* Decides whether the manifest a tune's interception selected belongs to the channel the profile selects, using the provider's own verifier when one exists. The
|
|
657
|
+
* provider and channelSelector gates run before the interception promise is awaited, so a stream with nothing to verify never waits on the interception
|
|
658
|
+
* here...the promise's settle time belongs only to the paths that consume it. Verification is gated to master-kind selections because a provider verifier like
|
|
659
|
+
* Fox's reads the channel call sign from a fixed segment of the master CDN URL, and a media (chunklist) URL has a different path shape the verifier was never
|
|
660
|
+
* calibrated for - a false tune-failure on a correct stream. Providers without a verifier, profiles without a channelSelector, non-master selections, and a null
|
|
661
|
+
* interception all verify vacuously: the gates upstream are the only identity signal we have there, so the honest answer is no objection rather than a guess.
|
|
662
|
+
*
|
|
663
|
+
* @param interceptionPromise - The interception promise from the handle the tune finalized, awaited only once the gates above admit a verifier.
|
|
664
|
+
* @param profile - The resolved site profile whose strategy names the provider and whose channelSelector names the expected channel.
|
|
665
|
+
* @returns A human-readable failure reason when the manifest belongs to a different channel, or null when it verifies (including every vacuous case).
|
|
666
|
+
*/
|
|
667
|
+
export async function verifyManifestSelection(interceptionPromise, profile) {
|
|
668
|
+
const provider = getProviderByStrategy(profile.channelSelection.strategy);
|
|
669
|
+
if (!provider?.verifyManifestForChannel || !profile.channelSelector) {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
const interception = await interceptionPromise;
|
|
673
|
+
if (interception?.selectedKind !== "master") {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
return provider.verifyManifestForChannel(interception.manifestUrl, profile.channelSelector);
|
|
677
|
+
}
|
|
678
|
+
// Channel Establishment.
|
|
679
|
+
/**
|
|
680
|
+
* Sizes the interception observation window for one establishment run. The observer has to outlive every phase that can feed it: the caller's own navigation
|
|
681
|
+
* allowance (retry-aware on the tune path, a single attempt on the refresh path), the bounded playback initialization, the settle the interceptor waits out
|
|
682
|
+
* before it resolves, and a margin covering the small span between those phases. A window that expired mid-establishment would resolve with whatever the page
|
|
683
|
+
* load captured rather than the channel the selection landed on, which is the outcome this budget forecloses. Because no healthy path waits on the timer - it is
|
|
684
|
+
* a leak bound for an establishment that dies without unwinding - its generosity costs nothing.
|
|
685
|
+
* @param navigationAllowanceMs - The caller's worst-case navigation allowance, including any retry backoff its own policy performs.
|
|
686
|
+
* @returns The observation window, in milliseconds.
|
|
687
|
+
*/
|
|
688
|
+
export function establishmentBudgetMs(navigationAllowanceMs) {
|
|
689
|
+
return navigationAllowanceMs + PLAYBACK_INIT_TIMEOUT + FINALIZE_SETTLE_DELAY + INTERCEPTION_BUDGET_MARGIN_MS;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Derives the direct-tune kind the interception is finalized against. A tune is direct when the route was a cached direct watch URL, when the strategy itself
|
|
693
|
+
* resolved a direct tune through API interception, or when the profile has no DOM-based channel-selection step to run at all. The refresh path always navigates
|
|
694
|
+
* the configured channel url with full selection, so it never has a cached direct URL to report and omits that term entirely.
|
|
695
|
+
* @param options - The resolved profile plus the tune facts the kind reads: whether a cached direct URL was taken (absent on paths that cannot take one) and
|
|
696
|
+
* whether the strategy resolved a direct tune of its own.
|
|
697
|
+
* @returns True when the interception should adjudicate as a direct tune.
|
|
698
|
+
*/
|
|
699
|
+
export function computeDirectTuneKind(options) {
|
|
700
|
+
return (options.usedDirectUrl ?? false) || options.strategyDirectTune || !isChannelSelectionProfile(options.profile);
|
|
701
|
+
}
|
|
702
|
+
const defaultEstablishChannelPlaybackDeps = { initializePlayback };
|
|
703
|
+
/**
|
|
704
|
+
* Establishes a channel on a page: navigate under the caller's own policy, stamp the interception's observation epoch, then run playback initialization under
|
|
705
|
+
* the module's safety-net bound. The tune path and the native refresh capability both run this, so the sequence and its ordering are written exactly once and
|
|
706
|
+
* only per-path policy differs.
|
|
707
|
+
* @param page - The page being established.
|
|
708
|
+
* @param profile - The resolved site profile whose strategy drives navigation and channel selection.
|
|
709
|
+
* @param handle - The interceptor observing this establishment, or null when nothing is observing it (a tab replacement, or an install that failed).
|
|
710
|
+
* @param options - The caller's navigation policy, its initialization options, and an optional settlement hook. See EstablishChannelPlaybackOptions.
|
|
711
|
+
* @param deps - Injected browser-boundary collaborator; defaults to the real playback initializer in production.
|
|
712
|
+
* @returns The result playback initialization produced: the video context for subsequent monitoring, and the strategy's direct-tune flag where it resolved one.
|
|
713
|
+
* @throws The caller's navigation failure, the initialization's own failure, or the bound's timeout error when the bound lapses first.
|
|
714
|
+
*/
|
|
715
|
+
export async function establishChannelPlayback(page, profile, handle, options, deps = defaultEstablishChannelPlaybackDeps) {
|
|
716
|
+
const { initOptions, navigate, onInitSettled } = options;
|
|
717
|
+
await navigate();
|
|
718
|
+
// Channel selection begins here, so the observation epoch is stamped now - manifests seen earlier belong to page load (a guide page's auto-played default
|
|
719
|
+
// channel), and guide-tune selection prefers a master observed after this point. A null handle means nothing is observing, so there is no epoch to stamp.
|
|
720
|
+
handle?.markChannelSelectionStart();
|
|
721
|
+
const initPromise = deps.initializePlayback(page, profile, initOptions);
|
|
722
|
+
/* The settlement hook rides the initialization's own settlement rather than a fixed step after the wait below, because only the promise knows which of
|
|
723
|
+
* success, failure, or a late completion actually happened - and a late completion is reachable, since the wait abandons rather than cancels: a lapsed
|
|
724
|
+
* initialization is not stopped, it winds down on its own bounded internal phases and can finish afterward. The trailing catch is the whole safety net for
|
|
725
|
+
* the hook itself, absorbing a hook that throws as well as a structurally-permitted async hook that rejects, so this chain can never become a rejection
|
|
726
|
+
* source of its own.
|
|
727
|
+
*/
|
|
728
|
+
if (onInitSettled) {
|
|
729
|
+
void initPromise.finally(() => onInitSettled()).catch(() => { });
|
|
730
|
+
}
|
|
731
|
+
return waitWithTimeout(initPromise, PLAYBACK_INIT_TIMEOUT, new Error("Playback initialization timed out after " + String(PLAYBACK_INIT_TIMEOUT) + "ms."));
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Adjudicates what an establishment's interception selected: finalize it against the honest direct-tune kind, then confirm the manifest belongs to the channel
|
|
735
|
+
* the profile names. The tune path and the native refresh capability both run this, so finalize-then-verify is written exactly once.
|
|
736
|
+
*
|
|
737
|
+
* The stage hands on the interception promise rather than a resolved result, which is what keeps verifyManifestSelection's latency rule holding - its provider
|
|
738
|
+
* and selector gates run before the promise is awaited, so a stream with nothing to verify never waits on the interception here.
|
|
739
|
+
* @param handle - The interceptor this establishment installed, still observing.
|
|
740
|
+
* @param profile - The resolved site profile whose strategy names the provider and whose channelSelector names the expected channel.
|
|
741
|
+
* @param directTune - The direct-tune kind to finalize against.
|
|
742
|
+
* @returns A human-readable failure reason when the manifest belongs to a different channel, or null when it verifies (including every vacuous case).
|
|
743
|
+
*/
|
|
744
|
+
export async function adjudicateChannelSelection(handle, profile, directTune) {
|
|
745
|
+
handle.finalize(directTune);
|
|
746
|
+
return verifyManifestSelection(handle.promise, profile);
|
|
747
|
+
}
|
|
440
748
|
/**
|
|
441
749
|
* Sets up a stream: validates input, creates browser page, initializes capture, navigates to URL, and starts health monitoring.
|
|
442
750
|
*
|
|
443
751
|
* This function handles all common stream setup logic. The caller is responsible for:
|
|
444
|
-
* -
|
|
752
|
+
* - Creating the segmenter and attaching it to the returned capture session (via captureSession.attachSegmenter), or upgrading to native streaming
|
|
445
753
|
* - Registering the stream in the registry
|
|
446
754
|
* - Triggering cleanup when the stream ends
|
|
447
755
|
*
|
|
448
756
|
* @param options - Stream configuration options.
|
|
449
757
|
* @param onCircuitBreak - Callback invoked when the circuit breaker trips (stream unrecoverable).
|
|
450
|
-
* @
|
|
758
|
+
* @param deps - The injected browser and overlay-poll collaborators, forwarded to every establishment attempt this function makes; defaults to
|
|
759
|
+
* defaultCreatePageWithCaptureDeps. Threaded for the same reason createPageWithCapture takes them: a test drives the establishment sequence - including its guide
|
|
760
|
+
* fallback - without a live Chrome by substituting the shared-browser accessor, the capture launcher, and the overlay poll.
|
|
761
|
+
* @returns Setup result with capture session, cleanup function, and metadata.
|
|
451
762
|
* @throws StreamSetupError if setup fails with appropriate status code and message.
|
|
452
763
|
*/
|
|
453
|
-
export async function setupStream(options, onCircuitBreak) {
|
|
454
|
-
const { channel, channelName, channelSelector, clickSelector, clickToPlay, onTabReplacementFactory, profileOverride, staticCapture, url } = options;
|
|
764
|
+
export async function setupStream(options, onCircuitBreak, deps = defaultCreatePageWithCaptureDeps) {
|
|
765
|
+
const { channel, channelName, channelSelector, clickSelector, clickToPlay, onTabReplacementFactory, probeIdentity, profileOverride, staticCapture, url } = options;
|
|
455
766
|
// Use pre-allocated IDs from a pending registry entry when available, or generate new ones. Pre-allocated IDs ensure the abort controller, health monitor, and
|
|
456
767
|
// tab replacement handler all reference the same stream identity as the pending entry in the registry.
|
|
457
768
|
const streamId = options.streamId ?? generateStreamId(channelName, url);
|
|
@@ -480,218 +791,348 @@ export async function setupStream(options, onCircuitBreak) {
|
|
|
480
791
|
let profileName = profileResult.profileName;
|
|
481
792
|
// Wrap the setup in stream context for log correlation.
|
|
482
793
|
return runWithStreamContext({ channelName: channel?.name, streamId, url }, async () => {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (
|
|
487
|
-
profile
|
|
488
|
-
|
|
489
|
-
|
|
794
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
795
|
+
try {
|
|
796
|
+
// Apply profile override if specified.
|
|
797
|
+
if (profileOverride) {
|
|
798
|
+
// An override may name any profile that exists: a builtin from any source through the single lookup, or one of the user's own. The UI profile catalog is
|
|
799
|
+
// not the oracle here - it omits the provider profiles, which a direct override may legitimately ask for.
|
|
800
|
+
if (Boolean(getBuiltinProfile(profileOverride)) || (profileOverride in getUserProfiles())) {
|
|
801
|
+
profile = resolveProfile(profileOverride);
|
|
802
|
+
profileName = profileOverride;
|
|
803
|
+
LOG.debug("streaming:setup", "Profile overridden to '%s' via query parameter.", profileOverride);
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
LOG.warn("Unknown profile override '%s', using resolved profile.", profileOverride);
|
|
807
|
+
}
|
|
490
808
|
}
|
|
491
|
-
|
|
492
|
-
|
|
809
|
+
/* A channel that named a profile and did not get it should hear about it once, at the moment it matters. Resolution reports the substitution on its return
|
|
810
|
+
* rather than logging it, because the playlist render and the channel table call the same resolver and would repeat the message on every fetch and every
|
|
811
|
+
* draw. Comparing profileName against the resolver's keeps a query-parameter override quiet: when ?profile= replaces the resolution wholesale, the
|
|
812
|
+
* substitution the resolver reported is not what tunes.
|
|
813
|
+
*/
|
|
814
|
+
if (profileResult.overriddenProfile && (profileName === profileResult.profileName)) {
|
|
815
|
+
LOG.warn("Channel %s specifies the %s profile, which requires a channel selector the channel does not define; tuning with the %s profile instead.", channel?.name ?? channelName ?? url, profileResult.overriddenProfile, profileName);
|
|
493
816
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
817
|
+
// Apply static capture override if specified.
|
|
818
|
+
if (staticCapture) {
|
|
819
|
+
profile = { ...profile, staticCapture: true };
|
|
820
|
+
}
|
|
821
|
+
// Merge the ad-hoc channel selector into the profile if provided. This must happen after the profile override block above, which replaces the profile object
|
|
822
|
+
// wholesale and would discard an earlier merge. For predefined channels, getProfileForChannel already handles the merge from channel.channelSelector.
|
|
823
|
+
if (channelSelector) {
|
|
824
|
+
profile = { ...profile, channelSelector };
|
|
825
|
+
}
|
|
826
|
+
// Merge the ad-hoc clickToPlay and clickSelector options into the profile. clickSelector implies clickToPlay. For ad-hoc streams, these enable clicking an
|
|
827
|
+
// element to start playback - either the video element (clickToPlay alone) or a play button overlay (clickToPlay + clickSelector).
|
|
828
|
+
if (clickToPlay || clickSelector) {
|
|
829
|
+
profile = { ...profile, clickToPlay: true, ...(clickSelector ? { clickSelector } : {}) };
|
|
830
|
+
}
|
|
831
|
+
// Compute the metadata comment for FFmpeg. Prefer the friendly channel name, fall back to the channel key, or extract the domain from the URL.
|
|
832
|
+
const metadataComment = channel?.name ?? channelName ?? extractDomain(url);
|
|
833
|
+
// Compute the friendly service display name once for use in both the monitor and the setup result.
|
|
834
|
+
const serviceName = getServiceDisplayName(url);
|
|
835
|
+
// Create the tab replacement handler if a factory was provided. This is done after profile resolution so the handler has access to the final profile.
|
|
836
|
+
const onTabReplacement = onTabReplacementFactory ? onTabReplacementFactory(numericStreamId, streamId, profile, metadataComment) : undefined;
|
|
837
|
+
// Validate URL.
|
|
838
|
+
const validation = validateStreamUrl(url);
|
|
839
|
+
if (!validation.valid) {
|
|
840
|
+
LOG.error("Invalid URL requested: %s - %s.", url, validation.reason ?? "Unknown error");
|
|
841
|
+
throw new StreamSetupError("Invalid URL: " + (validation.reason ?? "Unknown error"), 400, validation.reason ?? "Invalid URL.");
|
|
842
|
+
}
|
|
843
|
+
// Concurrent-stream capacity is reserved upstream at the registration site (reserveStreamSlot in hls.ts) before this stream's pending entry is registered, so
|
|
844
|
+
// the new stream is excluded from its own check. We deliberately do NOT re-check here: by the time setupStream runs, getStreamCount() already includes this
|
|
845
|
+
// stream's pending entry, so a count-based check would double-count it against its own slot and reject at the legitimate boundary - after the client has
|
|
846
|
+
// already received a preroll playlist. reserveStreamSlot is the single source of truth for the capacity decision; setupStream's sole caller (completeStreamSetup)
|
|
847
|
+
// always reserves before reaching here.
|
|
848
|
+
// Create page and start capture using the shared function. This handles browser page creation, capture initialization, FFmpeg spawning, and navigation with retry.
|
|
849
|
+
let captureResult;
|
|
850
|
+
try {
|
|
851
|
+
/* Skip CDP manifest interception when the channel is pinned to screen capture, or when the probe cache already knows this stream's binding resolves to
|
|
852
|
+
* DRM. The per-channel override short-circuits first, so a forced channel never installs the interceptor: nothing intercepts a manifest, no native
|
|
853
|
+
* attempt runs, no probe fires, and the encryption cache stays untouched by that stream. The cache half avoids creating a CDP session that sits idle for
|
|
854
|
+
* 15 seconds before the interceptor timeout cleans it up; every stream carries an identity, ad-hoc URLs included, so that lookup needs no guard.
|
|
855
|
+
*/
|
|
856
|
+
const skipInterception = (channel?.forceCapture === true) || (getCachedEncryption(probeIdentity) === "drm");
|
|
857
|
+
// Build the persistResolution closure for the active channel. When the resolution layer in selectChannel() converts a category selector to a concrete call
|
|
858
|
+
// sign, this closure writes the result to the user's channel store as a per-service-variant override - the same shape produced when a user manually edits
|
|
859
|
+
// the selector via the web UI. Omitted for ad-hoc URL streams (no channel record to update).
|
|
860
|
+
const serviceTag = getDomainConfig(url)?.serviceTag;
|
|
861
|
+
const persistResolution = (channelName && serviceTag) ?
|
|
862
|
+
buildPersistResolutionCallback(channelName, serviceTag) :
|
|
863
|
+
undefined;
|
|
864
|
+
const attemptOptions = {
|
|
865
|
+
comment: metadataComment,
|
|
866
|
+
numericStreamId,
|
|
867
|
+
onFFmpegError: onCircuitBreak,
|
|
868
|
+
persistResolution,
|
|
869
|
+
profile,
|
|
870
|
+
skipManifestInterception: skipInterception,
|
|
871
|
+
streamId,
|
|
872
|
+
url
|
|
873
|
+
};
|
|
874
|
+
/* The establishment, with its one guide fallback. A failure the coordinator blamed on a direct watch URL arrives here typed, with the hint already evicted;
|
|
875
|
+
* one more invocation with the resolution skipped is exactly what the next tune would do, and doing it now spares the client a whole failed request. The
|
|
876
|
+
* fallback runs after the first attempt's DisposableStack has unwound, so it gets a fresh page, a fresh manifest-interception handle, and usedDirectUrl
|
|
877
|
+
* naturally false - the direct-tune marker and the manifest finalizer are computed per invocation and need no reset. Only the typed error is caught here;
|
|
878
|
+
* every other failure, the fallback's own included, falls to setupStream's catch below so one classification block serves both attempts identically.
|
|
879
|
+
*
|
|
880
|
+
* The worst case it can produce: a stale hint plus a genuinely broken guide costs two establishments before the classified failure. On the preroll-fed HLS
|
|
881
|
+
* path the client stays fed throughout; on the blocking callers - pretune, MPEG-TS, an ad-hoc play - the alternative is the same second establishment run by
|
|
882
|
+
* the caller's own retry, one request later.
|
|
883
|
+
*/
|
|
884
|
+
const establish = async () => {
|
|
885
|
+
try {
|
|
886
|
+
return await createPageWithCapture(attemptOptions, deps);
|
|
887
|
+
}
|
|
888
|
+
catch (error) {
|
|
889
|
+
if (!(error instanceof DirectUrlEstablishmentError)) {
|
|
890
|
+
throw error;
|
|
891
|
+
}
|
|
892
|
+
LOG.warn("The direct watch URL for %s did not establish playback. Retrying once through the provider's guide.", metadataComment);
|
|
893
|
+
return await createPageWithCapture({ ...attemptOptions, skipDirectUrl: true }, deps);
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
captureResult = await establish();
|
|
897
|
+
}
|
|
898
|
+
catch (error) {
|
|
899
|
+
// The browser supervisor's acquire() rejects with these while the capture system is recovering: BrowserUnavailableError when the relaunch governor is cooling
|
|
900
|
+
// (degraded), BrowserSupersededError when an in-flight launch was abandoned by a readiness-loss. Both are transient "retry me" conditions, so they map to a 503
|
|
901
|
+
// back-off (Channels DVR honors the Retry-After the route attaches to a 503). We handle them first and WITHOUT an error log: the supervisor raises the loud
|
|
902
|
+
// degraded alarm once on the transition, so the per-request 503s during the cooldown must stay quiet rather than spam an error on every Channels DVR retry.
|
|
903
|
+
// Their messages carry no capture-infrastructure signature, so without this explicit branch the classifier below would make them a 500 the client never backs
|
|
904
|
+
// off from. This must precede isCaptureInfrastructureError.
|
|
905
|
+
if ((error instanceof BrowserUnavailableError) || (error instanceof BrowserSupersededError)) {
|
|
906
|
+
throw new StreamSetupError("Browser temporarily unavailable.", 503, "The capture system is recovering. Please retry shortly.", { cause: error });
|
|
907
|
+
}
|
|
908
|
+
// createPageWithCapture handles its own cleanup on failure (closes page, kills FFmpeg).
|
|
909
|
+
const errorMessage = formatError(error);
|
|
910
|
+
const lowerMessage = errorMessage.toLowerCase();
|
|
911
|
+
const benignPatterns = ["abort", "session closed"];
|
|
912
|
+
const isBenign = benignPatterns.some((pattern) => lowerMessage.includes(pattern));
|
|
913
|
+
if (!isBenign) {
|
|
914
|
+
LOG.error("Stream setup failed for %s: %s.", url, errorMessage);
|
|
915
|
+
}
|
|
916
|
+
// Capture infrastructure errors should return 503 to signal Channels DVR to back off. These include Chrome capture state issues, capture-lock turn-wait
|
|
917
|
+
// timeouts, and stream initialization failures. Using 503 with Retry-After prevents retry storms when there's a systemic issue. isCaptureInfrastructureError
|
|
918
|
+
// (recovery.ts) is the single source of truth for this classification, shared with the browser supervisor's readiness detection.
|
|
919
|
+
const isCaptureError = isCaptureInfrastructureError(errorMessage);
|
|
920
|
+
// A capture-infrastructure failure may mean the browser, though still connected, can no longer capture. Hand it to the passive mid-life detector, which (guarded
|
|
921
|
+
// and single-flight, in the background) re-verifies capture readiness and invalidates the browser for a governed relaunch if confirmed. Fire-and-forget: it must
|
|
922
|
+
// never delay this response.
|
|
923
|
+
if (isCaptureError) {
|
|
924
|
+
noteCaptureInfrastructureFailure(numericStreamId);
|
|
925
|
+
}
|
|
926
|
+
// A failed tune on a service currently marked needs-sign-in most likely failed AT the auth wall, so the user-facing message leads with the remedy.
|
|
927
|
+
throw new StreamSetupError("Stream error.", isCaptureError ? 503 : 500, withSignInGuidance("Failed to start stream.", channelName, serviceName), { cause: error });
|
|
928
|
+
}
|
|
929
|
+
const { captureSession, context, directTune, manifestInterception, page } = captureResult;
|
|
930
|
+
// Hold the page, capture session, and interceptor on a scope guard so a tune-verification failure below disposes them structurally rather than repeating the
|
|
931
|
+
// teardown inline. Push order mirrors the capture-setup resource stack (page, capture session, interception): the interception is registered last so it disposes
|
|
932
|
+
// first, but its disposal is a CDP observer detach with no ordering dependency on either peer; the pair that must stay ordered is capture-session-before-page,
|
|
933
|
+
// so the capture stream is destroyed and STOP_RECORDING fires while the browser is still connected, ahead of the page close. On success we move() the guard and
|
|
934
|
+
// hand ownership to the cleanup closure (and, once the session is installed on the registry entry, to terminateStream).
|
|
935
|
+
const owned = __addDisposableResource(env_2, new DisposableStack(), false);
|
|
936
|
+
owned.adopt(page, disposePage);
|
|
937
|
+
owned.use(captureSession);
|
|
938
|
+
if (manifestInterception) {
|
|
939
|
+
owned.use(manifestInterception);
|
|
940
|
+
}
|
|
941
|
+
// Tune verification. The shared adjudication stage finalizes the manifest interceptor and confirms the captured master manifest URL belongs to the channel
|
|
942
|
+
// that was just tuned. This step makes setupStream "verified by construction" - every consumer of StreamSetupResult (HLS preroll, HLS blocking, MPEG-TS,
|
|
943
|
+
// native proxy, capture mode) receives a stream guaranteed to be on the requested channel without having to opt in or coordinate. Streams with no manifest
|
|
944
|
+
// interception at all (DRM-cached channels, tab replacements) have nothing to adjudicate and skip the step entirely; verifyManifestSelection owns which of
|
|
945
|
+
// the remaining ones it can speak to. A failure reason throws StreamSetupError so the existing failure path marks channel health, terminates the pending
|
|
946
|
+
// registry entry, and surfaces a clear error - never silently delivers the wrong channel. The scope guard disposes the capture session, interceptor, and page
|
|
947
|
+
// as the throw unwinds.
|
|
948
|
+
if (manifestInterception) {
|
|
949
|
+
const verifyError = await adjudicateChannelSelection(manifestInterception, profile, directTune);
|
|
950
|
+
if (verifyError) {
|
|
951
|
+
await minimizeBrowserWindow();
|
|
952
|
+
const failureLabel = channel?.name ?? channelName ?? url;
|
|
953
|
+
throw new StreamSetupError("Tune verification failed: " + verifyError, 502, withSignInGuidance("Tune verification failed for " + failureLabel + ". " + verifyError, channelName, serviceName));
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
// Monitor stream info for status updates. The serviceTag enables service-specific monitoring flags (e.g., tinySegmentThreshold).
|
|
957
|
+
const monitorStreamInfo = {
|
|
958
|
+
channelName: channel?.name ?? null,
|
|
959
|
+
numericStreamId,
|
|
960
|
+
serviceName,
|
|
961
|
+
serviceTag: getDomainConfig(url)?.serviceTag,
|
|
962
|
+
startTime
|
|
963
|
+
};
|
|
964
|
+
// Start the health monitor for this stream.
|
|
965
|
+
const monitor = monitorPlaybackHealth(page, context, profile, url, streamId, monitorStreamInfo, onCircuitBreak, onTabReplacement);
|
|
966
|
+
// Cleanup function. Releases all resources associated with the stream. Safe to call more than once. completeStreamSetup uses it as the fallback teardown
|
|
967
|
+
// when the pending entry is terminated before the capture session is installed on it; once installed, terminateStream disposes the same session (safe to
|
|
968
|
+
// call disposal more than once). Disposes the capture session (kill -> destroy -> stop) before closing the page so STOP_RECORDING fires while the browser is
|
|
969
|
+
// connected.
|
|
970
|
+
let cleanupCompleted = false;
|
|
971
|
+
const cleanup = async () => {
|
|
972
|
+
if (cleanupCompleted) {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
cleanupCompleted = true;
|
|
976
|
+
monitor.dispose();
|
|
977
|
+
captureSession.dispose();
|
|
978
|
+
disposePage(page);
|
|
979
|
+
// Re-minimize the browser window.
|
|
980
|
+
await minimizeBrowserWindow();
|
|
981
|
+
};
|
|
982
|
+
// Success: transfer ownership out of the scope guard. The cleanup closure and, once the session is installed on the registry entry, terminateStream become
|
|
983
|
+
// responsible for disposing the page and capture session; the interceptor continues into tune verification and native streaming.
|
|
984
|
+
owned.move();
|
|
985
|
+
// Return the setup result.
|
|
986
|
+
return {
|
|
987
|
+
captureSession,
|
|
988
|
+
channelName: channel?.name ?? null,
|
|
989
|
+
cleanup,
|
|
990
|
+
directTune,
|
|
991
|
+
manifestInterception,
|
|
992
|
+
monitor,
|
|
993
|
+
numericStreamId,
|
|
994
|
+
page,
|
|
995
|
+
probeIdentity,
|
|
543
996
|
profile,
|
|
544
|
-
|
|
997
|
+
profileName,
|
|
998
|
+
serviceName,
|
|
999
|
+
startTime,
|
|
545
1000
|
streamId,
|
|
546
1001
|
url
|
|
547
|
-
}
|
|
1002
|
+
};
|
|
548
1003
|
}
|
|
549
|
-
catch (
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const lowerMessage = errorMessage.toLowerCase();
|
|
553
|
-
const benignPatterns = ["abort", "session closed"];
|
|
554
|
-
const isBenign = benignPatterns.some((pattern) => lowerMessage.includes(pattern));
|
|
555
|
-
if (!isBenign) {
|
|
556
|
-
LOG.error("Stream setup failed for %s: %s.", url, errorMessage);
|
|
557
|
-
}
|
|
558
|
-
// Capture infrastructure errors should return 503 to signal Channels DVR to back off. These include Chrome capture state issues, queue timeouts, and stream
|
|
559
|
-
// initialization failures. Using 503 with Retry-After prevents retry storms when there's a systemic issue.
|
|
560
|
-
const captureErrorPatterns = ["Cannot capture", "timed out", "Capture queue"];
|
|
561
|
-
const isCaptureError = captureErrorPatterns.some((pattern) => errorMessage.includes(pattern));
|
|
562
|
-
throw new StreamSetupError("Stream error.", isCaptureError ? 503 : 500, "Failed to start stream.", { cause: error });
|
|
1004
|
+
catch (e_2) {
|
|
1005
|
+
env_2.error = e_2;
|
|
1006
|
+
env_2.hasError = true;
|
|
563
1007
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
1008
|
+
finally {
|
|
1009
|
+
__disposeResources(env_2);
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
/* Re-establishes a stream's channel on its page and returns the fresh manifest interception, for the native token-refresh path. It runs the same establishment
|
|
1014
|
+
* composition the tune path runs - navigate under this path's own policy, stamp the observation epoch, initialize playback under the shared bound, then adjudicate
|
|
1015
|
+
* what the interception selected - so there is exactly one establishment sequence in this system. The deliberate divergences from the tune path are each owned here.
|
|
1016
|
+
* Navigation is single-attempt: both callers of the refresh chain bring their own retry ladders (the proactive timer re-arms failures with bounded backoff, and the
|
|
1017
|
+
* monitor escalates a persisting stall to capture fallback), so retrying inside would stack ladders and stretch a recovery the project wants fast. The route is
|
|
1018
|
+
* always the configured channel url with full channel selection: a cached direct URL and a skipped selection belong to the tune's own optimization, and the
|
|
1019
|
+
* re-establishment takes the click-verified route instead, so the kind it finalizes against carries no cached-direct-URL term. And a category selector's
|
|
1020
|
+
* re-resolved call sign is not persisted here: persistence belongs to the tune lifecycle, and the recovery paths already re-tune without it. Playback
|
|
1021
|
+
* initialization is bounded by the same race the tune uses, with the same abandon-on-timeout semantics; the remnant is bounded and act-limited - its phases expire
|
|
1022
|
+
* on their own internal timeouts, its consent poll can only reject cookie banners and dismiss per-site modals within its fixed window, and a later navigation on
|
|
1023
|
+
* the page force-settles whatever remains - so a subsequent refresh attempt starting on this page meets at worst a dying consent poll, never a competing channel
|
|
1024
|
+
* click. Failures normalize to null with a warning rather than throwing - the caller is a background refresh cycle whose ladder already owns the endgame. The body
|
|
1025
|
+
* runs under the stream's log context so the composed primitives' own lines carry the stream prefix: the ambient context is kept when a caller already established
|
|
1026
|
+
* one (the monitor's recovery path carries a richer context, show-name resolution included, that a nested run would replace rather than merge), and is supplied
|
|
1027
|
+
* only on the proactive timer's path, which has none. The page's audio is re-muted on the initialization's own settlement rather than at any fixed step: playback
|
|
1028
|
+
* establishment unmutes by direct property write, so an already-playing element stays audible until re-muted, and attaching the re-mute to the initialization
|
|
1029
|
+
* promise covers every outcome - success, failure, and a timed-out attempt whose establishment completes late - while the play-override the native upgrade
|
|
1030
|
+
* registered keeps future play() calls muted without re-registration.
|
|
1031
|
+
*
|
|
1032
|
+
* @param options - The stream facts the re-establishment closes over. See ReestablishChannelManifestOptions.
|
|
1033
|
+
* @returns The verified manifest interception, or null when the channel could not be re-established.
|
|
1034
|
+
*/
|
|
1035
|
+
export async function reestablishChannelManifest(options) {
|
|
1036
|
+
const { channelName, page, profile, streamIdStr, url } = options;
|
|
1037
|
+
const establish = async () => {
|
|
1038
|
+
try {
|
|
1039
|
+
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
1040
|
+
try {
|
|
1041
|
+
// The navigation allowance handed to the budget is a single attempt's timeout, since this path navigates once by design; establishmentBudgetMs owns why
|
|
1042
|
+
// the observer has to outlive that allowance and every step after it.
|
|
1043
|
+
const budgetMs = establishmentBudgetMs(CONFIG.streaming.navigationTimeout);
|
|
1044
|
+
// Scope-bind the interceptor with "using" so its CDP observer is disposed on every exit from this function, including the early returns below.
|
|
1045
|
+
const handle = __addDisposableResource(env_3, await installManifestInterceptor(page, budgetMs), false);
|
|
1046
|
+
if (!handle) {
|
|
1047
|
+
LOG.warn("The channel for %s could not be re-established: the manifest interceptor did not install.", channelName ?? url);
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
// Establish the channel through the shared composition: navigate, stamp the observation epoch - which fences anything the reloaded page auto-played so it
|
|
1051
|
+
// cannot win adjudication over the channel the selection lands on - then run playback initialization under the same bound the tune path uses.
|
|
1052
|
+
const tuneResult = await establishChannelPlayback(page, profile, handle, {
|
|
1053
|
+
initOptions: { requestedUrl: url },
|
|
1054
|
+
// Navigate through the profile's own wait strategy, single-attempt by design. A navigation failure throws to the catch below and normalizes to null.
|
|
1055
|
+
navigate: async () => {
|
|
1056
|
+
await navigateToPage(page, url, profile);
|
|
1057
|
+
},
|
|
1058
|
+
// Restore the page's mute when the initialization settles. Playback establishment unmutes by direct property write, so an element that is already
|
|
1059
|
+
// playing stays audible until it is re-muted, and attaching the restore to the settlement is what covers every outcome rather than only the happy one.
|
|
1060
|
+
onInitSettled: () => {
|
|
1061
|
+
void muteExistingVideos(page);
|
|
598
1062
|
}
|
|
1063
|
+
});
|
|
1064
|
+
// Adjudicate honestly: the kind formula gets no cached-direct-URL term here, because the route above never takes one.
|
|
1065
|
+
const verifyError = await adjudicateChannelSelection(handle, profile, computeDirectTuneKind({ profile, strategyDirectTune: tuneResult.directTune ?? false }));
|
|
1066
|
+
// On a verifier-bearing master path the adjudication has already awaited this promise; on the vacuous path this await is the one that waits out the
|
|
1067
|
+
// finalize settle. Reading it before the verification result is contract rather than preference: an establishment that intercepted nothing has to report
|
|
1068
|
+
// exactly that, and the verification gates raise no objection for a null interception anyway, so the check order is what keeps each warning accurate to
|
|
1069
|
+
// its own outcome.
|
|
1070
|
+
const interception = await handle.promise;
|
|
1071
|
+
if (!interception) {
|
|
1072
|
+
LOG.warn("The channel for %s could not be re-established: no manifest was intercepted.", channelName ?? url);
|
|
1073
|
+
return null;
|
|
599
1074
|
}
|
|
1075
|
+
if (verifyError) {
|
|
1076
|
+
LOG.warn("The re-established channel for %s did not verify: %s", channelName ?? url, verifyError);
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
return interception;
|
|
600
1080
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
channelName: channel?.name ?? null,
|
|
605
|
-
numericStreamId,
|
|
606
|
-
serviceName,
|
|
607
|
-
serviceTag: getDomainConfig(url)?.serviceTag,
|
|
608
|
-
startTime
|
|
609
|
-
};
|
|
610
|
-
// Start the health monitor for this stream.
|
|
611
|
-
const stopMonitor = monitorPlaybackHealth(page, context, profile, url, streamId, monitorStreamInfo, onCircuitBreak, onTabReplacement);
|
|
612
|
-
// Cleanup function. Releases all resources associated with the stream. Idempotent - safe to call multiple times.
|
|
613
|
-
let cleanupCompleted = false;
|
|
614
|
-
const cleanup = async () => {
|
|
615
|
-
if (cleanupCompleted) {
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
cleanupCompleted = true;
|
|
619
|
-
// Stop the health monitor first.
|
|
620
|
-
stopMonitor();
|
|
621
|
-
// Destroy the raw capture stream BEFORE closing the page. This triggers puppeteer-stream's close handler while the browser is still connected, ensuring
|
|
622
|
-
// STOP_RECORDING is called and chrome.tabCapture releases the capture. Without this, subsequent getStream() calls may hang with "active stream" errors.
|
|
623
|
-
if (!rawCaptureStream.destroyed) {
|
|
624
|
-
rawCaptureStream.destroy();
|
|
625
|
-
}
|
|
626
|
-
// Kill the FFmpeg process if using Matroska+FFmpeg mode.
|
|
627
|
-
if (ffmpegProcess) {
|
|
628
|
-
ffmpegProcess.kill();
|
|
1081
|
+
catch (e_3) {
|
|
1082
|
+
env_3.error = e_3;
|
|
1083
|
+
env_3.hasError = true;
|
|
629
1084
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
// Close the browser page (fire-and-forget to avoid blocking on stuck pages).
|
|
633
|
-
if (!page.isClosed()) {
|
|
634
|
-
page.close().catch((error) => {
|
|
635
|
-
LOG.debug("streaming:setup", "Page close error during cleanup: %s.", formatError(error));
|
|
636
|
-
});
|
|
1085
|
+
finally {
|
|
1086
|
+
__disposeResources(env_3);
|
|
637
1087
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
page,
|
|
651
|
-
profile,
|
|
652
|
-
profileName,
|
|
653
|
-
rawCaptureStream,
|
|
654
|
-
serviceName,
|
|
655
|
-
startTime,
|
|
656
|
-
stopMonitor,
|
|
657
|
-
streamId,
|
|
658
|
-
url
|
|
659
|
-
};
|
|
660
|
-
});
|
|
1088
|
+
}
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
LOG.warn("The channel for %s could not be re-established: %s.", channelName ?? url, formatError(error));
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
// Keep a caller's own context rather than nesting a thinner one inside it - the monitor's recovery context carries show-name resolution this frame cannot
|
|
1095
|
+
// rebuild - and establish one only where none exists, which is the proactive refresh timer's bare callback.
|
|
1096
|
+
if (getStreamContext()) {
|
|
1097
|
+
return establish();
|
|
1098
|
+
}
|
|
1099
|
+
return runWithStreamContext({ channelName: channelName ?? undefined, streamId: streamIdStr, url }, establish);
|
|
661
1100
|
}
|
|
662
|
-
//
|
|
1101
|
+
// Capture Readiness Verification.
|
|
663
1102
|
/**
|
|
664
|
-
* Verifies that Chrome
|
|
665
|
-
*
|
|
666
|
-
*
|
|
1103
|
+
* Verifies that a freshly-launched Chrome instance can actually capture, by running a real getStream against a throwaway page on it. This is the capability tier of
|
|
1104
|
+
* the browser launch gate: browser/index.ts injects it via setCaptureProbe and runs it inside launchReadyBrowser, so a browser is published as ready only after its
|
|
1105
|
+
* capture capability is verified - at boot AND at every relaunch, not just startup. It exercises the exact getStream path that hangs when the puppeteer-stream
|
|
1106
|
+
* extension is unregistered, so a dead extension is detected immediately rather than causing every subsequent stream request to hang.
|
|
667
1107
|
*
|
|
668
|
-
*
|
|
669
|
-
* puppeteer-stream's fire-and-forget STOP_RECORDING chain to complete before closing the page, preventing
|
|
1108
|
+
* Each attempt creates a temporary page, attempts a short capture, and tears down both cleanly. A 500ms delay after destroying the capture stream allows
|
|
1109
|
+
* puppeteer-stream's fire-and-forget STOP_RECORDING chain to complete before closing the page, preventing a stale capture cascade on the first real request.
|
|
670
1110
|
*
|
|
671
|
-
* After a system reboot, Chrome's display stack or capture extension may not be ready
|
|
672
|
-
*
|
|
673
|
-
*
|
|
1111
|
+
* After a system reboot or a fresh relaunch, Chrome's display stack or capture extension may not be ready immediately. The probe retries up to PROBE_MAX_ATTEMPTS
|
|
1112
|
+
* times with a delay between attempts, giving the system time to settle before giving up. At boot this prevents a rapid restart storm where the service manager
|
|
1113
|
+
* relaunches PrismCast repeatedly, each attempt orphaning a Chrome process; at relaunch it provides in-launch settling before the supervisor counts a launch failure.
|
|
674
1114
|
*
|
|
675
|
-
* If stale capture state is detected, the process exits immediately - Chrome restart cannot fix the leaked mutex, only a fresh process can.
|
|
1115
|
+
* If stale capture state is detected, the process exits immediately - a Chrome restart cannot fix the leaked module-level mutex, only a fresh process can.
|
|
1116
|
+
* @param browser - The Chrome instance to verify (the local instance being launched, passed in rather than re-acquired to avoid re-entering the launch in flight).
|
|
676
1117
|
*/
|
|
677
|
-
export async function verifyCaptureSystem() {
|
|
1118
|
+
export async function verifyCaptureSystem(browser) {
|
|
678
1119
|
const PROBE_MAX_ATTEMPTS = 3;
|
|
679
1120
|
const PROBE_RETRY_DELAY = 5000;
|
|
680
|
-
const PROBE_TIMEOUT = 5000;
|
|
681
1121
|
for (let attempt = 1; attempt <= PROBE_MAX_ATTEMPTS; attempt++) {
|
|
1122
|
+
// The launch gate runs its capture probe OFF the capture lock, deliberately: it fires pre-publish, when supervisor.current() is null, so a wedged gate task would
|
|
1123
|
+
// have no recovery target and would jam the shared lock. GATE mode keeps the internal getStream bound, and this path keeps its own poison exit below.
|
|
682
1124
|
// eslint-disable-next-line no-await-in-loop -- Sequential retries are intentional; each probe must complete before deciding whether to retry.
|
|
683
|
-
const result = await attemptCaptureProbe(
|
|
1125
|
+
const result = await attemptCaptureProbe(browser, { boundMs: CAPTURE_PROBE_TIMEOUT_MS, kind: "gate" });
|
|
684
1126
|
// Probe succeeded.
|
|
685
1127
|
if (result === null) {
|
|
686
1128
|
return;
|
|
687
1129
|
}
|
|
688
|
-
// Stale capture state is unrecoverable
|
|
689
|
-
//
|
|
690
|
-
//
|
|
691
|
-
if (result
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
process.exit(1);
|
|
1130
|
+
// Stale capture state is unrecoverable: puppeteer-stream's second lock section has no try/finally, so the mutex is permanently leaked and every subsequent
|
|
1131
|
+
// getStream() hangs. Chrome restart cannot fix module-level state, so escalate from the single home (which logs and schedules the deferred exit), then throw so no
|
|
1132
|
+
// further probe attempts run before the process exits.
|
|
1133
|
+
if (isStaleCaptureMutexError(result)) {
|
|
1134
|
+
escalateStaleCaptureMutex("the launch-gate capture probe");
|
|
1135
|
+
throw new Error("Capture system verification failed: stale capture state detected.");
|
|
695
1136
|
}
|
|
696
1137
|
// If we have retries remaining, log a warning and wait before the next attempt.
|
|
697
1138
|
if (attempt < PROBE_MAX_ATTEMPTS) {
|
|
@@ -705,17 +1146,29 @@ export async function verifyCaptureSystem() {
|
|
|
705
1146
|
}
|
|
706
1147
|
}
|
|
707
1148
|
/**
|
|
708
|
-
* Executes a single capture probe attempt. Creates a temporary page, tries to start a capture stream, and tears everything down cleanly.
|
|
709
|
-
*
|
|
1149
|
+
* Executes a single capture probe attempt. Creates a temporary page on the given browser, tries to start a capture stream, and tears everything down cleanly. It
|
|
1150
|
+
* NEVER throws in either mode: it returns null on success or an error-message string on failure, so callers branch on the string. The two modes differ only in how
|
|
1151
|
+
* getStream is bounded - see CaptureProbeMode.
|
|
1152
|
+
* @param browser - The Chrome instance to probe.
|
|
1153
|
+
* @param mode - The operating mode: gate (internal getStream race) or midlife (self-timed getStream on the lock).
|
|
1154
|
+
* @param clock - Clock used for the mid-life self-timing and the teardown settle. Defaults to realClock.
|
|
710
1155
|
* @returns Null on success, or an error message string on failure.
|
|
711
1156
|
*/
|
|
712
|
-
async function attemptCaptureProbe(
|
|
713
|
-
const browser = await getCurrentBrowser();
|
|
1157
|
+
async function attemptCaptureProbe(browser, mode, clock = realClock) {
|
|
714
1158
|
const page = await browser.newPage();
|
|
715
1159
|
registerManagedPage(page);
|
|
1160
|
+
// Tears the probe page down cleanly: retire the raw capture stream (destroy plus the STOP_RECORDING settle) while the browser is still connected, unregister the
|
|
1161
|
+
// managed page, then close it. Shared by every success and self-timed-failure path in both modes.
|
|
1162
|
+
const teardown = async (stream) => {
|
|
1163
|
+
await retireRawStream(stream, clock);
|
|
1164
|
+
unregisterManagedPage(page);
|
|
1165
|
+
if (!page.isClosed()) {
|
|
1166
|
+
await page.close();
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
716
1169
|
try {
|
|
717
|
-
// Use the same capture MIME type and viewport
|
|
718
|
-
//
|
|
1170
|
+
// Use the same capture MIME type and viewport (height/width) as the runtime. The stale state error occurs at the tabCapture API level before encoding matters,
|
|
1171
|
+
// so matching those runtime constraints ensures the probe exercises a representative getStream() call.
|
|
719
1172
|
const useFFmpeg = CONFIG.streaming.captureMode === "ffmpeg";
|
|
720
1173
|
const captureMimeType = useFFmpeg ? getCaptureMimeType() : NATIVE_FMP4_MIME_TYPE;
|
|
721
1174
|
const streamOptions = {
|
|
@@ -733,17 +1186,41 @@ async function attemptCaptureProbe(timeout) {
|
|
|
733
1186
|
}
|
|
734
1187
|
}
|
|
735
1188
|
};
|
|
736
|
-
|
|
737
|
-
//
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1189
|
+
// GATE mode: bound getStream with an internal timeout. On a lapse the getStream promise is still pending, so attach a both-callback handler that retires a
|
|
1190
|
+
// late-arriving stream (best-effort; the page may already be closing) and consumes a late rejection. The bounded wait already observes the promise's rejection,
|
|
1191
|
+
// so a fulfillment-only handler would create unhandled-rejection noise. This cleans up the orphan without serializing successive gate attempts against one another.
|
|
1192
|
+
if (mode.kind === "gate") {
|
|
1193
|
+
const streamPromise = getStream(page, streamOptions);
|
|
1194
|
+
const timeoutError = new Error(CAPTURE_PROBE_TIMEOUT_MESSAGE);
|
|
1195
|
+
let stream;
|
|
1196
|
+
try {
|
|
1197
|
+
stream = await waitWithTimeout(streamPromise, mode.boundMs, timeoutError);
|
|
1198
|
+
}
|
|
1199
|
+
catch (error) {
|
|
1200
|
+
// Only the internal timeout leaves getStream pending; an in-time rejection produced no stream to clean up and is already observed by the bounded wait.
|
|
1201
|
+
if (error === timeoutError) {
|
|
1202
|
+
void streamPromise.then((late) => {
|
|
1203
|
+
void retireRawStream(late, clock);
|
|
1204
|
+
}, (reason) => {
|
|
1205
|
+
LOG.debug("streaming:setup", "A late gate capture stream rejected after the probe timeout: %s.", formatError(reason));
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
throw error;
|
|
1209
|
+
}
|
|
1210
|
+
await teardown(stream);
|
|
1211
|
+
LOG.info("Capture system verified successfully.");
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
// MID-LIFE mode: await getStream raw and self-time it. The turn (owned by the lock) spans the whole task, but the pass/fail CRITERION is getStream's own latency
|
|
1215
|
+
// against boundMs, measured without racing or abandoning anything, so the teardown settle that follows never counts against it.
|
|
1216
|
+
const startedAt = clock.now();
|
|
1217
|
+
const stream = (await getStream(page, streamOptions));
|
|
1218
|
+
const elapsed = clock.now() - startedAt;
|
|
1219
|
+
await teardown(stream);
|
|
1220
|
+
// Report failure when the caller abandoned this probe at the lock's outer deadline (unobservable in practice - the lock already rejected the caller - but it keeps
|
|
1221
|
+
// the never-throw contract and prevents stranding a capture), or when getStream's own latency exceeded the criterion bound.
|
|
1222
|
+
if (mode.signal.aborted || (elapsed > mode.boundMs)) {
|
|
1223
|
+
return CAPTURE_PROBE_TIMEOUT_MESSAGE;
|
|
747
1224
|
}
|
|
748
1225
|
LOG.info("Capture system verified successfully.");
|
|
749
1226
|
return null;
|
|
@@ -758,4 +1235,103 @@ async function attemptCaptureProbe(timeout) {
|
|
|
758
1235
|
return errorMessage;
|
|
759
1236
|
}
|
|
760
1237
|
}
|
|
1238
|
+
// Passive Mid-Life Capture-Death Detection.
|
|
1239
|
+
/* A browser can be capture-ready at launch and lose its capture capability later - the extension wedges, tabCapture stalls - without ever firing a "disconnected"
|
|
1240
|
+
* event, so neither the launch gate nor the disconnect handler would catch it. This detector rides a signal that is already happening: a stream-setup failure
|
|
1241
|
+
* carrying a capture-infrastructure signature. It is deliberately conservative. The guard (the failing stream is the only active stream) preserves per-stream
|
|
1242
|
+
* isolation - if any other stream is active, the browser is either demonstrably capturing or those streams will trip their own circuit breakers and drain. The probe
|
|
1243
|
+
* is the authoritative arbiter, serialized through the capture lock so it can never race a real stream's getStream init, and it runs in the background, single-flight,
|
|
1244
|
+
* so it never delays a response or stacks up. On a confirmed failure, the one recovery action runs: invalidate the browser for a governed relaunch.
|
|
1245
|
+
*/
|
|
1246
|
+
// At most one mid-life re-verification runs at a time across the process, so a burst of capture-infrastructure failures triggers a single probe, not a storm.
|
|
1247
|
+
let captureReverificationInProgress = false;
|
|
1248
|
+
/**
|
|
1249
|
+
* Runs one capture probe as a task on the capture lock, so it cannot race a concurrent getStream initialization (which would draw a spurious "Cannot capture a tab
|
|
1250
|
+
* with an active stream"). The lock holds the turn across the probe's full task - getStream plus the STOP_RECORDING settle inside attemptCaptureProbe - so the next
|
|
1251
|
+
* initialization does not start until the probe's capture is fully released. The mid-life probe self-times its getStream against `timeout`; the lock's outer deadline
|
|
1252
|
+
* adds a teardown allowance over it as a safety net.
|
|
1253
|
+
* @param browser - The Chrome instance to probe.
|
|
1254
|
+
* @param timeout - Maximum time in milliseconds to wait for the probe's getStream() to respond (the self-timed criterion).
|
|
1255
|
+
* @returns Null when the browser captured successfully, or an error message when it could not (including a wedged-lock timeout).
|
|
1256
|
+
*/
|
|
1257
|
+
async function probeCaptureSerialized(browser, timeout) {
|
|
1258
|
+
try {
|
|
1259
|
+
return await captureLock.run((signal) => attemptCaptureProbe(browser, { boundMs: timeout, kind: "midlife", signal }), {
|
|
1260
|
+
deadlineMessage: CAPTURE_PROBE_TIMEOUT_MESSAGE,
|
|
1261
|
+
deadlineMs: timeout + PROBE_TEARDOWN_ALLOWANCE_MS,
|
|
1262
|
+
// The probe's wedge is a loud warning only, never an invalidate: by wedge time the detector has already invalidated this browser via the returned failure
|
|
1263
|
+
// string (the outer deadline fires roughly 22s earlier), so a wedge here means the kill-immune leaked-mutex hang, where a second invalidate is a guaranteed
|
|
1264
|
+
// identity-guard no-op.
|
|
1265
|
+
onWedge: () => {
|
|
1266
|
+
LOG.warn("A mid-life capture probe has wedged past the recovery bound; the capture mutex is likely leaked and awaiting a process restart.");
|
|
1267
|
+
},
|
|
1268
|
+
turnWaitMs: CONFIG.streaming.navigationTimeout
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
catch (error) {
|
|
1272
|
+
// The only throw here is a lock failure - a turn-wait timeout or the outer deadline; attemptCaptureProbe returns its own failures as strings. A jammed capture
|
|
1273
|
+
// lock is itself evidence the browser cannot capture, so surface it as a probe failure the detector routes into invalidateBrowser.
|
|
1274
|
+
return formatError(error);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Pure decision for whether a mid-life capture-infrastructure failure warrants re-verifying the browser. Re-verification is warranted iff no other re-verification
|
|
1279
|
+
* is already in flight, a browser is published to probe, and the failing stream is the only active stream - any other active stream means either the browser is
|
|
1280
|
+
* demonstrably capturing (so this failure is stream-specific - never invalidate) or those streams will trip their own circuit breakers and drain, after which a
|
|
1281
|
+
* later failure reaches the zero-other case. An empty registry (setup failed before the stream was registered) satisfies the isolation check vacuously and is the
|
|
1282
|
+
* common real-world shape of this failure.
|
|
1283
|
+
* @param inputs - The decision inputs.
|
|
1284
|
+
* @param inputs.activeStreamIds - The ids of every stream currently in the registry.
|
|
1285
|
+
* @param inputs.failingStreamId - The id of the stream whose setup just failed, excluded from the isolation check.
|
|
1286
|
+
* @param inputs.hasBrowser - Whether a browser instance is currently published to probe.
|
|
1287
|
+
* @param inputs.reverificationInProgress - Whether a re-verification is already deciding the browser's fate.
|
|
1288
|
+
* @returns True when the failure should trigger a re-verification probe.
|
|
1289
|
+
*/
|
|
1290
|
+
export function shouldReverifyCapture(inputs) {
|
|
1291
|
+
return !inputs.reverificationInProgress && inputs.hasBrowser && inputs.activeStreamIds.every((id) => id === inputs.failingStreamId);
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Passive mid-life capture-death detection, called from the stream-setup failure path when the failure carries a capture-infrastructure signature. If the failing
|
|
1295
|
+
* stream is the only active stream, it re-verifies the browser's capture capability with a lock-serialized probe in the background and, on confirmed failure,
|
|
1296
|
+
* invalidates the browser for a governed relaunch - catching a browser that is still connected (no "disconnected" event) but can no longer capture. Fire-and-forget
|
|
1297
|
+
* so the failing request's response is not delayed; single-flight so a burst of failures triggers at most one probe.
|
|
1298
|
+
* @param failingStreamId - The numeric id of the stream whose setup just failed, excluded from the active-stream guard.
|
|
1299
|
+
*/
|
|
1300
|
+
function noteCaptureInfrastructureFailure(failingStreamId) {
|
|
1301
|
+
// Single-flight: a re-verification is already deciding the browser's fate; do not stack another. Read first so a re-verify already in flight short-circuits
|
|
1302
|
+
// before the registry and browser lookups below are even performed.
|
|
1303
|
+
if (captureReverificationInProgress) {
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
// A readiness probe needs a connected browser to exercise. If none is published, a disconnect already handled the readiness loss. The !browser check is
|
|
1307
|
+
// combined into this one condition (rather than a separate guard) so TypeScript narrows browser from Nullable<Browser> to Browser for the probe and
|
|
1308
|
+
// invalidateBrowser calls below. hasBrowser and reverificationInProgress are passed as the values their guards already guarantee here - true (the !browser
|
|
1309
|
+
// guard has passed) and false (the single-flight guard above already returned if it were true).
|
|
1310
|
+
const browser = getBrowserInstance();
|
|
1311
|
+
if (!browser || !shouldReverifyCapture({ activeStreamIds: getAllStreams().map((entry) => entry.id), failingStreamId, hasBrowser: true,
|
|
1312
|
+
reverificationInProgress: false })) {
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
captureReverificationInProgress = true;
|
|
1316
|
+
// Run in the background so the failing request's 503 response is not delayed by the probe, which can take up to the probe timeout.
|
|
1317
|
+
void (async () => {
|
|
1318
|
+
try {
|
|
1319
|
+
const failure = await probeCaptureSerialized(browser, CAPTURE_PROBE_TIMEOUT_MS);
|
|
1320
|
+
if (failure !== null) {
|
|
1321
|
+
// The probe confirmed the browser cannot capture though it is still connected. invalidateBrowser is the single recovery action - relinquish readiness,
|
|
1322
|
+
// terminate the now-doomed streams, and close Chrome - so the next request relaunches a fresh, gate-verified browser. We pass the exact instance we probed:
|
|
1323
|
+
// invalidateBrowser no-ops if it was already superseded by a disconnect-and-relaunch during the probe, so we never tear down a healthy replacement. A
|
|
1324
|
+
// genuinely leaked module mutex, if that was the cause, surfaces again at the relaunch's gate probe and exits there; a merely-slow getStream that finally
|
|
1325
|
+
// settles instead lets the relaunch recover.
|
|
1326
|
+
await invalidateBrowser(browser, "a capture probe failed after a stream setup failure with no other active streams");
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
catch (error) {
|
|
1330
|
+
LOG.debug("streaming:setup", "Mid-life capture re-verification aborted: %s.", formatError(error));
|
|
1331
|
+
}
|
|
1332
|
+
finally {
|
|
1333
|
+
captureReverificationInProgress = false;
|
|
1334
|
+
}
|
|
1335
|
+
})();
|
|
1336
|
+
}
|
|
761
1337
|
//# sourceMappingURL=setup.js.map
|