prismcast 1.12.0 → 1.13.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 +41 -1
- package/dist/app.js +164 -76
- package/dist/app.js.map +1 -1
- package/dist/browser/blockedPage.d.ts +0 -1
- package/dist/browser/browserSupervisor.d.ts +34 -5
- package/dist/browser/browserSupervisor.js +39 -12
- package/dist/browser/browserSupervisor.js.map +1 -1
- package/dist/browser/cdp.d.ts +58 -6
- package/dist/browser/cdp.js +147 -9
- package/dist/browser/cdp.js.map +1 -1
- package/dist/browser/channelSelection.d.ts +18 -6
- package/dist/browser/channelSelection.js +47 -36
- package/dist/browser/channelSelection.js.map +1 -1
- package/dist/browser/consent.d.ts +33 -4
- package/dist/browser/consent.js +87 -50
- package/dist/browser/consent.js.map +1 -1
- package/dist/browser/display.d.ts +0 -1
- package/dist/browser/hlsPlaylistObserver.d.ts +3 -2
- package/dist/browser/hlsPlaylistObserver.js +11 -6
- package/dist/browser/hlsPlaylistObserver.js.map +1 -1
- package/dist/browser/index.d.ts +273 -59
- package/dist/browser/index.js +768 -255
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/launchGovernor.d.ts +0 -1
- package/dist/browser/login.d.ts +11 -10
- package/dist/browser/login.js +42 -33
- package/dist/browser/login.js.map +1 -1
- package/dist/browser/manifestInterceptor.d.ts +14 -5
- package/dist/browser/manifestInterceptor.js +26 -13
- package/dist/browser/manifestInterceptor.js.map +1 -1
- package/dist/browser/navigation.d.ts +28 -0
- package/dist/browser/navigation.js +57 -0
- package/dist/browser/navigation.js.map +1 -0
- package/dist/browser/pageStaleness.d.ts +1 -2
- package/dist/browser/pageStaleness.js +9 -9
- package/dist/browser/pageStaleness.js.map +1 -1
- package/dist/browser/precaching.d.ts +27 -13
- package/dist/browser/precaching.js +219 -73
- package/dist/browser/precaching.js.map +1 -1
- package/dist/browser/tabCapture.d.ts +97 -0
- package/dist/browser/tabCapture.js +314 -0
- package/dist/browser/tabCapture.js.map +1 -0
- package/dist/browser/tabNetworkObserver.d.ts +0 -1
- package/dist/browser/tabSelection.d.ts +117 -0
- package/dist/browser/tabSelection.js +547 -0
- package/dist/browser/tabSelection.js.map +1 -0
- package/dist/browser/tuning/cache.d.ts +0 -1
- package/dist/browser/tuning/comcastPolymer.d.ts +0 -1
- package/dist/browser/tuning/comcastPolymer.js +18 -19
- package/dist/browser/tuning/comcastPolymer.js.map +1 -1
- package/dist/browser/tuning/cox.d.ts +0 -1
- package/dist/browser/tuning/directv.d.ts +0 -1
- package/dist/browser/tuning/directv.js +12 -13
- package/dist/browser/tuning/directv.js.map +1 -1
- package/dist/browser/tuning/fox.d.ts +0 -1
- package/dist/browser/tuning/fox.js +2 -2
- package/dist/browser/tuning/fox.js.map +1 -1
- package/dist/browser/tuning/gridSearch.d.ts +0 -1
- package/dist/browser/tuning/hbo.d.ts +0 -1
- package/dist/browser/tuning/hbo.js +2 -2
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.d.ts +14 -2
- package/dist/browser/tuning/hulu.js +150 -24
- package/dist/browser/tuning/hulu.js.map +1 -1
- package/dist/browser/tuning/shared.d.ts +1 -2
- package/dist/browser/tuning/shared.js +1 -1
- package/dist/browser/tuning/sling.d.ts +0 -1
- package/dist/browser/tuning/sling.js +17 -22
- package/dist/browser/tuning/sling.js.map +1 -1
- package/dist/browser/tuning/spectrum.d.ts +0 -1
- package/dist/browser/tuning/thumbnailRow.d.ts +0 -1
- package/dist/browser/tuning/tileClick.d.ts +0 -1
- package/dist/browser/tuning/xfinity.d.ts +0 -1
- package/dist/browser/tuning/youtubeTv.d.ts +0 -1
- package/dist/browser/tuning/youtubeTv.js +1 -2
- package/dist/browser/tuning/youtubeTv.js.map +1 -1
- package/dist/browser/video.d.ts +55 -33
- package/dist/browser/video.js +213 -169
- package/dist/browser/video.js.map +1 -1
- package/dist/browser/windowSync.d.ts +45 -0
- package/dist/browser/windowSync.js +153 -0
- package/dist/browser/windowSync.js.map +1 -0
- package/dist/channels/index.d.ts +7 -1
- package/dist/channels/index.js +24 -8
- package/dist/channels/index.js.map +1 -1
- package/dist/config/channelForm.d.ts +0 -1
- package/dist/config/channelIdentity.d.ts +27 -0
- package/dist/config/channelIdentity.js +72 -0
- package/dist/config/channelIdentity.js.map +1 -0
- package/dist/config/channelSort.d.ts +25 -0
- package/dist/config/channelSort.js +101 -0
- package/dist/config/channelSort.js.map +1 -0
- package/dist/config/consistencyProbe.d.ts +0 -1
- package/dist/config/consistencyProbe.js +1 -1
- package/dist/config/consistencyProbe.js.map +1 -1
- package/dist/config/health.d.ts +3 -2
- package/dist/config/health.js +41 -31
- package/dist/config/health.js.map +1 -1
- package/dist/config/index.d.ts +12 -1
- package/dist/config/index.js +77 -31
- package/dist/config/index.js.map +1 -1
- package/dist/config/paths.d.ts +17 -2
- package/dist/config/paths.js +21 -1
- package/dist/config/paths.js.map +1 -1
- package/dist/config/persistence.context.d.ts +0 -1
- package/dist/config/persistence.d.ts +8 -4
- package/dist/config/persistence.js +143 -82
- package/dist/config/persistence.js.map +1 -1
- package/dist/config/presets.d.ts +0 -1
- package/dist/config/profiles.d.ts +9 -4
- package/dist/config/profiles.js +39 -6
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/providerLineups.d.ts +0 -1
- package/dist/config/providerLineups.js +0 -3
- package/dist/config/providerLineups.js.map +1 -1
- package/dist/config/reactivity.d.ts +0 -1
- package/dist/config/servicePacks.d.ts +0 -1
- package/dist/config/services.d.ts +1 -47
- package/dist/config/services.js +4 -135
- package/dist/config/services.js.map +1 -1
- package/dist/config/sites.d.ts +0 -9
- package/dist/config/sites.js +82 -98
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +22 -29
- package/dist/config/userChannels.js +47 -65
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +19 -4
- package/dist/config/userConfig.js +123 -25
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +2 -3
- package/dist/config/userProfiles.js +16 -13
- package/dist/config/userProfiles.js.map +1 -1
- package/dist/hdhr/channelMap.d.ts +0 -1
- package/dist/hdhr/deviceId.d.ts +0 -1
- package/dist/hdhr/discover.d.ts +0 -1
- package/dist/hdhr/getHandlers.d.ts +0 -1
- package/dist/hdhr/identity.d.ts +0 -1
- package/dist/hdhr/index.d.ts +0 -1
- package/dist/hdhr/protocol.d.ts +1 -2
- package/dist/hdhr/protocol.js +1 -1
- package/dist/hdhr/tunerState.d.ts +0 -1
- package/dist/hdhr/udp.d.ts +0 -1
- package/dist/hdhr/udp.js +16 -4
- package/dist/hdhr/udp.js.map +1 -1
- package/dist/identity.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/native/codecInference.d.ts +5 -4
- package/dist/native/codecInference.js +12 -7
- package/dist/native/codecInference.js.map +1 -1
- package/dist/native/decrypt.d.ts +10 -3
- package/dist/native/decrypt.js +9 -5
- package/dist/native/decrypt.js.map +1 -1
- package/dist/native/index.d.ts +26 -24
- package/dist/native/index.js +26 -20
- package/dist/native/index.js.map +1 -1
- package/dist/native/probe.d.ts +7 -10
- package/dist/native/probe.js +41 -37
- package/dist/native/probe.js.map +1 -1
- package/dist/native/proxy.d.ts +3 -4
- package/dist/native/proxy.js +91 -69
- package/dist/native/proxy.js.map +1 -1
- package/dist/native/tokenExpiry.d.ts +0 -1
- package/dist/routes/assets.d.ts +0 -1
- package/dist/routes/auth.d.ts +0 -1
- package/dist/routes/auth.js +1 -1
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/cdp.d.ts +11 -5
- package/dist/routes/cdp.js +91 -63
- package/dist/routes/cdp.js.map +1 -1
- package/dist/routes/channels.d.ts +0 -1
- package/dist/routes/clientActions.d.ts +0 -1
- package/dist/routes/components.d.ts +0 -1
- package/dist/routes/config/channels/endpoints/browse.d.ts +0 -1
- package/dist/routes/config/channels/endpoints/bulk.d.ts +0 -1
- package/dist/routes/config/channels/endpoints/bulk.js +3 -3
- package/dist/routes/config/channels/endpoints/bulk.js.map +1 -1
- package/dist/routes/config/channels/endpoints/crud.d.ts +0 -1
- package/dist/routes/config/channels/endpoints/crud.js +4 -3
- package/dist/routes/config/channels/endpoints/crud.js.map +1 -1
- package/dist/routes/config/channels/endpoints/importExport.d.ts +0 -1
- package/dist/routes/config/channels/endpoints/importExport.js +7 -3
- package/dist/routes/config/channels/endpoints/importExport.js.map +1 -1
- package/dist/routes/config/channels/endpoints/predefined.d.ts +0 -1
- package/dist/routes/config/channels/endpoints/prefs.d.ts +0 -1
- package/dist/routes/config/channels/endpoints/prefs.js +1 -1
- package/dist/routes/config/channels/endpoints/prefs.js.map +1 -1
- package/dist/routes/config/channels/endpoints/service.d.ts +0 -1
- package/dist/routes/config/channels/endpoints/service.js +8 -5
- package/dist/routes/config/channels/endpoints/service.js.map +1 -1
- package/dist/routes/config/channels/endpoints/tags.d.ts +0 -1
- package/dist/routes/config/channels/endpoints/tags.js +3 -3
- package/dist/routes/config/channels/endpoints/tags.js.map +1 -1
- package/dist/routes/config/channels/healthBridge.d.ts +4 -1
- package/dist/routes/config/channels/healthBridge.js +9 -5
- package/dist/routes/config/channels/healthBridge.js.map +1 -1
- package/dist/routes/config/channels/http/handler.d.ts +0 -1
- package/dist/routes/config/channels/http/playlistHint.d.ts +0 -1
- package/dist/routes/config/channels/http/serviceWarning.d.ts +0 -1
- package/dist/routes/config/channels/http/serviceWarning.js +1 -1
- package/dist/routes/config/channels/http/serviceWarning.js.map +1 -1
- package/dist/routes/config/channels/index.d.ts +0 -1
- package/dist/routes/config/channels/setup.d.ts +0 -1
- package/dist/routes/config/channels/table.d.ts +0 -1
- package/dist/routes/config/channels/table.js +66 -134
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/http/envelope.d.ts +1 -2
- package/dist/routes/config/index.d.ts +5 -5
- package/dist/routes/config/index.js +7 -5
- package/dist/routes/config/index.js.map +1 -1
- package/dist/routes/config/services.d.ts +0 -1
- package/dist/routes/config/services.js +17 -19
- package/dist/routes/config/services.js.map +1 -1
- package/dist/routes/config/settings.d.ts +6 -15
- package/dist/routes/config/settings.js +16 -35
- package/dist/routes/config/settings.js.map +1 -1
- package/dist/routes/debug.d.ts +0 -1
- package/dist/routes/debug.js +1 -1
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/health.d.ts +16 -8
- package/dist/routes/health.js +27 -12
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/hls.d.ts +0 -1
- package/dist/routes/icons.d.ts +0 -1
- package/dist/routes/index.d.ts +0 -1
- package/dist/routes/logs.d.ts +3 -2
- package/dist/routes/logs.js +54 -14
- package/dist/routes/logs.js.map +1 -1
- package/dist/routes/mpegts.d.ts +0 -1
- package/dist/routes/play.d.ts +0 -1
- package/dist/routes/playlist.d.ts +0 -1
- package/dist/routes/playlist.js +2 -1
- package/dist/routes/playlist.js.map +1 -1
- package/dist/routes/root/content.d.ts +0 -1
- package/dist/routes/root/content.js +32 -11
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/index.d.ts +10 -1
- package/dist/routes/root/index.js +17 -7
- package/dist/routes/root/index.js.map +1 -1
- package/dist/routes/root/scripts/channels.d.ts +0 -1
- package/dist/routes/root/scripts/channels.js +10 -20
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/clientEscape.d.ts +0 -1
- package/dist/routes/root/scripts/clientEscape.js +1 -1
- package/dist/routes/root/scripts/clientUrl.d.ts +0 -1
- package/dist/routes/root/scripts/config.d.ts +0 -1
- package/dist/routes/root/scripts/config.js +8 -29
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/index.d.ts +0 -1
- package/dist/routes/root/scripts/shared.d.ts +0 -1
- package/dist/routes/root/scripts/shared.js +16 -13
- package/dist/routes/root/scripts/shared.js.map +1 -1
- package/dist/routes/root/scripts/status.d.ts +0 -1
- package/dist/routes/root/scripts/status.handlers.d.ts +3 -1
- package/dist/routes/root/scripts/status.handlers.js +52 -26
- package/dist/routes/root/scripts/status.handlers.js.map +1 -1
- package/dist/routes/root/styles.d.ts +0 -1
- package/dist/routes/root/styles.js +10 -18
- package/dist/routes/root/styles.js.map +1 -1
- package/dist/routes/services.d.ts +0 -1
- package/dist/routes/services.js +1 -1
- package/dist/routes/services.js.map +1 -1
- package/dist/routes/sse.d.ts +3 -2
- package/dist/routes/sse.js +7 -3
- package/dist/routes/sse.js.map +1 -1
- package/dist/routes/streams.d.ts +4 -2
- package/dist/routes/streams.js +8 -5
- package/dist/routes/streams.js.map +1 -1
- package/dist/routes/theme.d.ts +0 -1
- package/dist/routes/theme.js +3 -3
- package/dist/routes/theme.js.map +1 -1
- package/dist/routes/ui.d.ts +0 -1
- package/dist/routes/ui.js +5 -5
- package/dist/routes/ui.js.map +1 -1
- package/dist/routes/upgrade.d.ts +0 -1
- package/dist/routes/upgrade.js +2 -1
- package/dist/routes/upgrade.js.map +1 -1
- package/dist/service/commands.context.d.ts +0 -1
- package/dist/service/commands.context.js +7 -1
- package/dist/service/commands.context.js.map +1 -1
- package/dist/service/commands.d.ts +0 -1
- package/dist/service/generators.context.d.ts +0 -1
- package/dist/service/generators.d.ts +0 -1
- package/dist/service/index.d.ts +0 -1
- package/dist/streaming/captureLock.d.ts +1 -2
- package/dist/streaming/captureLock.js +8 -7
- package/dist/streaming/captureLock.js.map +1 -1
- package/dist/streaming/captureSession.d.ts +0 -1
- package/dist/streaming/clients.d.ts +4 -3
- package/dist/streaming/clients.js +5 -4
- package/dist/streaming/clients.js.map +1 -1
- package/dist/streaming/codec.d.ts +0 -1
- package/dist/streaming/fmp4Segmenter.d.ts +11 -6
- package/dist/streaming/fmp4Segmenter.js +39 -17
- package/dist/streaming/fmp4Segmenter.js.map +1 -1
- package/dist/streaming/hls.d.ts +96 -3
- package/dist/streaming/hls.js +282 -171
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +8 -5
- package/dist/streaming/hlsResume.js +10 -8
- package/dist/streaming/hlsResume.js.map +1 -1
- package/dist/streaming/hlsSegments.d.ts +11 -8
- package/dist/streaming/hlsSegments.js +38 -18
- package/dist/streaming/hlsSegments.js.map +1 -1
- package/dist/streaming/lifecycle.d.ts +5 -4
- package/dist/streaming/lifecycle.js +43 -13
- package/dist/streaming/lifecycle.js.map +1 -1
- package/dist/streaming/monitor.d.ts +14 -3
- package/dist/streaming/monitor.js +447 -234
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/mp4Parser.d.ts +0 -1
- package/dist/streaming/mpegts.d.ts +11 -1
- package/dist/streaming/mpegts.js +92 -28
- package/dist/streaming/mpegts.js.map +1 -1
- package/dist/streaming/playlistBuilder.d.ts +0 -1
- package/dist/streaming/preroll.d.ts +34 -15
- package/dist/streaming/preroll.js +32 -30
- package/dist/streaming/preroll.js.map +1 -1
- package/dist/streaming/pretune.d.ts +2 -3
- package/dist/streaming/pretune.js +45 -45
- package/dist/streaming/pretune.js.map +1 -1
- package/dist/streaming/pretuneTimers.d.ts +16 -12
- package/dist/streaming/pretuneTimers.js +27 -37
- package/dist/streaming/pretuneTimers.js.map +1 -1
- package/dist/streaming/recovery.d.ts +97 -9
- package/dist/streaming/recovery.js +96 -18
- package/dist/streaming/recovery.js.map +1 -1
- package/dist/streaming/registry.d.ts +102 -15
- package/dist/streaming/registry.js +90 -5
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +90 -32
- package/dist/streaming/setup.js +379 -248
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +8 -4
- package/dist/streaming/showInfo.js +54 -45
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +5 -3
- package/dist/streaming/statusEmitter.js +20 -8
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/channels.d.ts +0 -1
- package/dist/types/channels.js +0 -4
- package/dist/types/channels.js.map +1 -1
- package/dist/types/config.d.ts +0 -1
- package/dist/types/index.d.ts +4 -4
- package/dist/types/index.js +2 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/profiles.d.ts +51 -9
- package/dist/types/profiles.js +35 -1
- package/dist/types/profiles.js.map +1 -1
- package/dist/types/selection.d.ts +10 -1
- package/dist/types/shared.d.ts +0 -1
- package/dist/types/streaming.d.ts +8 -1
- package/dist/types/streaming.js +7 -0
- package/dist/types/streaming.js.map +1 -1
- package/dist/upgrade/commands.context.d.ts +0 -1
- package/dist/upgrade/commands.d.ts +0 -1
- package/dist/upgrade/commands.js +1 -1
- package/dist/upgrade/commands.js.map +1 -1
- package/dist/upgrade/detection.context.d.ts +0 -1
- package/dist/upgrade/detection.d.ts +1 -2
- package/dist/upgrade/index.d.ts +0 -1
- package/dist/upgrade/lifecycle.context.d.ts +2 -1
- package/dist/upgrade/lifecycle.context.js +9 -4
- package/dist/upgrade/lifecycle.context.js.map +1 -1
- package/dist/upgrade/lifecycle.d.ts +0 -1
- package/dist/upgrade/pathHandle.d.ts +0 -1
- package/dist/utils/bootSession.context.d.ts +0 -1
- package/dist/utils/bootSession.context.js +3 -2
- package/dist/utils/bootSession.context.js.map +1 -1
- package/dist/utils/bootSession.d.ts +0 -1
- package/dist/utils/chromeFetch.d.ts +0 -1
- package/dist/utils/cliOutput.d.ts +0 -1
- package/dist/utils/debugFilter.d.ts +0 -1
- package/dist/utils/debugFilter.js +5 -3
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/utils/delay.d.ts +65 -13
- package/dist/utils/delay.js +96 -23
- package/dist/utils/delay.js.map +1 -1
- package/dist/utils/errors.d.ts +0 -11
- package/dist/utils/errors.js +3 -14
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/evaluate.d.ts +11 -3
- package/dist/utils/evaluate.js +4 -3
- package/dist/utils/evaluate.js.map +1 -1
- package/dist/utils/ffmpeg.context.d.ts +0 -1
- package/dist/utils/ffmpeg.d.ts +1 -1
- package/dist/utils/ffmpeg.js +2 -1
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/fileLogger.d.ts +26 -7
- package/dist/utils/fileLogger.js +241 -102
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/format.d.ts +36 -4
- package/dist/utils/format.js +66 -12
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/index.d.ts +1 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logEmitter.d.ts +0 -1
- package/dist/utils/logger.d.ts +0 -1
- package/dist/utils/m3u.d.ts +0 -1
- package/dist/utils/markup.d.ts +0 -1
- package/dist/utils/memo.d.ts +0 -1
- package/dist/utils/morganStream.d.ts +2 -3
- package/dist/utils/morganStream.js +6 -8
- package/dist/utils/morganStream.js.map +1 -1
- package/dist/utils/network.d.ts +0 -1
- package/dist/utils/never.d.ts +8 -0
- package/dist/utils/never.js +20 -0
- package/dist/utils/never.js.map +1 -0
- package/dist/utils/pid.d.ts +0 -1
- package/dist/utils/platform.d.ts +0 -1
- package/dist/utils/processInspector.context.d.ts +32 -2
- package/dist/utils/processInspector.context.js +117 -1
- package/dist/utils/processInspector.context.js.map +1 -1
- package/dist/utils/processInspector.d.ts +0 -32
- package/dist/utils/processInspector.js +10 -117
- package/dist/utils/processInspector.js.map +1 -1
- package/dist/utils/retry.d.ts +5 -4
- package/dist/utils/retry.js +43 -33
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/runtimeIdentity.context.d.ts +0 -1
- package/dist/utils/runtimeIdentity.context.js +3 -1
- package/dist/utils/runtimeIdentity.context.js.map +1 -1
- package/dist/utils/runtimeIdentity.d.ts +1 -1
- package/dist/utils/runtimeIdentity.js +1 -1
- package/dist/utils/runtimeIdentity.js.map +1 -1
- package/dist/utils/sanitize.d.ts +8 -1
- package/dist/utils/sanitize.js +19 -0
- package/dist/utils/sanitize.js.map +1 -1
- package/dist/utils/streamContext.d.ts +0 -1
- package/dist/utils/timing.d.ts +3 -4
- package/dist/utils/timing.js +4 -4
- package/dist/utils/timing.js.map +1 -1
- package/dist/utils/version.d.ts +14 -8
- package/dist/utils/version.js +35 -24
- package/dist/utils/version.js.map +1 -1
- package/package.json +7 -7
- package/dist/app.d.ts.map +0 -1
- package/dist/browser/blockedPage.d.ts.map +0 -1
- package/dist/browser/browserSupervisor.d.ts.map +0 -1
- package/dist/browser/cdp.d.ts.map +0 -1
- package/dist/browser/channelSelection.d.ts.map +0 -1
- package/dist/browser/consent.d.ts.map +0 -1
- package/dist/browser/display.d.ts.map +0 -1
- package/dist/browser/hlsPlaylistObserver.d.ts.map +0 -1
- package/dist/browser/index.d.ts.map +0 -1
- package/dist/browser/launchGovernor.d.ts.map +0 -1
- package/dist/browser/login.d.ts.map +0 -1
- package/dist/browser/manifestInterceptor.d.ts.map +0 -1
- package/dist/browser/pageStaleness.d.ts.map +0 -1
- package/dist/browser/precaching.d.ts.map +0 -1
- package/dist/browser/tabNetworkObserver.d.ts.map +0 -1
- package/dist/browser/tuning/cache.d.ts.map +0 -1
- package/dist/browser/tuning/comcastPolymer.d.ts.map +0 -1
- package/dist/browser/tuning/cox.d.ts.map +0 -1
- package/dist/browser/tuning/directv.d.ts.map +0 -1
- package/dist/browser/tuning/fox.d.ts.map +0 -1
- package/dist/browser/tuning/gridSearch.d.ts.map +0 -1
- package/dist/browser/tuning/hbo.d.ts.map +0 -1
- package/dist/browser/tuning/hulu.d.ts.map +0 -1
- package/dist/browser/tuning/shared.d.ts.map +0 -1
- package/dist/browser/tuning/sling.d.ts.map +0 -1
- package/dist/browser/tuning/spectrum.d.ts.map +0 -1
- package/dist/browser/tuning/thumbnailRow.d.ts.map +0 -1
- package/dist/browser/tuning/tileClick.d.ts.map +0 -1
- package/dist/browser/tuning/xfinity.d.ts.map +0 -1
- package/dist/browser/tuning/youtubeTv.d.ts.map +0 -1
- package/dist/browser/video.d.ts.map +0 -1
- package/dist/channels/index.d.ts.map +0 -1
- package/dist/config/channelForm.d.ts.map +0 -1
- package/dist/config/consistencyProbe.d.ts.map +0 -1
- package/dist/config/health.d.ts.map +0 -1
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/paths.d.ts.map +0 -1
- package/dist/config/persistence.context.d.ts.map +0 -1
- package/dist/config/persistence.d.ts.map +0 -1
- package/dist/config/presets.d.ts.map +0 -1
- package/dist/config/profiles.d.ts.map +0 -1
- package/dist/config/providerLineups.d.ts.map +0 -1
- package/dist/config/reactivity.d.ts.map +0 -1
- package/dist/config/servicePacks.d.ts.map +0 -1
- package/dist/config/services.d.ts.map +0 -1
- package/dist/config/sites.d.ts.map +0 -1
- package/dist/config/userChannels.d.ts.map +0 -1
- package/dist/config/userConfig.d.ts.map +0 -1
- package/dist/config/userProfiles.d.ts.map +0 -1
- package/dist/hdhr/channelMap.d.ts.map +0 -1
- package/dist/hdhr/deviceId.d.ts.map +0 -1
- package/dist/hdhr/discover.d.ts.map +0 -1
- package/dist/hdhr/getHandlers.d.ts.map +0 -1
- package/dist/hdhr/identity.d.ts.map +0 -1
- package/dist/hdhr/index.d.ts.map +0 -1
- package/dist/hdhr/protocol.d.ts.map +0 -1
- package/dist/hdhr/tunerState.d.ts.map +0 -1
- package/dist/hdhr/udp.d.ts.map +0 -1
- package/dist/identity.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/native/codecInference.d.ts.map +0 -1
- package/dist/native/decrypt.d.ts.map +0 -1
- package/dist/native/index.d.ts.map +0 -1
- package/dist/native/probe.d.ts.map +0 -1
- package/dist/native/proxy.d.ts.map +0 -1
- package/dist/native/tokenExpiry.d.ts.map +0 -1
- package/dist/routes/assets.d.ts.map +0 -1
- package/dist/routes/auth.d.ts.map +0 -1
- package/dist/routes/cdp.d.ts.map +0 -1
- package/dist/routes/channels.d.ts.map +0 -1
- package/dist/routes/clientActions.d.ts.map +0 -1
- package/dist/routes/components.d.ts.map +0 -1
- package/dist/routes/config/channels/endpoints/browse.d.ts.map +0 -1
- package/dist/routes/config/channels/endpoints/bulk.d.ts.map +0 -1
- package/dist/routes/config/channels/endpoints/crud.d.ts.map +0 -1
- package/dist/routes/config/channels/endpoints/importExport.d.ts.map +0 -1
- package/dist/routes/config/channels/endpoints/predefined.d.ts.map +0 -1
- package/dist/routes/config/channels/endpoints/prefs.d.ts.map +0 -1
- package/dist/routes/config/channels/endpoints/service.d.ts.map +0 -1
- package/dist/routes/config/channels/endpoints/tags.d.ts.map +0 -1
- package/dist/routes/config/channels/healthBridge.d.ts.map +0 -1
- package/dist/routes/config/channels/http/handler.d.ts.map +0 -1
- package/dist/routes/config/channels/http/playlistHint.d.ts.map +0 -1
- package/dist/routes/config/channels/http/serviceWarning.d.ts.map +0 -1
- package/dist/routes/config/channels/index.d.ts.map +0 -1
- package/dist/routes/config/channels/setup.d.ts.map +0 -1
- package/dist/routes/config/channels/table.d.ts.map +0 -1
- package/dist/routes/config/http/envelope.d.ts.map +0 -1
- package/dist/routes/config/index.d.ts.map +0 -1
- package/dist/routes/config/services.d.ts.map +0 -1
- package/dist/routes/config/settings.d.ts.map +0 -1
- package/dist/routes/debug.d.ts.map +0 -1
- package/dist/routes/health.d.ts.map +0 -1
- package/dist/routes/hls.d.ts.map +0 -1
- package/dist/routes/icons.d.ts.map +0 -1
- package/dist/routes/index.d.ts.map +0 -1
- package/dist/routes/logs.d.ts.map +0 -1
- package/dist/routes/mpegts.d.ts.map +0 -1
- package/dist/routes/play.d.ts.map +0 -1
- package/dist/routes/playlist.d.ts.map +0 -1
- package/dist/routes/root/content.d.ts.map +0 -1
- package/dist/routes/root/index.d.ts.map +0 -1
- package/dist/routes/root/scripts/channels.d.ts.map +0 -1
- package/dist/routes/root/scripts/clientEscape.d.ts.map +0 -1
- package/dist/routes/root/scripts/clientUrl.d.ts.map +0 -1
- package/dist/routes/root/scripts/config.d.ts.map +0 -1
- package/dist/routes/root/scripts/index.d.ts.map +0 -1
- package/dist/routes/root/scripts/shared.d.ts.map +0 -1
- package/dist/routes/root/scripts/status.d.ts.map +0 -1
- package/dist/routes/root/scripts/status.handlers.d.ts.map +0 -1
- package/dist/routes/root/styles.d.ts.map +0 -1
- package/dist/routes/services.d.ts.map +0 -1
- package/dist/routes/sse.d.ts.map +0 -1
- package/dist/routes/streams.d.ts.map +0 -1
- package/dist/routes/theme.d.ts.map +0 -1
- package/dist/routes/ui.d.ts.map +0 -1
- package/dist/routes/upgrade.d.ts.map +0 -1
- package/dist/service/commands.context.d.ts.map +0 -1
- package/dist/service/commands.d.ts.map +0 -1
- package/dist/service/generators.context.d.ts.map +0 -1
- package/dist/service/generators.d.ts.map +0 -1
- package/dist/service/index.d.ts.map +0 -1
- package/dist/streaming/captureLock.d.ts.map +0 -1
- package/dist/streaming/captureSession.d.ts.map +0 -1
- package/dist/streaming/clients.d.ts.map +0 -1
- package/dist/streaming/codec.d.ts.map +0 -1
- package/dist/streaming/fmp4Segmenter.d.ts.map +0 -1
- package/dist/streaming/hls.d.ts.map +0 -1
- package/dist/streaming/hlsResume.d.ts.map +0 -1
- package/dist/streaming/hlsSegments.d.ts.map +0 -1
- package/dist/streaming/lifecycle.d.ts.map +0 -1
- package/dist/streaming/monitor.d.ts.map +0 -1
- package/dist/streaming/mp4Parser.d.ts.map +0 -1
- package/dist/streaming/mpegts.d.ts.map +0 -1
- package/dist/streaming/playlistBuilder.d.ts.map +0 -1
- package/dist/streaming/preroll.d.ts.map +0 -1
- package/dist/streaming/pretune.d.ts.map +0 -1
- package/dist/streaming/pretuneTimers.d.ts.map +0 -1
- package/dist/streaming/recovery.d.ts.map +0 -1
- package/dist/streaming/registry.d.ts.map +0 -1
- package/dist/streaming/setup.d.ts.map +0 -1
- package/dist/streaming/showInfo.d.ts.map +0 -1
- package/dist/streaming/statusEmitter.d.ts.map +0 -1
- package/dist/types/channels.d.ts.map +0 -1
- package/dist/types/config.d.ts.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/profiles.d.ts.map +0 -1
- package/dist/types/selection.d.ts.map +0 -1
- package/dist/types/shared.d.ts.map +0 -1
- package/dist/types/streaming.d.ts.map +0 -1
- package/dist/upgrade/commands.context.d.ts.map +0 -1
- package/dist/upgrade/commands.d.ts.map +0 -1
- package/dist/upgrade/detection.context.d.ts.map +0 -1
- package/dist/upgrade/detection.d.ts.map +0 -1
- package/dist/upgrade/index.d.ts.map +0 -1
- package/dist/upgrade/lifecycle.context.d.ts.map +0 -1
- package/dist/upgrade/lifecycle.d.ts.map +0 -1
- package/dist/upgrade/pathHandle.d.ts.map +0 -1
- package/dist/utils/bootSession.context.d.ts.map +0 -1
- package/dist/utils/bootSession.d.ts.map +0 -1
- package/dist/utils/chromeFetch.d.ts.map +0 -1
- package/dist/utils/cliOutput.d.ts.map +0 -1
- package/dist/utils/clock.d.ts +0 -15
- package/dist/utils/clock.d.ts.map +0 -1
- package/dist/utils/clock.js +0 -31
- package/dist/utils/clock.js.map +0 -1
- package/dist/utils/debugFilter.d.ts.map +0 -1
- package/dist/utils/delay.d.ts.map +0 -1
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/evaluate.d.ts.map +0 -1
- package/dist/utils/ffmpeg.context.d.ts.map +0 -1
- package/dist/utils/ffmpeg.d.ts.map +0 -1
- package/dist/utils/fileLogger.d.ts.map +0 -1
- package/dist/utils/format.d.ts.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/logEmitter.d.ts.map +0 -1
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/m3u.d.ts.map +0 -1
- package/dist/utils/markup.d.ts.map +0 -1
- package/dist/utils/memo.d.ts.map +0 -1
- package/dist/utils/morganStream.d.ts.map +0 -1
- package/dist/utils/network.d.ts.map +0 -1
- package/dist/utils/pid.d.ts.map +0 -1
- package/dist/utils/platform.d.ts.map +0 -1
- package/dist/utils/processInspector.context.d.ts.map +0 -1
- package/dist/utils/processInspector.d.ts.map +0 -1
- package/dist/utils/retry.d.ts.map +0 -1
- package/dist/utils/runtimeIdentity.context.d.ts.map +0 -1
- package/dist/utils/runtimeIdentity.d.ts.map +0 -1
- package/dist/utils/sanitize.d.ts.map +0 -1
- package/dist/utils/streamContext.d.ts.map +0 -1
- package/dist/utils/timing.d.ts.map +0 -1
- package/dist/utils/version.d.ts.map +0 -1
package/dist/streaming/hls.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { LOG, formatError, runWithStreamContext, startTimer } from "../utils/index.js";
|
|
1
|
+
import { LOG, formatError, formatResolutionLabel, runWithStreamContext, startTimer } from "../utils/index.js";
|
|
2
2
|
import { StreamSetupError, createPageWithCapture, generateStreamId, reestablishChannelManifest, setupStream, validateStreamUrl } from "./setup.js";
|
|
3
|
+
import { applyNativeQualityRefresh, cancelPrerollTimer, createHLSState, getAllStreams, getNextStreamId, getStream, getStreamCount, isCaptureIdentity, makePendingCaptureIdentity, registerStream, updateLastAccess } from "./registry.js";
|
|
3
4
|
import { buildProbeCacheStamp, clearProbeCache } from "../native/probe.js";
|
|
4
|
-
import { cancelPrerollTimer, createHLSState, getAllStreams, getNextStreamId, getStream, getStreamCount, registerStream, updateLastAccess } from "./registry.js";
|
|
5
5
|
import { createInitialStreamStatus, emitStreamAdded } from "./statusEmitter.js";
|
|
6
6
|
import { deleteResumeData, getResumeSegmentIndex, peekResumeData } from "./hlsResume.js";
|
|
7
|
-
import { emitCurrentSystemStatus, isLoginModeActive, unregisterManagedPage } from "../browser/index.js";
|
|
7
|
+
import { emitCurrentSystemStatus, isLoginModeActive, syncWindowVisibility, unregisterManagedPage } from "../browser/index.js";
|
|
8
8
|
import { generatePrerollPlaylist, getPrerollCodec, getPrerollSegmentCount, isPrerollReady } from "./preroll.js";
|
|
9
9
|
import { getAllChannels, getChannelLogo, isPredefinedChannelDisabled } from "../config/userChannels.js";
|
|
10
10
|
import { getAudioPlaylist, getAudioSegment, getInitSegment, getNamedInitSegment, getPlaylist, getSegment, getVideoPlaylist, waitForPlaylist } from "./hlsSegments.js";
|
|
@@ -19,6 +19,7 @@ import { createHash } from "node:crypto";
|
|
|
19
19
|
import { getProviderBySlug } from "../browser/channelSelection.js";
|
|
20
20
|
import { registerClient } from "./clients.js";
|
|
21
21
|
import { suppressPageAudio } from "../browser/video.js";
|
|
22
|
+
import { systemClock } from "homebridge-plugin-utils";
|
|
22
23
|
import { triggerShowNameUpdate } from "./showInfo.js";
|
|
23
24
|
/* This module handles HLS (HTTP Live Streaming) output using fMP4 (fragmented MP4) segments. HLS mode uses MP4/AAC capture from puppeteer-stream, which is then
|
|
24
25
|
* segmented natively without any external dependencies. The stream initialization flow has three phases:
|
|
@@ -46,6 +47,9 @@ import { triggerShowNameUpdate } from "./showInfo.js";
|
|
|
46
47
|
// content directly. This ensures fast-tuning services (native HLS at 2-3s, most capture services at 4-7s) never see preroll, while slow services (Xfinity/Cox at
|
|
47
48
|
// 13-15s) get preroll content after the delay to prevent HTTP timeouts.
|
|
48
49
|
const PREROLL_DELAY_MS = 9000;
|
|
50
|
+
// How long a playlist request waits for a playlist to exist, in milliseconds. It bounds the window between a pending entry's registration and the first playlist
|
|
51
|
+
// from either the preroll timer or the segmenter, so a setup that hangs without ever failing cannot hold a client request open indefinitely.
|
|
52
|
+
const PLAYLIST_WAIT_MS = 30000;
|
|
49
53
|
/**
|
|
50
54
|
* Builds the onError/onStop callbacks for createFMP4Segmenter. Both callbacks share the same termination chain (skip-if-already-terminating guard, log error,
|
|
51
55
|
* terminate stream, emit current system status); the only differences between callsites are the log-message and termination-reason suffixes used to mark the
|
|
@@ -137,10 +141,45 @@ export function sendValidationError(validation, res) {
|
|
|
137
141
|
res.status(validation.statusCode).send(validation.body);
|
|
138
142
|
}
|
|
139
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Describes a stream setup failure as the status and message the client is answered with. A StreamSetupError carries its own status and user-facing text, which is
|
|
146
|
+
* the meaning the setup path assigned to the failure; anything else is unexpected and the client learns nothing beyond a 500.
|
|
147
|
+
* @param error - The error a setup path produced.
|
|
148
|
+
* @returns The status and message this failure answers with.
|
|
149
|
+
*/
|
|
150
|
+
export function describeSetupFailure(error) {
|
|
151
|
+
if (error instanceof StreamSetupError) {
|
|
152
|
+
return { statusCode: error.statusCode, userMessage: error.userMessage };
|
|
153
|
+
}
|
|
154
|
+
return { statusCode: 500, userMessage: "Internal server error." };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Sends a described setup failure to the client. Every streaming route answers a setup failure through here, so one failure means one response shape no matter
|
|
158
|
+
* which route the client arrived on.
|
|
159
|
+
*
|
|
160
|
+
* A 503 carries both back-off headers on every route: Channels DVR reads Retry-After and waits instead of retrying in a tight loop, and an HDHomeRun client reads
|
|
161
|
+
* X-HDHomeRun-Error to tell a busy tuner apart from a broken one. A response whose headers are already flushed can carry no status at all, so it is ended and the
|
|
162
|
+
* client sees the connection close.
|
|
163
|
+
*
|
|
164
|
+
* Logging stays with the callers: each route describes its own failure in its own words and at its own level.
|
|
165
|
+
* @param failure - The described failure, from describeSetupFailure or from a literal the caller composed.
|
|
166
|
+
* @param res - Express response object.
|
|
167
|
+
*/
|
|
168
|
+
export function sendSetupFailure(failure, res) {
|
|
169
|
+
if (res.headersSent) {
|
|
170
|
+
res.end();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (failure.statusCode === 503) {
|
|
174
|
+
res.setHeader("Retry-After", "10");
|
|
175
|
+
res.setHeader("X-HDHomeRun-Error", "All Tuners In Use");
|
|
176
|
+
}
|
|
177
|
+
res.status(failure.statusCode).send(failure.userMessage);
|
|
178
|
+
}
|
|
140
179
|
// Capacity Reservation.
|
|
141
180
|
/**
|
|
142
181
|
* Pure predicate for the concurrent-stream capacity decision. Returns true when a new stream may be admitted given the current active count and the configured
|
|
143
|
-
* limit. Extracted as a standalone function so the boundary arithmetic is
|
|
182
|
+
* limit. Extracted as a standalone function so the boundary arithmetic is asserted by a unit test without driving a browser. The count passed in must be the count
|
|
144
183
|
* BEFORE the new stream's pending entry is registered, so the new stream is naturally excluded from its own capacity check; admitting the final slot
|
|
145
184
|
* (active === maxConcurrent - 1) returns true.
|
|
146
185
|
* @param activeCount - Number of streams currently registered, excluding the stream being admitted.
|
|
@@ -203,11 +242,10 @@ export async function ensureChannelStream(channelName, req, res) {
|
|
|
203
242
|
// Reserve a capacity slot before registering the pending entry. The pending entry occupies a registry slot the instant it is registered, so the capacity
|
|
204
243
|
// decision must happen here - while the new stream is still excluded from the count - to avoid a later self-counting check rejecting it after the client has
|
|
205
244
|
// already received a preroll playlist. reserveStreamSlot is the single source of truth for this decision and reclaims an idle stream when at the limit. On
|
|
206
|
-
// failure we
|
|
245
|
+
// failure we answer here, before any registration or preroll response, through the shared responder that writes the back-off headers for every route.
|
|
207
246
|
if (!reserveStreamSlot()) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
res.status(503).send("Maximum concurrent streams (" + String(CONFIG.streaming.maxConcurrentStreams) + ") reached. Try again later.");
|
|
247
|
+
const refusal = "Maximum concurrent streams (" + String(CONFIG.streaming.maxConcurrentStreams) + ") reached. Try again later.";
|
|
248
|
+
sendSetupFailure({ statusCode: 503, userMessage: refusal }, res);
|
|
211
249
|
return null;
|
|
212
250
|
}
|
|
213
251
|
const clientAddress = req.ip ?? req.socket.remoteAddress ?? null;
|
|
@@ -262,6 +300,8 @@ export async function handleHLSPlaylist(req, res) {
|
|
|
262
300
|
* @param res - Express response object.
|
|
263
301
|
*/
|
|
264
302
|
export function handleHLSSegment(req, res) {
|
|
303
|
+
// The serve instant, read once at this request boundary and used for whichever access stamp the branch below writes.
|
|
304
|
+
const now = systemClock.now();
|
|
265
305
|
const channelName = req.params.name;
|
|
266
306
|
const segmentName = req.params.segment;
|
|
267
307
|
if (!channelName || !segmentName) {
|
|
@@ -280,7 +320,7 @@ export function handleHLSSegment(req, res) {
|
|
|
280
320
|
res.status(404).send("Init segment not found.");
|
|
281
321
|
return;
|
|
282
322
|
}
|
|
283
|
-
updateLastAccess(streamId);
|
|
323
|
+
updateLastAccess(streamId, now);
|
|
284
324
|
sendSegment(initSegment, "init.mp4", res);
|
|
285
325
|
return;
|
|
286
326
|
}
|
|
@@ -292,7 +332,7 @@ export function handleHLSSegment(req, res) {
|
|
|
292
332
|
res.status(404).send("Segment not found.");
|
|
293
333
|
return;
|
|
294
334
|
}
|
|
295
|
-
updateLastAccess(streamId);
|
|
335
|
+
updateLastAccess(streamId, now);
|
|
296
336
|
sendSegment(segment, segmentName, res);
|
|
297
337
|
}
|
|
298
338
|
/**
|
|
@@ -305,6 +345,8 @@ export function handleHLSSegment(req, res) {
|
|
|
305
345
|
* @param res - Express response object.
|
|
306
346
|
*/
|
|
307
347
|
export function handleHLSVariantPlaylist(req, res) {
|
|
348
|
+
// The serve instant, read once at this request boundary.
|
|
349
|
+
const now = systemClock.now();
|
|
308
350
|
const channelName = req.params.name;
|
|
309
351
|
// Extract the playlist filename from the URL path. The route is registered as two explicit paths (/video.m3u8 and /audio.m3u8) rather than a parameterized route,
|
|
310
352
|
// so there is no :playlist param to read.
|
|
@@ -330,7 +372,7 @@ export function handleHLSVariantPlaylist(req, res) {
|
|
|
330
372
|
res.status(404).send("Playlist not found.");
|
|
331
373
|
return;
|
|
332
374
|
}
|
|
333
|
-
updateLastAccess(streamId);
|
|
375
|
+
updateLastAccess(streamId, now);
|
|
334
376
|
sendPlaylist(playlist, res);
|
|
335
377
|
}
|
|
336
378
|
// Ad-Hoc Streaming.
|
|
@@ -393,15 +435,11 @@ export async function handlePlayStream(req, res) {
|
|
|
393
435
|
}
|
|
394
436
|
}
|
|
395
437
|
catch (error) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
res.status(error.statusCode).send(error.userMessage);
|
|
401
|
-
return;
|
|
438
|
+
// A StreamSetupError is an expected refusal whose message is already meant for the client, so only an unexpected failure earns an error log here.
|
|
439
|
+
if (!(error instanceof StreamSetupError)) {
|
|
440
|
+
LOG.error("Unexpected error during ad-hoc stream setup: %s.", formatError(error));
|
|
402
441
|
}
|
|
403
|
-
|
|
404
|
-
res.status(500).send("Internal server error.");
|
|
442
|
+
sendSetupFailure(describeSetupFailure(error), res);
|
|
405
443
|
return;
|
|
406
444
|
}
|
|
407
445
|
res.redirect(302, "/hls/" + channelName + "/stream.m3u8");
|
|
@@ -411,7 +449,8 @@ export async function handlePlayStream(req, res) {
|
|
|
411
449
|
* Sends the playlist for a stream. With the deferred preroll timer, the playlist may not be available immediately after stream registration - it arrives when either
|
|
412
450
|
* the timer fires (seeding preroll) or the segmenter produces real content, whichever comes first. This function awaits the playlistReady promise to handle that
|
|
413
451
|
* window. For the blocking fallback path (no FFmpeg / no preroll), the playlist is guaranteed to exist because initializeStream blocks until the segmenter produces
|
|
414
|
-
* it.
|
|
452
|
+
* it. A stream whose setup failed answers with that failure's status, so the client learns the tune was refused; a stream terminated for any other reason, or a
|
|
453
|
+
* wait that timed out, answers 404.
|
|
415
454
|
* @param streamId - The numeric stream ID.
|
|
416
455
|
* @param clientAddress - Client address for tracking.
|
|
417
456
|
* @param res - Express response object.
|
|
@@ -419,26 +458,40 @@ export async function handlePlayStream(req, res) {
|
|
|
419
458
|
async function sendPlaylistResponse(streamId, clientAddress, res) {
|
|
420
459
|
let playlist = getPlaylist(streamId);
|
|
421
460
|
// The playlist may not be populated yet if the deferred preroll timer hasn't fired and the segmenter hasn't produced content. Wait for the playlistReady promise
|
|
422
|
-
// which resolves when either source provides a playlist. The
|
|
461
|
+
// which resolves when either source provides a playlist. The bounded wait covers pathological cases like setup hanging.
|
|
423
462
|
if (!playlist) {
|
|
424
|
-
|
|
463
|
+
/* The reference a failed setup's status is read through, taken before the wait rather than after it. A setup failure records its status on this entry and then
|
|
464
|
+
* leaves the registry in the same synchronous run that wakes this wait, so a lookup on the far side would find nothing where the reference taken here still
|
|
465
|
+
* reaches the entry object.
|
|
466
|
+
*/
|
|
467
|
+
const waiting = getStream(streamId);
|
|
468
|
+
const ready = await waitForPlaylist(streamId, PLAYLIST_WAIT_MS);
|
|
425
469
|
if (ready) {
|
|
426
470
|
playlist = getPlaylist(streamId);
|
|
427
471
|
}
|
|
472
|
+
// A refused tune answers with the refusal's own status, which is what tells Channels DVR to back off - a 404 carries no back-off and invites an immediate
|
|
473
|
+
// retry of a stream that is not coming.
|
|
474
|
+
if (!playlist && waiting?.hls.setupFailure) {
|
|
475
|
+
sendSetupFailure(waiting.hls.setupFailure, res);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
428
478
|
}
|
|
429
479
|
if (!playlist) {
|
|
430
480
|
res.status(404).send("Stream not found.");
|
|
431
481
|
return;
|
|
432
482
|
}
|
|
433
|
-
|
|
434
|
-
|
|
483
|
+
// The serve instant, so the stream's access stamp and the client's last-seen both carry the moment the response is served.
|
|
484
|
+
const now = systemClock.now();
|
|
485
|
+
updateLastAccess(streamId, now);
|
|
486
|
+
registerClient(streamId, clientAddress, "hls", now);
|
|
435
487
|
// When still serving preroll, regenerate the progressive playlist on each poll so the sliding window advances based on elapsed wall-clock time. This simulates a
|
|
436
488
|
// live stream - the client sees new segments appear on each poll and keeps playing without stalling. Once real content arrives, hasRealPlaylist becomes true and
|
|
437
489
|
// the segmenter's playlist takes over.
|
|
438
490
|
const stream = getStream(streamId);
|
|
439
491
|
if (stream && !stream.hls.hasRealPlaylist) {
|
|
440
|
-
if (stream.hls.prerollBaseUrl && stream.hls.prerollCodec && stream.hls.prerollStartTime) {
|
|
441
|
-
playlist = generatePrerollPlaylist(stream.hls.prerollBaseUrl, stream.hls.prerollCodec,
|
|
492
|
+
if (stream.hls.prerollBaseUrl && stream.hls.prerollCodec && (stream.hls.prerollStartTime !== null)) {
|
|
493
|
+
playlist = generatePrerollPlaylist({ baseUrl: stream.hls.prerollBaseUrl, codec: stream.hls.prerollCodec, now,
|
|
494
|
+
prerollStartTime: stream.hls.prerollStartTime, startingSequence: stream.hls.resumeSegmentIndex });
|
|
442
495
|
}
|
|
443
496
|
LOG.debug("streaming:preroll", "Serving preroll playlist for stream %d.", streamId);
|
|
444
497
|
}
|
|
@@ -471,8 +524,6 @@ function sendSegment(data, segmentName, res) {
|
|
|
471
524
|
res.send(data);
|
|
472
525
|
}
|
|
473
526
|
// Stream Lifecycle.
|
|
474
|
-
// Maps vertical resolution heights to standard display labels. Used by formatNativeQuality() to convert "1920x1080" -> "1080p".
|
|
475
|
-
const RESOLUTION_LABELS = { "1080": "1080p", "2160": "4K", "360": "360p", "480": "480p", "720": "720p" };
|
|
476
527
|
/**
|
|
477
528
|
* Formats the native HLS quality string for the "Streaming..." log line. Combines codec, bandwidth (as Mbps), and resolution (as standard label like "1080p")
|
|
478
529
|
* into a compact suffix. Returns an empty string when no values are available, or a comma-prefixed string like ", H264 12.1Mbps 1080p" for inclusion in the log.
|
|
@@ -490,28 +541,79 @@ function formatNativeQuality(bandwidth, codec, resolution) {
|
|
|
490
541
|
parts.push((bandwidth / 1000000).toFixed(1) + "Mbps");
|
|
491
542
|
}
|
|
492
543
|
if (resolution) {
|
|
493
|
-
|
|
494
|
-
const height = resolution.split("x")[1];
|
|
495
|
-
parts.push((height ? RESOLUTION_LABELS[height] : undefined) ?? resolution);
|
|
544
|
+
parts.push(formatResolutionLabel(resolution));
|
|
496
545
|
}
|
|
497
546
|
if (parts.length === 0) {
|
|
498
547
|
return "";
|
|
499
548
|
}
|
|
500
549
|
return ", " + parts.join(" ");
|
|
501
550
|
}
|
|
551
|
+
const defaultTabReplacementDeps = { createPageWithCapture, unregisterManagedPage };
|
|
502
552
|
/**
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
553
|
+
* Swaps a freshly established capture pipeline in for the one a stream is running on. This is deliberately a plain, non-async function: everything below runs in
|
|
554
|
+
* a single synchronous frame, so no tick, client request, monitor pass, or termination can interleave between the old pipeline's disposal and the new one's
|
|
555
|
+
* installation. Two segmenters therefore never write one HLS state, the registry never holds two pipelines, and the monitor's post-replacement state reset -
|
|
556
|
+
* which reads the registry's segmenter immediately after this returns - is reading the new pipeline by construction rather than by timing.
|
|
557
|
+
*
|
|
558
|
+
* Keeping it a separate function rather than a block inside the handler is what protects that: an await added here is a syntax error rather than a review catch.
|
|
559
|
+
*
|
|
560
|
+
* The continuity read lives here, at the swap, and not at the top of the replacement. The old segmenter keeps producing throughout the minute or more the
|
|
561
|
+
* replacement page spends navigating and tuning, so its segment index, track timestamps, and session statistics all advance across that window - a read taken
|
|
562
|
+
* before the tune would seed the new segmenter with a stale sequence and rewind the playlist.
|
|
563
|
+
* @param options - The entry, the fresh pipeline, the outgoing page, and the encoder facts carried forward.
|
|
564
|
+
*/
|
|
565
|
+
function spliceReplacementCapture(options) {
|
|
566
|
+
const { captureResult, channelName, deps, entry, numericStreamId, oldCaptureCodec, oldHardwareAccelerated, oldPage, streamId } = options;
|
|
567
|
+
// Read the outgoing segmenter's continuity at the last possible instant, then tear its pipeline down. The CaptureSession kills the FFmpeg child first (which
|
|
568
|
+
// silences that pipeline's stream listeners for good), then destroys the capture stream - which MUST happen before the old page closes, so chrome.tabCapture
|
|
569
|
+
// releases and a later acquisition does not fail with "Cannot capture a tab with an active stream" - then stops the segmenter. The session is null on the
|
|
570
|
+
// capture-pending identity a native fallback arrives with, which is why the read is guarded.
|
|
571
|
+
const oldSession = (entry.identity.mode === "capture") ? entry.identity.captureSession : null;
|
|
572
|
+
const continuity = oldSession?.segmenter?.getContinuitySnapshot();
|
|
573
|
+
if (oldSession) {
|
|
574
|
+
LOG.debug("recovery:tab", "Disposing the outgoing capture pipeline at the swap.");
|
|
575
|
+
oldSession.dispose();
|
|
576
|
+
}
|
|
577
|
+
// Release the outgoing page. It has been serving the stream up to this instant, so the close is what ends it.
|
|
578
|
+
if (oldPage) {
|
|
579
|
+
deps.unregisterManagedPage(oldPage);
|
|
580
|
+
if (!oldPage.isClosed()) {
|
|
581
|
+
LOG.debug("recovery:tab", "Closing the outgoing page for stream %s.", streamId);
|
|
582
|
+
oldPage.close().catch((error) => {
|
|
583
|
+
LOG.debug("recovery:tab", "Page close error during tab replacement: %s.", formatError(error));
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// Build the new segmenter on the continuity just read, and mark its first segment with a discontinuity tag so clients flush decoder state. The tag is
|
|
588
|
+
// suppressed automatically when the new init segment turns out byte-identical to the old one.
|
|
589
|
+
const newSegmenter = createFMP4Segmenter({
|
|
590
|
+
...buildSegmenterTerminationHandlers(numericStreamId, channelName, { logSuffix: "after tab replacement", reasonSuffix: "after recovery" }),
|
|
591
|
+
...(continuity ? { continuity } : {}),
|
|
592
|
+
pendingDiscontinuity: true,
|
|
593
|
+
streamId: numericStreamId
|
|
594
|
+
});
|
|
595
|
+
// Wire the new segmenter into the new capture session (attachSegmenter pipes the session's capture output into it), then re-point the entry at the new
|
|
596
|
+
// pipeline and page. The identity is a pure literal because this is a within-mode replacement of the whole value, not a refresh of part of one.
|
|
597
|
+
captureResult.captureSession.attachSegmenter(newSegmenter);
|
|
598
|
+
entry.identity = { captureCodec: oldCaptureCodec, captureSession: captureResult.captureSession, hardwareAccelerated: oldHardwareAccelerated, mode: "capture" };
|
|
599
|
+
entry.page = captureResult.page;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Creates a tab replacement handler for recovery from unresponsive browser tabs. The replacement builds before it tears down:
|
|
603
|
+
*
|
|
604
|
+
* 1. Create a fresh page and acquire its capture, and tune it, while the existing pipeline keeps producing segments for the client
|
|
605
|
+
* 2. If anything fails before the swap, discard the fresh page and capture and leave the existing pipeline untouched - a logged recovery miss, not a lost recording
|
|
606
|
+
* 3. Otherwise swap in one synchronous frame: read the old segmenter's continuity, dispose the old pipeline, close the old page, splice the new one in
|
|
607
|
+
* 4. Return the new page and context for the monitor to continue on
|
|
608
|
+
*
|
|
609
|
+
* Because nothing is disposed until the swap, the monitor's single retry re-reads continuity from the still-live old segmenter on its second attempt, and a
|
|
610
|
+
* stream whose replacement keeps failing keeps serving its recording while the circuit breaker counts.
|
|
510
611
|
*
|
|
511
|
-
* The handler preserves existing HLS segments and marks a discontinuity so clients know the stream parameters may have changed.
|
|
612
|
+
* The handler preserves existing HLS segments and marks a discontinuity so clients know the stream parameters may have changed. The cost of the overlap is a
|
|
613
|
+
* second capture and encoder running for the length of the tune, which the CPU and GPU carry.
|
|
512
614
|
*
|
|
513
|
-
* Note: This handler is only invoked for capture-mode streams
|
|
514
|
-
* streams
|
|
615
|
+
* Note: This handler is only invoked for capture-mode streams, plus the native fallback that has already declared a pending capture identity. Native-mode
|
|
616
|
+
* streams bypass video element monitoring entirely, so evaluate timeouts that trigger tab replacement cannot occur on one.
|
|
515
617
|
*
|
|
516
618
|
* @param numericStreamId - The stream's numeric ID for registry lookups.
|
|
517
619
|
* @param streamId - The stream's string ID for logging.
|
|
@@ -520,9 +622,11 @@ function formatNativeQuality(bandwidth, codec, resolution) {
|
|
|
520
622
|
* @param profile - The site profile for video handling.
|
|
521
623
|
* @param metadataComment - Optional comment to embed in FFmpeg output metadata.
|
|
522
624
|
* @param onCircuitBreak - Callback for circuit breaker trips during replacement.
|
|
625
|
+
* @param deps - The injected establishment collaborators; defaults to defaultTabReplacementDeps. Threaded so a test drives the real handler - its phase tracking,
|
|
626
|
+
* its discard path, and the swap itself - against a substituted establishment rather than a live Chrome.
|
|
523
627
|
* @returns A handler function that performs tab replacement, or null if the stream no longer exists.
|
|
524
628
|
*/
|
|
525
|
-
function createTabReplacementHandler(numericStreamId, streamId, channelName, url, profile, metadataComment, onCircuitBreak) {
|
|
629
|
+
export function createTabReplacementHandler(numericStreamId, streamId, channelName, url, profile, metadataComment, onCircuitBreak, deps = defaultTabReplacementDeps) {
|
|
526
630
|
return async () => {
|
|
527
631
|
const tabElapsed = startTimer();
|
|
528
632
|
// Get the current stream entry.
|
|
@@ -531,41 +635,32 @@ function createTabReplacementHandler(numericStreamId, streamId, channelName, url
|
|
|
531
635
|
LOG.debug("recovery:tab", "Tab replacement requested but stream %s no longer exists.", streamId);
|
|
532
636
|
return null;
|
|
533
637
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
// Close the current page. The page may be null for pending entries whose async setup has not yet completed.
|
|
551
|
-
const oldPage = stream.page;
|
|
552
|
-
if (oldPage) {
|
|
553
|
-
unregisterManagedPage(oldPage);
|
|
554
|
-
if (!oldPage.isClosed()) {
|
|
555
|
-
LOG.debug("recovery:tab", "Closing unresponsive page for tab replacement.");
|
|
556
|
-
oldPage.close().catch((error) => {
|
|
557
|
-
LOG.debug("recovery:tab", "Page close error during tab replacement: %s.", formatError(error));
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
LOG.debug("timing:tab", "Old tab cleanup complete. (+%sms)", tabElapsed());
|
|
562
|
-
// Create a new page with capture.
|
|
638
|
+
/* The encoder facts the replacement carries forward, read now off the identity that is still serving the stream. A replacement changes the page, not the
|
|
639
|
+
* codec decision, so this is the only source for them: the fresh capture result carries no codec or acceleration of its own.
|
|
640
|
+
*/
|
|
641
|
+
const oldCaptureCodec = isCaptureIdentity(stream) ? stream.identity.captureCodec : null;
|
|
642
|
+
const oldHardwareAccelerated = isCaptureIdentity(stream) && stream.identity.hardwareAccelerated;
|
|
643
|
+
/* Where this attempt stands, from the perspective of the fresh pipeline's own FFmpeg error callback. One tri-state rather than a pair of flags, so "failed
|
|
644
|
+
* and committed at once" is not something the code can express. It is held on a small record because the callback and the frame awaiting establishment both
|
|
645
|
+
* have to see the same value across an await, which a captured local does not give the reader or the compiler.
|
|
646
|
+
*
|
|
647
|
+
* The phase is local to a single attempt, moves at most once, and is never reset: the monitor's retry runs this function again and gets a fresh one. While
|
|
648
|
+
* the attempt is pending, an FFmpeg fault belongs to a pipeline that is not the stream yet, so it must not reach the circuit breaker - the stream it would
|
|
649
|
+
* terminate is the one still running happily on the old page. Once the swap has committed, the new pipeline IS the stream and a fault is treated exactly as
|
|
650
|
+
* it always has been.
|
|
651
|
+
*/
|
|
652
|
+
const attempt = { phase: "pending" };
|
|
653
|
+
// Create the replacement page and acquire its capture, with the existing pipeline still producing throughout.
|
|
563
654
|
let captureResult;
|
|
564
655
|
try {
|
|
565
|
-
captureResult = await createPageWithCapture({
|
|
656
|
+
captureResult = await deps.createPageWithCapture({
|
|
566
657
|
comment: metadataComment,
|
|
567
|
-
numericStreamId,
|
|
568
658
|
onFFmpegError: (error) => {
|
|
659
|
+
if (attempt.phase !== "committed") {
|
|
660
|
+
LOG.warn("The replacement capture's FFmpeg failed before it took over: %s. The existing capture continues.", formatError(error));
|
|
661
|
+
attempt.phase = "failed";
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
569
664
|
LOG.error("FFmpeg error during tab replacement recovery: %s.", formatError(error));
|
|
570
665
|
onCircuitBreak();
|
|
571
666
|
},
|
|
@@ -576,42 +671,32 @@ function createTabReplacementHandler(numericStreamId, streamId, channelName, url
|
|
|
576
671
|
});
|
|
577
672
|
}
|
|
578
673
|
catch (error) {
|
|
579
|
-
LOG.warn("
|
|
674
|
+
LOG.warn("Could not establish a replacement capture for this stream: %s. The existing capture continues.", formatError(error));
|
|
580
675
|
return null;
|
|
581
676
|
}
|
|
582
677
|
LOG.debug("timing:tab", "New page with capture created. (+%sms)", tabElapsed());
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
678
|
+
/* Re-check before committing. The awaited establishment above yields the event loop for the length of a tune, so two things can have happened meanwhile: the
|
|
679
|
+
* stream can have terminated - in which case termination found exactly one pipeline in the registry, the old one, and disposed it - or the fresh pipeline's
|
|
680
|
+
* own FFmpeg can have failed, which the swap-aware callback recorded. Either way the fresh page and capture are discarded in the teardown order every other
|
|
681
|
+
* site uses (dispose the session first so chrome.tabCapture releases cleanly, then unregister the managed page, then close it) and the stream is left exactly
|
|
682
|
+
* as it was. A discarded session's FFmpeg is killed by that dispose, which is what keeps the discard silent.
|
|
683
|
+
*/
|
|
684
|
+
const entry = getStream(numericStreamId);
|
|
685
|
+
if (!entry || (attempt.phase === "failed")) {
|
|
588
686
|
captureResult.captureSession.dispose();
|
|
589
|
-
unregisterManagedPage(captureResult.page);
|
|
687
|
+
deps.unregisterManagedPage(captureResult.page);
|
|
590
688
|
if (!captureResult.page.isClosed()) {
|
|
591
689
|
captureResult.page.close().catch((error) => {
|
|
592
690
|
LOG.debug("recovery:tab", "Page close error during aborted tab replacement: %s.", formatError(error));
|
|
593
691
|
});
|
|
594
692
|
}
|
|
595
|
-
LOG.debug("recovery:tab", "
|
|
693
|
+
LOG.debug("recovery:tab", "Abandoned the tab replacement for stream %s - discarded the fresh page and capture session.", streamId);
|
|
596
694
|
return null;
|
|
597
695
|
}
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
...buildSegmenterTerminationHandlers(numericStreamId, channelName, { logSuffix: "after tab replacement", reasonSuffix: "after recovery" }),
|
|
603
|
-
pendingDiscontinuity: true,
|
|
604
|
-
previousInitSegment: currentInitSegment,
|
|
605
|
-
priorSessionStats: currentSessionStats,
|
|
606
|
-
startingInitVersion: currentInitVersion,
|
|
607
|
-
startingSegmentIndex: currentSegmentIndex,
|
|
608
|
-
streamId: numericStreamId
|
|
609
|
-
});
|
|
610
|
-
// Wire the new segmenter into the new capture session (attachSegmenter pipes the session's capture output into it), then install the session and page on the
|
|
611
|
-
// registry entry.
|
|
612
|
-
captureResult.captureSession.attachSegmenter(newSegmenter);
|
|
613
|
-
stream.captureSession = captureResult.captureSession;
|
|
614
|
-
stream.page = captureResult.page;
|
|
696
|
+
// The swap, and the commit that follows it in the same synchronous frame. Nothing separates the two, so the error callback can never observe a stream whose
|
|
697
|
+
// registry entry has moved on while the attempt still reads as pending.
|
|
698
|
+
spliceReplacementCapture({ captureResult, channelName, deps, entry, numericStreamId, oldCaptureCodec, oldHardwareAccelerated, oldPage: entry.page, streamId });
|
|
699
|
+
attempt.phase = "committed";
|
|
615
700
|
LOG.info("Tab replacement complete. New capture started with segment continuity.");
|
|
616
701
|
LOG.debug("timing:tab", "Tab replacement complete. Total: %sms.", tabElapsed());
|
|
617
702
|
return {
|
|
@@ -667,6 +752,8 @@ export async function initializeStream(options) {
|
|
|
667
752
|
* @returns The allocated stream IDs.
|
|
668
753
|
*/
|
|
669
754
|
function registerPendingStream(channelName, channel, clientAddress, req, codec) {
|
|
755
|
+
// The registration instant, read once at this composition point and used for the resume-index read, the entry's start time, and the preroll timer's arming.
|
|
756
|
+
const now = systemClock.now();
|
|
670
757
|
const numericStreamId = getNextStreamId();
|
|
671
758
|
const streamIdStr = generateStreamId(channelName, channel.url);
|
|
672
759
|
// Derive the base URL from the request for absolute preroll segment URLs.
|
|
@@ -675,16 +762,16 @@ function registerPendingStream(channelName, channel, clientAddress, req, codec)
|
|
|
675
762
|
const host = forwardedHost ? (forwardedHost.split(",")[0] ?? "").trim() : req.get("host");
|
|
676
763
|
const fallbackHost = CONFIG.server.host + ":" + String(CONFIG.server.port);
|
|
677
764
|
const baseUrl = protocol + "://" + (host ?? fallbackHost);
|
|
678
|
-
// Create HLS state with a deferred preroll timer
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
// state that has moved on.
|
|
765
|
+
// Create HLS state with a deferred preroll timer - a one-shot on the system clock whose handle the single disarm point disposes. The timer fires after
|
|
766
|
+
// PREROLL_DELAY_MS - if stream setup hasn't completed by then, preroll is seeded and the playlist response is unblocked. For resume streams, the preroll's
|
|
767
|
+
// MEDIA-SEQUENCE is offset by the saved segment index so it continues from the prior session's sequence range. The timer is disarmed through
|
|
768
|
+
// cancelPrerollTimer() once real content arrives or preroll state is otherwise invalidated, so it cannot fire against state that has moved on.
|
|
682
769
|
const hls = createHLSState();
|
|
683
770
|
// Snapshot the resume segment index once at registration. Both the preroll timer callback and completeStreamSetup() use this single snapshot, eliminating the TTL
|
|
684
771
|
// race that would occur if each read the resume map independently at different times.
|
|
685
|
-
hls.resumeSegmentIndex = getResumeSegmentIndex(channelName) ?? 0;
|
|
772
|
+
hls.resumeSegmentIndex = getResumeSegmentIndex(channelName, now) ?? 0;
|
|
686
773
|
// Capture the stream start time at registration. This timestamp is used for the registry's startTime field (stream age display, etc.).
|
|
687
|
-
const streamStartTime =
|
|
774
|
+
const streamStartTime = now;
|
|
688
775
|
if (isPrerollReady(codec)) {
|
|
689
776
|
// Set preroll metadata immediately at registration so the segmenter and native proxy can read it regardless of whether the deferred timer has fired. The segment
|
|
690
777
|
// count, codec, and base URL are structural properties of the preroll system - they determine segment index offsets, codec-aware URL paths, and composite playlist
|
|
@@ -693,11 +780,12 @@ function registerPendingStream(channelName, channel, clientAddress, req, codec)
|
|
|
693
780
|
hls.prerollBaseUrl = baseUrl;
|
|
694
781
|
hls.prerollCodec = codec;
|
|
695
782
|
hls.prerollSegmentCount = getPrerollSegmentCount(codec);
|
|
696
|
-
hls.prerollTimer =
|
|
783
|
+
hls.prerollTimer = systemClock.schedule(() => {
|
|
697
784
|
// Record the preroll start time and seed the initial progressive playlist. On subsequent polls, sendPlaylistResponse() regenerates the playlist with an
|
|
698
|
-
// advancing window based on elapsed time from this start time, simulating a live stream.
|
|
699
|
-
|
|
700
|
-
hls.
|
|
785
|
+
// advancing window based on elapsed time from this start time, simulating a live stream. The reveal at the fire instant is the initial window.
|
|
786
|
+
const firedAt = systemClock.now();
|
|
787
|
+
hls.prerollStartTime = firedAt;
|
|
788
|
+
hls.playlist = generatePrerollPlaylist({ baseUrl, codec, now: firedAt, prerollStartTime: firedAt, startingSequence: hls.resumeSegmentIndex });
|
|
701
789
|
hls.signalPlaylistReady();
|
|
702
790
|
}, PREROLL_DELAY_MS);
|
|
703
791
|
}
|
|
@@ -713,46 +801,51 @@ function registerPendingStream(channelName, channel, clientAddress, req, codec)
|
|
|
713
801
|
*/
|
|
714
802
|
function createPendingEntry(options) {
|
|
715
803
|
const { channel, channelName, hls, numericStreamId, streamIdStr, url } = options;
|
|
804
|
+
// The registration instant, read once so the access stamp and the entry's start time agree when the caller supplied no start of its own.
|
|
805
|
+
const now = systemClock.now();
|
|
716
806
|
registerStream({
|
|
717
|
-
captureCodec: null,
|
|
718
|
-
captureSession: null,
|
|
719
807
|
channelName: channel?.name ?? null,
|
|
720
808
|
clientAddress: options.clientAddress ?? null,
|
|
721
|
-
hardwareAccelerated: false,
|
|
722
809
|
hls,
|
|
723
810
|
id: numericStreamId,
|
|
811
|
+
identity: makePendingCaptureIdentity(),
|
|
724
812
|
info: {
|
|
725
|
-
lastPlaylistRequest:
|
|
813
|
+
lastPlaylistRequest: now,
|
|
726
814
|
storeKey: channelName
|
|
727
815
|
},
|
|
728
816
|
monitor: null,
|
|
729
817
|
mpegTsClientCount: 0,
|
|
730
|
-
nativeBandwidth: 0,
|
|
731
|
-
nativeContainer: null,
|
|
732
|
-
nativeProxy: null,
|
|
733
|
-
nativeResolution: null,
|
|
734
818
|
page: null,
|
|
735
819
|
preTuned: options.preTuned ?? false,
|
|
736
820
|
probeIdentity: null,
|
|
737
821
|
profile: null,
|
|
738
|
-
|
|
739
|
-
startTime: options.streamStartTime ?? new Date(),
|
|
822
|
+
startTime: options.streamStartTime ?? now,
|
|
740
823
|
streamIdStr,
|
|
741
|
-
streamingMode: "capture",
|
|
742
824
|
url
|
|
743
825
|
});
|
|
744
826
|
setChannelStreamId(channelName, numericStreamId);
|
|
745
827
|
}
|
|
746
828
|
/**
|
|
747
|
-
* Handles stream setup failure by marking channel health, terminating the
|
|
748
|
-
* logging because it re-throws for callers to handle. The non-blocking path (ensureChannelStream .catch) logs
|
|
829
|
+
* Handles stream setup failure by recording the failure's status on the pending entry, marking channel health, terminating the entry, and optionally logging the
|
|
830
|
+
* error. The blocking path (initializeStream) skips logging because it re-throws for callers to handle. The non-blocking path (ensureChannelStream .catch) logs
|
|
831
|
+
* because there is no outer handler.
|
|
832
|
+
*
|
|
833
|
+
* Exported for unit-test coverage of the ordering the recorded status depends on. Production callers reach this only through the setup paths above, never directly.
|
|
749
834
|
* @param numericStreamId - The pending entry's numeric stream ID.
|
|
750
835
|
* @param channelName - The channel key for health tracking and termination.
|
|
751
836
|
* @param channel - The channel definition, or undefined for ad-hoc streams (no health tracking).
|
|
752
837
|
* @param error - The error that caused the failure.
|
|
753
838
|
* @param logError - Whether to log the error. False when the caller will re-throw (blocking path), true when fire-and-forget (non-blocking path).
|
|
754
839
|
*/
|
|
755
|
-
function handleSetupFailure(numericStreamId, channelName, channel, error, logError = true) {
|
|
840
|
+
export function handleSetupFailure(numericStreamId, channelName, channel, error, logError = true) {
|
|
841
|
+
/* Record what this failure answers with before anything tears the entry down. terminateStream unregisters the entry in the same synchronous run in which it
|
|
842
|
+
* wakes every request waiting on this stream, so a request that resumes afterward finds nothing in the registry to read - the status has to be on the entry
|
|
843
|
+
* object by then. A waiting request holds its own reference to that object, taken before its wait, and reads the status through it.
|
|
844
|
+
*/
|
|
845
|
+
const entry = getStream(numericStreamId);
|
|
846
|
+
if (entry) {
|
|
847
|
+
entry.hls.setupFailure = describeSetupFailure(error);
|
|
848
|
+
}
|
|
756
849
|
// Mark channel health as failed. Only for predefined channels (channel is defined). Ad-hoc URL streams have no persistent channel identity.
|
|
757
850
|
if (channel) {
|
|
758
851
|
const failVariantKey = resolveServiceKey(channelName);
|
|
@@ -821,9 +914,7 @@ async function startNativeProxy(setup, numericStreamId, channelName, url, mpegTs
|
|
|
821
914
|
if (!refreshed) {
|
|
822
915
|
return;
|
|
823
916
|
}
|
|
824
|
-
refreshed
|
|
825
|
-
refreshed.nativeBandwidth = metadata.bandwidth;
|
|
826
|
-
refreshed.nativeResolution = metadata.resolution;
|
|
917
|
+
applyNativeQualityRefresh(refreshed, metadata);
|
|
827
918
|
},
|
|
828
919
|
page: setup.page,
|
|
829
920
|
prerollCodec: pendingForNative?.hls.prerollCodec ?? "h264",
|
|
@@ -857,7 +948,6 @@ async function startNativeProxy(setup, numericStreamId, channelName, url, mpegTs
|
|
|
857
948
|
// registration, so nulling the fields silences only sendPlaylistResponse's per-poll regeneration, not the one-shot overwrite the callback would still perform when
|
|
858
949
|
// it fires. Disarming here completes the invalidation. One residual is inherent to preroll-for-hasAudio: if the native tune already exceeded PREROLL_DELAY_MS the
|
|
859
950
|
// timer has already fired, so the nulled fields freeze the already-written preroll playlist until the proxy's first real content arrives - bounded, not a stall.
|
|
860
|
-
currentStream.captureSession = null;
|
|
861
951
|
currentStream.hls.hasAudio = nativeResult.hasAudio;
|
|
862
952
|
if (nativeResult.hasAudio) {
|
|
863
953
|
cancelPrerollTimer(currentStream.hls);
|
|
@@ -865,14 +955,19 @@ async function startNativeProxy(setup, numericStreamId, channelName, url, mpegTs
|
|
|
865
955
|
currentStream.hls.prerollCodec = null;
|
|
866
956
|
currentStream.hls.prerollSegmentCount = 0;
|
|
867
957
|
}
|
|
868
|
-
|
|
869
|
-
currentStream.
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
958
|
+
// The mode flip is one whole-identity replacement written as a pure literal, so the capture pipeline this stream no longer has cannot survive the transition.
|
|
959
|
+
currentStream.identity = {
|
|
960
|
+
captureCodec: nativeResult.codec,
|
|
961
|
+
mode: "native",
|
|
962
|
+
nativeBandwidth: nativeResult.bandwidth,
|
|
963
|
+
nativeContainer: nativeResult.container,
|
|
964
|
+
nativeProxy: nativeResult.proxy,
|
|
965
|
+
nativeResolution: nativeResult.resolution,
|
|
966
|
+
reestablishManifest
|
|
967
|
+
};
|
|
968
|
+
// The capture that was reading the compositor has ended, so this stream has no further claim on the window. Fire-and-forget: a native stream does not depend on
|
|
969
|
+
// the window at all, and the executor is serialized, so nothing here waits on a presentation change.
|
|
970
|
+
void syncWindowVisibility();
|
|
876
971
|
// Start the native proxy. Init readiness is conditional on the container. An MPEG-TS source carries its own PAT/PMT codec configuration in every segment and
|
|
877
972
|
// has no separate init to wait for, so readiness is signaled here and MPEG-TS clients are released immediately rather than blocking on waitForInitSegment()
|
|
878
973
|
// until it times out. An fMP4 source does have an init, and the store signals readiness when the video track's first one lands - video because the consumer
|
|
@@ -888,6 +983,30 @@ async function startNativeProxy(setup, numericStreamId, channelName, url, mpegTs
|
|
|
888
983
|
LOG.debug("native:coordinator", "Capture pipeline stopped for %s. Native proxy active.", channelName);
|
|
889
984
|
return { codec: nativeResult.codec, quality: formatNativeQuality(nativeResult.bandwidth, nativeResult.codec, nativeResult.resolution) };
|
|
890
985
|
}
|
|
986
|
+
/**
|
|
987
|
+
* Derives what a segmenter continues from, which on a fresh stream is nothing at all. A resume carries the prior session's timestamps and init version, and its
|
|
988
|
+
* init segment only when no preroll window precedes it - the preroll init differs from the real one, so a byte match there would suppress a discontinuity the
|
|
989
|
+
* boundary genuinely needs. Session statistics are deliberately absent: a resume from disk has none, and inventing an empty set would tell the segmenter a live
|
|
990
|
+
* prior session is continuing and count a tab replacement that never happened. The starting index accounts for both the resume offset and the preroll segment
|
|
991
|
+
* range whenever either contributes one.
|
|
992
|
+
*
|
|
993
|
+
* Each member is present only when this resume genuinely carries it, because the segmenter reads presence rather than value: an absent member and a member set
|
|
994
|
+
* to an empty or zero stand-in mean different things to it.
|
|
995
|
+
*
|
|
996
|
+
* @param options - The resume record and the preroll accounting to derive from.
|
|
997
|
+
* @returns The continuity the segmenter is constructed with.
|
|
998
|
+
*/
|
|
999
|
+
export function buildResumeContinuity(options) {
|
|
1000
|
+
const { baseSegmentIndex, prerollSegmentCount, resumeData } = options;
|
|
1001
|
+
return {
|
|
1002
|
+
...(resumeData ? {
|
|
1003
|
+
initialTrackTimestamps: resumeData.trackTimestamps,
|
|
1004
|
+
...((prerollSegmentCount === 0) ? { previousInitSegment: resumeData.initSegment } : {}),
|
|
1005
|
+
startingInitVersion: resumeData.initVersion
|
|
1006
|
+
} : {}),
|
|
1007
|
+
...((resumeData || (prerollSegmentCount > 0)) ? { startingSegmentIndex: baseSegmentIndex + prerollSegmentCount } : {})
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
891
1010
|
/**
|
|
892
1011
|
* Creates the fMP4 segmenter for capture mode streams. Reads resume data, creates the segmenter with preroll and resume configuration, and attaches it to the
|
|
893
1012
|
* capture session (which pipes the session's capture output into it). Resume data is consumed only after the segmenter is successfully attached to a non-disposed
|
|
@@ -895,37 +1014,30 @@ async function startNativeProxy(setup, numericStreamId, channelName, url, mpegTs
|
|
|
895
1014
|
* @param setup - The stream setup result from setupStream().
|
|
896
1015
|
* @param numericStreamId - The stream's numeric ID.
|
|
897
1016
|
* @param channelName - The channel key for resume data and logging.
|
|
1017
|
+
* @param now - The instant the resume data's TTL is measured against.
|
|
898
1018
|
* @returns True if the segmenter was attached, false if the stream was terminated during setup (the session was already disposed).
|
|
899
1019
|
*/
|
|
900
|
-
function createCaptureSegmenter(setup, numericStreamId, channelName) {
|
|
1020
|
+
function createCaptureSegmenter(setup, numericStreamId, channelName, now) {
|
|
901
1021
|
// Peek at resume data from a previous shutdown without consuming it. The data is consumed (deleted) only after the segmenter is successfully created and
|
|
902
1022
|
// stored in the registry. This ensures resume data survives if segmenter creation fails - the next stream start retries with the same resume state instead
|
|
903
1023
|
// of losing it and causing an HLS sequence reset.
|
|
904
|
-
const resumeData = peekResumeData(channelName);
|
|
1024
|
+
const resumeData = peekResumeData(channelName, now);
|
|
905
1025
|
const currentStream = getStream(numericStreamId);
|
|
906
1026
|
const prerollSegmentCount = currentStream?.hls.prerollSegmentCount ?? 0;
|
|
907
1027
|
// When preroll is active, use the snapshotted resume index (stored on HLS state at registration) so the segmenter's starting index is guaranteed to match
|
|
908
1028
|
// the preroll playlist's MEDIA-SEQUENCE offset. When preroll is inactive, use the resume data directly - no preroll playlist to be consistent with.
|
|
909
1029
|
const baseSegmentIndex = (prerollSegmentCount > 0) ? (currentStream?.hls.resumeSegmentIndex ?? 0) : (resumeData?.segmentIndex ?? 0);
|
|
910
|
-
|
|
911
|
-
// segmenter includes preroll entries in its sliding window via the compositor
|
|
912
|
-
//
|
|
913
|
-
// suppressed by an init-match comparison against the prior session.
|
|
1030
|
+
const continuity = buildResumeContinuity({ baseSegmentIndex, prerollSegmentCount, resumeData });
|
|
1031
|
+
// Create the fMP4 segmenter. When preroll is active, it includes preroll entries in its sliding window via the compositor, and the pending discontinuity at the
|
|
1032
|
+
// preroll-to-real boundary is always needed.
|
|
914
1033
|
const segmenter = createFMP4Segmenter({
|
|
915
|
-
|
|
916
|
-
initialTrackTimestamps: resumeData.trackTimestamps,
|
|
917
|
-
...((prerollSegmentCount === 0) ? { previousInitSegment: resumeData.initSegment } : {}),
|
|
918
|
-
startingInitVersion: resumeData.initVersion
|
|
919
|
-
} : {}),
|
|
1034
|
+
continuity,
|
|
920
1035
|
...((prerollSegmentCount > 0) ? {
|
|
921
1036
|
prerollBaseUrl: currentStream?.hls.prerollBaseUrl ?? null,
|
|
922
1037
|
prerollCodec: currentStream?.hls.prerollCodec ?? "h264",
|
|
923
1038
|
prerollSegmentCount
|
|
924
1039
|
} : {}),
|
|
925
|
-
...((resumeData || (prerollSegmentCount > 0)) ? {
|
|
926
|
-
pendingDiscontinuity: true,
|
|
927
|
-
startingSegmentIndex: baseSegmentIndex + prerollSegmentCount
|
|
928
|
-
} : {}),
|
|
1040
|
+
...((resumeData || (prerollSegmentCount > 0)) ? { pendingDiscontinuity: true } : {}),
|
|
929
1041
|
...buildSegmenterTerminationHandlers(numericStreamId, channelName),
|
|
930
1042
|
streamId: numericStreamId
|
|
931
1043
|
});
|
|
@@ -964,7 +1076,7 @@ async function completeStreamSetup(options) {
|
|
|
964
1076
|
* stamp assembled from a frame holding only part of the binding would silently describe a different stream than the one the tune reaches.
|
|
965
1077
|
*
|
|
966
1078
|
* The effective profile is the query override when one was supplied, otherwise the stored profile: an overridden tune reaches a different stream than the
|
|
967
|
-
* stored profile would, so the stamp
|
|
1079
|
+
* stored profile would, so the stamp differs and an entry cached under the stored profile re-probes once under the override rather than answering
|
|
968
1080
|
* for a page it never described.
|
|
969
1081
|
*/
|
|
970
1082
|
const probeIdentity = {
|
|
@@ -1010,6 +1122,8 @@ async function completeStreamSetup(options) {
|
|
|
1010
1122
|
streamId: streamIdStr,
|
|
1011
1123
|
url
|
|
1012
1124
|
}, onCircuitBreak);
|
|
1125
|
+
// The tune-complete instant, read once here so the resume data's TTL check and the tune-time line below measure against the same moment.
|
|
1126
|
+
const now = systemClock.now();
|
|
1013
1127
|
// Fill in the pending registry entry with the real browser state. The entry was registered in Phase 1 (registerPendingStream or initializeStream).
|
|
1014
1128
|
const stream = getStream(numericStreamId);
|
|
1015
1129
|
if (!stream) {
|
|
@@ -1019,7 +1133,9 @@ async function completeStreamSetup(options) {
|
|
|
1019
1133
|
});
|
|
1020
1134
|
return null;
|
|
1021
1135
|
}
|
|
1022
|
-
|
|
1136
|
+
// Setup produced the capture pipeline the entry was registered without. The identity is still the pending capture shape at this point - the native upgrade runs
|
|
1137
|
+
// later in this same function - so the write installs the pipeline and leaves the encoder facts to the completion write further down.
|
|
1138
|
+
stream.identity = { ...makePendingCaptureIdentity(), captureSession: setup.captureSession };
|
|
1023
1139
|
stream.monitor = setup.monitor;
|
|
1024
1140
|
stream.page = setup.page;
|
|
1025
1141
|
stream.probeIdentity = setup.probeIdentity;
|
|
@@ -1045,7 +1161,7 @@ async function completeStreamSetup(options) {
|
|
|
1045
1161
|
}
|
|
1046
1162
|
// If native streaming was not viable or not attempted, create the fMP4 segmenter for capture mode.
|
|
1047
1163
|
if (streamingMode === "capture") {
|
|
1048
|
-
if (!createCaptureSegmenter(setup, numericStreamId, channelName)) {
|
|
1164
|
+
if (!createCaptureSegmenter(setup, numericStreamId, channelName, now)) {
|
|
1049
1165
|
return null;
|
|
1050
1166
|
}
|
|
1051
1167
|
}
|
|
@@ -1057,7 +1173,7 @@ async function completeStreamSetup(options) {
|
|
|
1057
1173
|
const captureMode = (streamingMode === "native") ? ("native HLS" + nativeQuality) :
|
|
1058
1174
|
(CONFIG.streaming.captureMode === "ffmpeg" ? "FFmpeg [" + ffmpegCodec + "]" : "Native fMP4");
|
|
1059
1175
|
const displayName = channel?.name ?? url;
|
|
1060
|
-
const tuneTime = ((
|
|
1176
|
+
const tuneTime = ((now - setup.startTime) / 1000).toFixed(1);
|
|
1061
1177
|
LOG.info("Streaming %s: %s, %s, %s. Tuned in %ss%s.", displayName, setup.serviceName, setup.profileName, captureMode, tuneTime, setup.directTune ? " (direct)" : "");
|
|
1062
1178
|
// Mark channel health as successful. Only for predefined channels (channel is defined). Ad-hoc URL streams have no persistent channel identity. Domain
|
|
1063
1179
|
// auth is conditionally skipped when the provider module defines validateTune and the tuned channel does not prove paid access (e.g., Sling Freestream).
|
|
@@ -1069,14 +1185,14 @@ async function completeStreamSetup(options) {
|
|
|
1069
1185
|
const markAuth = !provider?.validateTune || provider.validateTune(channel.channelSelector ?? channelName);
|
|
1070
1186
|
markChannelSuccess(channelName, successAuthDomain, markAuth);
|
|
1071
1187
|
}
|
|
1072
|
-
//
|
|
1073
|
-
//
|
|
1188
|
+
// Record the codec and hardware acceleration this stream settled on, then emit the stream added event for the dashboard. The registry write is capture-only:
|
|
1189
|
+
// a native upgrade already wrote its manifest-derived codec into the native identity, and hardware acceleration is a capture fact the status projection
|
|
1190
|
+
// answers false for on a native stream. The two DTO values below are computed for both modes because the wire shape stays flat.
|
|
1074
1191
|
const streamCodec = (streamingMode === "native") ? nativeCodec : effectiveCodec.toUpperCase();
|
|
1075
1192
|
const hwAccelerated = (streamingMode !== "native") && captureHwAccel;
|
|
1076
1193
|
const currentEntry = getStream(numericStreamId);
|
|
1077
|
-
if (currentEntry) {
|
|
1078
|
-
currentEntry.
|
|
1079
|
-
currentEntry.hardwareAccelerated = hwAccelerated;
|
|
1194
|
+
if (currentEntry && isCaptureIdentity(currentEntry)) {
|
|
1195
|
+
currentEntry.identity = { ...currentEntry.identity, captureCodec: streamCodec, hardwareAccelerated: hwAccelerated };
|
|
1080
1196
|
}
|
|
1081
1197
|
emitStreamAdded(createInitialStreamStatus({
|
|
1082
1198
|
captureCodec: streamCodec,
|
|
@@ -1114,16 +1230,11 @@ async function startHLSStream(channelName, url, req, res, channel) {
|
|
|
1114
1230
|
return await initializeStream({ channel, channelName, clientAddress, profileOverride, url });
|
|
1115
1231
|
}
|
|
1116
1232
|
catch (error) {
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
res.setHeader("X-HDHomeRun-Error", "All Tuners In Use");
|
|
1121
|
-
}
|
|
1122
|
-
res.status(error.statusCode).send(error.userMessage);
|
|
1123
|
-
return null;
|
|
1233
|
+
// A StreamSetupError is an expected refusal whose message is already meant for the client, so only an unexpected failure earns an error log here.
|
|
1234
|
+
if (!(error instanceof StreamSetupError)) {
|
|
1235
|
+
LOG.error("Unexpected error during HLS stream setup: %s.", formatError(error));
|
|
1124
1236
|
}
|
|
1125
|
-
|
|
1126
|
-
res.status(500).send("Internal server error.");
|
|
1237
|
+
sendSetupFailure(describeSetupFailure(error), res);
|
|
1127
1238
|
return null;
|
|
1128
1239
|
}
|
|
1129
1240
|
}
|
|
@@ -1132,10 +1243,10 @@ async function startHLSStream(channelName, url, req, res, channel) {
|
|
|
1132
1243
|
* Returns all streams that have exceeded the idle timeout and have no active MPEG-TS clients. Pretuned streams are excluded - they have no clients by design and
|
|
1133
1244
|
* the pretune module manages their lifecycle via a safety timeout. The result is sorted by last access time (oldest first) so callers can efficiently pick the
|
|
1134
1245
|
* longest-idle stream for reclamation.
|
|
1246
|
+
* @param now - The instant each stream's idle window is measured against.
|
|
1135
1247
|
* @returns Idle streams sorted by last access time ascending (oldest first).
|
|
1136
1248
|
*/
|
|
1137
|
-
function getIdleStreams() {
|
|
1138
|
-
const now = Date.now();
|
|
1249
|
+
function getIdleStreams(now) {
|
|
1139
1250
|
return getAllStreams()
|
|
1140
1251
|
.filter((stream) => !stream.preTuned && (stream.mpegTsClientCount === 0) && ((now - stream.info.lastPlaylistRequest) >= CONFIG.hls.idleTimeout))
|
|
1141
1252
|
.sort((a, b) => a.info.lastPlaylistRequest - b.info.lastPlaylistRequest);
|
|
@@ -1144,7 +1255,7 @@ function getIdleStreams() {
|
|
|
1144
1255
|
* Checks for idle streams and terminates them. Called periodically by the idle detection interval.
|
|
1145
1256
|
*/
|
|
1146
1257
|
export function cleanupIdleStreams() {
|
|
1147
|
-
const idle = getIdleStreams();
|
|
1258
|
+
const idle = getIdleStreams(systemClock.now());
|
|
1148
1259
|
for (const stream of idle) {
|
|
1149
1260
|
terminateStream(stream.id, stream.info.storeKey, "no active clients");
|
|
1150
1261
|
}
|
|
@@ -1159,7 +1270,7 @@ export function cleanupIdleStreams() {
|
|
|
1159
1270
|
* @returns True if a stream was reclaimed, false if no idle streams exist.
|
|
1160
1271
|
*/
|
|
1161
1272
|
function reclaimIdleStream() {
|
|
1162
|
-
const idle = getIdleStreams();
|
|
1273
|
+
const idle = getIdleStreams(systemClock.now());
|
|
1163
1274
|
if (idle.length === 0) {
|
|
1164
1275
|
return false;
|
|
1165
1276
|
}
|