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