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/setup.js
CHANGED
|
@@ -50,31 +50,31 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
50
50
|
var e = new Error(message);
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
|
-
import { BrowserSupersededError, BrowserUnavailableError, getBrowserInstance,
|
|
54
|
-
import {
|
|
53
|
+
import { BrowserCaptureImpairedError, BrowserSupersededError, BrowserUnavailableError, acquireCaptureStream, confirmSharedWindowPlacement, emulateCaptureSurface, emulateLayoutSurface, getBrowserInstance, getCaptureImpairment, getCurrentBrowser, installActivationHeal, noteBrowserCaptureImpaired, registerManagedPage, resolveSharedWindowCarrier, setCaptureProbe, syncWindowVisibility, unregisterManagedPage } from "../browser/index.js";
|
|
54
|
+
import { CAPTURE_SOURCE_UNAVAILABLE_MESSAGE, isChannelSelectionProfile } from "../types/index.js";
|
|
55
|
+
import { CaptureAbandonedError, CaptureTurnTimeoutError, createCaptureLock } from "./captureLock.js";
|
|
55
56
|
import { FINALIZE_SETTLE_DELAY, installManifestInterceptor } from "../browser/manifestInterceptor.js";
|
|
56
|
-
import { LOG, delay, extractDomain, formatError, getStreamContext,
|
|
57
|
-
import { getAllStreams, getNextStreamId } from "./registry.js";
|
|
57
|
+
import { LOG, chromeFetch, delay, extractDomain, formatError, getStreamContext, maxRetryDuration, registerAbortController, resolveFFmpegPath, retryOperation, runWithStreamContext, spawnFFmpeg, startTimer, timeoutSignal, waitWithTimeout } from "../utils/index.js";
|
|
58
58
|
import { getAuthDomainForChannel, getServiceDisplayName, resolveServiceKey } from "../config/services.js";
|
|
59
|
-
import {
|
|
59
|
+
import { getDomainConfig, getProfileForChannel, getProfileForUrl, resolveProfile } from "../config/profiles.js";
|
|
60
60
|
import { getProviderByStrategy, invalidateDirectUrl, resolveDirectUrl } from "../browser/channelSelection.js";
|
|
61
61
|
import { initializePlayback, injectVideoSelector, muteExistingVideos, navigateToPage } from "../browser/video.js";
|
|
62
62
|
import { CONFIG } from "../config/index.js";
|
|
63
|
-
import { chromeFetch } from "../utils/index.js";
|
|
64
63
|
import { createCaptureSession } from "./captureSession.js";
|
|
64
|
+
import { getBuiltinProfile } from "../config/sites.js";
|
|
65
65
|
import { getCachedEncryption } from "../native/probe.js";
|
|
66
66
|
import { getCaptureMimeType } from "./codec.js";
|
|
67
67
|
import { getDomainAuthState } from "../config/health.js";
|
|
68
|
-
import {
|
|
69
|
-
import { getPresetViewport } from "../config/presets.js";
|
|
68
|
+
import { getNextStreamId } from "./registry.js";
|
|
70
69
|
import { getUserProfiles } from "../config/userProfiles.js";
|
|
71
70
|
import { isCaptureInfrastructureError } from "./recovery.js";
|
|
72
|
-
import { isChannelSelectionProfile } from "../types/index.js";
|
|
73
|
-
import { minimizeWindow } from "../browser/cdp.js";
|
|
74
71
|
import { monitorPlaybackHealth } from "./monitor.js";
|
|
75
72
|
import { mutateChannels } from "../config/userChannels.js";
|
|
73
|
+
import { openSharedWindowTab } from "../browser/tabSelection.js";
|
|
76
74
|
import { pipeline } from "node:stream/promises";
|
|
75
|
+
import { reaffirmCaptureSurface } from "../browser/cdp.js";
|
|
77
76
|
import { startOverlayHandling } from "../browser/consent.js";
|
|
77
|
+
import { systemClock } from "homebridge-plugin-utils";
|
|
78
78
|
/* This module contains the common stream setup logic for HLS streaming. The core logic is split into two functions:
|
|
79
79
|
*
|
|
80
80
|
* 1. createPageWithCapture(): Creates a browser page, starts media capture, navigates to the URL, and sets up video playback. This is the reusable core that both
|
|
@@ -99,8 +99,8 @@ import { startOverlayHandling } from "../browser/consent.js";
|
|
|
99
99
|
*/
|
|
100
100
|
// Native fMP4 capture uses MP4/AAC for direct HLS segmentation without transcoding.
|
|
101
101
|
const NATIVE_FMP4_MIME_TYPE = "video/mp4;codecs=avc1,mp4a.40.2";
|
|
102
|
-
// Capture initialization is serialized through a task-scoped lock. Chrome's tabCapture extension can only initialize one capture at a time -
|
|
103
|
-
//
|
|
102
|
+
// Capture initialization is serialized through a task-scoped lock. Chrome's tabCapture extension can only initialize one capture at a time - a second start while
|
|
103
|
+
// one is in flight fails with "Cannot capture a tab with an active stream" - so every acquisition runs as a task on this one process-wide lock, which holds the
|
|
104
104
|
// turn until the task's promise settles. Once a capture is established it runs concurrently with other captures without issue.
|
|
105
105
|
// The wedge-derivation policy for the capture lock. A task held past max(CAPTURE_WEDGE_FLOOR_MS, deadline + CAPTURE_WEDGE_MARGIN_MS) without settling invokes its
|
|
106
106
|
// onWedge callback so the call site can route it into browser recovery. The floor is the wedge bound at the normal navigation timeout; the margin keeps the wedge
|
|
@@ -109,15 +109,21 @@ const CAPTURE_WEDGE_FLOOR_MS = 30000;
|
|
|
109
109
|
const CAPTURE_WEDGE_MARGIN_MS = 5000;
|
|
110
110
|
// The one production capture lock. It takes no config-derived values: setup.ts's module body runs before initializeConfiguration(), and streaming.* saves mutate the
|
|
111
111
|
// live CONFIG binding mid-process, so every timing bound is read per call at the call sites instead.
|
|
112
|
-
const captureLock = createCaptureLock({ clock:
|
|
113
|
-
// The
|
|
114
|
-
// Destroying the stream
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
const captureLock = createCaptureLock({ clock: systemClock, wedgeFloorMs: CAPTURE_WEDGE_FLOOR_MS, wedgeMarginMs: CAPTURE_WEDGE_MARGIN_MS });
|
|
113
|
+
// The bound on how long the capture extension may take to confirm a recording stopped, after a raw capture stream is destroyed and before the owning page is
|
|
114
|
+
// closed. Destroying the stream sends the stop request; the extension answers by closing its socket once the recorder has stopped, its tracks have stopped, and
|
|
115
|
+
// its pending sends have drained, and that answer is what CaptureStream.stopped carries. The stop has to land while the browser is still connected, or Chrome's
|
|
116
|
+
// tabCapture state lingers and the next acquisition draws "Cannot capture a tab with an active stream". The ceiling bounds the case where the answer can never
|
|
117
|
+
// arrive - a browser that is already gone - so a page close is never held hostage to it.
|
|
118
|
+
const STOP_RECORDING_CEILING_MS = 3000;
|
|
119
|
+
const STOP_RECORDING_CEILING_MESSAGE = "The capture extension did not confirm the recording stopped in time.";
|
|
120
|
+
// The slack the probe's teardown gets beyond the stop confirmation itself: the managed-page unregister and the page close that follow it.
|
|
121
|
+
const PAGE_CLOSE_MARGIN_MS = 2000;
|
|
122
|
+
// The teardown allowance the mid-life probe's outer lock deadline adds over its acquisition criterion bound. The pass/fail criterion times the acquisition alone;
|
|
123
|
+
// this allowance lets the destroy-and-confirm teardown complete inside the turn without counting against that criterion. It is derived from the stop ceiling it
|
|
124
|
+
// has to outlast rather than written as a second number beside it, so the two cannot drift apart. A teardown that hangs past the allowance trips the outer
|
|
125
|
+
// deadline and is reported as a capture-infrastructure failure.
|
|
126
|
+
const PROBE_TEARDOWN_ALLOWANCE_MS = STOP_RECORDING_CEILING_MS + PAGE_CLOSE_MARGIN_MS;
|
|
121
127
|
// The playback-initialization safety-net timeout. Channel selection plus video setup runs after navigation with no outer timeout racing its internal click
|
|
122
128
|
// retries; for guideGrid strategies a selection failure triggers an overlay dismiss and retry, which doubles the channel-selection budget. 45 seconds accommodates
|
|
123
129
|
// that retry while still preventing a pathological hang if multiple internal timeouts chain sequentially. Consumed by both the phase-2 race and the interception
|
|
@@ -127,35 +133,30 @@ const PLAYBACK_INIT_TIMEOUT = 45000;
|
|
|
127
133
|
// resize and minimize, setupStream's pre-verification work) plus true slack. The interception window is a leak bound for a tune that dies without unwinding, not a
|
|
128
134
|
// latency bound - no healthy path waits on it - so its generosity costs nothing.
|
|
129
135
|
const INTERCEPTION_BUDGET_MARGIN_MS = 5000;
|
|
130
|
-
// The caller-visible capture-timeout messages, each defined once so the lock's deadline errors and any
|
|
131
|
-
// isCaptureInfrastructureError via its "timed out" substring, so they are exported for the classification
|
|
136
|
+
// The caller-visible capture-timeout messages, each defined once so the lock's deadline errors and any assertion test read the exact text. Both match
|
|
137
|
+
// isCaptureInfrastructureError via its "timed out" substring, so they are exported for the classification assertion that locks that contract.
|
|
132
138
|
export const STREAM_INIT_TIMEOUT_MESSAGE = "Stream initialization timed out.";
|
|
133
139
|
export const CAPTURE_PROBE_TIMEOUT_MESSAGE = "Capture probe timed out.";
|
|
134
140
|
/**
|
|
135
|
-
* Retires a raw capture stream: destroys it
|
|
136
|
-
*
|
|
137
|
-
* retire a capture stream produced after its caller had already abandoned the turn. It is the async sibling of captureSession.ts's disposer, which tears the
|
|
138
|
-
* FFmpeg pipeline down synchronously: this helper awaits the
|
|
139
|
-
* abandoned paths where the page may already be closed, so the
|
|
140
|
-
*
|
|
141
|
-
*
|
|
141
|
+
* Retires a raw capture stream: destroys it so the stop request goes out while the browser is still connected, then waits for the extension to confirm the
|
|
142
|
+
* recording stopped before the caller closes the page. This is the destroy-and-confirm core shared by the mid-life probe's success teardown and every path that
|
|
143
|
+
* must retire a capture stream produced after its caller had already abandoned the turn. It is the async sibling of captureSession.ts's disposer, which tears the
|
|
144
|
+
* composed FFmpeg pipeline down synchronously: this helper awaits the confirmation because a page close follows it directly, and unlike the composed disposer it
|
|
145
|
+
* is used on abandoned paths where the page may already be closed, so the stop side is then best-effort.
|
|
146
|
+
*
|
|
147
|
+
* It never throws. A confirmation that does not arrive inside the ceiling is warned about and then stepped past, because holding a page open for a browser that
|
|
148
|
+
* has stopped answering helps nobody.
|
|
149
|
+
* @param stream - The raw capture stream to retire.
|
|
150
|
+
* @param clock - Clock bounding the stop confirmation. Defaults to the system clock; tests inject a virtual clock.
|
|
142
151
|
*/
|
|
143
|
-
async function retireRawStream(stream, clock =
|
|
152
|
+
export async function retireRawStream(stream, clock = systemClock) {
|
|
144
153
|
stream.destroy();
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
* buffered file write and lose the diagnostic that explains the restart. Every caller throws or exits its own path right after, so no further capture work runs in the
|
|
152
|
-
* interim.
|
|
153
|
-
* @param siteContext - A short phrase naming where the stale mutex surfaced, attached as structured log context.
|
|
154
|
-
*/
|
|
155
|
-
function escalateStaleCaptureMutex(siteContext) {
|
|
156
|
-
LOG.error("Stale capture state detected. puppeteer-stream's internal capture mutex is now permanently locked and the capture system is unrecoverable. Exiting so " +
|
|
157
|
-
"the service manager can restart with a clean module state.", { site: siteContext });
|
|
158
|
-
setTimeout(() => process.exit(1), 100);
|
|
154
|
+
try {
|
|
155
|
+
await waitWithTimeout(stream.stopped, STOP_RECORDING_CEILING_MS, { clock, reason: new Error(STOP_RECORDING_CEILING_MESSAGE) });
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
LOG.warn("The capture extension did not confirm the recording stopped within %dms; closing the page regardless.", STOP_RECORDING_CEILING_MS);
|
|
159
|
+
}
|
|
159
160
|
}
|
|
160
161
|
/**
|
|
161
162
|
* Thrown by the capture-lock task when it finds the page already closed at the instant its turn is granted (a browser crash during the turn-wait). createPageWithCapture
|
|
@@ -185,12 +186,12 @@ export class DirectUrlEstablishmentError extends Error {
|
|
|
185
186
|
// Maximum number of times createPageWithCapture() will retry when it detects that the page was closed while waiting for its turn on the capture lock (e.g., due to a
|
|
186
187
|
// browser crash). An explicit guard prevents unbounded recursion.
|
|
187
188
|
const MAX_PAGE_CLOSED_RETRIES = 3;
|
|
188
|
-
// Maximum time in milliseconds to wait for a single capture probe's
|
|
189
|
+
// Maximum time in milliseconds to wait for a single capture probe's acquisition to respond. Shared by the launch-gate verification (verifyCaptureSystem) and the
|
|
189
190
|
// mid-life re-verification, so both tiers exercise the capture path with the same bound.
|
|
190
191
|
const CAPTURE_PROBE_TIMEOUT_MS = 5000;
|
|
191
|
-
// Wire the capture-readiness probe into the browser launch gate. setup.ts owns
|
|
192
|
-
//
|
|
193
|
-
// cycle, mirroring the
|
|
192
|
+
// Wire the capture-readiness probe into the browser launch gate. setup.ts owns the capture lock and the readiness probe; browser/index.ts owns the launch
|
|
193
|
+
// lifecycle. Injecting verifyCaptureSystem here (setup.ts already depends on browser/index.ts) keeps the dependency one-directional and breaks the
|
|
194
|
+
// cycle, mirroring the loginDeps boundary between login.ts and index.ts. It runs once at module load, before any browser launch.
|
|
194
195
|
setCaptureProbe(verifyCaptureSystem);
|
|
195
196
|
/**
|
|
196
197
|
* Error thrown when stream setup fails. Includes HTTP status code and user-friendly message for the response.
|
|
@@ -305,7 +306,14 @@ function disposePage(page) {
|
|
|
305
306
|
});
|
|
306
307
|
}
|
|
307
308
|
}
|
|
308
|
-
const defaultCreatePageWithCaptureDeps = {
|
|
309
|
+
const defaultCreatePageWithCaptureDeps = { acquireCaptureStream, awaitCaptureVerdict: noteClassifiedCaptureFailure,
|
|
310
|
+
emulateCaptureSurface, getCurrentBrowser, installActivationHeal, openSharedWindowTab, reaffirmCaptureSurface, spawnFFmpeg, startOverlayHandling,
|
|
311
|
+
syncWindowVisibility };
|
|
312
|
+
/* The window-topology answers the open primitive needs and cannot reach for itself: tabSelection.ts speaks to the capture extension alone, so the CDP-side carrier
|
|
313
|
+
* resolution and placement confirmation arrive from here, where both modules are already in view. One record, referenced by both call sites, because the two call
|
|
314
|
+
* sites want the identical pair.
|
|
315
|
+
*/
|
|
316
|
+
const SHARED_WINDOW_TOPOLOGY = { confirmPlacement: confirmSharedWindowPlacement, resolveCarrier: resolveSharedWindowCarrier };
|
|
309
317
|
/**
|
|
310
318
|
* Creates a browser page with media capture and navigates to the URL. This is the reusable core function used by both initial stream setup and tab replacement
|
|
311
319
|
* recovery. It handles:
|
|
@@ -313,6 +321,8 @@ const defaultCreatePageWithCaptureDeps = { getCurrentBrowser, getStream, startOv
|
|
|
313
321
|
* - Initializing media capture (native fMP4 or Matroska+FFmpeg)
|
|
314
322
|
* - Navigating to the URL with retry
|
|
315
323
|
* - Setting up video playback via navigateToPage() + initializePlayback()
|
|
324
|
+
* - Re-entering itself once, on a fresh page through a freshly acquired browser, when Chrome refuses the capture start and the mid-life probe's verdict says a
|
|
325
|
+
* second attempt has something to start from
|
|
316
326
|
*
|
|
317
327
|
* The caller is responsible for:
|
|
318
328
|
* - Creating the segmenter and attaching it to the capture session via captureSession.attachSegmenter()
|
|
@@ -322,7 +332,7 @@ const defaultCreatePageWithCaptureDeps = { getCurrentBrowser, getStream, startOv
|
|
|
322
332
|
*
|
|
323
333
|
* @param options - Options for page and capture creation.
|
|
324
334
|
* @param deps - The injected browser and overlay-poll collaborators; defaults to defaultCreatePageWithCaptureDeps. Threaded so a test drives this function without a
|
|
325
|
-
* live Chrome by substituting the shared-browser accessor, the capture
|
|
335
|
+
* live Chrome by substituting the shared-browser accessor, the capture acquisition, and the static-capture overlay poll.
|
|
326
336
|
* @returns The page, context, and capture session (which owns the raw capture stream and any FFmpeg child).
|
|
327
337
|
* @throws Error if page creation, capture initialization, or navigation fails.
|
|
328
338
|
*/
|
|
@@ -330,23 +340,39 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
|
|
|
330
340
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
331
341
|
try {
|
|
332
342
|
const captureElapsed = startTimer();
|
|
333
|
-
const { comment,
|
|
343
|
+
const { comment, onFFmpegError, profile, streamId, url } = options;
|
|
344
|
+
/* Bring the window on screen before anything else. Capture reads the compositor's output for the shared window, and that output is only composed correctly for a
|
|
345
|
+
* window the desktop is presenting, so the sync has to land ahead of capture acquisition rather than alongside it. The pending registry entry is already in
|
|
346
|
+
* capture mode by the time any caller reaches here, on both request paths, so the policy reads capture-active and this resolves to a window on screen.
|
|
347
|
+
*/
|
|
348
|
+
await deps.syncWindowVisibility();
|
|
334
349
|
// Acquire every resource on a DisposableStack so that any throw - in capture initialization, navigation, or playback setup - disposes them structurally as the
|
|
335
350
|
// function unwinds, in last-acquired-first order (capture session before page, so the capture stream is destroyed and STOP_RECORDING fires while the browser is
|
|
336
351
|
// still connected, before the page closes). On success we move() the stack to disarm it and transfer ownership to the caller. This centralizes teardown that
|
|
337
352
|
// would otherwise be repeated in each failure path, and closes the navigation-path leak of the manifest interceptor.
|
|
338
353
|
const resources = __addDisposableResource(env_1, new DisposableStack(), false);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
354
|
+
/* Create browser page. The establishment goes on to start a capture, so a browser that can no longer start one refuses here, before a page exists. The page is
|
|
355
|
+
* opened as a tab of the shared window rather than wherever Chrome would place it, because a capture tab rooted in a discovery window keeps that window alive
|
|
356
|
+
* and leaves the visibility policy managing a window the capture is not in. The tab is selected for the moment it arrives and the selection goes straight back,
|
|
357
|
+
* so a user working in another tab is never moved out of it.
|
|
358
|
+
*/
|
|
359
|
+
const browser = await deps.getCurrentBrowser("capture");
|
|
360
|
+
const page = await deps.openSharedWindowTab(browser, { deps: SHARED_WINDOW_TOPOLOGY });
|
|
361
|
+
// Register the page as held in flight: the registry does not record it against the stream until setup finishes, so the mark is what keeps stale page cleanup
|
|
362
|
+
// from closing it mid-tune.
|
|
363
|
+
registerManagedPage(page, { inFlight: true });
|
|
345
364
|
resources.adopt(page, disposePage);
|
|
365
|
+
// Emulate the capture surface before anything else touches the page and well before capture acquires it: the dimensions the encoder is held to and the pixel
|
|
366
|
+
// density the compositor rasters at are both settled here, on a page nothing has overridden yet, and a failure unwinds through the DisposableStack exactly as
|
|
367
|
+
// every later step does.
|
|
368
|
+
const surface = await deps.emulateCaptureSurface(page);
|
|
346
369
|
await page.setBypassCSP(true);
|
|
347
370
|
// Inject the shared video selector helper into the browser context. This must happen before navigation so the helper is available when evaluate calls run during
|
|
348
371
|
// initializePlayback (startVideoPlayback, applyVideoStyles, verifyFullscreen, lockVolumeProperties) and subsequent health monitoring (getVideoState).
|
|
349
372
|
await injectVideoSelector(page);
|
|
373
|
+
// Install the tab-activation heal alongside it, before capture is acquired, so the page is enrolled and carries its listener from its first document onward:
|
|
374
|
+
// whichever way this tab becomes the selected one, the capture's composition moves back to the emulated surface about a second later.
|
|
375
|
+
await deps.installActivationHeal(page);
|
|
350
376
|
// Select MIME type based on capture mode. FFmpeg mode is more stable for long recordings because Chrome's native fMP4 MediaRecorder can become unstable. The
|
|
351
377
|
// codec decision (H.264 vs HEVC) is delegated to the codec module, which considers the user's allowlist and GPU hardware capabilities.
|
|
352
378
|
const useFFmpeg = CONFIG.streaming.captureMode === "ffmpeg";
|
|
@@ -361,76 +387,60 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
|
|
|
361
387
|
// Initialize media stream capture. The whole capture-init phase runs inside one try so a browser crash detected at turn grant drives the closed-page recursion,
|
|
362
388
|
// while every other failure unwinds through the DisposableStack.
|
|
363
389
|
try {
|
|
364
|
-
const viewport = getPresetViewport(CONFIG);
|
|
365
390
|
const streamOptions = {
|
|
366
391
|
audio: true,
|
|
367
392
|
audioBitsPerSecond: CONFIG.streaming.audioBitsPerSecond,
|
|
368
393
|
mimeType: captureMimeType,
|
|
369
394
|
video: true,
|
|
370
395
|
videoBitsPerSecond: CONFIG.streaming.videoBitsPerSecond,
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
396
|
+
/* The dimension bounds hold the track to exactly the surface emulateCaptureSurface declared on this page, rather than to a second read of the configured
|
|
397
|
+
* preset, so a preset saved mid-establishment cannot leave the encoder constrained to dimensions the page was never emulated at.
|
|
398
|
+
*
|
|
399
|
+
* The frame rate is constrained to a 30-60 fps band: 60 is the live-TV ceiling, and a 30 floor keeps motion smooth even when the user configures a lower
|
|
400
|
+
* rate. The ceiling is fixed at 60 while the floor follows the user's configured rate (clamped into the band), so the encoder favours the requested rate but
|
|
401
|
+
* never drops below 30. The readiness probe (attemptCaptureProbe) instead holds both bounds to a flat 30 because its acquisition fails or succeeds at the
|
|
402
|
+
* tabCapture API level before encoding matters, so a representative-but-minimal constraint set suffices there.
|
|
403
|
+
*/
|
|
375
404
|
videoConstraints: {
|
|
376
405
|
mandatory: {
|
|
377
406
|
maxFrameRate: 60,
|
|
378
|
-
maxHeight:
|
|
379
|
-
maxWidth:
|
|
407
|
+
maxHeight: surface.height,
|
|
408
|
+
maxWidth: surface.width,
|
|
380
409
|
minFrameRate: Math.max(30, Math.min(60, CONFIG.streaming.frameRate)),
|
|
381
|
-
minHeight:
|
|
382
|
-
minWidth:
|
|
410
|
+
minHeight: surface.height,
|
|
411
|
+
minWidth: surface.width
|
|
383
412
|
}
|
|
384
413
|
}
|
|
385
414
|
};
|
|
386
|
-
// Acquire the raw capture stream through the capture lock. The lock serializes
|
|
387
|
-
// this task's promise settles, so no two inits collide. Every timing bound is read here, at the call, because the module-scope lock captured nothing
|
|
415
|
+
// Acquire the raw capture stream through the capture lock. The lock serializes acquisition against every other capture init process-wide and holds the turn
|
|
416
|
+
// until this task's promise settles, so no two inits collide. Every timing bound is read here, at the call, because the module-scope lock captured nothing
|
|
417
|
+
// from CONFIG.
|
|
388
418
|
const stream = await captureLock.run(async (signal) => {
|
|
389
419
|
// If Chrome crashed while this task waited for its turn, the page is dead. Throw a typed error so the turn releases at once and the closed-page recursion runs
|
|
390
420
|
// OUTSIDE the lock, on a fresh page.
|
|
391
421
|
if (page.isClosed()) {
|
|
392
422
|
throw new PageClosedDuringTurnError();
|
|
393
423
|
}
|
|
394
|
-
// Initialize capture.
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
catch (error) {
|
|
401
|
-
if (isStaleCaptureMutexError(error)) {
|
|
402
|
-
escalateStaleCaptureMutex("stream initialization");
|
|
403
|
-
}
|
|
404
|
-
throw error;
|
|
405
|
-
}
|
|
406
|
-
// The caller deadline fired while getStream was still running: retire the stream this task just produced - destroy plus the STOP_RECORDING settle - inside the
|
|
407
|
-
// turn, then reject, so no path strands a live capture on a closing page or mistakes a retired stream for a usable one.
|
|
424
|
+
// Initialize capture. The acquisition makes one start and reports what came back, so it is handed no deadline signal of its own; the check immediately
|
|
425
|
+
// below is where this task decides what becomes of a capture that arrived after the caller stopped waiting for it.
|
|
426
|
+
const raw = await deps.acquireCaptureStream(page, streamOptions);
|
|
427
|
+
// The caller deadline fired while the acquisition was still running: retire the stream this task just produced - destroy plus the stop confirmation - inside
|
|
428
|
+
// the turn, then reject, so no path strands a live capture on a closing page or mistakes a retired stream for a usable one.
|
|
408
429
|
if (signal.aborted) {
|
|
409
|
-
await retireRawStream(raw,
|
|
430
|
+
await retireRawStream(raw, systemClock);
|
|
410
431
|
throw new CaptureAbandonedError();
|
|
411
432
|
}
|
|
412
433
|
return raw;
|
|
413
434
|
}, {
|
|
414
435
|
deadlineMessage: STREAM_INIT_TIMEOUT_MESSAGE,
|
|
415
436
|
deadlineMs: CONFIG.streaming.navigationTimeout,
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
437
|
+
/* A capture initialization that holds the lock this long is evidence about the browser rather than about the stream: the wedge fires only after this task's
|
|
438
|
+
* own deadline has already failed the tune, so the tune is lost either way and what is left to decide is what the browser can still be trusted with.
|
|
439
|
+
* Recording it marks the instance this task ran against, which refuses later capture starts at once and relaunches the browser when nothing depends on it -
|
|
440
|
+
* the same response the probe's verdict takes, reached from the other direction.
|
|
441
|
+
*/
|
|
420
442
|
onWedge: () => {
|
|
421
|
-
|
|
422
|
-
reverificationInProgress: false })) {
|
|
423
|
-
// Route the wedge into the existing browser-recovery ladder, targeting the exact instance this task ran against; invalidateBrowser no-ops if it was already
|
|
424
|
-
// superseded. A rejection is warn-logged rather than rethrown out of the wedge callback.
|
|
425
|
-
void invalidateBrowser(browser, "capture initialization wedged past the recovery bound").catch((error) => {
|
|
426
|
-
LOG.warn("Invalidating a wedged capture browser failed: %s.", formatError(error));
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
else {
|
|
430
|
-
// Other streams are active, so hold rather than tear the browser down: waiters fail fast on their own turn-wait bounds, the wedged operation settles at
|
|
431
|
-
// worst at Puppeteer's protocolTimeout, and once the other streams drain a later wedge or setup failure reaches the zero-other case.
|
|
432
|
-
LOG.warn("Capture initialization has wedged past the recovery bound, but other streams are active; holding rather than invalidating the browser.", { failingStreamId: numericStreamId });
|
|
433
|
-
}
|
|
443
|
+
void noteBrowserCaptureImpaired(browser, "capture initialization wedged past the recovery bound");
|
|
434
444
|
},
|
|
435
445
|
turnWaitMs: CONFIG.streaming.navigationTimeout
|
|
436
446
|
});
|
|
@@ -439,17 +449,26 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
|
|
|
439
449
|
// pipeline wiring below are synchronous, so the stream is owned the instant it exists.
|
|
440
450
|
let ffmpegProcess = null;
|
|
441
451
|
if (useFFmpeg) {
|
|
442
|
-
const ffmpeg = spawnFFmpeg(ffmpegBin, CONFIG.streaming.audioBitsPerSecond, (error) => {
|
|
452
|
+
const ffmpeg = deps.spawnFFmpeg(ffmpegBin, CONFIG.streaming.audioBitsPerSecond, (error) => {
|
|
443
453
|
LOG.error("FFmpeg process error: %s.", formatError(error));
|
|
444
454
|
if (onFFmpegError) {
|
|
445
455
|
onFFmpegError(error);
|
|
446
456
|
}
|
|
447
457
|
}, streamId, comment);
|
|
448
458
|
ffmpegProcess = ffmpeg;
|
|
459
|
+
/* A disposed pipeline must never fire its error callback. The two listeners below sit on the process's own streams rather than inside the spawn wrapper,
|
|
460
|
+
* so they are the two places a post-kill teardown event can still reach a caller, and each gates on the same teardown flag the wrapper's own handlers use.
|
|
461
|
+
*
|
|
462
|
+
* The gate is what keeps a tab replacement's discard silent. A replacement disposes the outgoing pipeline in the same synchronous frame it installs the
|
|
463
|
+
* incoming one, so a stray event from the killed FFmpeg lands a tick later - against a registry that already holds a healthy new pipeline - and this
|
|
464
|
+
* closure's callback would break the circuit on the very stream the replacement just saved. String filters cannot cover that: they name the errors
|
|
465
|
+
* teardown is known to produce, and the point here is every error a killed pipeline produces.
|
|
466
|
+
*/
|
|
467
|
+
const isTornDown = () => ffmpeg.isShuttingDown();
|
|
449
468
|
// Handle pipe errors on stdout. Stdin errors are handled by pipeline() below.
|
|
450
469
|
ffmpeg.stdout.on("error", (error) => {
|
|
451
470
|
const errorMessage = formatError(error);
|
|
452
|
-
if (errorMessage.includes("EPIPE")) {
|
|
471
|
+
if (errorMessage.includes("EPIPE") || isTornDown()) {
|
|
453
472
|
LOG.debug("streaming:ffmpeg", "FFmpeg stdout pipe closed: %s.", errorMessage);
|
|
454
473
|
}
|
|
455
474
|
else {
|
|
@@ -464,8 +483,9 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
|
|
|
464
483
|
// destroys the source stream, preventing "write after end" errors that would occur with .pipe().
|
|
465
484
|
pipeline(stream, ffmpeg.stdin).catch((error) => {
|
|
466
485
|
const errorMessage = formatError(error);
|
|
467
|
-
// EPIPE, "write after end", and "Premature close" errors are expected during cleanup when FFmpeg is killed or the capture stream is destroyed
|
|
468
|
-
|
|
486
|
+
// EPIPE, "write after end", and "Premature close" errors are expected during cleanup when FFmpeg is killed or the capture stream is destroyed, and a
|
|
487
|
+
// pipeline whose FFmpeg is being torn down produces whatever it produces.
|
|
488
|
+
if (errorMessage.includes("EPIPE") || errorMessage.includes("write after end") || errorMessage.includes("Premature close") || isTornDown()) {
|
|
469
489
|
return;
|
|
470
490
|
}
|
|
471
491
|
// Unexpected pipeline errors require cleanup.
|
|
@@ -493,15 +513,42 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
|
|
|
493
513
|
}
|
|
494
514
|
return await createPageWithCapture({ ...options, _pageClosedRetries: retryCount + 1 }, deps);
|
|
495
515
|
}
|
|
496
|
-
|
|
497
|
-
|
|
516
|
+
/* Chrome refuses a first capture start now and then, browser-wide, and a fresh Chrome accepts at once - so this refusal is a question about the browser, and
|
|
517
|
+
* the probe is what answers it. The wait is worth it because the answer is exactly what the tune needs: every one of its outcomes IS the retry policy, read
|
|
518
|
+
* off getCurrentBrowser("capture") on the way back in. A `captured` verdict means the browser is healthy and the re-entry meets the same instance. A `failed`
|
|
519
|
+
* verdict on an otherwise idle browser means the relaunch has already run by the time the verdict settles, so the re-entry meets the fresh one. A `failed`
|
|
520
|
+
* verdict while a stream is established or another tune is mid-setup means the relaunch could not run, and the re-entry draws the impaired refusal the
|
|
521
|
+
* supervisor already answers with - a quiet 503 rather than a second failed establishment.
|
|
522
|
+
*
|
|
523
|
+
* The failed attempt's resources are released BEFORE the wait rather than on the way out. The relaunch a failed verdict triggers waits for every in-flight
|
|
524
|
+
* page to be gone, and this page is one of them, so a tune still holding its page would be waiting on a relaunch its own page forbids. Disposing here empties
|
|
525
|
+
* the stack, which leaves its scope-exit disposal a no-op on every path out of this block.
|
|
526
|
+
*
|
|
527
|
+
* One retry, and the marker travels through the closed-page recursion's options spread on purpose: a browser crash recovery that follows a refusal retry must
|
|
528
|
+
* not buy a second one, because the bound is per logical establishment rather than per invocation.
|
|
529
|
+
*/
|
|
530
|
+
if (formatError(error).includes(CAPTURE_SOURCE_UNAVAILABLE_MESSAGE) && !options._refusalRetried) {
|
|
531
|
+
resources.dispose();
|
|
532
|
+
const verdict = await deps.awaitCaptureVerdict(error);
|
|
533
|
+
// No published browser, or a probe that never obtained its turn: neither is evidence about the browser, so there is nothing a second establishment would
|
|
534
|
+
// be starting from. The refusal travels as the tune's own failure.
|
|
535
|
+
if (!verdict || (verdict.kind === "inconclusive")) {
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
LOG.info("Chrome refused the capture start; retrying once on the probe's verdict.", { verdict: verdict.kind });
|
|
539
|
+
return await createPageWithCapture({ ...options, _refusalRetried: true }, deps);
|
|
540
|
+
}
|
|
541
|
+
// Every other rejection - a caller-deadline CaptureDeadlineError, a refusal the one retry has already been spent on, or any other capture-init failure - just
|
|
542
|
+
// unwinds. The verdict still gets the failure, because a browser that cannot start captures is worth marking whoever asked; nothing waits on it here.
|
|
543
|
+
// Resource teardown (page, interceptor, and the capture session once built) is handled by the DisposableStack as this throw unwinds the function scope.
|
|
544
|
+
void deps.awaitCaptureVerdict(error);
|
|
498
545
|
throw error;
|
|
499
546
|
}
|
|
500
547
|
// Navigate and set up playback. For static capture profiles, just navigate without video setup.
|
|
501
548
|
let context;
|
|
502
549
|
let strategyDirectTune = false;
|
|
503
550
|
let usedDirectUrl = false;
|
|
504
|
-
// Install the CDP manifest interceptor immediately before the navigate-and-tune fork, so its observation window opens after the capture-lock and
|
|
551
|
+
// Install the CDP manifest interceptor immediately before the navigate-and-tune fork, so its observation window opens after the capture-lock and acquisition
|
|
505
552
|
// (which the observer would otherwise idle through) and spans exactly the phases that produce manifests: navigation with retry, channel selection, and video
|
|
506
553
|
// setup. The install decision is gated on tab replacement and the skip flag only - static-capture profiles install too, since they stay native-HLS eligible
|
|
507
554
|
// through the interception's presence, so the guard sits upstream of the fork and both branches inherit it. The navigation allowance handed to the budget is
|
|
@@ -569,10 +616,10 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
|
|
|
569
616
|
// If a direct watch URL was used, offer the failure to the cache coordinator, which evicts the live and persisted hints when the failure is evidence against
|
|
570
617
|
// the URL and reports that verdict back. The retention policy lives there and is read from there - this file never re-derives which failures count.
|
|
571
618
|
const urlEvidence = usedDirectUrl && invalidateDirectUrl(profile, error);
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
|
|
619
|
+
// Window presentation is deliberately left alone here. The failing stream's registry entry is still registered in capture mode at this point, so the policy
|
|
620
|
+
// would read capture-active and hold the window on screen anyway; the terminate path that tears the entry down is the moment that genuinely settles it.
|
|
621
|
+
// Resource teardown (capture session, interceptor, page) is handled by the DisposableStack as this throw unwinds the function scope; the capture session
|
|
622
|
+
// disposes first, destroying the capture stream before the page closes, so STOP_RECORDING ordering is preserved.
|
|
576
623
|
/* A failure the coordinator blamed on the URL is worth one more attempt down the guide path, so it leaves this function typed for setupStream to act on. An
|
|
577
624
|
* establishment timeout qualifies: the retry is a whole fresh invocation, so there is no abandoned first attempt still driving a page, no one-shot manifest
|
|
578
625
|
* interception handle to reuse, and no direct-tune marker to reset - the retry gets its own page, handle, and markers, which is exactly what a fresh tune
|
|
@@ -581,15 +628,27 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
|
|
|
581
628
|
if (urlEvidence) {
|
|
582
629
|
throw new DirectUrlEstablishmentError(error);
|
|
583
630
|
}
|
|
631
|
+
/* Classification sits below that branch, on the standing attempt, and not above it. A failure that leaves this function typed is not a verdict on the
|
|
632
|
+
* establishment at all - it is a request for one more attempt - and the attempt that follows raises its own failure through this same catch. Classifying
|
|
633
|
+
* above the branch would hand the readiness detector both of them for a single logical setup, which is one setup speaking twice about one browser's fate.
|
|
634
|
+
* The establishment timeout is exactly the shape where that matters, because it is both a failure the URL can be blamed for and one that carries a
|
|
635
|
+
* capture-infrastructure signature.
|
|
636
|
+
*
|
|
637
|
+
* The verdict is started here and not waited on. A failure this far in has already spent a navigation and a channel selection, so a second whole tune would
|
|
638
|
+
* double the client's wait for something no verdict can change: the capture start, the one step a fresh browser would do differently, already succeeded.
|
|
639
|
+
*/
|
|
640
|
+
void deps.awaitCaptureVerdict(error);
|
|
584
641
|
throw error;
|
|
585
642
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
643
|
+
/* Both establishment branches join here, which is why the re-affirmation sits at this point: capture acquisition selects this tab for the length of the start
|
|
644
|
+
* and hands the selection back, and a tune of a fullscreen-activating profile selects it again for its fullscreen sequence and hands it back once more.
|
|
645
|
+
* Re-issuing the page's own declared metrics is what leaves the capture composing the emulated surface rather than the window's fitted view of it, whatever the
|
|
646
|
+
* establishment did with the selection.
|
|
647
|
+
*/
|
|
648
|
+
await deps.reaffirmCaptureSurface(page);
|
|
649
|
+
// Re-settle the window now that the page is established, handing the sync the page it should use for the CDP session. Navigation activates the window on macOS,
|
|
650
|
+
// so a pass here settles it against the policy after the tune, as every other transition does.
|
|
651
|
+
await deps.syncWindowVisibility(page);
|
|
593
652
|
LOG.debug("timing:startup", "Page with capture ready. Total: %sms.", captureElapsed());
|
|
594
653
|
// Success: transfer ownership of the page, interceptor, and capture session out of the scope guard. move() empties the stack so its scope-exit disposal is a no-op
|
|
595
654
|
// and the caller becomes responsible for disposing what it now holds.
|
|
@@ -617,18 +676,23 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
|
|
|
617
676
|
* FruitDeepLinks) whose domain has no profile mapping. By following redirects, we discover the actual streaming site's domain and can resolve the correct profile.
|
|
618
677
|
*
|
|
619
678
|
* Uses a HEAD request to avoid downloading response bodies. The 3-second timeout ensures stream startup isn't blocked by slow or unreachable indirection services.
|
|
679
|
+
* This resolver holds no clock of its own, so that bound takes the port's default, the system clock.
|
|
620
680
|
*
|
|
621
681
|
* @param url - The URL to resolve.
|
|
622
682
|
* @returns The final URL after following all redirects, or null on any error.
|
|
623
683
|
*/
|
|
624
684
|
async function resolveRedirectUrl(url) {
|
|
685
|
+
const bound = timeoutSignal(3000);
|
|
625
686
|
try {
|
|
626
|
-
const response = await chromeFetch(url, { method: "HEAD", signal:
|
|
687
|
+
const response = await chromeFetch(url, { method: "HEAD", signal: bound.signal });
|
|
627
688
|
return response.url;
|
|
628
689
|
}
|
|
629
690
|
catch {
|
|
630
691
|
return null;
|
|
631
692
|
}
|
|
693
|
+
finally {
|
|
694
|
+
bound.cancel();
|
|
695
|
+
}
|
|
632
696
|
}
|
|
633
697
|
// Stream Setup.
|
|
634
698
|
/**
|
|
@@ -651,7 +715,7 @@ function buildPersistResolutionCallback(canonicalKey, serviceTag) {
|
|
|
651
715
|
const existing = data.channels[variantKey] ?? {};
|
|
652
716
|
data.channels[variantKey] = { ...existing, channelSelector: resolvedSelector };
|
|
653
717
|
});
|
|
654
|
-
LOG.debug("tuning", "Persisted resolved selector \"%s\" to channel store as \"%s\".", resolvedSelector, variantKey);
|
|
718
|
+
LOG.debug("tuning:selector", "Persisted resolved selector \"%s\" to channel store as \"%s\".", resolvedSelector, variantKey);
|
|
655
719
|
};
|
|
656
720
|
}
|
|
657
721
|
/* Decides whether the manifest a tune's interception selected belongs to the channel the profile selects, using the provider's own verifier when one exists. The
|
|
@@ -659,7 +723,7 @@ function buildPersistResolutionCallback(canonicalKey, serviceTag) {
|
|
|
659
723
|
* here...the promise's settle time belongs only to the paths that consume it. Verification is gated to master-kind selections because a provider verifier like
|
|
660
724
|
* Fox's reads the channel call sign from a fixed segment of the master CDN URL, and a media (chunklist) URL has a different path shape the verifier was never
|
|
661
725
|
* calibrated for - a false tune-failure on a correct stream. Providers without a verifier, profiles without a channelSelector, non-master selections, and a null
|
|
662
|
-
* interception all verify vacuously: the gates upstream are the only identity signal we have there, so the
|
|
726
|
+
* interception all verify vacuously: the gates upstream are the only identity signal we have there, so the answer the state supports is no objection rather than a guess.
|
|
663
727
|
*
|
|
664
728
|
* @param interceptionPromise - The interception promise from the handle the tune finalized, awaited only once the gates above admit a verifier.
|
|
665
729
|
* @param profile - The resolved site profile whose strategy names the provider and whose channelSelector names the expected channel.
|
|
@@ -720,7 +784,7 @@ export async function establishChannelPlayback(page, profile, handle, options, d
|
|
|
720
784
|
// channel), and guide-tune selection prefers a master observed after this point. A null handle means nothing is observing, so there is no epoch to stamp.
|
|
721
785
|
handle?.markChannelSelectionStart();
|
|
722
786
|
const initPromise = deps.initializePlayback(page, profile, initOptions);
|
|
723
|
-
/* The settlement hook
|
|
787
|
+
/* The settlement hook runs on the initialization's own settlement rather than a fixed step after the wait below, because only the promise knows which of
|
|
724
788
|
* success, failure, or a late completion actually happened - and a late completion is reachable, since the wait abandons rather than cancels: a lapsed
|
|
725
789
|
* initialization is not stopped, it winds down on its own bounded internal phases and can finish afterward. The trailing catch is the whole safety net for
|
|
726
790
|
* the hook itself, absorbing a hook that throws as well as a structurally-permitted async hook that rejects, so this chain can never become a rejection
|
|
@@ -729,10 +793,10 @@ export async function establishChannelPlayback(page, profile, handle, options, d
|
|
|
729
793
|
if (onInitSettled) {
|
|
730
794
|
void initPromise.finally(() => onInitSettled()).catch(() => { });
|
|
731
795
|
}
|
|
732
|
-
return waitWithTimeout(initPromise, PLAYBACK_INIT_TIMEOUT, new Error("Playback initialization timed out after " + String(PLAYBACK_INIT_TIMEOUT) + "ms."));
|
|
796
|
+
return waitWithTimeout(initPromise, PLAYBACK_INIT_TIMEOUT, { reason: new Error("Playback initialization timed out after " + String(PLAYBACK_INIT_TIMEOUT) + "ms.") });
|
|
733
797
|
}
|
|
734
798
|
/**
|
|
735
|
-
* Adjudicates what an establishment's interception selected: finalize it against the
|
|
799
|
+
* Adjudicates what an establishment's interception selected: finalize it against the accurate direct-tune kind, then confirm the manifest belongs to the channel
|
|
736
800
|
* the profile names. The tune path and the native refresh capability both run this, so finalize-then-verify is written exactly once.
|
|
737
801
|
*
|
|
738
802
|
* The stage hands on the interception promise rather than a resolved result, which is what keeps verifyManifestSelection's latency rule holding - its provider
|
|
@@ -768,7 +832,8 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
|
|
|
768
832
|
// tab replacement handler all reference the same stream identity as the pending entry in the registry.
|
|
769
833
|
const streamId = options.streamId ?? generateStreamId(channelName, url);
|
|
770
834
|
const numericStreamId = options.numericStreamId ?? getNextStreamId();
|
|
771
|
-
|
|
835
|
+
// Stream setup is a composition point - it builds the capture lock, the health monitor, and the page - so it reads the system clock at its own boundary.
|
|
836
|
+
const startTime = systemClock.now();
|
|
772
837
|
// Create and register the AbortController for this stream. This allows pending evaluate calls to be cancelled immediately when the stream is terminated.
|
|
773
838
|
const abortController = new AbortController();
|
|
774
839
|
registerAbortController(streamId, abortController);
|
|
@@ -849,12 +914,12 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
|
|
|
849
914
|
// Create page and start capture using the shared function. This handles browser page creation, capture initialization, FFmpeg spawning, and navigation with retry.
|
|
850
915
|
let captureResult;
|
|
851
916
|
try {
|
|
852
|
-
/* Skip CDP manifest interception when the channel is
|
|
917
|
+
/* Skip CDP manifest interception when the channel is locked to screen capture, or when the probe cache already knows this stream's binding resolves to
|
|
853
918
|
* DRM. The per-channel override short-circuits first, so a forced channel never installs the interceptor: nothing intercepts a manifest, no native
|
|
854
919
|
* attempt runs, no probe fires, and the encryption cache stays untouched by that stream. The cache half avoids creating a CDP session that sits idle for
|
|
855
920
|
* 15 seconds before the interceptor timeout cleans it up; every stream carries an identity, ad-hoc URLs included, so that lookup needs no guard.
|
|
856
921
|
*/
|
|
857
|
-
const skipInterception = (channel?.forceCapture === true) || (getCachedEncryption(probeIdentity) === "drm");
|
|
922
|
+
const skipInterception = (channel?.forceCapture === true) || (getCachedEncryption(probeIdentity, systemClock.now()) === "drm");
|
|
858
923
|
// Build the persistResolution closure for the active channel. When the resolution layer in selectChannel() converts a category selector to a concrete call
|
|
859
924
|
// sign, this closure writes the result to the user's channel store as a per-service-variant override - the same shape produced when a user manually edits
|
|
860
925
|
// the selector via the web UI. Omitted for ad-hoc URL streams (no channel record to update).
|
|
@@ -864,7 +929,6 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
|
|
|
864
929
|
undefined;
|
|
865
930
|
const attemptOptions = {
|
|
866
931
|
comment: metadataComment,
|
|
867
|
-
numericStreamId,
|
|
868
932
|
onFFmpegError: onCircuitBreak,
|
|
869
933
|
persistResolution,
|
|
870
934
|
profile,
|
|
@@ -906,6 +970,13 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
|
|
|
906
970
|
if ((error instanceof BrowserUnavailableError) || (error instanceof BrowserSupersededError)) {
|
|
907
971
|
throw new StreamSetupError("Browser temporarily unavailable.", 503, "The capture system is recovering. Please retry shortly.", { cause: error });
|
|
908
972
|
}
|
|
973
|
+
// The browser is alive and still serving the captures it started, but it can no longer start another, so this tune is refused before a page is even opened.
|
|
974
|
+
// Quiet like its sibling above, because the alarm fired once when the mark was recorded. The message differs because the wait ends differently: the client is
|
|
975
|
+
// waiting on the browser to be released by whatever still holds it rather than on a cooldown to elapse.
|
|
976
|
+
if (error instanceof BrowserCaptureImpairedError) {
|
|
977
|
+
throw new StreamSetupError("Browser temporarily unavailable.", 503, "The browser can no longer start captures and will relaunch as soon as nothing is " +
|
|
978
|
+
"using it. Please retry shortly.", { cause: error });
|
|
979
|
+
}
|
|
909
980
|
// createPageWithCapture handles its own cleanup on failure (closes page, kills FFmpeg).
|
|
910
981
|
const errorMessage = formatError(error);
|
|
911
982
|
const lowerMessage = errorMessage.toLowerCase();
|
|
@@ -914,16 +985,14 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
|
|
|
914
985
|
if (!isBenign) {
|
|
915
986
|
LOG.error("Stream setup failed for %s: %s.", url, errorMessage);
|
|
916
987
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
988
|
+
/* Capture infrastructure errors should return 503 to signal Channels DVR to back off. These include Chrome capture state issues, capture-lock turn-wait
|
|
989
|
+
* timeouts, and stream initialization failures. Using 503 with Retry-After prevents retry storms when there's a systemic issue. isCaptureInfrastructureError
|
|
990
|
+
* (recovery.ts) is the single source of truth for this judgment, shared with the browser supervisor's readiness detection.
|
|
991
|
+
*
|
|
992
|
+
* The read here is purely for the client-facing status. The side effect that judgment also drives - handing the failure to the passive mid-life capture
|
|
993
|
+
* detector - fires inside createPageWithCapture's own catches, where every caller of the acquisition reaches it rather than only this one.
|
|
994
|
+
*/
|
|
920
995
|
const isCaptureError = isCaptureInfrastructureError(errorMessage);
|
|
921
|
-
// A capture-infrastructure failure may mean the browser, though still connected, can no longer capture. Hand it to the passive mid-life detector, which (guarded
|
|
922
|
-
// and single-flight, in the background) re-verifies capture readiness and invalidates the browser for a governed relaunch if confirmed. Fire-and-forget: it must
|
|
923
|
-
// never delay this response.
|
|
924
|
-
if (isCaptureError) {
|
|
925
|
-
noteCaptureInfrastructureFailure(numericStreamId);
|
|
926
|
-
}
|
|
927
996
|
// A failed tune on a service currently marked needs-sign-in most likely failed AT the auth wall, so the user-facing message leads with the remedy.
|
|
928
997
|
throw new StreamSetupError("Stream error.", isCaptureError ? 503 : 500, withSignInGuidance("Failed to start stream.", channelName, serviceName), { cause: error });
|
|
929
998
|
}
|
|
@@ -949,7 +1018,6 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
|
|
|
949
1018
|
if (manifestInterception) {
|
|
950
1019
|
const verifyError = await adjudicateChannelSelection(manifestInterception, profile, directTune);
|
|
951
1020
|
if (verifyError) {
|
|
952
|
-
await minimizeBrowserWindow();
|
|
953
1021
|
const failureLabel = channel?.name ?? channelName ?? url;
|
|
954
1022
|
throw new StreamSetupError("Tune verification failed: " + verifyError, 502, withSignInGuidance("Tune verification failed for " + failureLabel + ". " + verifyError, channelName, serviceName));
|
|
955
1023
|
}
|
|
@@ -977,8 +1045,8 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
|
|
|
977
1045
|
monitor.dispose();
|
|
978
1046
|
captureSession.dispose();
|
|
979
1047
|
disposePage(page);
|
|
980
|
-
//
|
|
981
|
-
await
|
|
1048
|
+
// Settle the window against the policy now that this stream's resources are gone.
|
|
1049
|
+
await deps.syncWindowVisibility();
|
|
982
1050
|
};
|
|
983
1051
|
// Success: transfer ownership out of the scope guard. The cleanup closure and, once the session is installed on the registry entry, terminateStream become
|
|
984
1052
|
// responsible for disposing the page and capture session; the interceptor continues into tune verification and native streaming.
|
|
@@ -1062,7 +1130,7 @@ export async function reestablishChannelManifest(options) {
|
|
|
1062
1130
|
void muteExistingVideos(page);
|
|
1063
1131
|
}
|
|
1064
1132
|
});
|
|
1065
|
-
// Adjudicate
|
|
1133
|
+
// Adjudicate against this route's facts: the kind formula gets no cached-direct-URL term here, because the route above never takes one.
|
|
1066
1134
|
const verifyError = await adjudicateChannelSelection(handle, profile, computeDirectTuneKind({ profile, strategyDirectTune: tuneResult.directTune ?? false }));
|
|
1067
1135
|
// On a verifier-bearing master path the adjudication has already awaited this promise; on the vacuous path this await is the one that waits out the
|
|
1068
1136
|
// finalize settle. Reading it before the verification result is contract rather than preference: an establishment that intercepted nothing has to report
|
|
@@ -1101,19 +1169,17 @@ export async function reestablishChannelManifest(options) {
|
|
|
1101
1169
|
}
|
|
1102
1170
|
// Capture Readiness Verification.
|
|
1103
1171
|
/**
|
|
1104
|
-
* Verifies that a freshly-launched Chrome instance can actually capture, by running a real
|
|
1105
|
-
* the browser launch gate: browser/index.ts injects it via setCaptureProbe and runs it inside launchReadyBrowser, so a browser is published as
|
|
1106
|
-
* capture capability is verified - at boot AND at every relaunch, not just startup. It exercises the exact
|
|
1107
|
-
* extension is unregistered, so a dead extension is detected immediately rather than causing every subsequent stream request to
|
|
1172
|
+
* Verifies that a freshly-launched Chrome instance can actually capture, by running a real capture acquisition against a throwaway page on it. This is the
|
|
1173
|
+
* capability tier of the browser launch gate: browser/index.ts injects it via setCaptureProbe and runs it inside launchReadyBrowser, so a browser is published as
|
|
1174
|
+
* ready only after its capture capability is verified - at boot AND at every relaunch, not just startup. It exercises the exact acquisition path that fails when
|
|
1175
|
+
* the capture extension is unregistered, so a dead extension is detected immediately rather than causing every subsequent stream request to fail.
|
|
1108
1176
|
*
|
|
1109
|
-
* Each attempt creates a temporary page, attempts a short capture, and tears down both cleanly.
|
|
1110
|
-
*
|
|
1177
|
+
* Each attempt creates a temporary page, attempts a short capture, and tears down both cleanly. The teardown waits for the extension to confirm the recording
|
|
1178
|
+
* stopped, bounded by a ceiling, before closing the page, so Chrome's tabCapture state is released rather than lingering into the first real request.
|
|
1111
1179
|
*
|
|
1112
1180
|
* After a system reboot or a fresh relaunch, Chrome's display stack or capture extension may not be ready immediately. The probe retries up to PROBE_MAX_ATTEMPTS
|
|
1113
1181
|
* times with a delay between attempts, giving the system time to settle before giving up. At boot this prevents a rapid restart storm where the service manager
|
|
1114
1182
|
* relaunches PrismCast repeatedly, each attempt orphaning a Chrome process; at relaunch it provides in-launch settling before the supervisor counts a launch failure.
|
|
1115
|
-
*
|
|
1116
|
-
* If stale capture state is detected, the process exits immediately - a Chrome restart cannot fix the leaked module-level mutex, only a fresh process can.
|
|
1117
1183
|
* @param browser - The Chrome instance to verify (the local instance being launched, passed in rather than re-acquired to avoid re-entering the launch in flight).
|
|
1118
1184
|
*/
|
|
1119
1185
|
export async function verifyCaptureSystem(browser) {
|
|
@@ -1121,20 +1187,13 @@ export async function verifyCaptureSystem(browser) {
|
|
|
1121
1187
|
const PROBE_RETRY_DELAY = 5000;
|
|
1122
1188
|
for (let attempt = 1; attempt <= PROBE_MAX_ATTEMPTS; attempt++) {
|
|
1123
1189
|
// The launch gate runs its capture probe OFF the capture lock, deliberately: it fires pre-publish, when supervisor.current() is null, so a wedged gate task would
|
|
1124
|
-
// have no recovery target and would jam the shared lock. GATE mode keeps the
|
|
1190
|
+
// have no recovery target and would jam the shared lock. GATE mode keeps the acquisition bounded internally instead.
|
|
1125
1191
|
// eslint-disable-next-line no-await-in-loop -- Sequential retries are intentional; each probe must complete before deciding whether to retry.
|
|
1126
1192
|
const result = await attemptCaptureProbe(browser, { boundMs: CAPTURE_PROBE_TIMEOUT_MS, kind: "gate" });
|
|
1127
1193
|
// Probe succeeded.
|
|
1128
1194
|
if (result === null) {
|
|
1129
1195
|
return;
|
|
1130
1196
|
}
|
|
1131
|
-
// Stale capture state is unrecoverable: puppeteer-stream's second lock section has no try/finally, so the mutex is permanently leaked and every subsequent
|
|
1132
|
-
// getStream() hangs. Chrome restart cannot fix module-level state, so escalate from the single home (which logs and schedules the deferred exit), then throw so no
|
|
1133
|
-
// further probe attempts run before the process exits.
|
|
1134
|
-
if (isStaleCaptureMutexError(result)) {
|
|
1135
|
-
escalateStaleCaptureMutex("the launch-gate capture probe");
|
|
1136
|
-
throw new Error("Capture system verification failed: stale capture state detected.");
|
|
1137
|
-
}
|
|
1138
1197
|
// If we have retries remaining, log a warning and wait before the next attempt.
|
|
1139
1198
|
if (attempt < PROBE_MAX_ATTEMPTS) {
|
|
1140
1199
|
LOG.warn("Capture probe attempt %d of %d failed: %s. Retrying in %ds.", attempt, PROBE_MAX_ATTEMPTS, result, PROBE_RETRY_DELAY / 1000);
|
|
@@ -1158,16 +1217,24 @@ export async function verifyCaptureSystem(browser) {
|
|
|
1158
1217
|
/**
|
|
1159
1218
|
* Executes a single capture probe attempt. Creates a temporary page on the given browser, tries to start a capture stream, and tears everything down cleanly. It
|
|
1160
1219
|
* NEVER throws in either mode: it returns null on success or an error-message string on failure, so callers branch on the string. The two modes differ only in how
|
|
1161
|
-
*
|
|
1220
|
+
* the acquisition is bounded - see CaptureProbeMode.
|
|
1162
1221
|
* @param browser - The Chrome instance to probe.
|
|
1163
|
-
* @param mode - The operating mode: gate (internal
|
|
1164
|
-
* @param clock - Clock used for the mid-life self-timing and the teardown
|
|
1222
|
+
* @param mode - The operating mode: gate (internal acquisition race) or midlife (self-timed acquisition on the lock).
|
|
1223
|
+
* @param clock - Clock used for the mid-life self-timing and the teardown confirmation. Defaults to the system clock.
|
|
1165
1224
|
* @returns Null on success, or an error message string on failure.
|
|
1166
1225
|
*/
|
|
1167
|
-
async function attemptCaptureProbe(browser, mode, clock =
|
|
1168
|
-
|
|
1226
|
+
async function attemptCaptureProbe(browser, mode, clock = systemClock) {
|
|
1227
|
+
/* The mid-life probe runs against a browser that is serving streams, so its page is opened as a tab of the shared window for the reason a stream's page is: a
|
|
1228
|
+
* probe tab rooted in a discovery window would hold that window open for as long as the probe took. The launch gate keeps the plain create: it runs pre-publish,
|
|
1229
|
+
* when the browser has exactly one window and no discovery window can exist yet, so its tab has no wrong window to land in - and a gate that has to stay bounded
|
|
1230
|
+
* gains nothing from queuing behind the selection executor to prove it.
|
|
1231
|
+
*/
|
|
1232
|
+
const page = (mode.kind === "midlife") ? await openSharedWindowTab(browser, { deps: SHARED_WINDOW_TOPOLOGY }) : await browser.newPage({ background: true });
|
|
1169
1233
|
registerManagedPage(page);
|
|
1170
|
-
//
|
|
1234
|
+
// The probe page carries the preset-sized layout at the display's density, the surface a capture page starts from before its own declaration, so the probe's
|
|
1235
|
+
// acquisition keeps the shape the field validated.
|
|
1236
|
+
const surface = await emulateLayoutSurface(page);
|
|
1237
|
+
// Tears the probe page down cleanly: retire the raw capture stream (destroy plus the stop confirmation) while the browser is still connected, unregister the
|
|
1171
1238
|
// managed page, then close it. Shared by every success and self-timed-failure path in both modes.
|
|
1172
1239
|
const teardown = async (stream) => {
|
|
1173
1240
|
await retireRawStream(stream, clock);
|
|
@@ -1177,12 +1244,11 @@ async function attemptCaptureProbe(browser, mode, clock = realClock) {
|
|
|
1177
1244
|
}
|
|
1178
1245
|
};
|
|
1179
1246
|
try {
|
|
1180
|
-
// Use the same capture MIME type and
|
|
1181
|
-
//
|
|
1182
|
-
//
|
|
1247
|
+
// Use the same capture MIME type and surface as the runtime. The stale state error occurs at the tabCapture API level before encoding matters, so matching
|
|
1248
|
+
// those runtime constraints ensures the probe exercises a representative acquisition. The constraints are held to the dimensions the declaration above
|
|
1249
|
+
// returned, so the probe holds its track to the surface the page actually carries rather than to a second read of the preset.
|
|
1183
1250
|
const useFFmpeg = CONFIG.streaming.captureMode === "ffmpeg";
|
|
1184
1251
|
const captureMimeType = useFFmpeg ? getCaptureMimeType() : NATIVE_FMP4_MIME_TYPE;
|
|
1185
|
-
const viewport = getPresetViewport(CONFIG);
|
|
1186
1252
|
const streamOptions = {
|
|
1187
1253
|
audio: true,
|
|
1188
1254
|
mimeType: captureMimeType,
|
|
@@ -1190,26 +1256,26 @@ async function attemptCaptureProbe(browser, mode, clock = realClock) {
|
|
|
1190
1256
|
videoConstraints: {
|
|
1191
1257
|
mandatory: {
|
|
1192
1258
|
maxFrameRate: 30,
|
|
1193
|
-
maxHeight:
|
|
1194
|
-
maxWidth:
|
|
1259
|
+
maxHeight: surface.height,
|
|
1260
|
+
maxWidth: surface.width,
|
|
1195
1261
|
minFrameRate: 30,
|
|
1196
|
-
minHeight:
|
|
1197
|
-
minWidth:
|
|
1262
|
+
minHeight: surface.height,
|
|
1263
|
+
minWidth: surface.width
|
|
1198
1264
|
}
|
|
1199
1265
|
}
|
|
1200
1266
|
};
|
|
1201
|
-
// GATE mode: bound
|
|
1267
|
+
// GATE mode: bound the acquisition with an internal timeout. On a lapse the acquisition is still running, so attach a both-callback handler that retires a
|
|
1202
1268
|
// late-arriving stream (best-effort; the page may already be closing) and consumes a late rejection. The bounded wait already observes the promise's rejection,
|
|
1203
1269
|
// so a fulfillment-only handler would create unhandled-rejection noise. This cleans up the orphan without serializing successive gate attempts against one another.
|
|
1204
1270
|
if (mode.kind === "gate") {
|
|
1205
|
-
const streamPromise =
|
|
1271
|
+
const streamPromise = acquireCaptureStream(page, streamOptions);
|
|
1206
1272
|
const timeoutError = new Error(CAPTURE_PROBE_TIMEOUT_MESSAGE);
|
|
1207
1273
|
let stream;
|
|
1208
1274
|
try {
|
|
1209
|
-
stream = await waitWithTimeout(streamPromise, mode.boundMs, timeoutError);
|
|
1275
|
+
stream = await waitWithTimeout(streamPromise, mode.boundMs, { reason: timeoutError });
|
|
1210
1276
|
}
|
|
1211
1277
|
catch (error) {
|
|
1212
|
-
// Only the internal timeout leaves
|
|
1278
|
+
// Only the internal timeout leaves the acquisition running; an in-time rejection produced no stream to clean up and is already observed by the bounded wait.
|
|
1213
1279
|
if (error === timeoutError) {
|
|
1214
1280
|
void streamPromise.then((late) => {
|
|
1215
1281
|
void retireRawStream(late, clock);
|
|
@@ -1223,14 +1289,14 @@ async function attemptCaptureProbe(browser, mode, clock = realClock) {
|
|
|
1223
1289
|
LOG.info("Capture system verified successfully.");
|
|
1224
1290
|
return null;
|
|
1225
1291
|
}
|
|
1226
|
-
// MID-LIFE mode: await
|
|
1227
|
-
// against boundMs, measured without racing or abandoning anything, so the teardown
|
|
1292
|
+
// MID-LIFE mode: await the acquisition raw and self-time it. The turn (owned by the lock) spans the whole task, but the pass/fail CRITERION is the
|
|
1293
|
+
// acquisition's own latency against boundMs, measured without racing or abandoning anything, so the teardown that follows never counts against it.
|
|
1228
1294
|
const startedAt = clock.now();
|
|
1229
|
-
const stream =
|
|
1295
|
+
const stream = await acquireCaptureStream(page, streamOptions);
|
|
1230
1296
|
const elapsed = clock.now() - startedAt;
|
|
1231
1297
|
await teardown(stream);
|
|
1232
1298
|
// Report failure when the caller abandoned this probe at the lock's outer deadline (unobservable in practice - the lock already rejected the caller - but it keeps
|
|
1233
|
-
// the never-throw contract and prevents stranding a capture), or when
|
|
1299
|
+
// the never-throw contract and prevents stranding a capture), or when the acquisition's own latency exceeded the criterion bound.
|
|
1234
1300
|
if (mode.signal.aborted || (elapsed > mode.boundMs)) {
|
|
1235
1301
|
return CAPTURE_PROBE_TIMEOUT_MESSAGE;
|
|
1236
1302
|
}
|
|
@@ -1246,104 +1312,169 @@ async function attemptCaptureProbe(browser, mode, clock = realClock) {
|
|
|
1246
1312
|
}
|
|
1247
1313
|
return errorMessage;
|
|
1248
1314
|
}
|
|
1315
|
+
finally {
|
|
1316
|
+
/* The mid-life open restores the shared window on its way in, so the presentation is settled back here, on every exit this arm has: the success return, the
|
|
1317
|
+
* self-timed bound return, and the catch. It settles inside the probe rather than in the detector above it because the capture lock's outer deadline can
|
|
1318
|
+
* abandon this task while it runs on detached - a settle placed in the detector would fire before a detached probe's open had restored anything. The launch
|
|
1319
|
+
* gate opens no such tab and touches no window, so the settle is the mid-life arm's alone.
|
|
1320
|
+
*/
|
|
1321
|
+
if (mode.kind === "midlife") {
|
|
1322
|
+
await syncWindowVisibility();
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1249
1325
|
}
|
|
1250
1326
|
// Passive Mid-Life Capture-Death Detection.
|
|
1251
|
-
/* A browser can be capture-ready at launch and lose its capture capability later - the extension wedges, tabCapture stalls
|
|
1252
|
-
*
|
|
1253
|
-
*
|
|
1254
|
-
*
|
|
1255
|
-
* is
|
|
1256
|
-
*
|
|
1327
|
+
/* A browser can be capture-ready at launch and lose its capture capability later - the extension wedges, tabCapture stalls, a display reconfiguration leaves the
|
|
1328
|
+
* process unable to start another capture while the captures already running continue - without ever firing a "disconnected" event, so neither the launch gate nor
|
|
1329
|
+
* the disconnect handler would catch it. This detector uses a signal that is already happening: a stream-setup failure carrying a capture-infrastructure
|
|
1330
|
+
* signature. The probe is the arbiter, serialized through the capture lock so it can never race a real stream's capture acquisition. A probe that never obtained a
|
|
1331
|
+
* turn is no verdict at all, because a busy lock is evidence about load rather than about the browser. On a confirmed failure the one recovery action runs: mark
|
|
1332
|
+
* the browser, which leaves its running captures alone, refuses new capture starts at acquire(), and leaves the adapter to decide when the relaunch is safe.
|
|
1333
|
+
*
|
|
1334
|
+
* The verdict is a shared promise rather than a task nobody holds, and that is what lets a single caller wait on it. A tune Chrome refused the capture start for
|
|
1335
|
+
* is that caller: it has nothing to serve until the browser's fate is settled, and a relaunched browser is exactly what would serve it. Every other failure hands
|
|
1336
|
+
* its error over and walks on, so no healthy response is ever delayed by a probe. One slot holds the verdict for as long as it is unsettled, which is what makes
|
|
1337
|
+
* a burst cheap: a dozen tunes refused in the same second share one probe, one verdict, and one relaunch.
|
|
1257
1338
|
*/
|
|
1258
|
-
//
|
|
1259
|
-
let
|
|
1339
|
+
// The verdict being reached right now, or null when none is. Every caller that arrives while one is unsettled is handed this same promise.
|
|
1340
|
+
let captureVerdictInFlight = null;
|
|
1341
|
+
/**
|
|
1342
|
+
* Classifies a throw from the probe's run on the capture lock into a verdict or the absence of one. A turn-wait timeout means the probe never got to run: with
|
|
1343
|
+
* other streams tuning at the same moment it queued behind their legitimate acquisitions, and a lock that is busy says something about load, not about the browser
|
|
1344
|
+
* - a holder that is genuinely hung marks the browser through its own wedge. Every other value, the outer deadline above all, means the probe held its turn and its
|
|
1345
|
+
* own acquisition did not settle, which is exactly the failure the mark exists for. Pure and total, so the distinction the detector rests on is pinnable without a
|
|
1346
|
+
* browser.
|
|
1347
|
+
* @param error - The value the lock run threw.
|
|
1348
|
+
* @returns The outcome the detector acts on.
|
|
1349
|
+
*/
|
|
1350
|
+
export function classifyCaptureProbeFailure(error) {
|
|
1351
|
+
if (error instanceof CaptureTurnTimeoutError) {
|
|
1352
|
+
return { kind: "inconclusive", reason: formatError(error) };
|
|
1353
|
+
}
|
|
1354
|
+
return { kind: "failed", reason: formatError(error) };
|
|
1355
|
+
}
|
|
1260
1356
|
/**
|
|
1261
|
-
* Runs one capture probe as a task on the capture lock, so it cannot race a concurrent
|
|
1262
|
-
* with an active stream"). The lock holds the turn across the probe's full task -
|
|
1263
|
-
* initialization does not start until the probe's capture is fully released. The mid-life probe self-times its
|
|
1264
|
-
* adds a teardown allowance over it as a safety net.
|
|
1357
|
+
* Runs one capture probe as a task on the capture lock, so it cannot race a concurrent capture acquisition (which would draw a spurious "Cannot capture a tab
|
|
1358
|
+
* with an active stream"). The lock holds the turn across the probe's full task - the acquisition plus the stop confirmation inside attemptCaptureProbe - so the
|
|
1359
|
+
* next initialization does not start until the probe's capture is fully released. The mid-life probe self-times its acquisition against `timeout`; the lock's
|
|
1360
|
+
* outer deadline adds a teardown allowance over it as a safety net.
|
|
1265
1361
|
* @param browser - The Chrome instance to probe.
|
|
1266
|
-
* @param timeout - Maximum time in milliseconds to wait for the probe's
|
|
1267
|
-
* @returns
|
|
1362
|
+
* @param timeout - Maximum time in milliseconds to wait for the probe's acquisition to respond (the self-timed criterion).
|
|
1363
|
+
* @returns Captured when the browser started a capture, failed when it could not, inconclusive when the probe never obtained its turn.
|
|
1268
1364
|
*/
|
|
1269
1365
|
async function probeCaptureSerialized(browser, timeout) {
|
|
1270
1366
|
try {
|
|
1271
|
-
|
|
1367
|
+
const failure = await captureLock.run((signal) => attemptCaptureProbe(browser, { boundMs: timeout, kind: "midlife",
|
|
1368
|
+
signal }), {
|
|
1272
1369
|
deadlineMessage: CAPTURE_PROBE_TIMEOUT_MESSAGE,
|
|
1273
1370
|
deadlineMs: timeout + PROBE_TEARDOWN_ALLOWANCE_MS,
|
|
1274
|
-
// The probe's wedge is a loud warning only, never
|
|
1275
|
-
//
|
|
1276
|
-
// identity-guard no-op.
|
|
1371
|
+
// The probe's wedge is a loud warning only, never a second verdict: by wedge time the detector has already marked this browser from the returned failure
|
|
1372
|
+
// (the outer deadline fires roughly 22s earlier), so a mark raised here would land on an instance that already carries one.
|
|
1277
1373
|
onWedge: () => {
|
|
1278
|
-
LOG.warn("A mid-life capture probe has wedged past the recovery bound; the
|
|
1374
|
+
LOG.warn("A mid-life capture probe has wedged past the recovery bound; the browser was already marked when the probe's deadline fired, so this wedge " +
|
|
1375
|
+
"is informational.");
|
|
1279
1376
|
},
|
|
1280
1377
|
turnWaitMs: CONFIG.streaming.navigationTimeout
|
|
1281
1378
|
});
|
|
1379
|
+
return (failure === null) ? { kind: "captured" } : { kind: "failed", reason: failure };
|
|
1282
1380
|
}
|
|
1283
1381
|
catch (error) {
|
|
1284
|
-
|
|
1285
|
-
// lock is itself evidence the browser cannot capture, so surface it as a probe failure the detector routes into invalidateBrowser.
|
|
1286
|
-
return formatError(error);
|
|
1382
|
+
return classifyCaptureProbeFailure(error);
|
|
1287
1383
|
}
|
|
1288
1384
|
}
|
|
1289
1385
|
/**
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1295
|
-
*
|
|
1296
|
-
*
|
|
1297
|
-
*
|
|
1298
|
-
*
|
|
1299
|
-
*
|
|
1300
|
-
*
|
|
1386
|
+
* Classifies an establishment failure and, when it carries a capture-infrastructure signature, hands it to the passive mid-life detector.
|
|
1387
|
+
*
|
|
1388
|
+
* This sits at the acquisition chokepoint rather than in one caller's catch, so every path that establishes a capture gets the detection: a fresh stream's setup,
|
|
1389
|
+
* a tab replacement, and anything added later. Both of createPageWithCapture's own catch blocks call it, which is what extends the coverage across phases - the
|
|
1390
|
+
* acquisition catch sees a refused capture start, the establishment catch sees the playback-initialization timeout the pattern list also names.
|
|
1391
|
+
*
|
|
1392
|
+
* Every logical setup classifies at most once. What holds that rule at the two call sites is where each call sits: each classifies the failure it is about to
|
|
1393
|
+
* rethrow raw, and never one it is about to wrap for a retry. So a failure blamed on a direct watch URL classifies nothing, and the standing attempt that follows
|
|
1394
|
+
* it classifies for both. The alternative would hand the readiness detector two failures from one setup, which is two background probes deciding a single
|
|
1395
|
+
* browser's fate.
|
|
1396
|
+
*
|
|
1397
|
+
* DirectUrlEstablishmentError is excluded here as well, as the backstop that keeps the rule true of a call site added later rather than as a filter the current
|
|
1398
|
+
* two depend on: neither of them can reach this with one, because the type exists only to leave this function, and the path that wraps into it throws before
|
|
1399
|
+
* the classification line is reached.
|
|
1400
|
+
* @param error - The failure the establishment is about to rethrow.
|
|
1401
|
+
* @returns The verdict the detector reached, or null when this failure called for none.
|
|
1301
1402
|
*/
|
|
1302
|
-
|
|
1303
|
-
|
|
1403
|
+
async function noteClassifiedCaptureFailure(error) {
|
|
1404
|
+
if ((error instanceof DirectUrlEstablishmentError) || !isCaptureInfrastructureError(error)) {
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
return await noteCaptureInfrastructureFailure();
|
|
1304
1408
|
}
|
|
1409
|
+
// The production collaborators, with the probe bound to the criterion every mid-life verdict is judged against.
|
|
1410
|
+
const defaultCaptureVerdictDeps = { getBrowserInstance, getCaptureImpairment, noteBrowserCaptureImpaired,
|
|
1411
|
+
probe: (browser) => probeCaptureSerialized(browser, CAPTURE_PROBE_TIMEOUT_MS) };
|
|
1305
1412
|
/**
|
|
1306
|
-
* Passive mid-life capture-death detection, called from the stream-setup failure path when the failure carries a capture-infrastructure signature.
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1413
|
+
* Passive mid-life capture-death detection, called from the stream-setup failure path when the failure carries a capture-infrastructure signature. It re-verifies
|
|
1414
|
+
* the browser's capture capability with a lock-serialized probe and, on a confirmed failure, marks the browser: its running captures continue, new capture starts
|
|
1415
|
+
* are refused at acquire(), and the adapter relaunches it once nothing depends on it. Any refused start is worth a probe - a browser that is demonstrably
|
|
1416
|
+
* capturing for someone else can still be unable to start another - while a browser that already carries a mark needs no second verdict, so the reason on record
|
|
1417
|
+
* is handed back instead.
|
|
1418
|
+
*
|
|
1419
|
+
* The returned promise settles only once everything the verdict set in motion has settled, the relaunch a `failed` outcome triggers included. That is the whole
|
|
1420
|
+
* reason a caller can act on it: a tune that re-acquired the browser the instant the mark landed would meet the retiring instance's drain, where acquire() rejects
|
|
1421
|
+
* rather than joins, while one that waits for this promise meets the fresh browser or a refusal that means what it says. The promise never rejects; the absence of
|
|
1422
|
+
* a verdict is a null rather than a throw.
|
|
1423
|
+
*
|
|
1424
|
+
* It is deliberately not async: every caller in a burst has to hand back the SAME promise object, because what they are waiting on is the browser's one verdict
|
|
1425
|
+
* rather than their own call's. An async wrapper would give each caller a distinct promise that merely adopts it, and the shared identity the single-flight slot
|
|
1426
|
+
* exists to provide would stop being observable at this boundary.
|
|
1427
|
+
* @param deps - The injected collaborators; defaults to the production ones.
|
|
1428
|
+
* @returns The verdict, or null when no browser is published to reach one about.
|
|
1311
1429
|
*/
|
|
1312
|
-
function noteCaptureInfrastructureFailure(
|
|
1313
|
-
//
|
|
1314
|
-
//
|
|
1315
|
-
if (
|
|
1316
|
-
return;
|
|
1430
|
+
export function noteCaptureInfrastructureFailure(deps = defaultCaptureVerdictDeps) {
|
|
1431
|
+
// A verdict already being reached is the verdict every later caller gets. Read first so a burst short-circuits before the browser lookup below is even
|
|
1432
|
+
// performed, and so no second probe can ever spend the capture lock on a question that is already being answered.
|
|
1433
|
+
if (captureVerdictInFlight) {
|
|
1434
|
+
return captureVerdictInFlight;
|
|
1317
1435
|
}
|
|
1318
|
-
// A readiness probe needs a connected browser to exercise. If none is published, a disconnect already handled the readiness loss
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
const browser = getBrowserInstance();
|
|
1323
|
-
if (!browser || !shouldReverifyCapture({ activeStreamIds: getAllStreams().map((entry) => entry.id), failingStreamId, hasBrowser: true,
|
|
1324
|
-
reverificationInProgress: false })) {
|
|
1325
|
-
return;
|
|
1436
|
+
// A readiness probe needs a connected browser to exercise. If none is published, a disconnect already handled the readiness loss and there is nothing to judge.
|
|
1437
|
+
const browser = deps.getBrowserInstance();
|
|
1438
|
+
if (!browser) {
|
|
1439
|
+
return Promise.resolve(null);
|
|
1326
1440
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1441
|
+
/* A refused start against a browser that is already marked belongs to a tune that acquired it an instant before the mark landed. The verdict is recorded and
|
|
1442
|
+
* the relaunch is scheduled, so a probe here would spend the capture lock re-establishing what is already known. The recorded reason answers instead, and what
|
|
1443
|
+
* happens next is the caller's own re-acquire to discover from the supervisor.
|
|
1444
|
+
*/
|
|
1445
|
+
const impairment = deps.getCaptureImpairment();
|
|
1446
|
+
if (impairment) {
|
|
1447
|
+
return Promise.resolve({ kind: "failed", reason: impairment.reason });
|
|
1448
|
+
}
|
|
1449
|
+
captureVerdictInFlight = (async () => {
|
|
1330
1450
|
try {
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1451
|
+
const outcome = await deps.probe(browser);
|
|
1452
|
+
switch (outcome.kind) {
|
|
1453
|
+
case "failed": {
|
|
1454
|
+
/* The probe confirmed the browser cannot start a capture though it is still connected. We pass the exact instance we probed, so a disconnect and
|
|
1455
|
+
* relaunch during the probe leaves the fresh browser unmarked. The mark is awaited rather than fired off, because the relaunch it starts is the thing a
|
|
1456
|
+
* waiting tune is waiting for: settling this promise ahead of that relaunch would hand the tune a browser mid-teardown.
|
|
1457
|
+
*/
|
|
1458
|
+
await deps.noteBrowserCaptureImpaired(browser, outcome.reason);
|
|
1459
|
+
break;
|
|
1460
|
+
}
|
|
1461
|
+
case "inconclusive": {
|
|
1462
|
+
LOG.debug("streaming:setup", "Mid-life capture re-verification could not obtain a turn on the capture lock (%s); no verdict.", outcome.reason);
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
case "captured": {
|
|
1466
|
+
// The browser started a capture, so it is healthy and the setup failure that brought us here belonged to the stream rather than to the browser.
|
|
1467
|
+
break;
|
|
1468
|
+
}
|
|
1339
1469
|
}
|
|
1340
|
-
|
|
1341
|
-
catch (error) {
|
|
1342
|
-
LOG.debug("streaming:setup", "Mid-life capture re-verification aborted: %s.", formatError(error));
|
|
1470
|
+
return outcome;
|
|
1343
1471
|
}
|
|
1344
1472
|
finally {
|
|
1345
|
-
|
|
1473
|
+
// The slot empties as this verdict settles, so the next capture-infrastructure failure asks the browser afresh rather than reading an answer about a
|
|
1474
|
+
// moment that has passed.
|
|
1475
|
+
captureVerdictInFlight = null;
|
|
1346
1476
|
}
|
|
1347
1477
|
})();
|
|
1478
|
+
return captureVerdictInFlight;
|
|
1348
1479
|
}
|
|
1349
1480
|
//# sourceMappingURL=setup.js.map
|