prismcast 1.8.0 → 1.10.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/README.md +6 -6
- package/dist/app.d.ts +1 -1
- package/dist/app.js +41 -13
- package/dist/app.js.map +1 -1
- package/dist/browser/cdp.d.ts +4 -6
- package/dist/browser/cdp.js +18 -22
- package/dist/browser/cdp.js.map +1 -1
- package/dist/browser/channelSelection.d.ts +41 -11
- package/dist/browser/channelSelection.js +75 -24
- package/dist/browser/channelSelection.js.map +1 -1
- package/dist/browser/display.d.ts +1 -1
- package/dist/browser/index.d.ts +8 -8
- package/dist/browser/index.js +44 -45
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/login.d.ts +2 -2
- package/dist/browser/login.js +1 -1
- package/dist/browser/manifestInterceptor.d.ts +77 -0
- package/dist/browser/manifestInterceptor.js +323 -0
- package/dist/browser/manifestInterceptor.js.map +1 -0
- package/dist/browser/precaching.d.ts +1 -1
- package/dist/browser/precaching.js +23 -23
- package/dist/browser/precaching.js.map +1 -1
- package/dist/browser/tuning/comcastPolymer.d.ts +1 -1
- package/dist/browser/tuning/cox.d.ts +1 -1
- package/dist/browser/tuning/directv.d.ts +1 -1
- package/dist/browser/tuning/directv.js +65 -69
- package/dist/browser/tuning/directv.js.map +1 -1
- package/dist/browser/tuning/fox.d.ts +1 -1
- package/dist/browser/tuning/fox.js +172 -15
- package/dist/browser/tuning/fox.js.map +1 -1
- package/dist/browser/tuning/hbo.d.ts +1 -1
- package/dist/browser/tuning/hbo.js +7 -7
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.d.ts +1 -1
- package/dist/browser/tuning/hulu.js +120 -113
- package/dist/browser/tuning/hulu.js.map +1 -1
- package/dist/browser/tuning/shared.d.ts +3 -3
- package/dist/browser/tuning/shared.js +5 -5
- package/dist/browser/tuning/sling.d.ts +1 -1
- package/dist/browser/tuning/sling.js +30 -30
- package/dist/browser/tuning/sling.js.map +1 -1
- package/dist/browser/tuning/spectrum.d.ts +1 -1
- package/dist/browser/tuning/spectrum.js +21 -22
- package/dist/browser/tuning/spectrum.js.map +1 -1
- package/dist/browser/tuning/thumbnailRow.d.ts +1 -1
- package/dist/browser/tuning/thumbnailRow.js +1 -1
- package/dist/browser/tuning/tileClick.d.ts +1 -1
- package/dist/browser/tuning/tileClick.js +5 -5
- package/dist/browser/tuning/xfinity.d.ts +1 -1
- package/dist/browser/tuning/youtubeTv.d.ts +1 -1
- package/dist/browser/tuning/youtubeTv.js +17 -18
- package/dist/browser/tuning/youtubeTv.js.map +1 -1
- package/dist/browser/video.d.ts +20 -6
- package/dist/browser/video.js +30 -38
- package/dist/browser/video.js.map +1 -1
- package/dist/channels/index.d.ts +1 -2
- package/dist/channels/index.js +475 -442
- package/dist/channels/index.js.map +1 -1
- package/dist/config/channelForm.d.ts +58 -0
- package/dist/config/channelForm.js +129 -0
- package/dist/config/channelForm.js.map +1 -0
- package/dist/config/consistencyProbe.d.ts +7 -0
- package/dist/config/consistencyProbe.js +143 -0
- package/dist/config/consistencyProbe.js.map +1 -0
- package/dist/config/health.d.ts +7 -6
- package/dist/config/health.js +78 -65
- package/dist/config/health.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +21 -14
- package/dist/config/index.js.map +1 -1
- package/dist/config/paths.d.ts +9 -3
- package/dist/config/paths.js +12 -4
- package/dist/config/paths.js.map +1 -1
- package/dist/config/persistence.context.d.ts +8 -0
- package/dist/config/persistence.context.js +30 -0
- package/dist/config/persistence.context.js.map +1 -0
- package/dist/config/persistence.d.ts +157 -0
- package/dist/config/persistence.js +446 -0
- package/dist/config/persistence.js.map +1 -0
- package/dist/config/presets.d.ts +14 -4
- package/dist/config/presets.js +84 -75
- package/dist/config/presets.js.map +1 -1
- package/dist/config/profiles.d.ts +2 -2
- package/dist/config/profiles.js +7 -9
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/servicePacks.d.ts +46 -0
- package/dist/config/{providerPacks.js → servicePacks.js} +50 -53
- package/dist/config/servicePacks.js.map +1 -0
- package/dist/config/services.d.ts +228 -0
- package/dist/config/services.js +772 -0
- package/dist/config/services.js.map +1 -0
- package/dist/config/sites.d.ts +3 -3
- package/dist/config/sites.js +75 -75
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +522 -54
- package/dist/config/userChannels.js +1343 -366
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +108 -13
- package/dist/config/userConfig.js +296 -143
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +34 -16
- package/dist/config/userProfiles.js +189 -100
- package/dist/config/userProfiles.js.map +1 -1
- package/dist/hdhr/channelMap.d.ts +1 -1
- package/dist/hdhr/channelMap.js +10 -16
- package/dist/hdhr/channelMap.js.map +1 -1
- package/dist/hdhr/deviceId.js +10 -9
- package/dist/hdhr/deviceId.js.map +1 -1
- package/dist/hdhr/discover.js +4 -5
- package/dist/hdhr/discover.js.map +1 -1
- package/dist/hdhr/index.js +12 -19
- package/dist/hdhr/index.js.map +1 -1
- package/dist/index.js +12 -12
- package/dist/index.js.map +1 -1
- package/dist/native/codecInference.d.ts +44 -0
- package/dist/native/codecInference.js +327 -0
- package/dist/native/codecInference.js.map +1 -0
- package/dist/native/decrypt.d.ts +1 -1
- package/dist/native/index.d.ts +5 -5
- package/dist/native/index.js +45 -67
- package/dist/native/index.js.map +1 -1
- package/dist/native/probe.d.ts +27 -9
- package/dist/native/probe.js +159 -68
- package/dist/native/probe.js.map +1 -1
- package/dist/native/proxy.d.ts +4 -2
- package/dist/native/proxy.js +56 -53
- package/dist/native/proxy.js.map +1 -1
- package/dist/native/tokenExpiry.d.ts +22 -0
- package/dist/native/tokenExpiry.js +153 -0
- package/dist/native/tokenExpiry.js.map +1 -0
- package/dist/routes/assets.js +3 -3
- package/dist/routes/assets.js.map +1 -1
- package/dist/routes/auth.js +9 -13
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/components.js +2 -2
- package/dist/routes/components.js.map +1 -1
- package/dist/routes/config/channels/endpoints/browse.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/browse.js +195 -0
- package/dist/routes/config/channels/endpoints/browse.js.map +1 -0
- package/dist/routes/config/channels/endpoints/bulk.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/bulk.js +128 -0
- package/dist/routes/config/channels/endpoints/bulk.js.map +1 -0
- package/dist/routes/config/channels/endpoints/crud.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/crud.js +504 -0
- package/dist/routes/config/channels/endpoints/crud.js.map +1 -0
- package/dist/routes/config/channels/endpoints/importExport.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/importExport.js +131 -0
- package/dist/routes/config/channels/endpoints/importExport.js.map +1 -0
- package/dist/routes/config/channels/endpoints/predefined.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/predefined.js +78 -0
- package/dist/routes/config/channels/endpoints/predefined.js.map +1 -0
- package/dist/routes/config/channels/endpoints/prefs.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/prefs.js +58 -0
- package/dist/routes/config/channels/endpoints/prefs.js.map +1 -0
- package/dist/routes/config/channels/endpoints/service.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/service.js +149 -0
- package/dist/routes/config/channels/endpoints/service.js.map +1 -0
- package/dist/routes/config/channels/endpoints/tags.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/tags.js +185 -0
- package/dist/routes/config/channels/endpoints/tags.js.map +1 -0
- package/dist/routes/config/channels/http/handler.d.ts +10 -0
- package/dist/routes/config/channels/http/handler.js +20 -0
- package/dist/routes/config/channels/http/handler.js.map +1 -0
- package/dist/routes/config/channels/http/playlistHint.d.ts +25 -0
- package/dist/routes/config/channels/http/playlistHint.js +40 -0
- package/dist/routes/config/channels/http/playlistHint.js.map +1 -0
- package/dist/routes/config/channels/http/serviceWarning.d.ts +10 -0
- package/dist/routes/config/channels/http/serviceWarning.js +28 -0
- package/dist/routes/config/channels/http/serviceWarning.js.map +1 -0
- package/dist/routes/config/channels/index.d.ts +3 -3
- package/dist/routes/config/channels/index.js +2 -2
- package/dist/routes/config/channels/index.js.map +1 -1
- package/dist/routes/config/channels/{routes.d.ts → setup.d.ts} +1 -1
- package/dist/routes/config/channels/setup.js +23 -0
- package/dist/routes/config/channels/setup.js.map +1 -0
- package/dist/routes/config/channels/table.d.ts +13 -13
- package/dist/routes/config/channels/table.js +291 -184
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/http/envelope.d.ts +111 -0
- package/dist/routes/config/http/envelope.js +103 -0
- package/dist/routes/config/http/envelope.js.map +1 -0
- package/dist/routes/config/index.d.ts +9 -9
- package/dist/routes/config/index.js +5 -5
- package/dist/routes/config/index.js.map +1 -1
- package/dist/routes/config/{providers.js → services.js} +105 -99
- package/dist/routes/config/services.js.map +1 -0
- package/dist/routes/config/settings.d.ts +1 -1
- package/dist/routes/config/settings.js +85 -60
- package/dist/routes/config/settings.js.map +1 -1
- package/dist/routes/debug.js +17 -18
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/health.js +1 -1
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/icons.js +2 -2
- package/dist/routes/icons.js.map +1 -1
- package/dist/routes/index.js +2 -2
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/logs.js +23 -34
- package/dist/routes/logs.js.map +1 -1
- package/dist/routes/mpegts.js +1 -1
- package/dist/routes/playlist.d.ts +5 -5
- package/dist/routes/playlist.js +84 -61
- package/dist/routes/playlist.js.map +1 -1
- package/dist/routes/root/content.js +82 -61
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/index.js +2 -5
- package/dist/routes/root/index.js.map +1 -1
- package/dist/routes/root/scripts/channels.js +538 -581
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/config.js +868 -1022
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/index.d.ts +4 -4
- package/dist/routes/root/scripts/shared.js +445 -197
- package/dist/routes/root/scripts/shared.js.map +1 -1
- package/dist/routes/root/scripts/status.d.ts +4 -0
- package/dist/routes/root/scripts/status.handlers.d.ts +182 -0
- package/dist/routes/root/scripts/status.handlers.js +713 -0
- package/dist/routes/root/scripts/status.handlers.js.map +1 -0
- package/dist/routes/root/scripts/status.js +85 -436
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/root/styles.js +16 -8
- package/dist/routes/root/styles.js.map +1 -1
- package/dist/routes/services.d.ts +6 -0
- package/dist/routes/{providers.js → services.js} +58 -55
- package/dist/routes/services.js.map +1 -0
- package/dist/routes/sse.d.ts +16 -0
- package/dist/routes/sse.js +29 -0
- package/dist/routes/sse.js.map +1 -0
- package/dist/routes/streams.js +10 -23
- package/dist/routes/streams.js.map +1 -1
- package/dist/routes/ui.d.ts +2 -2
- package/dist/routes/ui.js +2 -0
- package/dist/routes/ui.js.map +1 -1
- package/dist/routes/upgrade.js +6 -7
- package/dist/routes/upgrade.js.map +1 -1
- package/dist/service/commands.context.d.ts +6 -0
- package/dist/service/commands.context.js +47 -0
- package/dist/service/commands.context.js.map +1 -0
- package/dist/service/commands.d.ts +53 -8
- package/dist/service/commands.js +172 -192
- package/dist/service/commands.js.map +1 -1
- package/dist/service/generators.context.d.ts +6 -0
- package/dist/service/generators.context.js +45 -0
- package/dist/service/generators.context.js.map +1 -0
- package/dist/service/generators.d.ts +56 -16
- package/dist/service/generators.js +509 -291
- package/dist/service/generators.js.map +1 -1
- package/dist/service/index.d.ts +2 -2
- package/dist/streaming/clients.d.ts +2 -2
- package/dist/streaming/clients.js +2 -2
- package/dist/streaming/codec.d.ts +2 -2
- package/dist/streaming/codec.js +0 -1
- package/dist/streaming/codec.js.map +1 -1
- package/dist/streaming/fmp4Segmenter.d.ts +2 -2
- package/dist/streaming/fmp4Segmenter.js +12 -12
- package/dist/streaming/hls.d.ts +7 -7
- package/dist/streaming/hls.js +104 -92
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +3 -3
- package/dist/streaming/hlsResume.js +6 -6
- package/dist/streaming/hlsResume.js.map +1 -1
- package/dist/streaming/hlsSegments.d.ts +5 -2
- package/dist/streaming/hlsSegments.js +14 -9
- package/dist/streaming/hlsSegments.js.map +1 -1
- package/dist/streaming/monitor.d.ts +12 -12
- package/dist/streaming/monitor.js +98 -66
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/mp4Parser.d.ts +34 -4
- package/dist/streaming/mp4Parser.js +111 -29
- package/dist/streaming/mp4Parser.js.map +1 -1
- package/dist/streaming/mpegts.d.ts +1 -1
- package/dist/streaming/mpegts.js +13 -10
- package/dist/streaming/mpegts.js.map +1 -1
- package/dist/streaming/playlistBuilder.d.ts +1 -1
- package/dist/streaming/playlistBuilder.js +2 -2
- package/dist/streaming/preroll.d.ts +9 -9
- package/dist/streaming/preroll.js +40 -42
- package/dist/streaming/preroll.js.map +1 -1
- package/dist/streaming/pretune.js +4 -4
- package/dist/streaming/pretune.js.map +1 -1
- package/dist/streaming/recovery.d.ts +3 -3
- package/dist/streaming/recovery.js +4 -4
- package/dist/streaming/registry.d.ts +6 -6
- package/dist/streaming/registry.js +4 -8
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +11 -10
- package/dist/streaming/setup.js +120 -71
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +9 -3
- package/dist/streaming/showInfo.js +33 -27
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +8 -7
- package/dist/streaming/statusEmitter.js +5 -5
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/channels.d.ts +139 -36
- package/dist/types/channels.js +39 -1
- package/dist/types/channels.js.map +1 -1
- package/dist/types/config.d.ts +22 -3
- package/dist/types/index.d.ts +9 -8
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/profiles.d.ts +14 -13
- package/dist/types/selection.d.ts +49 -5
- package/dist/types/shared.d.ts +1 -1
- package/dist/types/streaming.d.ts +2 -2
- package/dist/upgrade/commands.context.d.ts +6 -0
- package/dist/upgrade/commands.context.js +29 -0
- package/dist/upgrade/commands.context.js.map +1 -0
- package/dist/upgrade/commands.d.ts +29 -2
- package/dist/upgrade/commands.js +87 -134
- package/dist/upgrade/commands.js.map +1 -1
- package/dist/upgrade/detection.context.d.ts +6 -0
- package/dist/upgrade/detection.context.js +24 -0
- package/dist/upgrade/detection.context.js.map +1 -0
- package/dist/upgrade/detection.d.ts +140 -10
- package/dist/upgrade/detection.js +153 -66
- package/dist/upgrade/detection.js.map +1 -1
- package/dist/upgrade/index.d.ts +2 -2
- package/dist/utils/chromeFetch.d.ts +1 -1
- package/dist/utils/chromeFetch.js +2 -2
- package/dist/utils/clock.d.ts +14 -0
- package/dist/utils/clock.js +28 -0
- package/dist/utils/clock.js.map +1 -0
- package/dist/utils/debugFilter.d.ts +1 -1
- package/dist/utils/debugFilter.js +8 -6
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/utils/delay.d.ts +2 -3
- package/dist/utils/delay.js +10 -16
- package/dist/utils/delay.js.map +1 -1
- package/dist/utils/evaluate.js +12 -14
- package/dist/utils/evaluate.js.map +1 -1
- package/dist/utils/ffmpeg.context.d.ts +6 -0
- package/dist/utils/ffmpeg.context.js +31 -0
- package/dist/utils/ffmpeg.context.js.map +1 -0
- package/dist/utils/ffmpeg.d.ts +85 -29
- package/dist/utils/ffmpeg.js +126 -129
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/fileLogger.d.ts +18 -2
- package/dist/utils/fileLogger.js +60 -31
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/format.d.ts +7 -1
- package/dist/utils/format.js +22 -2
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/html.js +1 -3
- package/dist/utils/html.js.map +1 -1
- package/dist/utils/index.d.ts +21 -20
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.js +31 -45
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/m3u.d.ts +18 -3
- package/dist/utils/m3u.js +28 -14
- package/dist/utils/m3u.js.map +1 -1
- package/dist/utils/memo.d.ts +19 -0
- package/dist/utils/memo.js +53 -0
- package/dist/utils/memo.js.map +1 -0
- package/dist/utils/morganStream.js +4 -4
- package/dist/utils/morganStream.js.map +1 -1
- package/dist/utils/pid.d.ts +2 -2
- package/dist/utils/pid.js +2 -2
- package/dist/utils/platform.d.ts +1 -1
- package/dist/utils/platform.js +3 -2
- package/dist/utils/platform.js.map +1 -1
- package/dist/utils/retry.d.ts +2 -0
- package/dist/utils/retry.js +9 -7
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/sanitize.d.ts +2 -2
- package/dist/utils/sanitize.js +4 -4
- package/dist/utils/streamContext.d.ts +7 -0
- package/dist/utils/streamContext.js +10 -1
- package/dist/utils/streamContext.js.map +1 -1
- package/dist/utils/timing.d.ts +6 -3
- package/dist/utils/timing.js +10 -6
- package/dist/utils/timing.js.map +1 -1
- package/dist/utils/version.d.ts +3 -2
- package/dist/utils/version.js +15 -28
- package/dist/utils/version.js.map +1 -1
- package/package.json +14 -10
- package/dist/config/providerPacks.d.ts +0 -46
- package/dist/config/providerPacks.js.map +0 -1
- package/dist/config/providers.d.ts +0 -175
- package/dist/config/providers.js +0 -691
- package/dist/config/providers.js.map +0 -1
- package/dist/native/intercept.d.ts +0 -35
- package/dist/native/intercept.js +0 -184
- package/dist/native/intercept.js.map +0 -1
- package/dist/routes/config/channels/routes.js +0 -1477
- package/dist/routes/config/channels/routes.js.map +0 -1
- package/dist/routes/config/providers.js.map +0 -1
- package/dist/routes/providers.d.ts +0 -6
- package/dist/routes/providers.js.map +0 -1
- /package/dist/routes/config/{providers.d.ts → services.d.ts} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LOG, delay, evaluateWithAbort } from "../utils/index.js";
|
|
1
|
+
import { LOG, delay, evaluateWithAbort, formatError } from "../utils/index.js";
|
|
2
2
|
import { getDomainConfig, registerProviderModuleProfile } from "../config/sites.js";
|
|
3
3
|
import { CONFIG } from "../config/index.js";
|
|
4
4
|
import { coxProvider } from "./tuning/cox.js";
|
|
@@ -19,7 +19,7 @@ import { yttvProvider } from "./tuning/youtubeTv.js";
|
|
|
19
19
|
*
|
|
20
20
|
* Each provider tuning file exports a single ProviderModule object that bundles identity metadata (slug, label, guideUrl), the tuning strategy, and a
|
|
21
21
|
* discoverChannels implementation. The coordinator builds its strategy dispatch lookup from provider modules at evaluation time. Generic strategies
|
|
22
|
-
* (thumbnailRow, tileClick) remain bare ChannelStrategyEntry objects
|
|
22
|
+
* (thumbnailRow, tileClick) remain bare ChannelStrategyEntry objects - they are site-specific interaction patterns, not provider-level registrations.
|
|
23
23
|
*
|
|
24
24
|
* Shared utilities (scrollAndClick, normalizeChannelName, resolveMatchSelector, logAvailableChannels) live in tuning/shared.ts so that tuning strategy files can
|
|
25
25
|
* import them without creating a circular dependency with this coordinator. This module re-exports them for backward compatibility.
|
|
@@ -42,7 +42,7 @@ import { yttvProvider } from "./tuning/youtubeTv.js";
|
|
|
42
42
|
* utilities.
|
|
43
43
|
*/
|
|
44
44
|
// Provider module registry. The primary registry for all provider-level operations. Each entry bundles identity metadata, tuning strategy, and channel discovery.
|
|
45
|
-
// Future capabilities become additional methods on ProviderModule
|
|
45
|
+
// Future capabilities become additional methods on ProviderModule - no new registries needed.
|
|
46
46
|
const providerModules = [
|
|
47
47
|
coxProvider, directvProvider, foxProvider, hboProvider, huluProvider, slingProvider, spectrumProvider, xfinityProvider, yttvProvider
|
|
48
48
|
];
|
|
@@ -71,7 +71,6 @@ export async function resolveDirectUrl(profile, page) {
|
|
|
71
71
|
if (!channelSelector) {
|
|
72
72
|
return null;
|
|
73
73
|
}
|
|
74
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
75
74
|
return await strategies[channelSelection.strategy]?.resolveDirectUrl?.(channelSelector, page) ?? null;
|
|
76
75
|
}
|
|
77
76
|
/**
|
|
@@ -84,7 +83,6 @@ export function invalidateDirectUrl(profile) {
|
|
|
84
83
|
if (!channelSelector) {
|
|
85
84
|
return;
|
|
86
85
|
}
|
|
87
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
88
86
|
strategies[channelSelection.strategy]?.invalidateDirectUrl?.(channelSelector);
|
|
89
87
|
}
|
|
90
88
|
/**
|
|
@@ -104,6 +102,16 @@ export function clearChannelSelectionCaches() {
|
|
|
104
102
|
export function getProviderBySlug(slug) {
|
|
105
103
|
return providerModules.find((p) => p.slug === slug);
|
|
106
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Looks up a provider module by its channel-selection strategy name. Used by code paths that have a resolved profile (and therefore the strategy name) but not
|
|
107
|
+
* a slug. Returns undefined when no provider registers the given strategy or when the strategy is one of the generic non-provider strategies (thumbnailRow,
|
|
108
|
+
* tileClick, etc.).
|
|
109
|
+
* @param strategyName - The ChannelSelectionStrategy value (e.g., "foxGrid", "huluLive", "slingLive").
|
|
110
|
+
* @returns The matching provider module or undefined.
|
|
111
|
+
*/
|
|
112
|
+
export function getProviderByStrategy(strategyName) {
|
|
113
|
+
return providerModules.find((p) => p.strategyName === strategyName);
|
|
114
|
+
}
|
|
107
115
|
/**
|
|
108
116
|
* Returns all registered provider module slugs. Used for validation in the checkboxList setting for precache providers.
|
|
109
117
|
* @returns Array of provider slugs.
|
|
@@ -114,7 +122,7 @@ export function getProviderSlugs() {
|
|
|
114
122
|
/**
|
|
115
123
|
* Returns slug, label, domain, and optional icon URL for all registered provider modules. Used by the checkboxList setting for precache labels and by the
|
|
116
124
|
* browse modal for provider picker cards with icons. The domain is extracted from the guide URL. The icon URL is derived from the provider's DOMAIN_CONFIG
|
|
117
|
-
* entry
|
|
125
|
+
* entry - the single source of truth for provider icon URLs.
|
|
118
126
|
* @returns Array of objects with domain, iconUrl, label, noDirectTuneOptimization, and slug properties.
|
|
119
127
|
*/
|
|
120
128
|
export function getProviderModuleInfo() {
|
|
@@ -171,35 +179,78 @@ export function getCachedProviderChannels() {
|
|
|
171
179
|
export { logAvailableChannels, normalizeChannelName, resolveMatchSelector, scrollAndClick } from "./tuning/shared.js";
|
|
172
180
|
/**
|
|
173
181
|
* Selects a channel from a multi-channel player UI using the strategy specified in the profile. This is the main entry point for channel selection, called by
|
|
174
|
-
*
|
|
182
|
+
* initializePlayback() after page navigation.
|
|
175
183
|
*
|
|
176
|
-
* The function handles:
|
|
177
|
-
* -
|
|
178
|
-
* -
|
|
179
|
-
* -
|
|
180
|
-
*
|
|
181
|
-
*
|
|
184
|
+
* The function handles, in order:
|
|
185
|
+
* - No-op short-circuit for single-channel sites (strategy "none" or no channelSelector).
|
|
186
|
+
* - Category resolution: when the active provider declares categorySelectors and the profile's selector matches, resolveCategorySelector() is invoked to convert
|
|
187
|
+
* the category to a concrete per-user identifier. The resolved value replaces profile.channelSelector for the rest of the call and is persisted via the
|
|
188
|
+
* options.persistResolution callback if supplied. Resolution failures fall through with the original category selector - the strategy attempts a best-effort
|
|
189
|
+
* match and the verifier fails open.
|
|
190
|
+
* - Pre-selection scroll phase to force lazy-loaded content into the DOM (when scrollToBottom or scrollSelector+scrollTarget is set).
|
|
191
|
+
* - Polling for channel element readiness before strategy dispatch (when profile.channelSelection.matchSelector is set).
|
|
192
|
+
* - Strategy dispatch based on profile.channelSelection.strategy.
|
|
182
193
|
* @param page - The Puppeteer page object.
|
|
183
194
|
* @param profile - The resolved site profile containing channelSelection config and channelSelector slug.
|
|
195
|
+
* @param options - Optional callbacks for the resolution layer. Currently the only field is persistResolution.
|
|
184
196
|
* @returns Result object with success status and optional failure reason.
|
|
185
197
|
*/
|
|
186
|
-
export async function selectChannel(page, profile) {
|
|
198
|
+
export async function selectChannel(page, profile, options = {}) {
|
|
187
199
|
const { channelSelection } = profile;
|
|
188
200
|
// No channel selection needed if strategy is "none" or no channelSelector is specified.
|
|
189
201
|
if ((channelSelection.strategy === "none") || !isChannelSelectionProfile(profile)) {
|
|
190
202
|
return { success: true };
|
|
191
203
|
}
|
|
204
|
+
// The narrowed profile is used for the rest of this function. Resolution may produce a refined profile with a concrete channelSelector replacing the original
|
|
205
|
+
// category value; the strategy receives the refined version. Reassigning the parameter would lose the narrowing TypeScript inferred from isChannelSelectionProfile
|
|
206
|
+
// above, so we keep a separate local instead.
|
|
207
|
+
let activeProfile = profile;
|
|
208
|
+
// Category resolution. When the profile's selector is a category value declared by the active provider, we delegate to the provider's category resolver to
|
|
209
|
+
// convert the category to a concrete per-user identifier (e.g., "FOXD2C" -> "WFLD" for a Chicago-market user). The resolved value replaces the profile's selector
|
|
210
|
+
// for the remainder of this function so the strategy and verifier act on a concrete identifier. Persistence is fire-and-forget on the caller's persistResolution
|
|
211
|
+
// callback - failures there do not abort the tune; the resolved selector still flows through this attempt, only the disk write is lost. When resolution returns
|
|
212
|
+
// a CategoryResolutionFailure, the framework's response is governed by categories.requireResolution: strict providers abort the tune with the resolver-authored
|
|
213
|
+
// reason; permissive providers log the reason and let the strategy attempt a best-effort match while the verifier fails open for the wildcard case.
|
|
214
|
+
//
|
|
215
|
+
// The whole resolution feature is gated by a single optional sub-object on the provider, so one optional-chain access decides whether to engage the layer at
|
|
216
|
+
// all. The type system guarantees that if `categories` is present, every field needed for resolution is also present.
|
|
217
|
+
const provider = getProviderByStrategy(channelSelection.strategy);
|
|
218
|
+
const categoryResolution = provider?.categoryResolution;
|
|
219
|
+
if (categoryResolution?.selectors.includes(activeProfile.channelSelector)) {
|
|
220
|
+
const resolution = await categoryResolution.resolve(activeProfile.channelSelector, page);
|
|
221
|
+
if ("callSign" in resolution) {
|
|
222
|
+
LOG.debug("tuning", "Resolved category selector \"%s\" to \"%s\" via %s.", activeProfile.channelSelector, resolution.callSign, provider?.label ?? "unknown");
|
|
223
|
+
activeProfile = { ...activeProfile, channelSelector: resolution.callSign };
|
|
224
|
+
if (options.persistResolution) {
|
|
225
|
+
const resolved = resolution.callSign;
|
|
226
|
+
options.persistResolution(resolved).catch((persistError) => {
|
|
227
|
+
LOG.debug("tuning", "Failed to persist resolved category selector to \"%s\": %s.", resolved, formatError(persistError));
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (categoryResolution.requireResolution) {
|
|
232
|
+
// Strict provider: a category selector that cannot be resolved is an unrecoverable error. The resolver authored the reason - it has the knowledge to write
|
|
233
|
+
// a complete, domain-shaped sentence including any selector- or provider-specific remediation guidance - so the framework relays it verbatim into the
|
|
234
|
+
// setup-failure path that marks channel health, terminates the pending stream, and surfaces the message to the user.
|
|
235
|
+
return { reason: resolution.reason, success: false };
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// Permissive provider: log the resolver-authored reason at debug level and fall through. The strategy will attempt a best-effort match against the original
|
|
239
|
+
// category selector, and the verifier will fail open for the wildcard case if the captured manifest URL cannot be cross-checked against discovered category
|
|
240
|
+
// members.
|
|
241
|
+
LOG.debug("tuning", "%s", resolution.reason);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
192
244
|
const entry = strategies[channelSelection.strategy];
|
|
193
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
194
245
|
if (!entry) {
|
|
195
246
|
LOG.warn("Unknown channel selection strategy: %s.", channelSelection.strategy);
|
|
196
247
|
return { reason: "Unknown channel selection strategy.", success: false };
|
|
197
248
|
}
|
|
198
|
-
// Pre-selection scroll phase. Some sites (e.g., Disney+) lazy-load entire page sections
|
|
249
|
+
// Pre-selection scroll phase. Some sites (e.g., Disney+) lazy-load entire page sections - headings, tiles, and images only appear in the DOM after scrolling
|
|
199
250
|
// them into the viewport. Two scroll modes are supported: scrollToBottom scrolls the page to the bottom to force all lazy content into the DOM, and
|
|
200
251
|
// scrollSelector+scrollTarget progressively scrolls until a specific element with matching text content is found and scrolled into view. Both modes gate on a
|
|
201
|
-
// readiness signal before scrolling
|
|
202
|
-
// first matching DOM element
|
|
252
|
+
// readiness signal before scrolling - scrollToBottom waits for the page to become scrollable (scrollHeight > innerHeight), while scrollSelector waits for the
|
|
253
|
+
// first matching DOM element - since SPAs typically fire the load event before React/framework rendering completes.
|
|
203
254
|
if (channelSelection.scrollToBottom) {
|
|
204
255
|
// Wait for the SPA to render enough content to make the page scrollable. SPAs fire the load event before the framework renders page sections, so scrollHeight
|
|
205
256
|
// equals innerHeight immediately after navigation. We poll until scrollHeight exceeds innerHeight, indicating content has been rendered and there is somewhere
|
|
@@ -217,7 +268,7 @@ export async function selectChannel(page, profile) {
|
|
|
217
268
|
else if (channelSelection.scrollSelector && channelSelection.scrollTarget) {
|
|
218
269
|
// Targeted scroll: find a specific element matching scrollSelector whose text content equals scrollTarget, then scroll it into view. This is used when only a
|
|
219
270
|
// particular section needs to be visible rather than the entire page. Progressively scrolls in viewport-sized increments, checking after each step whether the
|
|
220
|
-
// target element has appeared
|
|
271
|
+
// target element has appeared - necessary because sites with IntersectionObserver-based lazy loading only add sections to the DOM as they enter the viewport.
|
|
221
272
|
let found = false;
|
|
222
273
|
// Wait for at least one element matching the selector to appear so the SPA has started rendering content.
|
|
223
274
|
try {
|
|
@@ -262,12 +313,12 @@ export async function selectChannel(page, profile) {
|
|
|
262
313
|
LOG.debug("tuning:tileClick", "Scroll target \"%s\" via \"%s\": not found after %sms. Found headings: %s.", channelSelection.scrollTarget, channelSelection.scrollSelector, CONFIG.streaming.videoTimeout, JSON.stringify(headings));
|
|
263
314
|
}
|
|
264
315
|
}
|
|
265
|
-
// Poll for the channel element to appear and become visible. Only run when matchSelector is explicitly configured
|
|
316
|
+
// Poll for the channel element to appear and become visible. Only run when matchSelector is explicitly configured - the default fallback in
|
|
266
317
|
// resolveMatchSelector() is for strategy-internal use, and guide-based strategies that don't set matchSelector skip this wait entirely. For <img> elements, we
|
|
267
318
|
// also verify load completion (img.complete + naturalWidth) to prevent race conditions where the element exists with the correct src but hasn't finished
|
|
268
319
|
// rendering, which can cause layout instability and click failures.
|
|
269
320
|
if (channelSelection.matchSelector) {
|
|
270
|
-
const selector = resolveMatchSelector(
|
|
321
|
+
const selector = resolveMatchSelector(activeProfile);
|
|
271
322
|
LOG.debug("tuning:tileClick", "Polling for matchSelector: %s (timeout: %sms).", selector, CONFIG.playback.channelSelectorDelay);
|
|
272
323
|
try {
|
|
273
324
|
await page.waitForFunction((sel) => {
|
|
@@ -288,12 +339,12 @@ export async function selectChannel(page, profile) {
|
|
|
288
339
|
LOG.debug("tuning:tileClick", "matchSelector poll succeeded: element found and visible.");
|
|
289
340
|
}
|
|
290
341
|
catch {
|
|
291
|
-
// Timeout
|
|
342
|
+
// Timeout - the element hasn't appeared or loaded yet. Proceed anyway and let the strategy evaluate and report not-found naturally.
|
|
292
343
|
LOG.debug("tuning:tileClick", "matchSelector poll timed out after %sms. Element not found or not visible.", CONFIG.playback.channelSelectorDelay);
|
|
293
344
|
}
|
|
294
345
|
}
|
|
295
|
-
// Dispatch to the appropriate strategy via the registry.
|
|
296
|
-
const result = await entry.execute(page,
|
|
346
|
+
// Dispatch to the appropriate strategy via the registry. The strategy receives activeProfile so it sees the resolved category selector when applicable.
|
|
347
|
+
const result = await entry.execute(page, activeProfile);
|
|
297
348
|
return result;
|
|
298
349
|
}
|
|
299
350
|
//# sourceMappingURL=channelSelection.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channelSelection.js","sourceRoot":"","sources":["../../src/browser/channelSelection.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"channelSelection.js","sourceRoot":"","sources":["../../src/browser/channelSelection.ts"],"names":[],"mappings":"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"}
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Browser, LaunchOptions, Page } from "puppeteer-core";
|
|
2
|
-
import { clearLoginState, isLoginModeActive } from "./login.
|
|
2
|
+
import { clearLoginState, isLoginModeActive } from "./login.ts";
|
|
3
3
|
import { getStream } from "puppeteer-stream";
|
|
4
|
-
import type { Nullable } from "../types/index.
|
|
4
|
+
import type { Nullable } from "../types/index.ts";
|
|
5
5
|
/**
|
|
6
6
|
* Returns true if graceful shutdown is in progress.
|
|
7
7
|
*/
|
|
@@ -11,7 +11,7 @@ export declare function isGracefulShutdown(): boolean;
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function setGracefulShutdown(value: boolean): void;
|
|
13
13
|
export { clearLoginState, isLoginModeActive };
|
|
14
|
-
export { type LoginStatus, endLoginMode, getLoginPage, getLoginStatus, startLoginMode } from "./login.
|
|
14
|
+
export { type LoginStatus, endLoginMode, getLoginPage, getLoginStatus, startLoginMode } from "./login.ts";
|
|
15
15
|
/**
|
|
16
16
|
* Computes the current system status and emits it to SSE subscribers. Called when browser state changes significantly or when streams are added/removed.
|
|
17
17
|
*/
|
|
@@ -43,16 +43,16 @@ export declare function ensureDataDirectory(): Promise<void>;
|
|
|
43
43
|
/**
|
|
44
44
|
* Ensures a clean slate for browser launch by terminating any stale Chrome processes and removing orphaned profile lock files. Chrome locks its profile directory
|
|
45
45
|
* while running, and if a previous instance crashed without releasing the lock, we cannot launch a new browser with the same profile. This function uses the
|
|
46
|
-
* saved Chrome PID to find and terminate the process via process.kill(), then polls for exit using signal 0. This approach is fully cross-platform
|
|
46
|
+
* saved Chrome PID to find and terminate the process via process.kill(), then polls for exit using signal 0. This approach is fully cross-platform - it does not
|
|
47
47
|
* rely on Unix-only tools like pkill or pgrep.
|
|
48
48
|
*
|
|
49
49
|
* The termination strategy escalates from SIGTERM to SIGKILL. SIGTERM is sent first, giving Chrome up to 5 seconds to flush its profile databases (LevelDB,
|
|
50
50
|
* extension state, session storage) and exit cleanly. If Chrome does not exit, SIGKILL is sent as a fallback. This escalation is critical when called from the
|
|
51
|
-
* process exit handler
|
|
51
|
+
* process exit handler - Chrome may be running normally (e.g., after a capture probe timeout), and an immediate SIGKILL would corrupt its profile databases,
|
|
52
52
|
* poisoning the Docker volume for subsequent container restarts.
|
|
53
53
|
*
|
|
54
54
|
* If no PID is available (first run or clean shutdown where the PID file was already removed), process killing is skipped entirely and only lock file cleanup
|
|
55
|
-
* runs. When a PID file does exist but the process is gone (Docker restart with a mounted volume
|
|
55
|
+
* runs. When a PID file does exist but the process is gone (Docker restart with a mounted volume - the PID belongs to the previous container's PID namespace),
|
|
56
56
|
* process.kill() throws ESRCH, which is caught gracefully.
|
|
57
57
|
*
|
|
58
58
|
* This is called at startup before launching the browser and from the process exit handler as a crash recovery fallback. It's safe to call even when no stale
|
|
@@ -133,11 +133,11 @@ export declare function getBrowserPages(): Promise<Page[]>;
|
|
|
133
133
|
*
|
|
134
134
|
* - browserRef.close() sends CDP Browser.close and waits for WebSocket teardown, which hangs 3-5 seconds even after Chrome exits.
|
|
135
135
|
* - browserRef.disconnect() drops the WebSocket instantly but orphans Chrome as a Node child process, creating a zombie that process.kill(pid, 0) cannot detect.
|
|
136
|
-
* - Synchronous polling (Atomics.wait) blocks the event loop, preventing Node from processing SIGCHLD to reap the child
|
|
136
|
+
* - Synchronous polling (Atomics.wait) blocks the event loop, preventing Node from processing SIGCHLD to reap the child - Chrome becomes a zombie regardless
|
|
137
137
|
* of how SIGTERM was sent.
|
|
138
138
|
*
|
|
139
139
|
* Instead, we send SIGTERM through the ChildProcess handle and listen for the `exit` event. This keeps the event loop running so Node can process SIGCHLD and
|
|
140
|
-
* reap Chrome properly. The exit event fires only after the process is fully reaped
|
|
140
|
+
* reap Chrome properly. The exit event fires only after the process is fully reaped - no zombies, no polling, no event loop blocking.
|
|
141
141
|
*/
|
|
142
142
|
export declare function closeBrowser(): Promise<void>;
|
|
143
143
|
/**
|
package/dist/browser/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getGpuCapabilities, setBrowserChrome, setGpuCapabilities, setMaxSupportedViewport } from "./display.js";
|
|
2
|
-
import { LOG, cancellableTimeout, clearPidFile, evaluateWithAbort, formatError, isProcessRunning, readPidFile, startTimer, writePidFile } from "../utils/index.js";
|
|
2
|
+
import { LOG, cancellableTimeout, clearPidFile, delay, evaluateWithAbort, formatError, isProcessRunning, readPidFile, startTimer, writePidFile } from "../utils/index.js";
|
|
3
3
|
import { clearLoginState, isLoginModeActive, setBrowserAccessors } from "./login.js";
|
|
4
4
|
import { getAllStreams, getStreamCount } from "../streaming/registry.js";
|
|
5
5
|
import { getChromeDataDir, getChromePidFilePath, getDataDir, getExtensionDir } from "../config/paths.js";
|
|
@@ -25,7 +25,7 @@ const { promises: fsPromises } = fs;
|
|
|
25
25
|
* - dataDir: The filesystem location for persistent data (Chrome profile, extension files). Resolved via config/paths.ts, which is created on startup if it doesn't
|
|
26
26
|
* exist.
|
|
27
27
|
*
|
|
28
|
-
* Stream tracking and ID generation
|
|
28
|
+
* Stream tracking and ID generation live in streaming/registry.ts for unified stream management across all output types (HLS, MPEG-TS, etc.).
|
|
29
29
|
*/
|
|
30
30
|
// The shared browser instance used by all streaming sessions. Created on first stream request or during warmup. Set to null when the browser is not running or
|
|
31
31
|
// has disconnected.
|
|
@@ -34,7 +34,7 @@ let currentBrowser = null;
|
|
|
34
34
|
// can be cleaned up after a crash or container restart without relying on Unix-only tools like pkill/pgrep.
|
|
35
35
|
let chromePid = null;
|
|
36
36
|
// Tracks whether this process has taken ownership of Chrome cleanup by running killStaleChrome() during startup. The exit handler checks this flag to avoid
|
|
37
|
-
// killing Chrome that belongs to another running PrismCast instance
|
|
37
|
+
// killing Chrome that belongs to another running PrismCast instance - e.g., when a duplicate instance is rejected by the instance guard and exits before
|
|
38
38
|
// killStaleChrome() runs in the startup sequence.
|
|
39
39
|
let ownsChromeCleanup = false;
|
|
40
40
|
// The Chrome version string (e.g., "Chrome/144.0.7559.110") captured when the browser launches. Cleared when the browser disconnects. Used by the
|
|
@@ -217,7 +217,7 @@ function saveChromePid(pid) {
|
|
|
217
217
|
}
|
|
218
218
|
/**
|
|
219
219
|
* Loads the Chrome PID from the module-level variable (fast path) or falls back to reading the PID file on disk (crash recovery path). Returns null if no PID
|
|
220
|
-
* is available
|
|
220
|
+
* is available - either first run or the PID file was already cleaned up.
|
|
221
221
|
* @returns The Chrome process ID, or null if unavailable.
|
|
222
222
|
*/
|
|
223
223
|
function loadChromePid() {
|
|
@@ -241,16 +241,16 @@ function syncSleep(ms) {
|
|
|
241
241
|
/**
|
|
242
242
|
* Ensures a clean slate for browser launch by terminating any stale Chrome processes and removing orphaned profile lock files. Chrome locks its profile directory
|
|
243
243
|
* while running, and if a previous instance crashed without releasing the lock, we cannot launch a new browser with the same profile. This function uses the
|
|
244
|
-
* saved Chrome PID to find and terminate the process via process.kill(), then polls for exit using signal 0. This approach is fully cross-platform
|
|
244
|
+
* saved Chrome PID to find and terminate the process via process.kill(), then polls for exit using signal 0. This approach is fully cross-platform - it does not
|
|
245
245
|
* rely on Unix-only tools like pkill or pgrep.
|
|
246
246
|
*
|
|
247
247
|
* The termination strategy escalates from SIGTERM to SIGKILL. SIGTERM is sent first, giving Chrome up to 5 seconds to flush its profile databases (LevelDB,
|
|
248
248
|
* extension state, session storage) and exit cleanly. If Chrome does not exit, SIGKILL is sent as a fallback. This escalation is critical when called from the
|
|
249
|
-
* process exit handler
|
|
249
|
+
* process exit handler - Chrome may be running normally (e.g., after a capture probe timeout), and an immediate SIGKILL would corrupt its profile databases,
|
|
250
250
|
* poisoning the Docker volume for subsequent container restarts.
|
|
251
251
|
*
|
|
252
252
|
* If no PID is available (first run or clean shutdown where the PID file was already removed), process killing is skipped entirely and only lock file cleanup
|
|
253
|
-
* runs. When a PID file does exist but the process is gone (Docker restart with a mounted volume
|
|
253
|
+
* runs. When a PID file does exist but the process is gone (Docker restart with a mounted volume - the PID belongs to the previous container's PID namespace),
|
|
254
254
|
* process.kill() throws ESRCH, which is caught gracefully.
|
|
255
255
|
*
|
|
256
256
|
* This is called at startup before launching the browser and from the process exit handler as a crash recovery fallback. It's safe to call even when no stale
|
|
@@ -263,7 +263,7 @@ export function killStaleChrome() {
|
|
|
263
263
|
if (pid !== null) {
|
|
264
264
|
try {
|
|
265
265
|
// Send SIGTERM first to give Chrome a chance to flush its profile databases (LevelDB, extension state, session storage) before exiting. This is critical
|
|
266
|
-
// when called from the process exit handler
|
|
266
|
+
// when called from the process exit handler - Chrome may be running normally (e.g., after a capture probe timeout) and SIGKILL would corrupt its profile
|
|
267
267
|
// databases, poisoning the Docker volume for subsequent restarts.
|
|
268
268
|
process.kill(pid, "SIGTERM");
|
|
269
269
|
LOG.debug("browser:lifecycle", "Sent SIGTERM to Chrome process %d.", pid);
|
|
@@ -277,7 +277,7 @@ export function killStaleChrome() {
|
|
|
277
277
|
process.kill(pid, "SIGKILL");
|
|
278
278
|
}
|
|
279
279
|
catch (_error) {
|
|
280
|
-
// ESRCH
|
|
280
|
+
// ESRCH - Chrome exited between the poll check and the kill call.
|
|
281
281
|
}
|
|
282
282
|
const KILL_WAIT_MS = 2000;
|
|
283
283
|
if (!waitForChromeExit(pid, KILL_WAIT_MS, POLL_INTERVAL_MS)) {
|
|
@@ -286,7 +286,7 @@ export function killStaleChrome() {
|
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
catch (error) {
|
|
289
|
-
// ESRCH means the process does not exist
|
|
289
|
+
// ESRCH means the process does not exist - expected when there are no stale processes from a clean shutdown, or in Docker where the PID belongs to a
|
|
290
290
|
// previous container's PID namespace.
|
|
291
291
|
if (error.code !== "ESRCH") {
|
|
292
292
|
LOG.warn("Failed to signal Chrome process %d: %s.", pid, formatError(error));
|
|
@@ -308,7 +308,7 @@ export function canCleanupChrome() {
|
|
|
308
308
|
return ownsChromeCleanup;
|
|
309
309
|
}
|
|
310
310
|
/**
|
|
311
|
-
* Polls until the Chrome process with the given PID has exited, or the timeout expires. Uses process.kill(pid, 0) to check process existence
|
|
311
|
+
* 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
|
|
312
312
|
* when the process is gone. Between polls, sleeps synchronously using Atomics.wait() for cross-platform compatibility.
|
|
313
313
|
* @param pid - The Chrome process ID to wait for.
|
|
314
314
|
* @param timeoutMs - Maximum time to wait in milliseconds.
|
|
@@ -501,14 +501,6 @@ async function launchWithCustomArgs(opts) {
|
|
|
501
501
|
}
|
|
502
502
|
return puppeteerLaunch(opts);
|
|
503
503
|
}
|
|
504
|
-
/**
|
|
505
|
-
* Detects the maximum supported viewport dimensions based on the user's display. This function measures the available screen space and subtracts browser chrome to
|
|
506
|
-
* determine the largest viewport we can use for video capture.
|
|
507
|
-
*
|
|
508
|
-
* The detection uses a temporary page (or existing page if available) to evaluate screen dimensions via JavaScript. The result is cached in the display module for
|
|
509
|
-
* use by the preset system when determining effective viewport.
|
|
510
|
-
* @param browser - The browser instance to use for detection.
|
|
511
|
-
*/
|
|
512
504
|
/**
|
|
513
505
|
* Formats the GPU capabilities into a human-readable suffix for the "Chrome ready" log line. The renderer string is already cleaned (ANGLE wrapper and Metal
|
|
514
506
|
* prefix stripped) at detection time, so this function uses it directly and appends hardware-accelerated codec names in brackets when available.
|
|
@@ -524,13 +516,21 @@ function formatGpuSuffix(gpu) {
|
|
|
524
516
|
if (codecs.length > 0) {
|
|
525
517
|
return " (GPU: " + gpu.renderer + " [" + codecs.join(", ") + "])";
|
|
526
518
|
}
|
|
527
|
-
// No hardware encoding available. A GPU may be present for rendering but lack hardware encoding
|
|
519
|
+
// No hardware encoding available. A GPU may be present for rendering but lack hardware encoding - show the GPU name without codecs if we have a non-trivial
|
|
528
520
|
// renderer string, otherwise label as software rendering.
|
|
529
521
|
if (gpu.renderer && (gpu.renderer !== "unknown")) {
|
|
530
522
|
return " (GPU: " + gpu.renderer + ")";
|
|
531
523
|
}
|
|
532
524
|
return " (software rendering)";
|
|
533
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Detects the maximum supported viewport dimensions based on the user's display. This function measures the available screen space and subtracts browser chrome to
|
|
528
|
+
* determine the largest viewport we can use for video capture.
|
|
529
|
+
*
|
|
530
|
+
* The detection uses a temporary page (or existing page if available) to evaluate screen dimensions via JavaScript. The result is cached in the display module for
|
|
531
|
+
* use by the preset system when determining effective viewport.
|
|
532
|
+
* @param browser - The browser instance to use for detection.
|
|
533
|
+
*/
|
|
534
534
|
async function detectDisplayDimensions(browser) {
|
|
535
535
|
let tempPage = null;
|
|
536
536
|
let usingTempPage = false;
|
|
@@ -565,11 +565,11 @@ async function detectDisplayDimensions(browser) {
|
|
|
565
565
|
if ((dimensions.chromeWidth >= 0) && (dimensions.chromeHeight >= 0)) {
|
|
566
566
|
break;
|
|
567
567
|
}
|
|
568
|
-
// Chrome dimensions are negative
|
|
568
|
+
// Chrome dimensions are negative - the window manager is still transitioning. Wait briefly and remeasure.
|
|
569
569
|
if (attempt < 2) {
|
|
570
570
|
LOG.debug("browser:lifecycle", "Display detection measured negative chrome dimensions (%s\u00d7%s, attempt %s). Retrying after window state settles.", dimensions.chromeWidth, dimensions.chromeHeight, attempt + 1);
|
|
571
571
|
// eslint-disable-next-line no-await-in-loop
|
|
572
|
-
await
|
|
572
|
+
await delay(100);
|
|
573
573
|
}
|
|
574
574
|
}
|
|
575
575
|
// If all attempts produced negative chrome dimensions, skip caching to avoid poisoning window sizing. The preset system will use the configured preset without
|
|
@@ -588,7 +588,7 @@ async function detectDisplayDimensions(browser) {
|
|
|
588
588
|
setBrowserChrome(dimensions.chromeWidth, dimensions.chromeHeight);
|
|
589
589
|
setMaxSupportedViewport(maxWidth, maxHeight);
|
|
590
590
|
LOG.debug("browser:lifecycle", "Display detection complete: screen %s\u00d7%s, chrome %s\u00d7%s, max viewport %s\u00d7%s.", dimensions.availWidth, dimensions.availHeight, dimensions.chromeWidth, dimensions.chromeHeight, maxWidth, maxHeight);
|
|
591
|
-
// Detect GPU capabilities via CDP SystemInfo.getInfo. This is the authoritative source for GPU identity and hardware encoding capabilities
|
|
591
|
+
// Detect GPU capabilities via CDP SystemInfo.getInfo. This is the authoritative source for GPU identity and hardware encoding capabilities - it runs at the
|
|
592
592
|
// browser level (no page context or secure context required) and returns the actual list of hardware-accelerated video encoding profiles.
|
|
593
593
|
try {
|
|
594
594
|
const cdpSession = await browser.target().createCDPSession();
|
|
@@ -596,7 +596,7 @@ async function detectDisplayDimensions(browser) {
|
|
|
596
596
|
const sysInfo = await cdpSession.send("SystemInfo.getInfo");
|
|
597
597
|
// Extract the GPU renderer from the primary device. The WebGL unmasked renderer provides a richer string (includes ANGLE backend info), so we query
|
|
598
598
|
// that as well and prefer it when available.
|
|
599
|
-
const deviceName =
|
|
599
|
+
const deviceName = sysInfo.gpu.devices[0]?.deviceString ?? "unknown";
|
|
600
600
|
// Get the unmasked WebGL renderer for a more descriptive GPU identity string.
|
|
601
601
|
const webglRenderer = await evaluateWithAbort(targetPage, () => {
|
|
602
602
|
const canvas = document.createElement("canvas");
|
|
@@ -610,9 +610,9 @@ async function detectDisplayDimensions(browser) {
|
|
|
610
610
|
// Extract the meaningful GPU name. ANGLE wraps the actual GPU identity: "ANGLE (Vendor, GPU Name, API Version)". The GPU name is the second
|
|
611
611
|
// comma-separated field. For non-ANGLE renderers, use the device string from CDP.
|
|
612
612
|
let renderer = webglRenderer;
|
|
613
|
-
const
|
|
614
|
-
if (
|
|
615
|
-
renderer =
|
|
613
|
+
const anglePart = /^ANGLE \([^,]+, ([^,]+)/.exec(webglRenderer)?.[1];
|
|
614
|
+
if (anglePart) {
|
|
615
|
+
renderer = anglePart.trim();
|
|
616
616
|
// Strip the "ANGLE Metal Renderer: " prefix that macOS adds.
|
|
617
617
|
const metalPrefix = "ANGLE Metal Renderer: ";
|
|
618
618
|
if (renderer.startsWith(metalPrefix)) {
|
|
@@ -623,18 +623,18 @@ async function detectDisplayDimensions(browser) {
|
|
|
623
623
|
renderer = deviceName;
|
|
624
624
|
}
|
|
625
625
|
// Determine hardware encoding capability. Two paths:
|
|
626
|
-
// 1. featureStatus.video_encode === "enabled"
|
|
626
|
+
// 1. featureStatus.video_encode === "enabled" - authoritative Chrome-level flag indicating the platform's hardware encoding framework is active
|
|
627
627
|
// (VideoToolbox on macOS, VA-API on Linux, DXVA on Windows). When enabled, H.264 hardware encoding is always available.
|
|
628
|
-
// 2. videoEncoding profile array
|
|
628
|
+
// 2. videoEncoding profile array - lists specific hardware-accelerated codec profiles (e.g., "H264 Main", "HEVC Main"). Populated on Linux/Windows
|
|
629
629
|
// via VA-API/DXVA but empty on macOS where VideoToolbox doesn't enumerate through this interface.
|
|
630
|
-
const videoEncodeEnabled = sysInfo.gpu.featureStatus?.video_encode === "enabled";
|
|
630
|
+
const videoEncodeEnabled = sysInfo.gpu.featureStatus?.["video_encode"] === "enabled";
|
|
631
631
|
const h264FromProfiles = sysInfo.gpu.videoEncoding.some((e) => e.profile.startsWith("H264"));
|
|
632
632
|
const hevcFromProfiles = sysInfo.gpu.videoEncoding.some((e) => e.profile.startsWith("HEVC"));
|
|
633
633
|
const av1FromProfiles = sysInfo.gpu.videoEncoding.some((e) => e.profile.startsWith("AV1"));
|
|
634
634
|
// H.264 hardware encoding is available when either the feature flag or the profile list confirms it.
|
|
635
635
|
const h264Hardware = videoEncodeEnabled || h264FromProfiles;
|
|
636
636
|
// HEVC and AV1 hardware encoding: check the profile list first (authoritative on Linux/Windows). On macOS (empty profile list), probe via MediaRecorder
|
|
637
|
-
// in the page context
|
|
637
|
+
// in the page context - MediaRecorder.isTypeSupported works in non-secure contexts unlike VideoEncoder.
|
|
638
638
|
let hevcHardware = hevcFromProfiles;
|
|
639
639
|
let av1Hardware = av1FromProfiles;
|
|
640
640
|
if (videoEncodeEnabled && (!hevcHardware || !av1Hardware)) {
|
|
@@ -651,7 +651,7 @@ async function detectDisplayDimensions(browser) {
|
|
|
651
651
|
av1Hardware ||= av1Supported;
|
|
652
652
|
}
|
|
653
653
|
setGpuCapabilities({ av1HardwareEncoding: av1Hardware, h264HardwareEncoding: h264Hardware, hevcHardwareEncoding: hevcHardware, renderer });
|
|
654
|
-
LOG.debug("browser:lifecycle", "GPU detection: device=%s, renderer=%s, H.264=%s, HEVC=%s, AV1=%s, video_encode=%s, encoding profiles=%s.", deviceName, renderer, h264Hardware, hevcHardware, av1Hardware, sysInfo.gpu.featureStatus?.video_encode ?? "unknown", sysInfo.gpu.videoEncoding.map((e) => e.profile).join(", ") || "none");
|
|
654
|
+
LOG.debug("browser:lifecycle", "GPU detection: device=%s, renderer=%s, H.264=%s, HEVC=%s, AV1=%s, video_encode=%s, encoding profiles=%s.", deviceName, renderer, h264Hardware, hevcHardware, av1Hardware, sysInfo.gpu.featureStatus?.["video_encode"] ?? "unknown", sysInfo.gpu.videoEncoding.map((e) => e.profile).join(", ") || "none");
|
|
655
655
|
}
|
|
656
656
|
finally {
|
|
657
657
|
void cdpSession.detach().catch(() => { });
|
|
@@ -785,7 +785,7 @@ async function launchBrowser() {
|
|
|
785
785
|
currentBrowser.on("disconnected", handleBrowserDisconnect);
|
|
786
786
|
LOG.debug("timing:browser", "Chrome process spawned. (+%sms)", browserElapsed());
|
|
787
787
|
// Poll for the puppeteer-stream extension to finish initializing. The extension injects a START_RECORDING function into its options page context. We poll
|
|
788
|
-
// for this function's existence rather than using a fixed delay, so the browser is ready as soon as the extension loads
|
|
788
|
+
// for this function's existence rather than using a fixed delay, so the browser is ready as soon as the extension loads - typically 200-500ms rather than the
|
|
789
789
|
// full configured timeout. Uses getExtensionPage() from puppeteer-stream to locate the extension's options page.
|
|
790
790
|
try {
|
|
791
791
|
const extensionPage = await getExtensionPage(currentBrowser);
|
|
@@ -802,7 +802,7 @@ async function launchBrowser() {
|
|
|
802
802
|
await detectDisplayDimensions(currentBrowser);
|
|
803
803
|
LOG.debug("timing:browser", "Display detection complete. (+%sms)", browserElapsed());
|
|
804
804
|
// Log the Chrome version for diagnostic reference. This helps correlate browser behavior changes (tab unresponsiveness, memory pressure, capture issues)
|
|
805
|
-
// with specific Chrome releases. We also capture the User-Agent string so that server-side fetch() calls to
|
|
805
|
+
// with specific Chrome releases. We also capture the User-Agent string so that server-side fetch() calls to service CDNs can match Chrome's identity.
|
|
806
806
|
const chromeVersion = await currentBrowser.version();
|
|
807
807
|
const userAgent = await currentBrowser.userAgent();
|
|
808
808
|
browserLaunchTime = Date.now();
|
|
@@ -814,7 +814,7 @@ async function launchBrowser() {
|
|
|
814
814
|
LOG.debug("timing:browser", "Browser ready. Total: %sms.", browserElapsed());
|
|
815
815
|
// Emit system status update for SSE subscribers.
|
|
816
816
|
await emitCurrentSystemStatus();
|
|
817
|
-
// Start background precaching of selected
|
|
817
|
+
// Start background precaching of selected service channel lineups. Fire-and-forget - the setTimeout inside startPrecaching() ensures the actual work is fully
|
|
818
818
|
// async and non-blocking.
|
|
819
819
|
startPrecaching();
|
|
820
820
|
}
|
|
@@ -880,7 +880,7 @@ export async function minimizeBrowserWindow() {
|
|
|
880
880
|
}
|
|
881
881
|
// Delegate to resizeAndMinimizeWindow for the actual CDP operations. This ensures consistent resize+minimize behavior and maintains a single source of
|
|
882
882
|
// truth for the viewport sizing logic.
|
|
883
|
-
await resizeAndMinimizeWindow(targetPage
|
|
883
|
+
await resizeAndMinimizeWindow(targetPage);
|
|
884
884
|
// Clean up the temporary page if we created one.
|
|
885
885
|
if (usingTempPage && tempPage) {
|
|
886
886
|
unregisterManagedPage(tempPage);
|
|
@@ -927,11 +927,11 @@ export async function getBrowserPages() {
|
|
|
927
927
|
*
|
|
928
928
|
* - browserRef.close() sends CDP Browser.close and waits for WebSocket teardown, which hangs 3-5 seconds even after Chrome exits.
|
|
929
929
|
* - browserRef.disconnect() drops the WebSocket instantly but orphans Chrome as a Node child process, creating a zombie that process.kill(pid, 0) cannot detect.
|
|
930
|
-
* - Synchronous polling (Atomics.wait) blocks the event loop, preventing Node from processing SIGCHLD to reap the child
|
|
930
|
+
* - Synchronous polling (Atomics.wait) blocks the event loop, preventing Node from processing SIGCHLD to reap the child - Chrome becomes a zombie regardless
|
|
931
931
|
* of how SIGTERM was sent.
|
|
932
932
|
*
|
|
933
933
|
* Instead, we send SIGTERM through the ChildProcess handle and listen for the `exit` event. This keeps the event loop running so Node can process SIGCHLD and
|
|
934
|
-
* reap Chrome properly. The exit event fires only after the process is fully reaped
|
|
934
|
+
* reap Chrome properly. The exit event fires only after the process is fully reaped - no zombies, no polling, no event loop blocking.
|
|
935
935
|
*/
|
|
936
936
|
export async function closeBrowser() {
|
|
937
937
|
// Ensure the flag is set so the disconnect handler knows this is intentional. Normally set earlier by app.ts shutdown(), but set here as a fallback for direct
|
|
@@ -954,9 +954,8 @@ export async function closeBrowser() {
|
|
|
954
954
|
const KILL_WAIT_MS = 2000;
|
|
955
955
|
// Listen for the exit event before sending the signal. The event fires after the OS reaps the process, so there is no zombie window. Resolves to true so
|
|
956
956
|
// Promise.race can distinguish exit from timeout.
|
|
957
|
-
const exitPromise =
|
|
958
|
-
|
|
959
|
-
});
|
|
957
|
+
const { promise: exitPromise, resolve: signalExit } = Promise.withResolvers();
|
|
958
|
+
chromeProcess.on("exit", () => { signalExit(true); });
|
|
960
959
|
chromeProcess.kill("SIGTERM");
|
|
961
960
|
LOG.debug("browser:lifecycle", "Sent SIGTERM to Chrome process %d.", chromeProcess.pid);
|
|
962
961
|
// Wait for Chrome to exit after SIGTERM, with a timeout. If Chrome doesn't exit in time, escalate to SIGKILL.
|
|
@@ -1138,7 +1137,7 @@ function checkBrowserRestart() {
|
|
|
1138
1137
|
// If there are active streams, cancel any pending quiet timer and return. Streams that start during the quiet period reset the countdown.
|
|
1139
1138
|
if (getStreamCount() > 0) {
|
|
1140
1139
|
if (restartQuietTimer) {
|
|
1141
|
-
LOG.debug("browser:lifecycle", "Browser restart quiet period cancelled
|
|
1140
|
+
LOG.debug("browser:lifecycle", "Browser restart quiet period cancelled - streams are active.");
|
|
1142
1141
|
clearTimeout(restartQuietTimer);
|
|
1143
1142
|
restartQuietTimer = null;
|
|
1144
1143
|
}
|
|
@@ -1146,7 +1145,7 @@ function checkBrowserRestart() {
|
|
|
1146
1145
|
}
|
|
1147
1146
|
// No active streams and the browser is old enough. Start the quiet timer if one is not already running.
|
|
1148
1147
|
if (!restartQuietTimer) {
|
|
1149
|
-
LOG.debug("browser:lifecycle", "Browser uptime exceeds threshold. Quiet period started
|
|
1148
|
+
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));
|
|
1150
1149
|
restartQuietTimer = setTimeout(() => {
|
|
1151
1150
|
void executeBrowserRestart();
|
|
1152
1151
|
}, BROWSER_RESTART_QUIET_PERIOD);
|
|
@@ -1162,7 +1161,7 @@ async function executeBrowserRestart() {
|
|
|
1162
1161
|
// 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
|
|
1163
1162
|
// mode was activated, or the browser disconnected on its own).
|
|
1164
1163
|
if (gracefulShutdownInProgress || isLoginModeActive() || (getStreamCount() > 0) || !currentBrowser || !currentBrowser.connected || !browserLaunchTime) {
|
|
1165
|
-
LOG.debug("browser:lifecycle", "Browser restart aborted
|
|
1164
|
+
LOG.debug("browser:lifecycle", "Browser restart aborted - preconditions no longer met.");
|
|
1166
1165
|
return;
|
|
1167
1166
|
}
|
|
1168
1167
|
const age = Date.now() - browserLaunchTime;
|
|
@@ -1172,7 +1171,7 @@ async function executeBrowserRestart() {
|
|
|
1172
1171
|
try {
|
|
1173
1172
|
// closeBrowser() sets gracefulShutdownInProgress = true internally and performs SIGTERM-based Chrome termination.
|
|
1174
1173
|
await closeBrowser();
|
|
1175
|
-
// Reset the flag since the server is NOT shutting down
|
|
1174
|
+
// Reset the flag since the server is NOT shutting down - only the browser is restarting.
|
|
1176
1175
|
setGracefulShutdown(false);
|
|
1177
1176
|
// Launch a fresh browser instance so it is ready for the next stream request.
|
|
1178
1177
|
await getCurrentBrowser();
|