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/browser/index.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { LOG, boundedWait, evaluateWithAbort, formatError, isProcessRunning, listProcesses,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { LOG, boundedWait, evaluateWithAbort, formatError, isProcessRunning, listProcesses, setChromeUserAgent, startTimer } from "../utils/index.js";
|
|
2
|
+
import { TimerRegistry, systemClock } from "homebridge-plugin-utils";
|
|
3
|
+
import { clearLoginState, isLoginModeActive, setLoginDeps } from "./login.js";
|
|
4
|
+
import { getAllStreams, getStreamCount, hasActiveCaptureStreams, hasEstablishedStreams, isCaptureIdentity } from "../streaming/registry.js";
|
|
5
|
+
import { getCachedTabId, installStrayOpenTabReaper, onTabActivation } from "./tabSelection.js";
|
|
4
6
|
import { getChromeDataDir, getDataDir, getExtensionDir } from "../config/paths.js";
|
|
5
|
-
import { getExtensionPage,
|
|
7
|
+
import { getExtensionPage, launch } from "puppeteer-stream";
|
|
6
8
|
import { getGpuCapabilities, setGpuCapabilities } from "./display.js";
|
|
7
|
-
import { minimizeWindow, unminimizeWindow, withCDPSession } from "./cdp.js";
|
|
9
|
+
import { minimizeWindow, readWindowPlacement, reaffirmCaptureSurface, unminimizeWindow, withCDPSession } from "./cdp.js";
|
|
8
10
|
import { CONFIG } from "../config/index.js";
|
|
11
|
+
import { EXTENSION_READY_EXPRESSION } from "./tabCapture.js";
|
|
9
12
|
import { clearChannelSelectionCaches } from "./channelSelection.js";
|
|
10
13
|
import { createBrowserSupervisor } from "./browserSupervisor.js";
|
|
14
|
+
import { createWindowVisibilitySync } from "./windowSync.js";
|
|
11
15
|
import { emitSystemStatusChanged } from "../streaming/statusEmitter.js";
|
|
12
16
|
import { evaluateStalePages } from "./pageStaleness.js";
|
|
13
17
|
import fs from "node:fs";
|
|
14
18
|
import { getPresetViewport } from "../config/presets.js";
|
|
15
19
|
import path from "node:path";
|
|
16
20
|
import { launch as puppeteerLaunch } from "puppeteer-core";
|
|
17
|
-
import { setChromeUserAgent } from "../utils/index.js";
|
|
18
21
|
import { startPrecaching } from "./precaching.js";
|
|
19
|
-
import { terminateStream } from "../streaming/lifecycle.js";
|
|
20
22
|
const { promises: fsPromises } = fs;
|
|
21
23
|
/* Global variables maintain the application's runtime state across all operations. We minimize global state where possible, but some values must be shared across
|
|
22
24
|
* the application lifecycle:
|
|
@@ -69,17 +71,19 @@ function buildRelaunchPolicy() {
|
|
|
69
71
|
}
|
|
70
72
|
/* The one browser capture-readiness supervisor for the process lifetime. It owns the lifecycle state (absent/launching/ready/degraded/trialing) that unifies the
|
|
71
73
|
* browser reference, launch promise, and launch timestamp, and routes every relaunch through one loop-safe governor. The adapter injects the impure
|
|
72
|
-
* ports: launchReadyBrowser (spawn Chrome and run the readiness gate), closeBrowserInstance (teardown),
|
|
74
|
+
* ports: launchReadyBrowser (spawn Chrome and run the readiness gate), closeBrowserInstance (teardown), systemClock.now (time), buildRelaunchPolicy (live config
|
|
73
75
|
* bounds), and onSupervisorStateChange (the loud degraded alarm and the recovery notice). All browser access flows through it: getCurrentBrowser is acquire(); the
|
|
74
76
|
* non-launching reads derive from current() and currentLaunchTime(). The injected ports are hoisted function declarations, so referencing them here is safe even
|
|
75
77
|
* though they are defined further down the module.
|
|
76
78
|
*/
|
|
77
|
-
const supervisor = createBrowserSupervisor({ close: closeBrowserInstance, launch: launchReadyBrowser, now:
|
|
78
|
-
policy: buildRelaunchPolicy });
|
|
79
|
+
const supervisor = createBrowserSupervisor({ close: closeBrowserInstance, launch: launchReadyBrowser, now: () => systemClock.now(),
|
|
80
|
+
onStateChange: onSupervisorStateChange, policy: buildRelaunchPolicy });
|
|
79
81
|
/**
|
|
80
82
|
* Observes supervisor lifecycle transitions purely for operator-visible signals; it never affects the transition (the supervisor treats it as best-effort, so a
|
|
81
|
-
* throwing logger cannot corrupt the lifecycle). It raises the loud degraded alarm when the relaunch governor trips,
|
|
82
|
-
* restored after a degraded period, and emits the SSE system status
|
|
83
|
+
* throwing logger cannot corrupt the lifecycle). It raises the loud degraded alarm when the relaunch governor trips, the capture-impairment alarm when a published
|
|
84
|
+
* browser is marked as unable to start captures, an info notice when capture readiness is restored after a degraded period, and emits the SSE system status
|
|
85
|
+
* whenever a ready browser is published. It reports rather than acts: the caller holding a verdict is what drives any recovery from it, because a restart begun
|
|
86
|
+
* from inside this observer would re-enter the supervisor while the transition that notified it is still on the stack.
|
|
83
87
|
* @param next - The state being entered.
|
|
84
88
|
* @param previous - The state being left.
|
|
85
89
|
*/
|
|
@@ -87,7 +91,7 @@ function onSupervisorStateChange(next, previous) {
|
|
|
87
91
|
// The governor just tripped: relaunches are paused while the browser's capture system cools down. We log loudly at ERROR with the cooldown horizon so the
|
|
88
92
|
// condition is never invisible - the enforceable form of "it is impossible to be silently un-tunable."
|
|
89
93
|
if ((next.kind === "degraded") && (previous.kind !== "degraded")) {
|
|
90
|
-
const cooldownMinutes = Math.max(1, Math.round((next.until -
|
|
94
|
+
const cooldownMinutes = Math.max(1, Math.round((next.until - systemClock.now()) / 60000));
|
|
91
95
|
LOG.error("The browser capture system has degraded and the relaunch governor has tripped: %s Relaunches are paused for approximately %d minute(s) while it " +
|
|
92
96
|
"cools down; new stream requests will receive a 503 back-off until it recovers.", next.reason, cooldownMinutes);
|
|
93
97
|
}
|
|
@@ -96,29 +100,54 @@ function onSupervisorStateChange(next, previous) {
|
|
|
96
100
|
// silent; only a recovery from trialing/degraded is worth an operator notice.
|
|
97
101
|
LOG.info("The browser capture system has recovered and is serving captures again.");
|
|
98
102
|
}
|
|
103
|
+
else if ((next.kind === "ready") && (next.impairment !== null) && ((previous.kind !== "ready") || (previous.impairment === null))) {
|
|
104
|
+
// The published browser has been marked as unable to start captures. The transition carrying the mark happens once per instance, so the alarm fires once per
|
|
105
|
+
// instance too, and it says what an operator watching a stalled tune needs: the running captures are unaffected, new requests back off, and the cure arrives
|
|
106
|
+
// on its own as soon as nothing on this browser holds a page.
|
|
107
|
+
LOG.error("The browser can no longer start captures (%s). Its running captures continue, new stream requests receive a 503 back-off, and it will relaunch as " +
|
|
108
|
+
"soon as nothing holds a page on it.", next.impairment.reason);
|
|
109
|
+
}
|
|
99
110
|
// The browser's connectivity is part of the SSE system status, so emit when a ready browser is published. Readiness-loss emits are owned by handleBrowserDisconnect
|
|
100
111
|
// (genuine disconnect) and the shutdown path; emitSystemStatusChanged dedupes, so a redundant emit is a cheap no-op.
|
|
101
112
|
if (next.kind === "ready") {
|
|
102
113
|
void emitCurrentSystemStatus();
|
|
103
114
|
}
|
|
104
115
|
}
|
|
105
|
-
/* The capture-readiness probe (capability tier of the launch gate). Null until streaming/setup.ts injects the real
|
|
106
|
-
* order
|
|
107
|
-
* launchReadyBrowser refuses to publish a browser if it is somehow still null (see the call
|
|
116
|
+
/* The capture-readiness probe (capability tier of the launch gate). Null until streaming/setup.ts injects the real capture probe at module load, which the import
|
|
117
|
+
* order places ahead of any launch: index.ts value-imports app.ts, app.ts value-imports streaming/hls.ts, and hls.ts value-imports streaming/setup.ts, so the
|
|
118
|
+
* injection has run by the time index.ts's own body calls startServer. launchReadyBrowser refuses to publish a browser if it is somehow still null (see the call
|
|
119
|
+
* site), rather than serving an unverified one.
|
|
108
120
|
*/
|
|
109
121
|
let captureProbe = null;
|
|
110
122
|
/**
|
|
111
123
|
* Injects the capture-readiness probe used as the capability tier of the launch gate. Called once from streaming/setup.ts at module load (which always precedes any
|
|
112
124
|
* launch, since the streaming layer is imported during server startup). Separating the wiring from the call keeps browser/index.ts free of a streaming-layer import.
|
|
113
|
-
* @param probe - The probe to run against a freshly-launched browser; it resolves when the browser can capture and rejects
|
|
114
|
-
* unrecoverable stale-mutex case).
|
|
125
|
+
* @param probe - The probe to run against a freshly-launched browser; it resolves when the browser can capture and rejects when it cannot.
|
|
115
126
|
*/
|
|
116
127
|
export function setCaptureProbe(probe) {
|
|
117
128
|
captureProbe = probe;
|
|
118
129
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
130
|
+
/* The authoritative stream terminator. Null until streaming/lifecycle.ts injects terminateStream at module load, which the import order places ahead of any
|
|
131
|
+
* browser launch: index.ts value-imports app.ts, app.ts value-imports streaming/lifecycle.ts, and a module body finishes before the body of anything that
|
|
132
|
+
* imports it, so the injection has run by the time index.ts's own body calls startServer - the earliest moment a launch, and so a disconnect, can happen. A
|
|
133
|
+
* still-null terminator at readiness loss is reported at ERROR and the teardown carries on (see the call site), because losing the rest of the teardown on top
|
|
134
|
+
* of the stream cleanup would make a recoverable situation worse.
|
|
135
|
+
*/
|
|
136
|
+
let streamTerminator = null;
|
|
137
|
+
/**
|
|
138
|
+
* Injects the authoritative stream terminator used when browser readiness is lost. Called once from streaming/lifecycle.ts at module load. Separating the wiring
|
|
139
|
+
* from the call keeps browser/index.ts free of a streaming-layer import.
|
|
140
|
+
* @param terminator - The function that tears down one stream and all of the resources it owns.
|
|
141
|
+
*/
|
|
142
|
+
export function setStreamTerminator(terminator) {
|
|
143
|
+
streamTerminator = terminator;
|
|
144
|
+
}
|
|
145
|
+
// The identity of the periodic sweep on the stale-page owner's registry.
|
|
146
|
+
const STALE_PAGE_SWEEP_KEY = "sweep";
|
|
147
|
+
/* The stale-page owner's timers: the periodic sweep that closes browser pages no active stream is using, so a long session cannot exhaust resources. The registry
|
|
148
|
+
* is built on the clock the start receives and disposed by the stop, so the sweep can never outlive the owner. Null until started, and again once stopped.
|
|
149
|
+
*/
|
|
150
|
+
let stalePageTimers = null;
|
|
122
151
|
/* Opportunistic browser restart state. Chrome accumulates memory pressure, GPU process issues, and general flakiness over multi-hour sessions with continuous
|
|
123
152
|
* media playback. We proactively restart Chrome after it has been running for BROWSER_MAX_AGE, waiting for a quiet period with zero active streams before
|
|
124
153
|
* executing the restart. After the restart, a fresh browser is launched immediately so it is ready for the next stream request.
|
|
@@ -129,11 +158,23 @@ const BROWSER_MAX_AGE = 6 * 60 * 60 * 1000;
|
|
|
129
158
|
const BROWSER_RESTART_QUIET_PERIOD = 5 * 60 * 1000;
|
|
130
159
|
// How often to check whether the browser qualifies for a restart (30 seconds).
|
|
131
160
|
const BROWSER_RESTART_CHECK_INTERVAL = 30000;
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
161
|
+
// The identity of the periodic eligibility check on the restart owner's registry.
|
|
162
|
+
const RESTART_CHECK_KEY = "check";
|
|
163
|
+
// The identity of the quiet-period countdown on the restart owner's registry.
|
|
164
|
+
const RESTART_QUIET_KEY = "quiet";
|
|
165
|
+
/* The restart owner's timers, keyed on one registry built on the clock its start receives: the periodic eligibility check, and - while the browser has exceeded
|
|
166
|
+
* BROWSER_MAX_AGE and we are waiting for BROWSER_RESTART_QUIET_PERIOD to elapse with zero active streams - the quiet-period countdown, which a stream starting
|
|
167
|
+
* during the wait cancels. Holding both on one registry is what makes the stop drain the countdown along with the check. Null until the check is started, and
|
|
168
|
+
* again once it is stopped.
|
|
169
|
+
*/
|
|
170
|
+
let restartTimers = null;
|
|
171
|
+
/**
|
|
172
|
+
* Cancels the scheduled-restart quiet period if one is pending, so the countdown it was running cannot fire against a browser it no longer describes. Reads the
|
|
173
|
+
* module binding rather than a handed-in registry because executeBrowserRestart calls it as well, which also makes it a no-op once the owner has been stopped.
|
|
174
|
+
*/
|
|
175
|
+
function cancelRestartQuietTimer() {
|
|
176
|
+
restartTimers?.clear(RESTART_QUIET_KEY);
|
|
177
|
+
}
|
|
137
178
|
// Flag indicating that the browser is being closed intentionally via closeBrowser(). When true, the disconnect handler skips error logging and stream termination
|
|
138
179
|
// since these are handled by the shutdown code path. This prevents false "unexpected disconnect" errors during graceful shutdown.
|
|
139
180
|
let gracefulShutdownInProgress = false;
|
|
@@ -166,23 +207,205 @@ const managedPageIds = new Set();
|
|
|
166
207
|
// Map from page ID to timestamp when a page was first observed as potentially stale (not associated with an active stream). Pages must remain in this state for
|
|
167
208
|
// the configured grace period before being closed. This prevents race conditions where pages are briefly untracked during initialization or cleanup transitions.
|
|
168
209
|
const potentiallyStalePages = new Map();
|
|
169
|
-
/* Set of IDs for pages
|
|
170
|
-
* it completes, which on a slow tune is long enough for the
|
|
171
|
-
*
|
|
172
|
-
*
|
|
210
|
+
/* Set of IDs for pages an operation owns for its whole duration while nothing the cleanup walk reads records that ownership. Stream setup is one such owner: it
|
|
211
|
+
* writes the registry's page reference only once it completes, which on a slow tune is long enough for the walk to see the page as unowned and close it out from
|
|
212
|
+
* under the setup driving it. A channel discovery walk is another: its page is never a stream page, so no registry entry will ever speak for it, and the walk's
|
|
213
|
+
* own cleanup is what releases it. Membership here exempts the page from staleness for exactly the window its owner holds it. An entry leaves the set when
|
|
214
|
+
* unregisterManagedPage releases the page as its owner finishes, when the cleanup walk sees the registry record the ownership the mark stood in for, and when
|
|
215
|
+
* clearPageTracking wipes the collections at the end of a browser session.
|
|
173
216
|
*/
|
|
174
|
-
const
|
|
217
|
+
const inFlightPageIds = new Set();
|
|
218
|
+
/* The pages that live in a window of their own rather than the shared one. A CDP window command acts on the window of the page whose session carries it, so
|
|
219
|
+
* which page a command travels on decides which window moves...and a page in its own window would move that one. Membership here is what keeps such a page
|
|
220
|
+
* out of every site that reaches for a convenient page to command the shared window through.
|
|
221
|
+
*
|
|
222
|
+
* Keyed on the page and never cleared. The fact is intrinsic to how the page was created rather than a phase of its life, an entry dies with the page that
|
|
223
|
+
* holds it, and the picker below already declines a closed page - so unregistration, teardown order, and clearPageTracking have nothing to maintain here.
|
|
224
|
+
*/
|
|
225
|
+
const ownWindowPages = new WeakSet();
|
|
226
|
+
/**
|
|
227
|
+
* Reports whether a page may carry a CDP command aimed at the shared window: it exists, it is still open, and it does not live in a window of its own. The
|
|
228
|
+
* parameter accepts the absent case so every caller narrows through this one test rather than guarding beside it.
|
|
229
|
+
* @param page - The candidate page, which may be absent.
|
|
230
|
+
* @returns True when the page can carry a shared-window command.
|
|
231
|
+
*/
|
|
232
|
+
export function isCarrierPage(page) {
|
|
233
|
+
return !!page && !page.isClosed() && !ownWindowPages.has(page);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Picks the page a shared-window command should travel on from the pages a browser has open, taking the first that qualifies.
|
|
237
|
+
* @param pages - The candidate pages, in the order the browser reports them.
|
|
238
|
+
* @returns The page to carry the command, or null when none of them qualifies.
|
|
239
|
+
*/
|
|
240
|
+
export function pickCarrierPage(pages) {
|
|
241
|
+
return pages.find(isCarrierPage) ?? null;
|
|
242
|
+
}
|
|
243
|
+
/* Which window Chrome's own tables call the shared one, keyed on the browser that owns it. The value belongs to CDP's window table and is never comparable with
|
|
244
|
+
* the capture extension's chrome.windows ids: those are two independent tables, and this is the only place a value from the CDP side is held.
|
|
245
|
+
*
|
|
246
|
+
* Keyed on the browser and never cleared, for the reason the own-window mark above is: the identity is a fact about how that browser was created, an entry dies
|
|
247
|
+
* with the browser holding it, and a relaunch records its own. An id that outlived its browser is exactly what would place a capture tab in the wrong window
|
|
248
|
+
* while every check agreed, so there is deliberately no way to write one a live browser did not report.
|
|
249
|
+
*/
|
|
250
|
+
const sharedWindowIds = new WeakMap();
|
|
251
|
+
/* The window each page was found in, cached for the page's life in the tabIds idiom of tabSelection.ts. The lookup below attaches a CDP session the shared helper
|
|
252
|
+
* does not detach, so reading a candidate fresh on every tune would leave a session behind on each launch-era page every time. A page a user drags into another
|
|
253
|
+
* window keeps the window it was first read in; a stale entry costs a carrier that has since moved out of the shared window, which the placement confirmation
|
|
254
|
+
* catches and answers with the plain create rather than with a tab in the wrong place.
|
|
255
|
+
*/
|
|
256
|
+
const pageWindowIds = new WeakMap();
|
|
257
|
+
// The URL prefix the capture extension's own pages carry. The resolver reads it to prefer a plain-origin carrier, since an open evaluated on the extension's
|
|
258
|
+
// options page is territory nothing has measured.
|
|
259
|
+
const EXTENSION_PAGE_PREFIX = "chrome-extension://";
|
|
260
|
+
/**
|
|
261
|
+
* Reads the window a page sits in, answering from the cache once the page has been read.
|
|
262
|
+
* @param page - The page to locate.
|
|
263
|
+
* @returns The window's CDP id, or undefined when the window cannot be read at all - a closed page, a target that yields no window, or a CDP failure.
|
|
264
|
+
*/
|
|
265
|
+
async function readPageWindow(page) {
|
|
266
|
+
const cached = pageWindowIds.get(page);
|
|
267
|
+
if (cached !== undefined) {
|
|
268
|
+
return cached;
|
|
269
|
+
}
|
|
270
|
+
const windowId = await withCDPSession(page, async (_session, id) => id);
|
|
271
|
+
if (windowId !== undefined) {
|
|
272
|
+
pageWindowIds.set(page, windowId);
|
|
273
|
+
}
|
|
274
|
+
return windowId;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Records which window the shared one is, reading it from a page that sits in it.
|
|
278
|
+
*
|
|
279
|
+
* This is the one way an identity is written. The launch path calls it once the launch-era pages exist, so every browser this process publishes carries the
|
|
280
|
+
* answer before anything asks for a capture tab, and a probe running against a browser of its own calls the same function rather than reaching for the map.
|
|
281
|
+
* @param browser - The browser whose shared window is being recorded.
|
|
282
|
+
* @param page - A page that sits in that window.
|
|
283
|
+
*/
|
|
284
|
+
export async function noteSharedWindow(browser, page) {
|
|
285
|
+
const windowId = await readPageWindow(page);
|
|
286
|
+
if (windowId === undefined) {
|
|
287
|
+
LOG.debug("browser:lifecycle", "The shared browser window could not be identified, so capture tabs will open wherever Chrome places them.");
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
sharedWindowIds.set(browser, windowId);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Picks the page an opener-anchored tab should be opened from: one the browser has open, living in the shared window rather than a window of its own, and
|
|
294
|
+
* confirmed to sit in the window this process recorded.
|
|
295
|
+
*
|
|
296
|
+
* A plain-origin page is preferred over the capture extension's own options page, which is the carrier of last resort. The options page is recognized by the URL
|
|
297
|
+
* prefix its own pages carry rather than through the library's resolver, whose wait for a missing extension would spend thirty seconds inside a caller's turn.
|
|
298
|
+
* When no identity was ever recorded the confirmation has nothing to test against, and the first qualifying page is the answer: an anchor to some window of the
|
|
299
|
+
* browser's is still better than letting Chrome choose one.
|
|
300
|
+
* @param browser - The browser to resolve a carrier in.
|
|
301
|
+
* @returns The page to evaluate the open on, or null when none of the browser's pages qualifies.
|
|
302
|
+
*/
|
|
303
|
+
export async function resolveSharedWindowCarrier(browser) {
|
|
304
|
+
const sharedWindowId = sharedWindowIds.get(browser);
|
|
305
|
+
const candidates = (await browser.pages()).filter(isCarrierPage);
|
|
306
|
+
for (const page of [...candidates.filter((candidate) => !candidate.url().startsWith(EXTENSION_PAGE_PREFIX)),
|
|
307
|
+
...candidates.filter((candidate) => candidate.url().startsWith(EXTENSION_PAGE_PREFIX))]) {
|
|
308
|
+
if (sharedWindowId === undefined) {
|
|
309
|
+
return page;
|
|
310
|
+
}
|
|
311
|
+
// eslint-disable-next-line no-await-in-loop -- A candidate is only asked for its window once the candidates ahead of it have failed to qualify.
|
|
312
|
+
if ((await readPageWindow(page)) === sharedWindowId) {
|
|
313
|
+
return page;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Reports whether a page ended up in the shared window.
|
|
320
|
+
*
|
|
321
|
+
* Advisory when no identity was ever recorded: there is nothing to contradict the placement with, so the answer is true and the caller proceeds. A window that
|
|
322
|
+
* cannot be READ is the opposite answer, though, and deliberately so - the shared lookup reports undefined for a closed page, an empty response, and a CDP
|
|
323
|
+
* failure alike, and reading any of those as a confirmation would confirm a wrong-window tab in exactly the case this check exists for.
|
|
324
|
+
* @param page - The page to test.
|
|
325
|
+
* @returns True when the page sits in the recorded window, or when no window was ever recorded.
|
|
326
|
+
*/
|
|
327
|
+
export async function confirmSharedWindowPlacement(page) {
|
|
328
|
+
const sharedWindowId = sharedWindowIds.get(page.browser());
|
|
329
|
+
if (sharedWindowId === undefined) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
return (await readPageWindow(page)) === sharedWindowId;
|
|
333
|
+
}
|
|
175
334
|
// Login mode management. State and functions live in login.ts; re-exported here so existing consumers don't need import path changes. clearLoginState,
|
|
176
|
-
// isLoginModeActive, and
|
|
335
|
+
// isLoginModeActive, and setLoginDeps are imported above; the first two for internal use, setLoginDeps for one-time initialization below.
|
|
177
336
|
export { clearLoginState, isLoginModeActive };
|
|
178
337
|
export { endLoginMode, getLoginPage, getLoginStatus, setLoginModeEndObserver, startLoginMode } from "./login.js";
|
|
179
338
|
// Re-export the supervisor's acquire() rejection classes through the browser surface so the stream-setup layer can map them to a 503 back-off without reaching into
|
|
180
|
-
// the supervisor module directly.
|
|
181
|
-
// when a launch was abandoned mid-flight by a readiness-loss
|
|
182
|
-
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
339
|
+
// the supervisor module directly. Each signals a transient "retry me" condition: BrowserUnavailableError while the relaunch governor is cooling,
|
|
340
|
+
// BrowserSupersededError when a launch was abandoned mid-flight by a readiness-loss, and BrowserCaptureImpairedError when the published browser can no longer start
|
|
341
|
+
// captures and is waiting for its streams to end. The purpose and impairment types cross the same boundary, so a caller declares what it needs the browser for and
|
|
342
|
+
// reads the mark without importing the supervisor.
|
|
343
|
+
export { BrowserCaptureImpairedError, BrowserSupersededError, BrowserUnavailableError } from "./browserSupervisor.js";
|
|
344
|
+
/* The one window-presentation executor for the process lifetime. Its collaborators all live in this module: the registry predicate that says whether capture is
|
|
345
|
+
* reading the compositor, login mode's own flag, the shutdown gate, the two CDP primitives, and a page resolver built on the supervisor.
|
|
346
|
+
*
|
|
347
|
+
* The resolver prefers the page its caller handed over, borrows any page the browser already has open when there is none, and creates a temporary page only when
|
|
348
|
+
* the browser has nothing open at all. A temporary page is the only resolution that carries a dispose, so the executor releases what the resolver created and
|
|
349
|
+
* leaves a borrowed page alone. Both the preference and the borrow go through the shared carrier rule, because a command travels on a page's own session and
|
|
350
|
+
* so acts on that page's window: a page living in a window of its own would move that window instead of the shared one, whether it arrived as the caller's
|
|
351
|
+
* preference or as the first page the browser happened to report.
|
|
352
|
+
*
|
|
353
|
+
* The instance stays private to this module and every caller reaches it through the exported function just below. That split is what makes the symbol safe to
|
|
354
|
+
* reference at module-evaluation time: a function declaration is hoisted, so the setLoginDeps call further down - and any sibling module whose own body
|
|
355
|
+
* evaluates while this module is still evaluating, precaching.ts among them - resolves it whatever the declaration order turns out to be. A bare exported const
|
|
356
|
+
* would leave those readers in the temporal dead zone, which no placement inside this file can fix for a reader in another file.
|
|
357
|
+
*/
|
|
358
|
+
const windowVisibilitySync = createWindowVisibilitySync({
|
|
359
|
+
hasActiveCaptureStreams,
|
|
360
|
+
isLoginModeActive,
|
|
361
|
+
isShuttingDown: isGracefulShutdown,
|
|
362
|
+
minimize: minimizeWindow,
|
|
363
|
+
resolvePage: async (preferred) => {
|
|
364
|
+
const browser = supervisor.current();
|
|
365
|
+
if (!browser?.connected) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
if (isCarrierPage(preferred)) {
|
|
369
|
+
return { dispose: null, page: preferred };
|
|
370
|
+
}
|
|
371
|
+
const borrowed = pickCarrierPage(await browser.pages());
|
|
372
|
+
if (borrowed) {
|
|
373
|
+
return { dispose: null, page: borrowed };
|
|
374
|
+
}
|
|
375
|
+
// A page that exists only to carry a CDP command never needs its tab selected.
|
|
376
|
+
const temporary = await browser.newPage({ background: true });
|
|
377
|
+
// Registered so stale page cleanup recognizes the page as ours for the moment it exists.
|
|
378
|
+
registerManagedPage(temporary);
|
|
379
|
+
return {
|
|
380
|
+
dispose: async () => {
|
|
381
|
+
unregisterManagedPage(temporary);
|
|
382
|
+
try {
|
|
383
|
+
await temporary.close();
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
// The browser may already have taken the page with it. Nothing downstream depends on the close succeeding.
|
|
387
|
+
LOG.debug("browser:lifecycle", "Could not close the temporary window-sync page: %s.", formatError(error));
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
page: temporary
|
|
391
|
+
};
|
|
392
|
+
},
|
|
393
|
+
unminimize: unminimizeWindow
|
|
394
|
+
});
|
|
395
|
+
/**
|
|
396
|
+
* Brings the shared browser window into agreement with the visibility policy. This is the one entry point every layer uses - streaming, recovery, login,
|
|
397
|
+
* precaching, and startup all call it and none of them decide window state themselves.
|
|
398
|
+
* @param page - The page whose CDP session should carry the command, when the caller has one in hand. The executor falls back to any open page, or a temporary
|
|
399
|
+
* one, when it is omitted or the page has closed.
|
|
400
|
+
* @returns A promise that resolves once a pass that began at or after this call has completed.
|
|
401
|
+
*/
|
|
402
|
+
export async function syncWindowVisibility(page) {
|
|
403
|
+
return windowVisibilitySync(page);
|
|
404
|
+
}
|
|
405
|
+
// Inject login's dependency set. This breaks the circular dependency (login needs getBrowserInstance and the window sync, index needs login functions) using the
|
|
406
|
+
// same setter/getter pattern as setChromeUserAgent in chromeFetch.ts. Both accessors are hoisted function declarations, so this call reads them at module
|
|
407
|
+
// evaluation time regardless of where they sit in the file. No clock is supplied, so login runs on the system clock the port defaults to.
|
|
408
|
+
setLoginDeps({ getBrowserInstance, syncWindowVisibility });
|
|
186
409
|
/**
|
|
187
410
|
* Computes the current system status and emits it to SSE subscribers. Called when browser state changes significantly or when streams are added/removed.
|
|
188
411
|
*/
|
|
@@ -203,6 +426,12 @@ export async function emitCurrentSystemStatus() {
|
|
|
203
426
|
const memUsage = process.memoryUsage();
|
|
204
427
|
const status = {
|
|
205
428
|
browser: {
|
|
429
|
+
/* Read at compose time, beside the connectivity read below rather than as a snapshot taken before the page-count await. This function is called un-awaited
|
|
430
|
+
* from many sites, so two calls can be in flight around the instant a mark lands; a pre-mark snapshot settling after the mark's own emit would broadcast
|
|
431
|
+
* false over the cached true and the dedupe would honor it. Reading here makes every emit describe the state at the moment consumers receive it, so the
|
|
432
|
+
* later of two racing emits is also the truer one.
|
|
433
|
+
*/
|
|
434
|
+
captureImpaired: supervisor.captureImpairment() !== null,
|
|
206
435
|
connected: !!browser && browser.connected,
|
|
207
436
|
pageCount
|
|
208
437
|
},
|
|
@@ -225,8 +454,9 @@ export async function emitCurrentSystemStatus() {
|
|
|
225
454
|
* Each registered page receives a unique ID that persists for the page's lifetime. This ID is used for comparison and staleness tracking, avoiding potential
|
|
226
455
|
* issues with Page object reference identity.
|
|
227
456
|
* @param page - The Puppeteer Page to register.
|
|
228
|
-
* @param options - Registration options. Set
|
|
229
|
-
*
|
|
457
|
+
* @param options - Registration options. Set inFlight when an operation owns the page for its duration but nothing the stale page cleanup reads records that
|
|
458
|
+
* ownership - a stream setup that has not yet written its page into the registry, or a discovery walk whose page belongs to no stream at all - so the cleanup
|
|
459
|
+
* never closes a page out from under the operation driving it.
|
|
230
460
|
*/
|
|
231
461
|
export function registerManagedPage(page, options = {}) {
|
|
232
462
|
// Generate a unique ID for this page.
|
|
@@ -235,9 +465,9 @@ export function registerManagedPage(page, options = {}) {
|
|
|
235
465
|
pageToId.set(page, pageId);
|
|
236
466
|
// Track the ID as managed.
|
|
237
467
|
managedPageIds.add(pageId);
|
|
238
|
-
// Exempt the page from staleness for as long as
|
|
239
|
-
if (options.
|
|
240
|
-
|
|
468
|
+
// Exempt the page from staleness for as long as the operation that owns it is in flight.
|
|
469
|
+
if (options.inFlight) {
|
|
470
|
+
inFlightPageIds.add(pageId);
|
|
241
471
|
}
|
|
242
472
|
}
|
|
243
473
|
/**
|
|
@@ -251,8 +481,8 @@ export function unregisterManagedPage(page) {
|
|
|
251
481
|
managedPageIds.delete(pageId);
|
|
252
482
|
// Also remove from potentially stale tracking since we're intentionally closing it.
|
|
253
483
|
potentiallyStalePages.delete(pageId);
|
|
254
|
-
// Release any in-flight
|
|
255
|
-
|
|
484
|
+
// Release any in-flight mark. The page is leaving our management, so the exemption it carried has nothing left to protect.
|
|
485
|
+
inFlightPageIds.delete(pageId);
|
|
256
486
|
// Note: We don't delete from pageToId because WeakMap handles cleanup automatically when the Page is garbage collected.
|
|
257
487
|
}
|
|
258
488
|
}
|
|
@@ -274,7 +504,7 @@ function getManagedPageId(page) {
|
|
|
274
504
|
function clearPageTracking() {
|
|
275
505
|
managedPageIds.clear();
|
|
276
506
|
potentiallyStalePages.clear();
|
|
277
|
-
|
|
507
|
+
inFlightPageIds.clear();
|
|
278
508
|
}
|
|
279
509
|
/**
|
|
280
510
|
* Ensures the data directory exists, creating it if necessary. This should be called during application startup before any operations that depend on the data
|
|
@@ -434,15 +664,16 @@ export function killStaleChrome() {
|
|
|
434
664
|
}
|
|
435
665
|
/**
|
|
436
666
|
* Polls until the Chrome process with the given PID has exited, or the timeout expires. Uses process.kill(pid, 0) to check process existence - throws ESRCH
|
|
437
|
-
* when the process is gone.
|
|
667
|
+
* when the process is gone. The wait runs inside the synchronous exit handler, so it reads the port for its instants and sleeps synchronously between them
|
|
668
|
+
* using Atomics.wait() for cross-platform compatibility.
|
|
438
669
|
* @param pid - The Chrome process ID to wait for.
|
|
439
670
|
* @param timeoutMs - Maximum time to wait in milliseconds.
|
|
440
671
|
* @param pollIntervalMs - Time between existence checks in milliseconds.
|
|
441
672
|
* @returns True if the process exited within the timeout, false otherwise.
|
|
442
673
|
*/
|
|
443
674
|
function waitForChromeExit(pid, timeoutMs, pollIntervalMs) {
|
|
444
|
-
const deadline =
|
|
445
|
-
while (
|
|
675
|
+
const deadline = systemClock.now() + timeoutMs;
|
|
676
|
+
while (systemClock.now() < deadline) {
|
|
446
677
|
if (!isProcessRunning(pid)) {
|
|
447
678
|
return true;
|
|
448
679
|
}
|
|
@@ -580,6 +811,9 @@ export function getExecutablePath() {
|
|
|
580
811
|
* the full rationale.
|
|
581
812
|
*/
|
|
582
813
|
const PUPPETEER_STREAM_EXTENSION_ID = "jjndjgheafjngoipoacpjgeicjeomjli";
|
|
814
|
+
// Chrome's documented value for disabling the device-scale-factor part of a metrics override, leaving the display's own density in effect. Naming it keeps the
|
|
815
|
+
// sentinel from reading as a literal density of zero.
|
|
816
|
+
const NATIVE_DENSITY = 0;
|
|
583
817
|
/**
|
|
584
818
|
* Assembles the configuration options for launching Chrome with Puppeteer. These options are critical for reliable streaming:
|
|
585
819
|
*
|
|
@@ -590,7 +824,6 @@ const PUPPETEER_STREAM_EXTENSION_ID = "jjndjgheafjngoipoacpjgeicjeomjli";
|
|
|
590
824
|
* @returns Puppeteer launch options.
|
|
591
825
|
*/
|
|
592
826
|
export function buildLaunchOptions() {
|
|
593
|
-
const viewport = getPresetViewport(CONFIG);
|
|
594
827
|
return {
|
|
595
828
|
/* Chrome command-line arguments. Each flag serves a specific purpose for reliable streaming:
|
|
596
829
|
*
|
|
@@ -602,7 +835,7 @@ export function buildLaunchOptions() {
|
|
|
602
835
|
* is a defensive duplicate: if a future puppeteer-stream release drops the flag again in favor of granting activeTab via a synthetic keystroke,
|
|
603
836
|
* CDP-synthesized keystrokes do not satisfy chrome.commands under automation (the renderer sees the event but the browser-process accelerator dispatcher
|
|
604
837
|
* does not), so capture would be denied at the API level (see github.com/Flam3rboy/puppeteer-stream/issues/206) without this fallback in place. Confirm
|
|
605
|
-
* whether this duplicate is still warranted whenever the
|
|
838
|
+
* whether this duplicate is still warranted whenever the locked puppeteer-stream version changes.
|
|
606
839
|
*
|
|
607
840
|
* --autoplay-policy=no-user-gesture-required: Allows video and audio to play without requiring a user click first. Essential for automated streaming
|
|
608
841
|
* since we cannot simulate genuine user interaction for autoplay policy purposes.
|
|
@@ -645,18 +878,30 @@ export function buildLaunchOptions() {
|
|
|
645
878
|
"--hide-scrollbars",
|
|
646
879
|
"--no-first-run"
|
|
647
880
|
],
|
|
648
|
-
/*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
881
|
+
/* No launch-wide viewport, stated as an explicit null because an omitted option is not the same thing: Puppeteer reads an absent defaultViewport as its own
|
|
882
|
+
* 800x600 default and applies that override to every page it creates, while a null leaves each page carrying no override at all. Two consequences follow, and
|
|
883
|
+
* both are the point. A page renders at whatever surface PrismCast declares on it and nothing else - the capture surface on a capture page, the preset-sized
|
|
884
|
+
* layout on the discovery and capture-probe pages - so no page inherits an emulation it never asked for. And puppeteer-stream derives Chrome's --window-size
|
|
885
|
+
* and --ozone-override-screen-size flags from a sized default, where a --window-size flag stops Chrome restoring the placement it persisted in the profile, so
|
|
886
|
+
* with no default the window's size and placement are Chrome's and the user's.
|
|
887
|
+
*
|
|
888
|
+
* The return type names the null so the key cannot be dropped as dead configuration without a compile error, since its absence is silently a viewport rather
|
|
889
|
+
* than none.
|
|
654
890
|
*/
|
|
655
|
-
defaultViewport:
|
|
891
|
+
defaultViewport: null,
|
|
656
892
|
// Path to the Chrome executable, either from environment variable or autodetected.
|
|
657
893
|
executablePath: getExecutablePath(),
|
|
658
|
-
|
|
659
|
-
|
|
894
|
+
/* PrismCast owns process signals, so the launcher's own listeners are off. Left on, the puppeteer launcher installs handlers for SIGHUP, SIGINT, and SIGTERM
|
|
895
|
+
* on the node process and, on SIGTERM or SIGHUP, ends Chrome's entire process group with SIGKILL - and on SIGINT it kills the group and then exits the process
|
|
896
|
+
* outright. Either way it races the shutdown handlers in app.ts and preempts closeBrowserInstance's SIGTERM ladder below, so Chrome gets no shutdown at all
|
|
897
|
+
* and forfeits whatever it had pending: a window placement inside its save debounce, its session state. With the listeners off, a signal reaches PrismCast's
|
|
898
|
+
* own handler, the graceful path runs, and Chrome exits through the ladder with its pending writes committed.
|
|
899
|
+
*/
|
|
900
|
+
handleSIGHUP: false,
|
|
901
|
+
handleSIGINT: false,
|
|
902
|
+
handleSIGTERM: false,
|
|
903
|
+
// Run Chrome in headed (visible) mode, not headless. The puppeteer-stream extension captures the compositor's output for a real window, which a headless
|
|
904
|
+
// browser does not have. The window is on screen while any capture stream runs or a login session is active, and minimized otherwise.
|
|
660
905
|
headless: false,
|
|
661
906
|
/* Prevent Puppeteer from adding certain default arguments that would interfere with streaming:
|
|
662
907
|
*
|
|
@@ -692,6 +937,233 @@ export function buildLaunchOptions() {
|
|
|
692
937
|
userDataDir: getChromeDataDir(CONFIG)
|
|
693
938
|
};
|
|
694
939
|
}
|
|
940
|
+
/**
|
|
941
|
+
* Declares a device-metrics override on a page: the configured quality preset's dimensions at the density the caller names. Every surface this module declares is
|
|
942
|
+
* built here, so the preset reaches a page's emulation through exactly one read and one command.
|
|
943
|
+
* @param page - The page to declare the surface on.
|
|
944
|
+
* @param deviceScaleFactor - The pixel density to declare. NATIVE_DENSITY leaves the display's own density in effect.
|
|
945
|
+
* @returns The dimensions declared on the page.
|
|
946
|
+
*/
|
|
947
|
+
async function declareSurface(page, deviceScaleFactor) {
|
|
948
|
+
const viewport = getPresetViewport(CONFIG);
|
|
949
|
+
await page.setViewport({ deviceScaleFactor, height: viewport.height, width: viewport.width });
|
|
950
|
+
return { height: viewport.height, width: viewport.width };
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Emulates the capture surface on a page that is about to be captured: the configured quality preset's dimensions, declared at the pixel density the display
|
|
954
|
+
* actually has. Chrome composes an active tab's capture from the window presentation unless the page's device-metrics override declares a density explicitly
|
|
955
|
+
* and that density matches the display's real one, so the density is read from the page itself - a page carrying no declared density reports the display's own -
|
|
956
|
+
* and declared back before capture acquires the page. The declared dimensions are returned so the caller holds its capture constraints to the surface that was
|
|
957
|
+
* emulated rather than reading the preset a second time.
|
|
958
|
+
* @param page - The page to emulate the capture surface on.
|
|
959
|
+
* @returns The dimensions declared on the page.
|
|
960
|
+
*/
|
|
961
|
+
export async function emulateCaptureSurface(page) {
|
|
962
|
+
const reportedDensity = await evaluateWithAbort(page, () => window.devicePixelRatio);
|
|
963
|
+
// A reading that is not a positive finite number is no measurement at all, and the compositor still needs an explicit density...1 is the density of an
|
|
964
|
+
// unscaled display, which is the safest thing to declare when the page cannot say.
|
|
965
|
+
const densityIsUsable = Number.isFinite(reportedDensity) && (reportedDensity > 0);
|
|
966
|
+
const deviceScaleFactor = densityIsUsable ? reportedDensity : 1;
|
|
967
|
+
if (!densityIsUsable) {
|
|
968
|
+
LOG.warn("The capture page reported a pixel density of %s. Emulating the capture surface at a density of %d instead.", reportedDensity, deviceScaleFactor);
|
|
969
|
+
}
|
|
970
|
+
const surface = await declareSurface(page, deviceScaleFactor);
|
|
971
|
+
LOG.debug("browser:lifecycle", "Emulated the capture surface at %dx%d with a pixel density of %s.", surface.width, surface.height, deviceScaleFactor);
|
|
972
|
+
return surface;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Emulates the layout surface on a page that is laid out but never captured: the configured quality preset's dimensions at the display's own density. The guide
|
|
976
|
+
* walks were written against that layout and the launch-gate capture probe acquires against it, so declaring it is what keeps both on the surface they were
|
|
977
|
+
* validated on. The density stays the display's, because nothing reads this page's pixels.
|
|
978
|
+
* @param page - The page to emulate the layout surface on.
|
|
979
|
+
* @returns The dimensions declared on the page.
|
|
980
|
+
*/
|
|
981
|
+
export async function emulateLayoutSurface(page) {
|
|
982
|
+
return declareSurface(page, NATIVE_DENSITY);
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Derives the creation bounds for a window that should open exactly where an existing window rests. The four numbers always travel, so the new window lands on
|
|
986
|
+
* the same frame. The state travels only when it is "maximized", because that is the one state Chrome saves alongside the frame and a maximized window mirrored
|
|
987
|
+
* as a normal one would drop it. Every other state is left off deliberately: a fullscreen window's frame is its whole screen and a normal window at that frame
|
|
988
|
+
* is the right stand-in for it, and a minimized window reports the frame it will return to, which is a placement worth inheriting without the minimization.
|
|
989
|
+
* @param placement - The placement of the window to mirror.
|
|
990
|
+
* @returns The bounds to create the new window with.
|
|
991
|
+
*/
|
|
992
|
+
export function mirrorPlacement(placement) {
|
|
993
|
+
const { height, left, top, width } = placement;
|
|
994
|
+
return (placement.windowState === "maximized") ? { height, left, top, width, windowState: "maximized" } : { height, left, top, width };
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Opens a page for a channel guide discovery walk in a browser window of its own, and marks it as belonging to that window.
|
|
998
|
+
*
|
|
999
|
+
* A document renders only while Chrome presents it: the active tab of a window the desktop is showing, or a page Chrome counts as captured. The shared window
|
|
1000
|
+
* rests minimized whenever nothing is capturing and no sign-in holds it on screen, with its selected tab belonging to the user. A guide walk needs its page to
|
|
1001
|
+
* render - an observer-driven channel rail fills its tiles from rendering updates, a virtualized grid re-renders as the walk scrolls it, and every wait the walk
|
|
1002
|
+
* makes polls on the page's animation frames - yet it is never captured, so it has no claim on the shared window's presentation and no business moving the
|
|
1003
|
+
* user's selection. A window of its own resolves the tab half: the page is that window's active tab from the moment it exists. The window is created in the
|
|
1004
|
+
* background, so Chrome shows it inactive and moves no focus (measured 2026-08-31), and such a window's document is one Chrome does not present on its own: it
|
|
1005
|
+
* reports itself hidden and unfocused and delivers no animation frame beyond a document load's first ones (measured 2026-09-13). Focus emulation resolves the
|
|
1006
|
+
* presentation half, for the page's whole life - Puppeteer's own emulateFocusedPage carries the mechanism, on the page's own session.
|
|
1007
|
+
*
|
|
1008
|
+
* The window opens at the shared window's own placement, so the window placement Chrome persists for the profile never changes - readWindowPlacement carries
|
|
1009
|
+
* the reasoning. The caller declares the layout surface on the page and owns its registration, and closing the page closes the window with it.
|
|
1010
|
+
* @param browser - The browser to open the window in.
|
|
1011
|
+
* @returns The page, as the active tab of its own window.
|
|
1012
|
+
*/
|
|
1013
|
+
export async function createDiscoveryPage(browser) {
|
|
1014
|
+
const carrier = pickCarrierPage(await browser.pages());
|
|
1015
|
+
const placement = carrier ? await readWindowPlacement(carrier) : null;
|
|
1016
|
+
// The mark is set with nothing awaited between the creation and it, so no page read can observe this page as a candidate to carry a shared-window command.
|
|
1017
|
+
const page = await browser.newPage({ background: true, type: "window", windowBounds: placement ? mirrorPlacement(placement) : undefined });
|
|
1018
|
+
ownWindowPages.add(page);
|
|
1019
|
+
/* The page is presented from before its first load, so every wait a walk makes - a visible selector, a condition polled from inside the page - runs as it
|
|
1020
|
+
* would in a window the desktop is showing. Chrome counts a focus-emulated page as captured, which is what presents its document, and Puppeteer keeps the
|
|
1021
|
+
* state on the page's own session for as long as the page lives (measured 2026-09-13). The mark above stays first: nothing is awaited between the creation
|
|
1022
|
+
* and it. A page that refuses the emulation is one no walk can use, and nothing else holds it yet - the caller never receives it and the managed-page sweep
|
|
1023
|
+
* never sees it - so the creator closes it, which closes the window with it, before the failure propagates.
|
|
1024
|
+
*/
|
|
1025
|
+
try {
|
|
1026
|
+
await page.emulateFocusedPage(true);
|
|
1027
|
+
}
|
|
1028
|
+
catch (error) {
|
|
1029
|
+
try {
|
|
1030
|
+
await page.close();
|
|
1031
|
+
}
|
|
1032
|
+
catch {
|
|
1033
|
+
// The page is already gone, which is the state the close was asking for.
|
|
1034
|
+
}
|
|
1035
|
+
throw error;
|
|
1036
|
+
}
|
|
1037
|
+
LOG.debug("browser:lifecycle", "Opened the discovery page in a window of its own, %s.", placement ? "mirroring the shared window's placement" : "with no shared window to read a placement from");
|
|
1038
|
+
return page;
|
|
1039
|
+
}
|
|
1040
|
+
/* The follow-up re-issue schedule a tab activation runs, expressed as offsets in milliseconds from the activation's own invocation rather than as gaps between
|
|
1041
|
+
* shots. Chrome switches an activated tab's capture to the window's fitted presentation only after the focus event has fired, so the immediate re-issue that
|
|
1042
|
+
* event triggers lands too early to stick (measured 2026-08-30: a selected capture tab's recording stayed fitted for upwards of forty seconds, until the
|
|
1043
|
+
* monitor's periodic re-affirmation healed it completely with the tab still selected). These offsets bracket the switch so one shot lands just past it, the last
|
|
1044
|
+
* rung allowing for platforms slower than the one measured. Every shot beyond the one that sticks is a no-op by reaffirmCaptureSurface's own contract, and the
|
|
1045
|
+
* monitor's periodic re-affirmation remains the backstop bounding anything the whole ladder misses.
|
|
1046
|
+
*/
|
|
1047
|
+
const ACTIVATION_REAFFIRM_LADDER_MS = [250, 750, 1500, 3000];
|
|
1048
|
+
/* The heal enrolled for each capture page, which is the trigger the selection primitive's activation report fires. Keying on the page rather than on a tab id is
|
|
1049
|
+
* what makes the enrollment expire on its own: an entry lives exactly as long as the page it heals. The callback held here is the very instance that page's focus
|
|
1050
|
+
* binding invokes, so the two triggers share one generation counter and an activation arriving through either route supersedes an in-flight ladder rather than
|
|
1051
|
+
* running a second one beside it.
|
|
1052
|
+
*/
|
|
1053
|
+
const activationHeals = new WeakMap();
|
|
1054
|
+
/**
|
|
1055
|
+
* Builds the callback the page's focus binding invokes: an immediate re-issue, then a ladder of follow-ups at ACTIVATION_REAFFIRM_LADDER_MS's offsets from this
|
|
1056
|
+
* invocation. The ladder is what makes the heal land, because the compositor's switch outlasts the focus event announcing it - the immediate shot alone cannot
|
|
1057
|
+
* reach the far side of the switch, so later shots have to.
|
|
1058
|
+
*
|
|
1059
|
+
* Invocations supersede rather than accumulate. A generation counter makes the newest invocation the owner of the rung schedule: the duplicate focus events a
|
|
1060
|
+
* single activation fires collapse into one schedule a few milliseconds newer than the first, while an activation that genuinely arrives later - a second
|
|
1061
|
+
* stream's tab give-back landing on this page, another click - gets a full bracket timed from its own moment instead of inheriting the tail of an in-flight one.
|
|
1062
|
+
* The counter lives in this closure, so a callback supersedes only the ladders it started itself and two streams' pages cannot reach each other's schedules.
|
|
1063
|
+
*
|
|
1064
|
+
* Nothing cancels a superseded ladder, because nothing has to: it returns at its next generation check, a shot against a page mid-teardown rejects into the
|
|
1065
|
+
* swallow below, and a shot that never settles leaves its ladder awaiting a promise nobody holds. The counter needs no reset for the same reason - a newer
|
|
1066
|
+
* invocation is itself the invalidation - and the schedule holds no timers, only awaited sleeps.
|
|
1067
|
+
*
|
|
1068
|
+
* A rejection is swallowed into a debug line because a focus event races page teardown by nature - the tab a user just selected can be the one a terminating
|
|
1069
|
+
* stream is closing - and the periodic re-affirmation corrects anything a lost re-issue leaves behind.
|
|
1070
|
+
* @param page - The capture page this callback re-affirms.
|
|
1071
|
+
* @param reaffirm - The re-issue to invoke. A parameter rather than a direct call so the callback's behavior can be driven on its own: what it does with the page
|
|
1072
|
+
* it was built for, what schedule it keeps, and what it does with a rejection, is the whole of its contract.
|
|
1073
|
+
* @param clock - The time source the ladder's waits run on. Defaults to the system clock.
|
|
1074
|
+
* @returns The callback, which never rejects. Its promise settles once the ladder is spent, which the page-side listener does not wait on.
|
|
1075
|
+
*/
|
|
1076
|
+
export function makeFocusReaffirmCallback(page, reaffirm, clock = systemClock) {
|
|
1077
|
+
// The generation this closure hands out. It only ever increments: an invocation claims the next one and owns the rung schedule until a later invocation claims
|
|
1078
|
+
// a higher one.
|
|
1079
|
+
let generation = 0;
|
|
1080
|
+
// One rung's re-issue. The offset it was scheduled at is carried on the debug line, so a swallowed failure says which shot spoke; the immediate shot reports zero.
|
|
1081
|
+
const fireShot = async (offsetMs) => {
|
|
1082
|
+
try {
|
|
1083
|
+
await reaffirm(page);
|
|
1084
|
+
}
|
|
1085
|
+
catch (error) {
|
|
1086
|
+
LOG.debug("browser:lifecycle", "Could not re-affirm the capture surface after a tab activation (+%sms): %s.", offsetMs, formatError(error));
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
return async () => {
|
|
1090
|
+
const mine = ++generation;
|
|
1091
|
+
await fireShot(0);
|
|
1092
|
+
let previousOffsetMs = 0;
|
|
1093
|
+
for (const offsetMs of ACTIVATION_REAFFIRM_LADDER_MS) {
|
|
1094
|
+
// eslint-disable-next-line no-await-in-loop -- The pacing is the point: each rung waits out the distance left to its own offset before anything else runs.
|
|
1095
|
+
await clock.delay(offsetMs - previousOffsetMs);
|
|
1096
|
+
// A newer activation owns the schedule, and it carries its own rungs timed from its own moment, so this ladder has nothing left to contribute.
|
|
1097
|
+
if (generation !== mine) {
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
// eslint-disable-next-line no-await-in-loop -- Sequential by design: a rung's re-issue settles before the wait toward the next rung begins.
|
|
1101
|
+
await fireShot(offsetMs);
|
|
1102
|
+
previousOffsetMs = offsetMs;
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
// The production collaborators.
|
|
1107
|
+
const defaultActivationHealDeps = { clock: systemClock, reaffirm: reaffirmCaptureSurface };
|
|
1108
|
+
/**
|
|
1109
|
+
* Installs the tab-activation heal on a capture page. Chrome composes the capture of a selected tab from the window's fitted presentation, so the instant a
|
|
1110
|
+
* capture tab becomes the selected one that capture starts recording a clipped view of itself; the re-issue ladder this installs moves the composition back to
|
|
1111
|
+
* the emulated surface about a second later.
|
|
1112
|
+
*
|
|
1113
|
+
* The heal has two triggers and one ladder between them. The exposed binding is fired by a focus listener inside the page, which covers the activation that
|
|
1114
|
+
* arrives with a user's click focusing the browser window. The enrollment in activationHeals is fired by the selection primitive's activation report, which
|
|
1115
|
+
* covers every activation PrismCast performs for itself - the give-back returning a user to a capture tab, the selection step, a re-assert - and a captured page
|
|
1116
|
+
* hears none of those: capture holds it visible and the extension's update moves no operating-system focus, so no page event fires at all (measured 2026-08-30).
|
|
1117
|
+
* Both triggers hold the same callback instance, which is what lets them share its generation counter instead of running two schedules against one page.
|
|
1118
|
+
*
|
|
1119
|
+
* The listener is registered through evaluateOnNewDocument so it survives the page's navigations, exactly as the shared video-selector helper does, and the
|
|
1120
|
+
* binding survives them on Puppeteer's own terms.
|
|
1121
|
+
* @param page - The capture page to install the heal on.
|
|
1122
|
+
* @param deps - The clock and the re-issue the ladder runs on. Defaults to the production pair.
|
|
1123
|
+
*/
|
|
1124
|
+
export async function installActivationHeal(page, deps = defaultActivationHealDeps) {
|
|
1125
|
+
const callback = makeFocusReaffirmCallback(page, deps.reaffirm, deps.clock);
|
|
1126
|
+
// Enrolled ahead of the binding, so the report-side trigger is live from the first moment either trigger could be. An exposeFunction that fails takes the page
|
|
1127
|
+
// down with it, and the entry goes when the page does.
|
|
1128
|
+
activationHeals.set(page, callback);
|
|
1129
|
+
await page.exposeFunction("__prismcastReaffirmSurface", callback);
|
|
1130
|
+
await page.evaluateOnNewDocument(() => {
|
|
1131
|
+
window.addEventListener("focus", () => {
|
|
1132
|
+
void window.__prismcastReaffirmSurface?.();
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Heals the capture page showing in a tab the selection primitive has just activated.
|
|
1138
|
+
*
|
|
1139
|
+
* The report names a tab and what needs healing is a page, so the match walks the registry for a capture-mode stream whose page was identified as that tab. A
|
|
1140
|
+
* report matching nothing is the ordinary case rather than a failure - a give-back returning the selection to the user's own tab, a native stream that composes
|
|
1141
|
+
* no capture at all, a page registered before its setup reached the install point - and every one of them is a quiet no-op.
|
|
1142
|
+
* @param tabId - The tab the selection primitive activated.
|
|
1143
|
+
* @param deps - The registry read and the page-to-tab lookup the match runs through. Defaults to the production pair.
|
|
1144
|
+
*/
|
|
1145
|
+
export function healActivatedCaptureTab(tabId, deps = { getAllStreams, getCachedTabId }) {
|
|
1146
|
+
for (const entry of deps.getAllStreams()) {
|
|
1147
|
+
// The mode is read through the registry's own published predicate rather than off the entry, keeping this layer's knowledge of a stream's shape to what the
|
|
1148
|
+
// registry chooses to publish. A stream's page is nullable, because an entry is registered before its setup has produced one, so the narrowing comes ahead
|
|
1149
|
+
// of the lookup that needs a page to ask about.
|
|
1150
|
+
if (!isCaptureIdentity(entry) || (entry.page === null)) {
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
if (deps.getCachedTabId(entry.page) !== tabId) {
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
// A page whose install point has not run yet has nothing enrolled to fire, and the monitor's periodic re-affirmation is what covers it in the meantime.
|
|
1157
|
+
void activationHeals.get(entry.page)?.();
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
/* The extension-side trigger, subscribed once for the life of the process. Every activation PrismCast performs goes through the selection primitive and is
|
|
1161
|
+
* reported by it, which is the only way an activation of a captured page can be known at all; the page's own focus listener covers the one activation the
|
|
1162
|
+
* primitive does not perform, a user's click that focuses the window. A browser relaunch changes neither module, so nothing here is re-subscribed.
|
|
1163
|
+
*/
|
|
1164
|
+
onTabActivation((tabId) => {
|
|
1165
|
+
healActivatedCaptureTab(tabId);
|
|
1166
|
+
});
|
|
695
1167
|
/**
|
|
696
1168
|
* Custom launch function that modifies Chrome arguments when running as a packaged executable. The packaged version cannot load extensions from node_modules
|
|
697
1169
|
* (which is bundled inside the executable), so we point the extension paths to our extracted extension files in the data directory.
|
|
@@ -736,28 +1208,8 @@ function formatGpuSuffix(gpu) {
|
|
|
736
1208
|
return " (software rendering)";
|
|
737
1209
|
}
|
|
738
1210
|
/**
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
* the display...treating either as evidence would let a CDP hiccup or an unfinished window transition pose as a small display, so both yield null. Beyond that,
|
|
742
|
-
* either dimension falling below the capture surface is enough: a frame that does not fit the window in one axis is already a frame the screen cannot show whole.
|
|
743
|
-
* @param bounds - The window bounds the browser reported, or undefined when the read did not complete.
|
|
744
|
-
* @param captureSurface - The dimensions every page renders at, derived from the configured quality preset.
|
|
745
|
-
* @returns The granted window dimensions when the display falls short of the capture surface, null otherwise.
|
|
746
|
-
*/
|
|
747
|
-
export function getUndersizedDisplayBounds(bounds, captureSurface) {
|
|
748
|
-
if ((bounds?.windowState !== "normal") || (bounds.width === undefined) || (bounds.height === undefined)) {
|
|
749
|
-
return null;
|
|
750
|
-
}
|
|
751
|
-
if ((bounds.width >= captureSurface.width) && (bounds.height >= captureSurface.height)) {
|
|
752
|
-
return null;
|
|
753
|
-
}
|
|
754
|
-
return { height: bounds.height, width: bounds.width };
|
|
755
|
-
}
|
|
756
|
-
/**
|
|
757
|
-
* Probes the browser's GPU hardware-encoding capabilities and reports how the display compares to the configured capture surface. The GPU half queries CDP
|
|
758
|
-
* SystemInfo.getInfo for renderer identity and H.264/HEVC/AV1 hardware encoding support, falling back to a MediaRecorder capability probe where the CDP data is
|
|
759
|
-
* incomplete, and caches the result for codec selection to read. The display half is advisory: every page renders at the configured preset, so a display too
|
|
760
|
-
* small to show a whole frame changes nothing about capture and is worth stating once.
|
|
1211
|
+
* Probes the browser's GPU hardware-encoding capabilities. It queries CDP SystemInfo.getInfo for renderer identity and H.264/HEVC/AV1 hardware encoding support,
|
|
1212
|
+
* falls back to a MediaRecorder capability probe where the CDP data is incomplete, and caches the result for codec selection to read.
|
|
761
1213
|
*
|
|
762
1214
|
* The probe runs against an existing page where one is open, and a temporary page otherwise.
|
|
763
1215
|
* @param browser - The browser instance to use for detection.
|
|
@@ -765,17 +1217,16 @@ export function getUndersizedDisplayBounds(bounds, captureSurface) {
|
|
|
765
1217
|
async function detectBrowserCapabilities(browser) {
|
|
766
1218
|
let tempPage = null;
|
|
767
1219
|
try {
|
|
768
|
-
// Try to use an existing page first to avoid window activation issues on macOS.
|
|
769
|
-
|
|
770
|
-
let targetPage =
|
|
1220
|
+
// Try to use an existing page first to avoid window activation issues on macOS. The pick goes through the shared carrier rule, because the restore below
|
|
1221
|
+
// acts on the window of whichever page carries it and a page living in its own window would restore that one instead of the shared one.
|
|
1222
|
+
let targetPage = pickCarrierPage(await browser.pages());
|
|
771
1223
|
if (!targetPage) {
|
|
772
|
-
|
|
1224
|
+
// A probe page never needs its tab selected either.
|
|
1225
|
+
tempPage = await browser.newPage({ background: true });
|
|
773
1226
|
targetPage = tempPage;
|
|
774
1227
|
}
|
|
775
1228
|
/* Restore the window to its normal state before probing. Chrome restores window state from the persistent user data directory, so after a scheduled browser
|
|
776
|
-
* restart the window can come up minimized, and
|
|
777
|
-
* which would make the display advisory below fire against a size no display ever imposed. It also keeps the GPU probe's environment representative of the
|
|
778
|
-
* one capture runs in.
|
|
1229
|
+
* restart the window can come up minimized, and the GPU probe wants an environment representative of the one capture runs in.
|
|
779
1230
|
*/
|
|
780
1231
|
await unminimizeWindow(targetPage);
|
|
781
1232
|
// Detect GPU capabilities via CDP SystemInfo.getInfo. This is the authoritative source for GPU identity and hardware encoding capabilities - it runs at the
|
|
@@ -850,19 +1301,6 @@ async function detectBrowserCapabilities(browser) {
|
|
|
850
1301
|
catch (gpuError) {
|
|
851
1302
|
LOG.debug("browser:lifecycle", "GPU detection failed: %s.", String(gpuError));
|
|
852
1303
|
}
|
|
853
|
-
/* Report a display that cannot show a whole frame. The bounds come from the browser rather than from the page, because an emulated page reports the emulated
|
|
854
|
-
* size for screen.* and window.* alike, which would make the comparison circular - Browser.getWindowBounds is window state the page's emulation cannot reach.
|
|
855
|
-
*/
|
|
856
|
-
const bounds = await withCDPSession(targetPage, async (session, windowId) => {
|
|
857
|
-
const result = await session.send("Browser.getWindowBounds", { windowId });
|
|
858
|
-
return result.bounds;
|
|
859
|
-
});
|
|
860
|
-
const captureSurface = getPresetViewport(CONFIG);
|
|
861
|
-
const granted = getUndersizedDisplayBounds(bounds, captureSurface);
|
|
862
|
-
if (granted) {
|
|
863
|
-
LOG.info("The display is smaller than the configured %s\u00d7%s capture surface (the browser window was granted %s\u00d7%s). Capture is unaffected: every " +
|
|
864
|
-
"page renders at the configured preset regardless of display or window size.", captureSurface.width, captureSurface.height, granted.width, granted.height);
|
|
865
|
-
}
|
|
866
1304
|
}
|
|
867
1305
|
catch (error) {
|
|
868
1306
|
LOG.warn("Browser capability detection failed: %s. Hardware encoding capabilities are unknown for this session.", formatError(error));
|
|
@@ -881,10 +1319,11 @@ async function detectBrowserCapabilities(browser) {
|
|
|
881
1319
|
}
|
|
882
1320
|
/**
|
|
883
1321
|
* Relinquishes the current browser's capture readiness and tears down everything that depended on it. This is the single source of truth for "the published browser
|
|
884
|
-
* is no longer usable"
|
|
1322
|
+
* is no longer usable", and the disconnect handler is its one caller: the browser is gone, so nothing that depended on it can be salvaged. It drops the
|
|
885
1323
|
* supervisor's readiness first (which supersedes any launch in flight, clears the governor's health anchor, and transitions the lifecycle to absent so the next
|
|
886
1324
|
* request relaunches through the gate and governor), clears the adapter-held metadata and caches, ends login mode, terminates every active stream (they were
|
|
887
|
-
* capturing on the now-unusable browser), and emits status.
|
|
1325
|
+
* capturing on the now-unusable browser), and emits status. The caller logs the specific cause. A browser that is still connected but can no longer start captures
|
|
1326
|
+
* takes the far gentler noteBrowserCaptureImpaired path instead, which keeps its running captures.
|
|
888
1327
|
* @param streamTerminationReason - The reason recorded against each terminated stream, for the stream-end logs.
|
|
889
1328
|
*/
|
|
890
1329
|
function relinquishBrowserReadiness(streamTerminationReason) {
|
|
@@ -895,10 +1334,7 @@ function relinquishBrowserReadiness(streamTerminationReason) {
|
|
|
895
1334
|
currentChromeVersion = null;
|
|
896
1335
|
setChromeUserAgent(null);
|
|
897
1336
|
// Cancel any pending scheduled-restart quiet timer since the browser this readiness applied to is gone.
|
|
898
|
-
|
|
899
|
-
clearTimeout(restartQuietTimer);
|
|
900
|
-
restartQuietTimer = null;
|
|
901
|
-
}
|
|
1337
|
+
cancelRestartQuietTimer();
|
|
902
1338
|
// Clear all channel selection caches. Cached state (guide row positions, discovered page URLs) belongs to the old browser session.
|
|
903
1339
|
clearChannelSelectionCaches();
|
|
904
1340
|
// End login mode if it was active. We use clearLoginState() rather than endLoginMode() because the browser may already be gone and we do not want to attempt any
|
|
@@ -906,10 +1342,21 @@ function relinquishBrowserReadiness(streamTerminationReason) {
|
|
|
906
1342
|
if (clearLoginState() && !gracefulShutdownInProgress) {
|
|
907
1343
|
LOG.info("Login mode ended due to browser readiness loss.");
|
|
908
1344
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1345
|
+
/* Terminate every active stream through the streaming layer's authoritative terminator, so a readiness loss cleans up exactly the way every other termination
|
|
1346
|
+
* path does. Kept even during graceful shutdown as a defensive measure - termination is safe to call more than once, so streams the caller already tore down
|
|
1347
|
+
* leave this a harmless pass over an empty array.
|
|
1348
|
+
*/
|
|
1349
|
+
const activeStreams = getAllStreams();
|
|
1350
|
+
if (streamTerminator) {
|
|
1351
|
+
for (const streamInfo of activeStreams) {
|
|
1352
|
+
streamTerminator(streamInfo.id, streamInfo.info.storeKey, streamTerminationReason);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
/* An unwired terminator means the streaming layer never loaded, so nothing here can run the streams' cleanup sequence. Report it and keep going: the page
|
|
1357
|
+
* tracking and status emission below are still worth doing, and abandoning them would compound a wiring failure with a half-finished teardown.
|
|
1358
|
+
*/
|
|
1359
|
+
LOG.error("Stream cleanup was skipped on browser readiness loss because no stream terminator is wired. Active streams left untouched: %s.", activeStreams.length);
|
|
913
1360
|
}
|
|
914
1361
|
// The session those streams captured on is over, so whatever page tracking survived their termination belongs to a browser that is gone.
|
|
915
1362
|
clearPageTracking();
|
|
@@ -933,43 +1380,45 @@ function handleBrowserDisconnect() {
|
|
|
933
1380
|
relinquishBrowserReadiness("browser disconnect");
|
|
934
1381
|
}
|
|
935
1382
|
/**
|
|
936
|
-
*
|
|
937
|
-
* single recovery action
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
1383
|
+
* Records that a still-connected browser can no longer start captures - a mid-life capture death that no "disconnected" event would surface - and runs the
|
|
1384
|
+
* relaunch that cures it. This is the single recovery action for a browser that is alive and still serving: the mark lives on the supervisor's ready state, so its
|
|
1385
|
+
* running captures continue untouched, new stream requests are refused at acquire() with a 503 back-off, the recovery ladder stops offering tab replacement, and
|
|
1386
|
+
* the relaunch waits until nothing depends on the browser. Exported for the streaming layer to call once its probe or its wedge has produced the verdict.
|
|
1387
|
+
*
|
|
1388
|
+
* The returned promise settles once the relaunch this mark triggered has settled, which is what a caller that means to use the fresh browser awaits: the teardown
|
|
1389
|
+
* holds the supervisor in its draining state, where acquire() rejects rather than joins, so acquiring any earlier draws that rejection instead of the new
|
|
1390
|
+
* instance. A caller with nothing waiting on the outcome voids it and the relaunch proceeds on its own.
|
|
941
1391
|
*
|
|
942
|
-
* The
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
* @param browser - The specific browser instance the caller verified as unable to
|
|
946
|
-
* @param reason - A short description of
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
//
|
|
951
|
-
|
|
1392
|
+
* The restart trigger uses this function's return value rather than the supervisor's transition observer, deliberately. The observer runs inside transition(), so a
|
|
1393
|
+
* restart begun there would call noteReadinessLost re-entrantly while the marking transition's notification is still on the stack. The observer stays a reporter -
|
|
1394
|
+
* the alarm and the status emit - and the caller that holds the verdict acts on it once the transition has completed.
|
|
1395
|
+
* @param browser - The specific browser instance the caller verified as unable to start captures.
|
|
1396
|
+
* @param reason - A short description of the evidence behind the verdict, carried in the alarm log and the impairment record.
|
|
1397
|
+
* @returns A promise settling once the relaunch this mark triggered has settled, or at once when the verdict landed on nothing.
|
|
1398
|
+
*/
|
|
1399
|
+
export async function noteBrowserCaptureImpaired(browser, reason) {
|
|
1400
|
+
// A false answer means the verdict landed on nothing: the instance was superseded by a disconnect and relaunch while the caller was confirming it, or the browser
|
|
1401
|
+
// already carries a mark whose alarm and status emit have already fired. Either way there is no new state to act on.
|
|
1402
|
+
if (!supervisor.noteCaptureImpaired(browser, reason)) {
|
|
952
1403
|
return;
|
|
953
1404
|
}
|
|
954
|
-
|
|
955
|
-
relinquishBrowserReadiness("capture system failure");
|
|
956
|
-
// Readiness was relinquished first, because that is what supersedes an in-flight launch; publishing the teardown synchronously, before any await, then keeps the
|
|
957
|
-
// launch window shut for the whole drain so nothing spawns a second Chrome against the profile lock this one still holds.
|
|
958
|
-
const teardown = closeBrowserInstance(browser);
|
|
959
|
-
supervisor.noteTeardownBegun(teardown, BROWSER_TEARDOWN_DRAIN_BOUND_MS);
|
|
960
|
-
await teardown;
|
|
1405
|
+
await restartBrowserIfImpairedAndIdle();
|
|
961
1406
|
}
|
|
962
1407
|
/**
|
|
963
1408
|
* Provides access to the capture-ready browser, launching one if needed. This is the single gated entry point for all browser access: it delegates to the
|
|
964
1409
|
* supervisor's acquire(), which returns the ready browser, joins an in-flight launch (single-flight, so concurrent callers never contend on Chrome's profile lock),
|
|
965
1410
|
* lazily launches when absent, or - while the relaunch governor is cooling after repeated failures - rejects fast with a BrowserUnavailableError WITHOUT spawning
|
|
966
1411
|
* Chrome (the loop bound). The launch it drives runs the readiness gate, so a returned browser is verified capture-ready, not merely connected.
|
|
1412
|
+
*
|
|
1413
|
+
* The purpose is what the caller intends to do with the browser, and it is the gate on a marked one: a "capture" caller is refused, because that is precisely the
|
|
1414
|
+
* operation the browser can no longer perform, while a "page" caller - precaching, the startup warm-up, the relaunch itself - is served as usual.
|
|
1415
|
+
* @param purpose - Whether the caller goes on to start a capture or only needs a page to open.
|
|
967
1416
|
* @returns The capture-ready browser instance.
|
|
968
|
-
* @throws BrowserUnavailableError while the governor is cooling,
|
|
969
|
-
* launch error when a launch attempt fails.
|
|
1417
|
+
* @throws BrowserUnavailableError while the governor is cooling, BrowserCaptureImpairedError when a capture purpose meets a browser that can no longer start
|
|
1418
|
+
* captures, BrowserSupersededError if an in-flight launch was abandoned by a readiness-loss, or the underlying launch error when a launch attempt fails.
|
|
970
1419
|
*/
|
|
971
|
-
export async function getCurrentBrowser() {
|
|
972
|
-
return supervisor.acquire();
|
|
1420
|
+
export async function getCurrentBrowser(purpose) {
|
|
1421
|
+
return supervisor.acquire(purpose);
|
|
973
1422
|
}
|
|
974
1423
|
/**
|
|
975
1424
|
* The supervisor's `launch` port: spawns Chrome, runs the readiness gate, performs post-launch initialization (display detection, version/UA capture, precaching),
|
|
@@ -993,21 +1442,31 @@ async function launchReadyBrowser() {
|
|
|
993
1442
|
// Readiness gate, handshake tier (cheap, on-suspicion). Poll for the puppeteer-stream extension to finish initializing - it injects a START_RECORDING function
|
|
994
1443
|
// into its options page context, so its presence is the extension's own readiness signal. We poll rather than fixed-delay so the browser is ready as soon as the
|
|
995
1444
|
// extension loads (typically 200-500ms). On failure this THROWS rather than warning-and-proceeding: an unregistered extension means chrome.tabs is undefined and
|
|
996
|
-
// every
|
|
1445
|
+
// every capture acquisition would hang, so the instance is not capture-ready and must not be published. We reclassify the raw waitForFunction timeout into a
|
|
997
1446
|
// capture-infrastructure error carrying "timed out" so the setup layer maps it to a 503 back-off (the same as the capability-tier probe failure), rather than a
|
|
998
1447
|
// 500 the client would not back off from - an unregistered extension is a capture-infrastructure fault, and a fresh relaunch usually clears it.
|
|
999
1448
|
try {
|
|
1000
1449
|
const extensionPage = await getExtensionPage(browser);
|
|
1001
|
-
await extensionPage.waitForFunction(
|
|
1450
|
+
await extensionPage.waitForFunction(EXTENSION_READY_EXPRESSION, { timeout: CONFIG.browser.initTimeout });
|
|
1002
1451
|
}
|
|
1003
1452
|
catch (handshakeError) {
|
|
1004
1453
|
throw new Error("The capture extension handshake timed out after " + String(CONFIG.browser.initTimeout) + " ms.", { cause: handshakeError });
|
|
1005
1454
|
}
|
|
1006
1455
|
LOG.debug("timing:browser", "Extension initialized. (+%sms)", browserElapsed());
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1456
|
+
/* The tab the window rests on. It is created once and selected by construction because nothing asks otherwise, so whatever the window shows when nothing has
|
|
1457
|
+
* asked for a tab is a blank page rather than the capture extension's own options page - which the library opens selected at launch. It is left unmanaged, as
|
|
1458
|
+
* stale page cleanup judges only the pages stream setup and discovery create; it carries no correctness duty, so it needs no re-validation, and a user
|
|
1459
|
+
* closing it costs nothing.
|
|
1460
|
+
*
|
|
1461
|
+
* It is also the page the shared window's identity is read from. This tab is in that window by construction, and reading it here - before the capture probe,
|
|
1462
|
+
* the capability detection, or any stream - is what lets every later capture tab be anchored to the window rather than placed by Chrome.
|
|
1463
|
+
*/
|
|
1464
|
+
const restingTab = await browser.newPage();
|
|
1465
|
+
await noteSharedWindow(browser, restingTab);
|
|
1466
|
+
// Readiness gate, capability tier (the authoritative arbiter). Run the injected capture probe - a real capture acquisition against a throwaway page on THIS
|
|
1467
|
+
// instance - so "ready" means "really captured," not merely "the extension handshake responded." This predicate must run at every (re)launch:
|
|
1468
|
+
// it exercises the exact acquisition path that fails when the extension is unregistered. A probe failure throws, so the supervisor counts the launch failure
|
|
1469
|
+
// and the browser is never published.
|
|
1011
1470
|
//
|
|
1012
1471
|
// If the probe is not wired (the injection point left unset by a refactor - impossible in the normal import order, which always wires it before any launch),
|
|
1013
1472
|
// we reject the launch rather than publish a handshake-only browser: serving an unverified browser would be the "proceed and hope" path this design
|
|
@@ -1031,9 +1490,15 @@ async function launchReadyBrowser() {
|
|
|
1031
1490
|
const gpuSuffix = gpu ? formatGpuSuffix(gpu) : "";
|
|
1032
1491
|
LOG.info("Chrome ready: %s%s.", chromeVersion, gpuSuffix);
|
|
1033
1492
|
LOG.debug("timing:browser", "Browser ready. Total: %sms.", browserElapsed());
|
|
1034
|
-
// Start background precaching of selected service channel lineups. Fire-and-forget - the
|
|
1035
|
-
// launch settles and the supervisor has published the ready browser, so its getCurrentBrowser() resolves immediately rather than re-entering this
|
|
1493
|
+
// Start background precaching of selected service channel lineups. Fire-and-forget - the delay startPrecaching() arms on its own clock defers the work until
|
|
1494
|
+
// after this launch settles and the supervisor has published the ready browser, so its getCurrentBrowser() resolves immediately rather than re-entering this
|
|
1495
|
+
// launch.
|
|
1036
1496
|
startPrecaching();
|
|
1497
|
+
/* The reaper belongs to the browser instance the supervisor is about to publish: puppeteer drops a closed browser's listeners along with the instance, and
|
|
1498
|
+
* a relaunch comes back through this same path and installs its own. It goes in ahead of the disconnect handler so that handler stays the last step before
|
|
1499
|
+
* publication, for the reason its own comment gives.
|
|
1500
|
+
*/
|
|
1501
|
+
installStrayOpenTabReaper(browser);
|
|
1037
1502
|
// Arm the disconnect handler only now, as the very last step before the supervisor publishes this browser as ready. It is deliberately NOT armed earlier: during
|
|
1038
1503
|
// the gate/init window above, a Chrome crash surfaces as a thrown init step (CDP and waitForFunction reject on a dead browser), which the supervisor counts as a
|
|
1039
1504
|
// launch failure and feeds to the governor - keeping the relaunch loop bounded even if Chrome dies repeatedly during init. Arming the handler earlier would let
|
|
@@ -1069,6 +1534,15 @@ export function getChromeVersion() {
|
|
|
1069
1534
|
export function getBrowserInstance() {
|
|
1070
1535
|
return supervisor.current();
|
|
1071
1536
|
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Returns the impairment recorded on the published browser, or null when nothing is published or the published browser can still start captures. Like
|
|
1539
|
+
* getBrowserInstance this does not launch, so callers that only need to know whether captures can be started - the recovery ladder, the status composition, the
|
|
1540
|
+
* health endpoint - read it without touching the lifecycle.
|
|
1541
|
+
* @returns The impairment record, or null.
|
|
1542
|
+
*/
|
|
1543
|
+
export function getCaptureImpairment() {
|
|
1544
|
+
return supervisor.captureImpairment();
|
|
1545
|
+
}
|
|
1072
1546
|
/**
|
|
1073
1547
|
* Checks if the browser is currently connected and usable. This is a synchronous check that can be used before attempting browser operations.
|
|
1074
1548
|
* @returns True if the browser is connected and ready for use, false otherwise.
|
|
@@ -1077,54 +1551,6 @@ export function isBrowserConnected() {
|
|
|
1077
1551
|
const browser = supervisor.current();
|
|
1078
1552
|
return !!browser && browser.connected;
|
|
1079
1553
|
}
|
|
1080
|
-
/**
|
|
1081
|
-
* Minimizes the browser window. The window's presentation state is all this touches - the surface pages render at is emulated from the configured preset and is
|
|
1082
|
-
* unaffected by whether the window is on screen.
|
|
1083
|
-
*
|
|
1084
|
-
* To avoid issues with creating temporary pages (which can cause the window to restore on macOS), we prefer using an existing page if one is available. Only if
|
|
1085
|
-
* no pages exist do we create a temporary page.
|
|
1086
|
-
*/
|
|
1087
|
-
export async function minimizeBrowserWindow() {
|
|
1088
|
-
// Guard against calling this when no browser is running.
|
|
1089
|
-
const browser = supervisor.current();
|
|
1090
|
-
if (!browser?.connected) {
|
|
1091
|
-
return;
|
|
1092
|
-
}
|
|
1093
|
-
let tempPage = null;
|
|
1094
|
-
try {
|
|
1095
|
-
// Try to use an existing page first. Creating a new page can cause the window to restore/activate on macOS, which defeats the purpose of minimizing.
|
|
1096
|
-
const existingPages = await browser.pages();
|
|
1097
|
-
let targetPage = existingPages.find((p) => !p.isClosed()) ?? null;
|
|
1098
|
-
// If no existing pages, we must create a temporary one. This is less ideal but necessary to get a CDP session target.
|
|
1099
|
-
if (!targetPage) {
|
|
1100
|
-
tempPage = await browser.newPage();
|
|
1101
|
-
// Register the temp page so stale cleanup knows it's ours.
|
|
1102
|
-
registerManagedPage(tempPage);
|
|
1103
|
-
targetPage = tempPage;
|
|
1104
|
-
}
|
|
1105
|
-
// Delegate to the CDP helper for the window-state change, so every minimize in the codebase issues the same command sequence.
|
|
1106
|
-
await minimizeWindow(targetPage);
|
|
1107
|
-
// Clean up the temporary page if we created one.
|
|
1108
|
-
if (tempPage) {
|
|
1109
|
-
unregisterManagedPage(tempPage);
|
|
1110
|
-
await tempPage.close();
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
catch (error) {
|
|
1114
|
-
// If we created a temp page, make sure to unregister it even on error.
|
|
1115
|
-
if (tempPage) {
|
|
1116
|
-
unregisterManagedPage(tempPage);
|
|
1117
|
-
try {
|
|
1118
|
-
await tempPage.close();
|
|
1119
|
-
}
|
|
1120
|
-
catch (_closeError) {
|
|
1121
|
-
// Ignore close errors during error handling.
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
// Minimizing is not critical - log a warning but don't fail the operation.
|
|
1125
|
-
LOG.debug("browser:lifecycle", "Could not minimize the browser window: %s.", formatError(error));
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
1554
|
/**
|
|
1129
1555
|
* Gets all open browser pages (tabs). This is used by the health check endpoint to report page count.
|
|
1130
1556
|
* @returns Array of pages, or empty array if the browser is not connected.
|
|
@@ -1241,8 +1667,8 @@ export async function closeBrowser() {
|
|
|
1241
1667
|
*
|
|
1242
1668
|
* 4. Minimum page preservation: We always keep at least one page open to prevent Chrome from exiting.
|
|
1243
1669
|
*
|
|
1244
|
-
* 5. In-flight
|
|
1245
|
-
*
|
|
1670
|
+
* 5. In-flight exemption: Pages an operation still holds (tracked in inFlightPageIds) are never considered stale. The registry records a stream's page only once
|
|
1671
|
+
* setup completes, and a discovery page reaches the registry at no point at all, so without this a slow tune or a running walk would lose its own page.
|
|
1246
1672
|
*
|
|
1247
1673
|
* The safeguards are expressed as rules in browser/pageStaleness.ts, which decides from a snapshot what to close, track, forget, and unmark. This function is the
|
|
1248
1674
|
* I/O shell around that decision: it reads Chrome's page list, applies the decision to the tracking collections, and performs the closes.
|
|
@@ -1253,11 +1679,13 @@ export async function closeBrowser() {
|
|
|
1253
1679
|
*
|
|
1254
1680
|
* The cleanup uses a multi-stage filtering process:
|
|
1255
1681
|
* 1. Only consider pages we created (in managedPageIds)
|
|
1256
|
-
* 2. Exclude pages associated with active streams, and pages
|
|
1682
|
+
* 2. Exclude pages associated with active streams, and pages an operation still holds in flight
|
|
1257
1683
|
* 3. Apply a grace period before closing (to handle race conditions)
|
|
1258
1684
|
* 4. Preserve at least one page to keep the browser alive
|
|
1685
|
+
* @param now - The instant the sweep judges staleness against, read from the interval's own clock so the staleness clocks this sweep starts and the ones it
|
|
1686
|
+
* later reads are stamped on one time source.
|
|
1259
1687
|
*/
|
|
1260
|
-
export async function cleanupStalePages() {
|
|
1688
|
+
export async function cleanupStalePages(now) {
|
|
1261
1689
|
// Guard against calling this when no ready browser is running.
|
|
1262
1690
|
const browser = supervisor.current();
|
|
1263
1691
|
if (!browser?.connected) {
|
|
@@ -1279,7 +1707,6 @@ export async function cleanupStalePages() {
|
|
|
1279
1707
|
}
|
|
1280
1708
|
}
|
|
1281
1709
|
}
|
|
1282
|
-
const now = Date.now();
|
|
1283
1710
|
// Project the browser's pages into the shape the decision core reads: the managed ids in the browser's own order, with undefined standing in for pages we
|
|
1284
1711
|
// did not create, plus a lookup back to the Page objects so the ids it returns can be resolved to something closable.
|
|
1285
1712
|
const idToPage = new Map();
|
|
@@ -1292,7 +1719,7 @@ export async function cleanupStalePages() {
|
|
|
1292
1719
|
}
|
|
1293
1720
|
}
|
|
1294
1721
|
// The staleness judgment - clocks, exemptions, the dead-entry sweep, and the preserve-one budget - belongs to the pure core; this function only carries it out.
|
|
1295
|
-
const actions = evaluateStalePages({ activePageIds, gracePeriodMs: CONFIG.recovery.stalePageGracePeriod,
|
|
1722
|
+
const actions = evaluateStalePages({ activePageIds, gracePeriodMs: CONFIG.recovery.stalePageGracePeriod, inFlightPageIds, now, pageIds,
|
|
1296
1723
|
staleFirstSeen: potentiallyStalePages });
|
|
1297
1724
|
// Bring the tracking collections in line with the decision before any close runs, so a close that fails cannot leave the bookkeeping half-applied.
|
|
1298
1725
|
for (const pageId of actions.forgetTrackedIds) {
|
|
@@ -1302,7 +1729,7 @@ export async function cleanupStalePages() {
|
|
|
1302
1729
|
potentiallyStalePages.set(pageId, now);
|
|
1303
1730
|
}
|
|
1304
1731
|
for (const pageId of actions.clearInFlightIds) {
|
|
1305
|
-
|
|
1732
|
+
inFlightPageIds.delete(pageId);
|
|
1306
1733
|
}
|
|
1307
1734
|
let closedCount = 0;
|
|
1308
1735
|
for (const pageId of actions.closeIds) {
|
|
@@ -1334,90 +1761,165 @@ export async function cleanupStalePages() {
|
|
|
1334
1761
|
}
|
|
1335
1762
|
}
|
|
1336
1763
|
/**
|
|
1337
|
-
* Starts the periodic stale page cleanup
|
|
1338
|
-
*
|
|
1764
|
+
* Starts the periodic stale page cleanup. This should be called once during server startup, after the browser is initialized. The sweep runs indefinitely until
|
|
1765
|
+
* stopStalePageCleanup() is called (typically during graceful shutdown). A second start while one is running changes nothing.
|
|
1766
|
+
* @param clock - The clock the sweep's interval arms on and whose reading each sweep judges staleness against, so the cadence and the judgment share one time
|
|
1767
|
+
* source. Defaults to the system clock.
|
|
1339
1768
|
*/
|
|
1340
|
-
export function startStalePageCleanup() {
|
|
1341
|
-
|
|
1769
|
+
export function startStalePageCleanup(clock = systemClock) {
|
|
1770
|
+
if (stalePageTimers) {
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
const timers = new TimerRegistry({ clock });
|
|
1774
|
+
timers.setInterval(STALE_PAGE_SWEEP_KEY, () => { void cleanupStalePages(clock.now()); }, CONFIG.recovery.stalePageCleanupInterval);
|
|
1775
|
+
stalePageTimers = timers;
|
|
1342
1776
|
}
|
|
1343
1777
|
/**
|
|
1344
|
-
* Stops the stale page cleanup
|
|
1345
|
-
*
|
|
1778
|
+
* Stops the periodic stale page cleanup. This should be called during graceful shutdown to prevent the sweep from running after we've started shutting down the
|
|
1779
|
+
* browser and streams. Disposing the registry drains the interval, and a stop with nothing running is a no-op.
|
|
1346
1780
|
*/
|
|
1347
1781
|
export function stopStalePageCleanup() {
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1782
|
+
stalePageTimers?.dispose();
|
|
1783
|
+
stalePageTimers = null;
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Decides whether the browser may be torn down for the given cause, from facts read at the call. Each cause asks a different question of the same registry, so
|
|
1787
|
+
* the decision is stated once here rather than spelled out at each guard.
|
|
1788
|
+
*
|
|
1789
|
+
* Maintenance is opportunistic housekeeping, so it waits for an empty registry outright: a pending entry is a tune in progress, and replacing the browser under
|
|
1790
|
+
* one would fail it for nothing better than a fresher instance. Impairment is a repair the tunes themselves are waiting on, so it asks the narrower question of
|
|
1791
|
+
* what a teardown would actually destroy - a stream established on this browser, whose entry holds its page, or a page an operation still holds in flight. A tune
|
|
1792
|
+
* refused a capture start holds neither while it waits for the relaunch, so the very pending entry that maintenance would defer to is not a reason to leave an
|
|
1793
|
+
* unusable browser in place. A tune that has acquired the browser but not yet opened its page falls outside every dependency source for the same reason and
|
|
1794
|
+
* deliberately so: it holds nothing a relaunch would destroy, neither a page nor a capture, and its own capture start would meet the impaired browser in any
|
|
1795
|
+
* case, so the relaunch may run under it.
|
|
1796
|
+
* @param cause - Why the restart wants to run.
|
|
1797
|
+
* @param facts - The registry facts read at the call.
|
|
1798
|
+
* @returns True when the browser may be torn down for this cause.
|
|
1799
|
+
*/
|
|
1800
|
+
export function isBrowserIdleForRestart(cause, facts) {
|
|
1801
|
+
switch (cause) {
|
|
1802
|
+
case "impairment": {
|
|
1803
|
+
return !facts.establishedStreams && (facts.inFlightPages === 0);
|
|
1804
|
+
}
|
|
1805
|
+
case "maintenance": {
|
|
1806
|
+
return facts.streamCount === 0;
|
|
1807
|
+
}
|
|
1351
1808
|
}
|
|
1352
1809
|
}
|
|
1353
|
-
|
|
1354
|
-
*
|
|
1355
|
-
*
|
|
1810
|
+
/**
|
|
1811
|
+
* Reads the registry facts the idleness decision is made from, at the moment of the call.
|
|
1812
|
+
* @returns The facts.
|
|
1813
|
+
*/
|
|
1814
|
+
function readRestartFacts() {
|
|
1815
|
+
return { establishedStreams: hasEstablishedStreams(), inFlightPages: inFlightPageIds.size, streamCount: getStreamCount() };
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Relaunches a browser that can no longer start captures, as soon as nothing depends on it. Every trigger routes here - the mark itself, each stream termination,
|
|
1819
|
+
* and the periodic restart check as the backstop for a moment when the other two could not act (login mode above all) - so the decision lives in one place rather
|
|
1820
|
+
* than being re-derived by each. Idleness rather than age is the condition, because the mark makes the browser useless for new tunes immediately while its running
|
|
1821
|
+
* captures are still worth finishing, so the earliest safe moment is exactly the moment the last thing depending on this browser lets go of it.
|
|
1822
|
+
*
|
|
1823
|
+
* The returned promise settles once the relaunch has settled, for the caller that goes on to acquire the fresh browser; a trigger with nothing waiting on the
|
|
1824
|
+
* outcome voids it. An early return resolves at once, because there is nothing for such a caller to wait for.
|
|
1825
|
+
* @returns A promise settling once the relaunch has settled, or at once when no relaunch runs.
|
|
1356
1826
|
*/
|
|
1827
|
+
export async function restartBrowserIfImpairedAndIdle() {
|
|
1828
|
+
if (supervisor.captureImpairment() === null) {
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
// A marked browser's restart belongs to this path, so a maintenance quiet period pending from before the mark is retired the first time any trigger observes the
|
|
1832
|
+
// mark - whether or not the browser is idle yet - rather than being left to fire against the browser the relaunch will have replaced.
|
|
1833
|
+
cancelRestartQuietTimer();
|
|
1834
|
+
if (!isBrowserIdleForRestart("impairment", readRestartFacts())) {
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
await executeBrowserRestart("impairment");
|
|
1838
|
+
}
|
|
1357
1839
|
/**
|
|
1358
|
-
* Checks whether the browser qualifies for
|
|
1359
|
-
*
|
|
1360
|
-
* supervisor's health-gated governor reset
|
|
1361
|
-
* countdown)
|
|
1840
|
+
* Checks whether the browser qualifies for a restart. Called periodically by the restart check interval. The check skips when any of these conditions hold:
|
|
1841
|
+
* graceful shutdown in progress, login mode active, browser not ready. A marked browser is handed to the impairment path and the tick ends there. Otherwise the
|
|
1842
|
+
* tick drives the supervisor's health-gated governor reset and applies the maintenance rules: skip below the age threshold, cancel any pending quiet timer while
|
|
1843
|
+
* active streams exist (streams started during the quiet period reset the countdown), and otherwise start a quiet timer if one is not already running.
|
|
1844
|
+
* @param timers - The restart owner's registry, whose quiet period this check arms and reads. Handed in by the interval closure, so the check never reads the
|
|
1845
|
+
* nullable module binding and needs no null branch of its own.
|
|
1362
1846
|
*/
|
|
1363
|
-
function checkBrowserRestart() {
|
|
1847
|
+
function checkBrowserRestart(timers) {
|
|
1364
1848
|
// Skip if the server is shutting down or login mode is active.
|
|
1365
1849
|
if (gracefulShutdownInProgress || isLoginModeActive()) {
|
|
1366
1850
|
return;
|
|
1367
1851
|
}
|
|
1368
1852
|
// Read the ready browser and its launch time from the supervisor. Both are non-null only in the ready state, so a single guard covers "no ready browser." The
|
|
1369
|
-
// launch time is
|
|
1853
|
+
// launch time is read from the supervisor's clock (systemClock.now), so age is measured against that same clock rather than against any other time source.
|
|
1370
1854
|
const browser = supervisor.current();
|
|
1371
1855
|
const launchTime = supervisor.currentLaunchTime();
|
|
1372
1856
|
if (!browser?.connected || (launchTime === null)) {
|
|
1373
1857
|
return;
|
|
1374
1858
|
}
|
|
1859
|
+
/* A marked browser waits for idleness rather than for age, so the maintenance rules below have nothing to say about it. It is not evidence of sustained health
|
|
1860
|
+
* either - the governor's reset waits for the fresh browser - so this tick neither feeds the health hold nor logs the recovery notice for it. The stream-end
|
|
1861
|
+
* trigger normally restarts it well before this tick runs; the tick is the backstop for a trigger that could not act, a login session active at the time above
|
|
1862
|
+
* all. The quiet period this tick would otherwise cancel below is retired inside the call, so the early return leaves no timer unobserved.
|
|
1863
|
+
*/
|
|
1864
|
+
if (supervisor.captureImpairment() !== null) {
|
|
1865
|
+
void restartBrowserIfImpairedAndIdle();
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1375
1868
|
// Health-gated governor reset. On every eligible tick, tell the supervisor the browser is still ready; once it has been continuously ready for the policy's
|
|
1376
1869
|
// hold, this resets the relaunch governor to its normal state and returns true, so we log the recovery exactly once.
|
|
1377
1870
|
if (supervisor.noteSustainedHealth()) {
|
|
1378
1871
|
LOG.info("Browser capture readiness has been sustained; the relaunch governor has reset to its normal state.");
|
|
1379
1872
|
}
|
|
1380
1873
|
// Skip if the browser has not exceeded the maximum age.
|
|
1381
|
-
const age =
|
|
1874
|
+
const age = systemClock.now() - launchTime;
|
|
1382
1875
|
if (age < BROWSER_MAX_AGE) {
|
|
1383
1876
|
return;
|
|
1384
1877
|
}
|
|
1385
1878
|
// If there are active streams, cancel any pending quiet timer and return. Streams that start during the quiet period reset the countdown.
|
|
1386
1879
|
if (getStreamCount() > 0) {
|
|
1387
|
-
if (
|
|
1880
|
+
if (timers.has(RESTART_QUIET_KEY)) {
|
|
1388
1881
|
LOG.debug("browser:lifecycle", "Browser restart quiet period cancelled - streams are active.");
|
|
1389
|
-
clearTimeout(restartQuietTimer);
|
|
1390
|
-
restartQuietTimer = null;
|
|
1391
1882
|
}
|
|
1883
|
+
cancelRestartQuietTimer();
|
|
1392
1884
|
return;
|
|
1393
1885
|
}
|
|
1394
1886
|
// No active streams and the browser is old enough. Start the quiet timer if one is not already running.
|
|
1395
|
-
if (!
|
|
1887
|
+
if (!timers.has(RESTART_QUIET_KEY)) {
|
|
1396
1888
|
LOG.debug("browser:lifecycle", "Browser uptime exceeds threshold. Quiet period started - restart will proceed if no streams start within %s minutes.", Math.round(BROWSER_RESTART_QUIET_PERIOD / 60000));
|
|
1397
|
-
|
|
1398
|
-
void executeBrowserRestart();
|
|
1889
|
+
timers.setTimeout(RESTART_QUIET_KEY, () => {
|
|
1890
|
+
void executeBrowserRestart("maintenance");
|
|
1399
1891
|
}, BROWSER_RESTART_QUIET_PERIOD);
|
|
1400
1892
|
}
|
|
1401
1893
|
}
|
|
1402
1894
|
/**
|
|
1403
|
-
* Executes
|
|
1404
|
-
*
|
|
1895
|
+
* Executes a browser restart: a final guard check, then the current instance is retired and torn down and a fresh one is launched in its place. Every cause shares
|
|
1896
|
+
* that whole sequence; only the log line that announces the restart differs, since only the cause knows why the browser is being replaced.
|
|
1897
|
+
* @param cause - Why the restart is running, for the announcement.
|
|
1405
1898
|
*/
|
|
1406
|
-
async function executeBrowserRestart() {
|
|
1407
|
-
//
|
|
1408
|
-
|
|
1899
|
+
async function executeBrowserRestart(cause) {
|
|
1900
|
+
// Retire any pending quiet period. For a maintenance restart this is the timer that just fired, and clearing a fired handle costs nothing. For an impairment
|
|
1901
|
+
// restart it may be a countdown started before the mark, which has to go: the browser it was measured against is about to be replaced, and letting it fire would
|
|
1902
|
+
// restart the fresh instance a second time.
|
|
1903
|
+
cancelRestartQuietTimer();
|
|
1409
1904
|
// Final guard: re-check all preconditions. Conditions may have changed during the quiet period (e.g., a stream started just before the timer fired, login mode
|
|
1410
|
-
// was activated, or the browser disconnected on its own). Reading current()/currentLaunchTime() together keeps the ready-state check and the age source
|
|
1905
|
+
// was activated, or the browser disconnected on its own). Reading current()/currentLaunchTime() together keeps the ready-state check and the age source
|
|
1906
|
+
// consistent. The idleness question is asked for this restart's own cause, because what a maintenance sweep must defer to and what a repair must defer to are
|
|
1907
|
+
// not the same set of streams.
|
|
1411
1908
|
const browser = supervisor.current();
|
|
1412
1909
|
const launchTime = supervisor.currentLaunchTime();
|
|
1413
|
-
if (gracefulShutdownInProgress || isLoginModeActive() || (
|
|
1910
|
+
if (gracefulShutdownInProgress || isLoginModeActive() || !isBrowserIdleForRestart(cause, readRestartFacts()) || !browser?.connected || (launchTime === null)) {
|
|
1414
1911
|
LOG.debug("browser:lifecycle", "Browser restart aborted - preconditions no longer met.");
|
|
1415
1912
|
return;
|
|
1416
1913
|
}
|
|
1417
|
-
const age =
|
|
1914
|
+
const age = systemClock.now() - launchTime;
|
|
1418
1915
|
const hours = Math.floor(age / 3600000);
|
|
1419
1916
|
const minutes = Math.floor((age % 3600000) / 60000);
|
|
1420
|
-
|
|
1917
|
+
if (cause === "impairment") {
|
|
1918
|
+
LOG.info("Restarting the browser because it can no longer start captures (uptime: %sh %sm).", hours, minutes);
|
|
1919
|
+
}
|
|
1920
|
+
else {
|
|
1921
|
+
LOG.info("Restarting browser for scheduled maintenance (uptime: %sh %sm).", hours, minutes);
|
|
1922
|
+
}
|
|
1421
1923
|
try {
|
|
1422
1924
|
// Retire the current instance from the lifecycle, tear it down, then acquire a fresh one through the supervisor. We do NOT touch the graceful-shutdown flag (the
|
|
1423
1925
|
// server is not shutting down): closeBrowserInstance removes the disconnect listener, which is what makes this intentional teardown quiet. acquire() publishes
|
|
@@ -1430,6 +1932,12 @@ async function executeBrowserRestart() {
|
|
|
1430
1932
|
const teardown = closeBrowserInstance(browser);
|
|
1431
1933
|
supervisor.noteTeardownBegun(teardown, BROWSER_TEARDOWN_DRAIN_BOUND_MS);
|
|
1432
1934
|
await teardown;
|
|
1935
|
+
/* The relaunch ends the browser session the channel-selection caches describe (guide row positions, discovered page URLs, watch URLs), exactly as the
|
|
1936
|
+
* disconnect path ends it, so they are cleared here for the same reason. The clear waits for the process to be gone rather than running beside the teardown,
|
|
1937
|
+
* because a page of the retiring session can still deliver a response event while Chrome drains - the Comcast channelmap listener writes its caches from
|
|
1938
|
+
* exactly such an event - and a clear issued ahead of the drain would be undone by it.
|
|
1939
|
+
*/
|
|
1940
|
+
clearChannelSelectionCaches();
|
|
1433
1941
|
// The preconditions were checked before the teardown, but a shutdown can begin during the seconds it takes, and relaunching then would spawn Chrome into a
|
|
1434
1942
|
// dying process. Re-check on the far side of the await, for the same reason the guard above re-checks on the far side of the quiet period.
|
|
1435
1943
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the shutdown path sets this while the teardown is awaited; TS cannot see that.
|
|
@@ -1437,10 +1945,12 @@ async function executeBrowserRestart() {
|
|
|
1437
1945
|
LOG.debug("browser:lifecycle", "Browser restart relaunch declined because shutdown began while the previous instance was closing.");
|
|
1438
1946
|
return;
|
|
1439
1947
|
}
|
|
1440
|
-
// Launch a fresh browser instance so it is ready for the next stream request.
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1948
|
+
// Launch a fresh browser instance so it is ready for the next stream request. The relaunch needs nothing but the browser itself, and the instance it replaces
|
|
1949
|
+
// may be refusing captures, so it acquires for a page.
|
|
1950
|
+
await getCurrentBrowser("page");
|
|
1951
|
+
// Bring the fresh window into agreement with the policy. A scheduled restart only runs against an idle registry, so this settles it minimized; a stream
|
|
1952
|
+
// arriving while the relaunch is in flight triggers its own sync during establishment.
|
|
1953
|
+
await syncWindowVisibility();
|
|
1444
1954
|
LOG.info("Browser restart complete. Fresh instance is ready.");
|
|
1445
1955
|
}
|
|
1446
1956
|
catch (error) {
|
|
@@ -1448,25 +1958,25 @@ async function executeBrowserRestart() {
|
|
|
1448
1958
|
}
|
|
1449
1959
|
}
|
|
1450
1960
|
/**
|
|
1451
|
-
* Starts the periodic browser restart eligibility check. This should be called once during server startup, after the browser is initialized. The
|
|
1452
|
-
* indefinitely until stopBrowserRestartChecking() is called (typically during graceful shutdown).
|
|
1961
|
+
* Starts the periodic browser restart eligibility check. This should be called once during server startup, after the browser is initialized. The check runs
|
|
1962
|
+
* indefinitely until stopBrowserRestartChecking() is called (typically during graceful shutdown). A second start while one is running changes nothing.
|
|
1963
|
+
* @param clock - The clock the check's interval and the quiet period it arms both run on. Defaults to the system clock.
|
|
1453
1964
|
*/
|
|
1454
|
-
export function startBrowserRestartChecking() {
|
|
1455
|
-
|
|
1965
|
+
export function startBrowserRestartChecking(clock = systemClock) {
|
|
1966
|
+
if (restartTimers) {
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
const timers = new TimerRegistry({ clock });
|
|
1970
|
+
timers.setInterval(RESTART_CHECK_KEY, () => { checkBrowserRestart(timers); }, BROWSER_RESTART_CHECK_INTERVAL);
|
|
1971
|
+
restartTimers = timers;
|
|
1456
1972
|
}
|
|
1457
1973
|
/**
|
|
1458
|
-
* Stops the browser restart
|
|
1459
|
-
*
|
|
1974
|
+
* Stops the periodic browser restart eligibility check. This should be called during graceful shutdown to prevent a restart from racing with server shutdown.
|
|
1975
|
+
* Disposing the registry drains the check and any pending quiet period together, and a stop with nothing running is a no-op.
|
|
1460
1976
|
*/
|
|
1461
1977
|
export function stopBrowserRestartChecking() {
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
restartCheckInterval = null;
|
|
1465
|
-
}
|
|
1466
|
-
if (restartQuietTimer) {
|
|
1467
|
-
clearTimeout(restartQuietTimer);
|
|
1468
|
-
restartQuietTimer = null;
|
|
1469
|
-
}
|
|
1978
|
+
restartTimers?.dispose();
|
|
1979
|
+
restartTimers = null;
|
|
1470
1980
|
}
|
|
1471
1981
|
/* When running as a packaged executable (created by the `pkg` tool), the application is bundled into a single binary. Node modules like puppeteer-stream are
|
|
1472
1982
|
* included in the bundle, but Chrome cannot load extensions from within the packaged binary - it needs actual files on the filesystem.
|
|
@@ -1523,6 +2033,9 @@ export async function prepareExtension() {
|
|
|
1523
2033
|
throw error;
|
|
1524
2034
|
}
|
|
1525
2035
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
2036
|
+
/* Re-export the capture acquisition for the streaming module. The browser directory owns every conversation with puppeteer-stream - index.ts launches it and
|
|
2037
|
+
* tabCapture.ts speaks its extension's protocol - so no other layer imports the library, and the streaming layer asks this module for a capture the same way it
|
|
2038
|
+
* asks it for a browser.
|
|
2039
|
+
*/
|
|
2040
|
+
export { acquireCaptureStream } from "./tabCapture.js";
|
|
1528
2041
|
//# sourceMappingURL=index.js.map
|