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