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