prismcast 1.9.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/dist/app.d.ts +1 -1
- package/dist/app.js +27 -9
- package/dist/app.js.map +1 -1
- package/dist/browser/cdp.js +7 -7
- 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 +42 -43
- 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.js +4 -4
- 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 +61 -62
- package/dist/browser/tuning/directv.js.map +1 -1
- package/dist/browser/tuning/fox.d.ts +1 -1
- package/dist/browser/tuning/fox.js +171 -14
- package/dist/browser/tuning/fox.js.map +1 -1
- package/dist/browser/tuning/hbo.d.ts +1 -1
- package/dist/browser/tuning/hbo.js +6 -6
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.d.ts +1 -1
- package/dist/browser/tuning/hulu.js +117 -110
- 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 +28 -28
- 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 +73 -40
- 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 +4 -3
- package/dist/config/health.js +75 -62
- package/dist/config/health.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +19 -12
- 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 +110 -12
- package/dist/config/persistence.js +351 -32
- package/dist/config/persistence.js.map +1 -1
- 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 +2 -2
- package/dist/config/servicePacks.js +28 -28
- package/dist/config/servicePacks.js.map +1 -1
- package/dist/config/services.d.ts +62 -36
- package/dist/config/services.js +150 -136
- package/dist/config/services.js.map +1 -1
- package/dist/config/sites.d.ts +3 -3
- package/dist/config/sites.js +16 -16
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +500 -41
- package/dist/config/userChannels.js +1201 -312
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +103 -10
- package/dist/config/userConfig.js +277 -117
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +4 -2
- package/dist/config/userProfiles.js +72 -26
- package/dist/config/userProfiles.js.map +1 -1
- package/dist/hdhr/channelMap.js +8 -11
- 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 +6 -10
- 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 +42 -64
- package/dist/native/index.js.map +1 -1
- package/dist/native/probe.d.ts +27 -9
- package/dist/native/probe.js +158 -67
- package/dist/native/probe.js.map +1 -1
- package/dist/native/proxy.d.ts +4 -2
- package/dist/native/proxy.js +52 -49
- 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/auth.js +6 -10
- 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 +1 -1
- 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 +9 -9
- package/dist/routes/config/channels/table.js +124 -76
- 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 +2 -2
- package/dist/routes/config/index.js.map +1 -1
- package/dist/routes/config/services.js +69 -68
- package/dist/routes/config/services.js.map +1 -1
- package/dist/routes/config/settings.d.ts +1 -1
- package/dist/routes/config/settings.js +45 -38
- package/dist/routes/config/settings.js.map +1 -1
- package/dist/routes/debug.js +12 -11
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/icons.js +2 -2
- package/dist/routes/icons.js.map +1 -1
- package/dist/routes/logs.js +19 -30
- package/dist/routes/logs.js.map +1 -1
- package/dist/routes/mpegts.js +1 -1
- package/dist/routes/playlist.d.ts +1 -1
- package/dist/routes/playlist.js +37 -26
- package/dist/routes/playlist.js.map +1 -1
- package/dist/routes/root/content.js +27 -6
- 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 +16 -16
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/config.js +59 -59
- 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 +24 -10
- 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 +86 -488
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/root/styles.js +5 -5
- package/dist/routes/services.js +18 -15
- package/dist/routes/services.js.map +1 -1
- 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/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 +171 -188
- 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 +499 -383
- 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 +3 -3
- package/dist/streaming/hls.js +81 -70
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +3 -3
- package/dist/streaming/hlsResume.js +5 -5
- 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 +80 -54
- 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 +39 -41
- 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/setup.d.ts +9 -8
- package/dist/streaming/setup.js +97 -49
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +9 -3
- package/dist/streaming/showInfo.js +26 -18
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +4 -4
- package/dist/streaming/statusEmitter.js +2 -2
- package/dist/types/channels.d.ts +133 -34
- package/dist/types/channels.js +39 -1
- package/dist/types/channels.js.map +1 -1
- package/dist/types/config.d.ts +20 -1
- 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 +5 -4
- package/dist/types/selection.d.ts +48 -4
- package/dist/types/streaming.d.ts +1 -1
- 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 +1 -1
- 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 +6 -4
- 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 +58 -30
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/format.d.ts +1 -1
- package/dist/utils/format.js +2 -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 +16 -20
- 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 +2 -2
- 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 -3
- 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/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 +14 -27
- package/dist/utils/version.js.map +1 -1
- package/package.json +13 -7
- package/dist/native/intercept.d.ts +0 -35
- package/dist/native/intercept.js +0 -181
- package/dist/native/intercept.js.map +0 -1
- package/dist/routes/config/channels/routes.js +0 -1505
- package/dist/routes/config/channels/routes.js.map +0 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
2
|
*
|
|
3
|
-
* hlsSegments.ts: HLS segment storage functions for PrismCast.
|
|
3
|
+
* hlsSegments.ts: HLS segment storage functions for PrismCast. The wait-for-readiness helpers (waitForPlaylist, waitForInitSegment) route their timeout race
|
|
4
|
+
* through the Clock port (see utils/clock.ts) so tests can deterministically simulate "promise resolves before timeout" vs "timeout fires before promise"
|
|
5
|
+
* without depending on real timers - the race is exactly the pattern Node's synchronous mock.timers.tick cannot drive reliably.
|
|
4
6
|
*/
|
|
5
|
-
import { LOG,
|
|
7
|
+
import { LOG, realClock } from "../utils/index.js";
|
|
6
8
|
import { CONFIG } from "../config/index.js";
|
|
7
9
|
import { getStream } from "./registry.js";
|
|
8
10
|
/* This module provides functions for storing and retrieving HLS segments, playlists, and init segments. All data is stored in the stream registry's HLSState, which is
|
|
@@ -218,7 +220,7 @@ export function updatePlaylist(streamId, content) {
|
|
|
218
220
|
stream.hls.signalPlaylistReady();
|
|
219
221
|
// Cancel the deferred preroll timer now that real content is available. The timer may still be running for native streams where browser setup completed quickly
|
|
220
222
|
// but the proxy's first poll cycle took longer than the preroll delay (PREROLL_DELAY_MS). Without this, the timer would fire after real content is already
|
|
221
|
-
// flowing, uselessly seeding preroll state. For streams where the timer already fired (preroll is active), this is a no-op
|
|
223
|
+
// flowing, uselessly seeding preroll state. For streams where the timer already fired (preroll is active), this is a no-op - the timer handle is already null.
|
|
222
224
|
if (stream.hls.prerollTimer) {
|
|
223
225
|
clearTimeout(stream.hls.prerollTimer);
|
|
224
226
|
stream.hls.prerollTimer = null;
|
|
@@ -240,19 +242,21 @@ export function getPlaylist(streamId) {
|
|
|
240
242
|
* Waits for the first playlist to be available for a stream.
|
|
241
243
|
* @param streamId - The numeric stream ID.
|
|
242
244
|
* @param timeout - Maximum time to wait in milliseconds.
|
|
245
|
+
* @param clock - Clock used for the timeout race. Defaults to realClock; tests inject a fake to drive the race deterministically.
|
|
243
246
|
* @returns True if playlist is ready, false if timeout or stream not found.
|
|
244
247
|
*/
|
|
245
|
-
export async function waitForPlaylist(streamId, timeout) {
|
|
246
|
-
return waitForReady(streamId, async (stream) => stream.hls.playlistReady, timeout);
|
|
248
|
+
export async function waitForPlaylist(streamId, timeout, clock = realClock) {
|
|
249
|
+
return waitForReady(streamId, async (stream) => stream.hls.playlistReady, timeout, clock);
|
|
247
250
|
}
|
|
248
251
|
/**
|
|
249
252
|
* Waits for the first init segment to be available for a stream. Used by MPEG-TS consumers to wait for codec configuration before starting their FFmpeg remuxer.
|
|
250
253
|
* @param streamId - The numeric stream ID.
|
|
251
254
|
* @param timeout - Maximum time to wait in milliseconds.
|
|
255
|
+
* @param clock - Clock used for the timeout race. Defaults to realClock; tests inject a fake to drive the race deterministically.
|
|
252
256
|
* @returns True if init segment is ready, false if timeout or stream not found.
|
|
253
257
|
*/
|
|
254
|
-
export async function waitForInitSegment(streamId, timeout) {
|
|
255
|
-
return waitForReady(streamId, async (stream) => stream.hls.initSegmentReady, timeout);
|
|
258
|
+
export async function waitForInitSegment(streamId, timeout, clock = realClock) {
|
|
259
|
+
return waitForReady(streamId, async (stream) => stream.hls.initSegmentReady, timeout, clock);
|
|
256
260
|
}
|
|
257
261
|
// Internal Helpers.
|
|
258
262
|
/**
|
|
@@ -261,13 +265,14 @@ export async function waitForInitSegment(streamId, timeout) {
|
|
|
261
265
|
* @param streamId - The numeric stream ID.
|
|
262
266
|
* @param getPromise - Accessor that extracts the readiness promise from the stream's HLS state.
|
|
263
267
|
* @param timeout - Maximum time to wait in milliseconds.
|
|
268
|
+
* @param clock - Clock providing the timeout race; the public wrappers thread their own clock through.
|
|
264
269
|
* @returns True if ready before timeout, false otherwise.
|
|
265
270
|
*/
|
|
266
|
-
async function waitForReady(streamId, getPromise, timeout) {
|
|
271
|
+
async function waitForReady(streamId, getPromise, timeout, clock) {
|
|
267
272
|
const stream = getStream(streamId);
|
|
268
273
|
if (!stream) {
|
|
269
274
|
return false;
|
|
270
275
|
}
|
|
271
|
-
return raceWithTimeout(getPromise(stream).then(() => true), timeout).catch(() => false);
|
|
276
|
+
return clock.raceWithTimeout(getPromise(stream).then(() => true), timeout).catch(() => false);
|
|
272
277
|
}
|
|
273
278
|
//# sourceMappingURL=hlsSegments.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hlsSegments.js","sourceRoot":"","sources":["../../src/streaming/hlsSegments.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"hlsSegments.js","sourceRoot":"","sources":["../../src/streaming/hlsSegments.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import type { RecoveryMetrics, TabReplacementResult } from "./recovery.
|
|
1
|
+
import type { RecoveryMetrics, TabReplacementResult } from "./recovery.ts";
|
|
2
2
|
import type { Frame, Page } from "puppeteer-core";
|
|
3
|
-
import type { Nullable, ResolvedSiteProfile } from "../types/index.
|
|
3
|
+
import type { Nullable, ResolvedSiteProfile } from "../types/index.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Stream info passed to the monitor for status updates.
|
|
6
|
+
*/
|
|
7
|
+
export interface MonitorStreamInfo {
|
|
8
|
+
channelName: Nullable<string>;
|
|
9
|
+
numericStreamId: number;
|
|
10
|
+
serviceName: string;
|
|
11
|
+
serviceTag?: string;
|
|
12
|
+
startTime: Date;
|
|
13
|
+
}
|
|
4
14
|
/**
|
|
5
15
|
* Monitors video playback health and attempts escalating recovery when issues are detected. This function runs on an interval, checking video state and triggering
|
|
6
16
|
* increasingly aggressive recovery actions when playback stalls, pauses, or errors occur.
|
|
@@ -25,14 +35,4 @@ import type { Nullable, ResolvedSiteProfile } from "../types/index.js";
|
|
|
25
35
|
* If null/undefined, tab replacement is not available and timeouts will eventually trip the circuit breaker.
|
|
26
36
|
* @returns A cleanup function to stop the monitor.
|
|
27
37
|
*/
|
|
28
|
-
/**
|
|
29
|
-
* Stream info passed to the monitor for status updates.
|
|
30
|
-
*/
|
|
31
|
-
export interface MonitorStreamInfo {
|
|
32
|
-
channelName: Nullable<string>;
|
|
33
|
-
numericStreamId: number;
|
|
34
|
-
serviceName: string;
|
|
35
|
-
serviceTag?: string;
|
|
36
|
-
startTime: Date;
|
|
37
|
-
}
|
|
38
38
|
export declare function monitorPlaybackHealth(page: Page, context: Frame | Page, profile: ResolvedSiteProfile, url: string, streamId: string, streamInfo: MonitorStreamInfo, onCircuitBreak: () => void, onTabReplacement?: () => Promise<Nullable<TabReplacementResult>>): () => RecoveryMetrics;
|
|
@@ -12,6 +12,30 @@ import { getProviderBySlug } from "../browser/channelSelection.js";
|
|
|
12
12
|
import { getShowName } from "./showInfo.js";
|
|
13
13
|
import { refreshNativeManifest } from "../native/index.js";
|
|
14
14
|
import { resizeAndMinimizeWindow } from "../browser/cdp.js";
|
|
15
|
+
/**
|
|
16
|
+
* Monitors video playback health and attempts escalating recovery when issues are detected. This function runs on an interval, checking video state and triggering
|
|
17
|
+
* increasingly aggressive recovery actions when playback stalls, pauses, or errors occur.
|
|
18
|
+
*
|
|
19
|
+
* The monitor includes a circuit breaker that terminates the stream after a configurable number of consecutive failures within a time window. This prevents endless
|
|
20
|
+
* recovery attempts on fundamentally broken streams.
|
|
21
|
+
*
|
|
22
|
+
* Tab replacement recovery: When the browser tab becomes unresponsive (3+ consecutive evaluate timeouts), the monitor triggers tab replacement via the
|
|
23
|
+
* onTabReplacement callback. This closes the hung tab, creates a fresh one with new capture, and continues the stream. This is more reliable than page.goto-based
|
|
24
|
+
* recovery because a hung tab may not respond to navigation commands.
|
|
25
|
+
*
|
|
26
|
+
* The returned cleanup function should be called when the stream ends to stop monitoring and release resources.
|
|
27
|
+
*
|
|
28
|
+
* @param page - The Puppeteer page object.
|
|
29
|
+
* @param context - The frame or page containing the video element.
|
|
30
|
+
* @param profile - The site profile for behavior configuration.
|
|
31
|
+
* @param url - The URL of the stream, needed for page reload recovery.
|
|
32
|
+
* @param streamId - A concise identifier for the stream, used in log messages.
|
|
33
|
+
* @param streamInfo - Stream metadata for status updates.
|
|
34
|
+
* @param onCircuitBreak - Callback function called when circuit breaker trips.
|
|
35
|
+
* @param onTabReplacement - Optional callback for tab replacement recovery. When provided and 3+ consecutive timeouts occur, this is called to replace the hung tab.
|
|
36
|
+
* If null/undefined, tab replacement is not available and timeouts will eventually trip the circuit breaker.
|
|
37
|
+
* @returns A cleanup function to stop the monitor.
|
|
38
|
+
*/
|
|
15
39
|
export function monitorPlaybackHealth(page, context, profile, url, streamId, streamInfo, onCircuitBreak, onTabReplacement) {
|
|
16
40
|
/* Monitor state. These track the video's behavior over time and control recovery decisions. Mutable variables are organized into typed state objects by subsystem
|
|
17
41
|
* (recovery, segments, native health, resolution) to clarify ownership and interaction boundaries. Variables that don't belong to a specific subsystem remain as
|
|
@@ -56,29 +80,31 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
56
80
|
// Last issue tracking for UI display. Stores what triggered recovery and when, so users can see stream history.
|
|
57
81
|
let lastIssueType = null;
|
|
58
82
|
let lastIssueTime = null;
|
|
59
|
-
// Recovery grace
|
|
60
|
-
// action.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
|
|
83
|
+
// Recovery grace periods in milliseconds, indexed by recovery level (L0 = no recovery, L1 = play/unmute, L2 = source reload, L3 = page reload). After a recovery
|
|
84
|
+
// action we wait this long before checking for new issues to give the action time to take effect. L1 is a quick action. L2 and L3 need more time for
|
|
85
|
+
// rebuffering or navigation to complete.
|
|
86
|
+
const recoveryGracePeriods = [0, 3_000, 10_000, 10_000];
|
|
87
|
+
// Segment stall timeout (10 seconds). After L2/L3 recovery completes, if no new segments are produced within this window, the capture pipeline is considered
|
|
88
|
+
// dead and we escalate directly to tab replacement. This catches the case where recovery reports success but the MediaRecorder/FFmpeg pipeline has silently died.
|
|
89
|
+
const SEGMENT_STALL_TIMEOUT = 10_000;
|
|
65
90
|
// Tiny segment detection thresholds. Used for continuous segment size monitoring to detect dead capture pipelines. When video capture dies but audio continues,
|
|
66
91
|
// segments contain only audio data. Audio is transcoded at a controlled bitrate (max 512Kbps), so audio-only segments are at most ~192KB for 3-second segments.
|
|
67
|
-
// The 500KB threshold catches both dead captures (18 bytes) and audio-only captures while staying well below the smallest video preset (480p/3Mbps
|
|
68
|
-
|
|
69
|
-
const
|
|
92
|
+
// The 500KB threshold catches both dead captures (18 bytes) and audio-only captures while staying well below the smallest video preset (480p/3Mbps ~ 750KB/segment).
|
|
93
|
+
// The default count trigger (10) requires roughly 20 seconds of consecutive tiny segments before action is taken, balancing responsiveness against false positives.
|
|
94
|
+
const TINY_SEGMENT_THRESHOLD = 512_000;
|
|
95
|
+
const TINY_SEGMENT_COUNT_TRIGGER = 10;
|
|
70
96
|
// Resolve the service-specific tiny segment count threshold once at monitor startup. Services with extended static content (e.g., Xfinity commercial
|
|
71
97
|
// placeholders) set a higher value to tolerate longer periods of small segments without false positive tab replacements. Dead capture pipelines (segments with
|
|
72
98
|
// no video trafs) always use TINY_SEGMENT_COUNT_TRIGGER regardless of this setting.
|
|
73
99
|
const providerModule = streamInfo.serviceTag ? getProviderBySlug(streamInfo.serviceTag) : undefined;
|
|
74
100
|
const providerTinySegmentThreshold = providerModule?.tinySegmentThreshold ?? TINY_SEGMENT_COUNT_TRIGGER;
|
|
75
101
|
// Segment staleness timeout. When no new segments have been produced for this duration, the capture pipeline is considered dead even though the video element may
|
|
76
|
-
// appear healthy. This catches the case where Chrome's MediaRecorder silently stops emitting data without raising an error
|
|
102
|
+
// appear healthy. This catches the case where Chrome's MediaRecorder silently stops emitting data without raising an error - the input stream stays "open" but no
|
|
77
103
|
// data events fire. The 20-second threshold is 4x the maximum expected moof delivery interval (5 seconds) to avoid false positives during normal bursty delivery.
|
|
78
|
-
const SEGMENT_STALENESS_TIMEOUT =
|
|
104
|
+
const SEGMENT_STALENESS_TIMEOUT = 20_000;
|
|
79
105
|
// Resolution degradation detection. When the video element's intrinsic resolution is significantly below the configured viewport, the service's ABR is delivering
|
|
80
|
-
// low-quality content. The threshold is expressed as a ratio
|
|
81
|
-
// 50% catches clear ABR degradation (
|
|
106
|
+
// low-quality content. The threshold is expressed as a ratio - if either dimension is below this fraction of the viewport, the resolution is considered degraded.
|
|
107
|
+
// 50% catches clear ABR degradation (768x432 on 1080p = 40%) while allowing legitimate 720p content on 1080p (67% > 50%).
|
|
82
108
|
const RESOLUTION_RATIO_THRESHOLD = 0.5;
|
|
83
109
|
// Grace period in milliseconds after stream start and after each recovery action. Gives ABR time to ramp up before flagging degradation.
|
|
84
110
|
const RESOLUTION_GRACE_PERIOD = 30000;
|
|
@@ -95,7 +121,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
95
121
|
// Pre-compute the selector type string for video element selection. This is passed to evaluate() calls.
|
|
96
122
|
const selectorType = buildVideoSelectorType(profile);
|
|
97
123
|
// Capture stream context for re-establishing on each interval tick. AsyncLocalStorage context is lost when entering setInterval callbacks. The show name
|
|
98
|
-
// resolver is lazy
|
|
124
|
+
// resolver is lazy - it reads from the live show name cache at log time, so messages always reflect the current program even as shows change mid-stream.
|
|
99
125
|
const streamContext = {
|
|
100
126
|
channelName: streamInfo.channelName ?? undefined,
|
|
101
127
|
showNameResolver: () => getShowName(streamInfo.numericStreamId),
|
|
@@ -146,12 +172,12 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
146
172
|
* Checks segment delivery health for native streams. Detects stalled streams by comparing the proxy's segment index and last segment timestamp against thresholds.
|
|
147
173
|
* Recovery follows three escalation levels per the plan:
|
|
148
174
|
*
|
|
149
|
-
* - L1: Re-fetch manifest (handled by the proxy's internal retry loop
|
|
175
|
+
* - L1: Re-fetch manifest (handled by the proxy's internal retry loop - consecutive failures up to the threshold)
|
|
150
176
|
* - L2: Reload page for fresh tokens (same mechanism as proactive token refresh, but triggered by segment staleness)
|
|
151
177
|
* - L3: Fall back to capture mode via tab replacement (stops native proxy, creates fresh page with capture pipeline)
|
|
152
178
|
*
|
|
153
179
|
* Note: The `recoveryState.inProgress` guard at the top of the interval callback prevents re-entry during async L2/L3 recovery. The native path does not need its
|
|
154
|
-
* own guard
|
|
180
|
+
* own guard - it reuses the shared flag.
|
|
155
181
|
*
|
|
156
182
|
* @param entry - The stream registry entry for the native stream.
|
|
157
183
|
*/
|
|
@@ -212,7 +238,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
212
238
|
}
|
|
213
239
|
const staleSec = Math.round(stalenessMs / 1000);
|
|
214
240
|
LOG.debug("native:monitor", "Native stream stalled for %s. No new segments in %ss (threshold: %ss).", storeKey, staleSec, Math.round(stalenessThreshold / 1000));
|
|
215
|
-
// L2: At
|
|
241
|
+
// L2: At 4x target duration, attempt a page reload for fresh tokens. This recovers from auth expiry where the manifest URL is still valid but segment URLs
|
|
216
242
|
// are rejected. The proxy continues serving cached segments during the reload.
|
|
217
243
|
if ((stalenessMs > (targetDuration * 4 * 1000)) && (nativeHealthState.recoveryAttempts === 0)) {
|
|
218
244
|
LOG.warn("Native stream stalled for %s. No new segments in %ss. Attempting recovery.", storeKey, staleSec);
|
|
@@ -222,7 +248,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
222
248
|
});
|
|
223
249
|
return;
|
|
224
250
|
}
|
|
225
|
-
// L3: At
|
|
251
|
+
// L3: At 6x target duration (or if L2 was already attempted), fall back to capture mode via tab replacement.
|
|
226
252
|
if ((stalenessMs > (targetDuration * 6 * 1000)) || ((stalenessMs > (targetDuration * 4 * 1000)) && (nativeHealthState.recoveryAttempts > 0))) {
|
|
227
253
|
LOG.warn("Falling back to capture mode for %s: native streaming stalled after recovery attempt.", storeKey);
|
|
228
254
|
void runWithStreamContext(streamContext, async () => {
|
|
@@ -484,7 +510,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
484
510
|
}
|
|
485
511
|
/**
|
|
486
512
|
* Checks the page reload rate limit. Prunes expired timestamps, checks if the limit has been reached, and records the current timestamp if allowed. Callers are
|
|
487
|
-
* responsible for logging and handling the rate-limited case
|
|
513
|
+
* responsible for logging and handling the rate-limited case - consequences differ by context (circuit break, deferral, fallback to L2).
|
|
488
514
|
* @returns True if a page reload is allowed and the timestamp has been recorded, false if the rate limit has been reached.
|
|
489
515
|
*/
|
|
490
516
|
function isPageReloadAllowed() {
|
|
@@ -510,7 +536,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
510
536
|
*/
|
|
511
537
|
function setRecoveryGracePeriod(level) {
|
|
512
538
|
pendingReMinimize = true;
|
|
513
|
-
recoveryState.graceUntil = Date.now() + recoveryGracePeriods[level];
|
|
539
|
+
recoveryState.graceUntil = Date.now() + (recoveryGracePeriods[level] ?? 0);
|
|
514
540
|
}
|
|
515
541
|
/**
|
|
516
542
|
* Handles tab replacement failure by checking the circuit breaker. If the breaker trips, terminates the stream. Returns the appropriate outcome for the caller.
|
|
@@ -520,7 +546,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
520
546
|
function handleTabReplacementFailure(context) {
|
|
521
547
|
const cbResult = checkCircuitBreaker(circuitBreaker, Date.now());
|
|
522
548
|
if (cbResult.shouldTrip) {
|
|
523
|
-
LOG.error("Recovery exhausted (%s)
|
|
549
|
+
LOG.error("Recovery exhausted (%s) - terminating stream.", context);
|
|
524
550
|
clearInterval(interval);
|
|
525
551
|
onCircuitBreak();
|
|
526
552
|
return { outcome: "terminated" };
|
|
@@ -557,13 +583,13 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
557
583
|
// Clear stale recovery metrics so the deferred-success check does not falsely log "Recovered" from leftover state set by recordRecoveryAttempt.
|
|
558
584
|
metrics.currentRecoveryStartTime = null;
|
|
559
585
|
metrics.currentRecoveryMethod = null;
|
|
560
|
-
LOG.warn("Tab replacement unsuccessful after retry
|
|
586
|
+
LOG.warn("Tab replacement unsuccessful after retry - stream will be terminated.");
|
|
561
587
|
const failureOutcome = handleTabReplacementFailure(context);
|
|
562
588
|
// If the circuit breaker did not trip but the old page is gone (handler destroyed it before createPageWithCapture failed), the stream is unrecoverable. The
|
|
563
|
-
// next monitor tick would silently clear the interval via currentPage.isClosed() with no termination log, no status emission, and no cleanup
|
|
589
|
+
// next monitor tick would silently clear the interval via currentPage.isClosed() with no termination log, no status emission, and no cleanup - creating a
|
|
564
590
|
// zombie entry in the registry. Terminate explicitly instead.
|
|
565
591
|
if ((failureOutcome.outcome === "failed") && currentPage.isClosed()) {
|
|
566
|
-
LOG.error("Tab replacement failed and the original page is no longer available
|
|
592
|
+
LOG.error("Tab replacement failed and the original page is no longer available - terminating stream.");
|
|
567
593
|
clearInterval(interval);
|
|
568
594
|
onCircuitBreak();
|
|
569
595
|
return { outcome: "terminated" };
|
|
@@ -595,7 +621,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
595
621
|
recordRecoveryAttempt(metrics, RECOVERY_METHODS.tabReplacement);
|
|
596
622
|
try {
|
|
597
623
|
let result = await onTabReplacement();
|
|
598
|
-
// First attempt failed
|
|
624
|
+
// First attempt failed - retry once. See idempotency notes in the JSDoc above.
|
|
599
625
|
if (!result) {
|
|
600
626
|
LOG.debug("recovery:tab", "Tab replacement attempt 1/2 failed. Retrying...");
|
|
601
627
|
try {
|
|
@@ -612,7 +638,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
612
638
|
return handleExhaustedTabReplacement("tab replacement unsuccessful");
|
|
613
639
|
}
|
|
614
640
|
catch (error) {
|
|
615
|
-
// Unexpected error (not from onTabReplacement
|
|
641
|
+
// Unexpected error (not from onTabReplacement - those are caught internally by the handler in hls.ts and return null). Guard against registry corruption,
|
|
616
642
|
// getStream failures, or other unexpected errors.
|
|
617
643
|
LOG.debug("recovery:tab", "Tab replacement attempt 1/2 failed: %s. Retrying...", formatError(error));
|
|
618
644
|
try {
|
|
@@ -684,7 +710,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
684
710
|
* Handles the case where no video element was found in the current context. Applies a grace period before triggering recovery: first failure waits, second failure
|
|
685
711
|
* attempts frame re-search, third failure escalates to full page navigation. Distinguishes "no video element" from "video exists but not ready" (buffering).
|
|
686
712
|
*
|
|
687
|
-
* Always exits the tick
|
|
713
|
+
* Always exits the tick - no path falls through to subsequent health checks.
|
|
688
714
|
* @param now - Current timestamp for timing calculations.
|
|
689
715
|
*/
|
|
690
716
|
async function handleVideoNotFound(now) {
|
|
@@ -737,11 +763,11 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
737
763
|
return;
|
|
738
764
|
}
|
|
739
765
|
// After 3+ consecutive failures, escalate to full page navigation recovery.
|
|
740
|
-
LOG.warn("Video element not found
|
|
766
|
+
LOG.warn("Video element not found - recovering via %s.", RECOVERY_METHODS.pageNavigation);
|
|
741
767
|
// Check circuit breaker for too many failures.
|
|
742
768
|
const cbResult = checkCircuitBreaker(circuitBreaker, now);
|
|
743
769
|
if (cbResult.shouldTrip) {
|
|
744
|
-
LOG.error("Recovery failed after %s attempts
|
|
770
|
+
LOG.error("Recovery failed after %s attempts - terminating stream.", cbResult.totalCount);
|
|
745
771
|
clearInterval(interval);
|
|
746
772
|
onCircuitBreak();
|
|
747
773
|
return;
|
|
@@ -756,7 +782,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
756
782
|
recordRecoveryAttempt(metrics, RECOVERY_METHODS.pageNavigation);
|
|
757
783
|
// Check page reload limit before attempting recovery.
|
|
758
784
|
if (!isPageReloadAllowed()) {
|
|
759
|
-
LOG.error("Page navigation rate limit reached (%s in %s minutes)
|
|
785
|
+
LOG.error("Page navigation rate limit reached (%s in %s minutes) - cannot recover without video element.", CONFIG.playback.maxPageReloads, Math.round(CONFIG.playback.pageReloadWindow / 60000));
|
|
760
786
|
clearInterval(interval);
|
|
761
787
|
onCircuitBreak();
|
|
762
788
|
return;
|
|
@@ -806,14 +832,14 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
806
832
|
const entry = getStream(streamInfo.numericStreamId);
|
|
807
833
|
const currentIndex = entry?.segmenter?.getSegmentIndex() ?? null;
|
|
808
834
|
if ((currentIndex !== null) && (currentIndex > segmentState.preRecoveryIndex)) {
|
|
809
|
-
// Segments are flowing
|
|
835
|
+
// Segments are flowing - recovery actually succeeded. Clear tracking state.
|
|
810
836
|
segmentState.preRecoveryIndex = null;
|
|
811
837
|
segmentState.waitStartTime = null;
|
|
812
838
|
segmentState.productionStalled = false;
|
|
813
839
|
}
|
|
814
840
|
else if ((now - segmentState.waitStartTime) > SEGMENT_STALL_TIMEOUT) {
|
|
815
841
|
// No new segments for SEGMENT_STALL_TIMEOUT after recovery grace period. The capture pipeline is dead.
|
|
816
|
-
LOG.warn("No segments produced for %ss after recovery
|
|
842
|
+
LOG.warn("No segments produced for %ss after recovery - capture pipeline may have stalled.", SEGMENT_STALL_TIMEOUT / 1000);
|
|
817
843
|
segmentState.productionStalled = true;
|
|
818
844
|
}
|
|
819
845
|
}
|
|
@@ -834,7 +860,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
834
860
|
const effectiveThreshold = (hasVideo === false) ? TINY_SEGMENT_COUNT_TRIGGER : providerTinySegmentThreshold;
|
|
835
861
|
LOG.debug("recovery:tracks", "Below-threshold segment: %d bytes, hasVideo=%s, consecutive=%d, threshold=%d.", segmentSize, String(hasVideo), segmentState.consecutiveTinySegments, effectiveThreshold);
|
|
836
862
|
if (segmentState.consecutiveTinySegments >= effectiveThreshold) {
|
|
837
|
-
LOG.warn("Detected %d consecutive undersized segments (%dKB)
|
|
863
|
+
LOG.warn("Detected %d consecutive undersized segments (%dKB) - capture pipeline may have stalled.", segmentState.consecutiveTinySegments, Math.round(segmentSize / 1024));
|
|
838
864
|
// Trigger tab replacement if available, otherwise let circuit breaker handle it via segmentState.productionStalled.
|
|
839
865
|
if (onTabReplacement && !recoveryState.inProgress) {
|
|
840
866
|
await executeTabReplacement("tiny segments");
|
|
@@ -859,7 +885,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
859
885
|
else if (sizeCheckEntry && (segmentState.lastCheckedIndex > 0) && !withinRecoveryGrace &&
|
|
860
886
|
((now - segmentState.lastSegmentAdvanceTime) > SEGMENT_STALENESS_TIMEOUT)) {
|
|
861
887
|
/* Segment staleness detection. The segment index has not advanced for longer than SEGMENT_STALENESS_TIMEOUT. This catches the case where Chrome's
|
|
862
|
-
* MediaRecorder silently stops emitting data
|
|
888
|
+
* MediaRecorder silently stops emitting data - the input Readable stream stays "open" (no end/error events) but no data events fire. The segmenter
|
|
863
889
|
* receives nothing, produces no new segments, and the playlist freezes at the last known sequence number. The video element on the page continues
|
|
864
890
|
* playing normally (currentTime advances, no errors), so all video health checks pass. Without this check, the stale playlist persists indefinitely.
|
|
865
891
|
*
|
|
@@ -867,7 +893,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
867
893
|
* during stream startup before the first segment has been produced. The recovery grace guard prevents false triggering during legitimate pauses (e.g.,
|
|
868
894
|
* after tab replacement while the new capture pipeline is initializing).
|
|
869
895
|
*/
|
|
870
|
-
LOG.warn("No new segments produced for %ss
|
|
896
|
+
LOG.warn("No new segments produced for %ss - capture pipeline may have stalled.", SEGMENT_STALENESS_TIMEOUT / 1000);
|
|
871
897
|
if (onTabReplacement && !recoveryState.inProgress) {
|
|
872
898
|
await executeTabReplacement("segment staleness");
|
|
873
899
|
return true;
|
|
@@ -912,7 +938,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
912
938
|
LOG.warn("Video resolution has been degraded for %ss (%s\u00d7%s in %s\u00d7%s viewport). Attempting recovery via %s.", String(degradedDuration), String(state.videoWidth), String(state.videoHeight), String(viewport.width), String(viewport.height), RECOVERY_METHODS.pageNavigation);
|
|
913
939
|
recoveryState.inProgress = true;
|
|
914
940
|
if (!isPageReloadAllowed()) {
|
|
915
|
-
LOG.warn("Resolution recovery deferred
|
|
941
|
+
LOG.warn("Resolution recovery deferred - page navigation rate limit reached (%s in %s minutes).", CONFIG.playback.maxPageReloads, Math.round(CONFIG.playback.pageReloadWindow / 60000));
|
|
916
942
|
// Defer by pushing grace end forward to avoid re-triggering every 2 seconds.
|
|
917
943
|
resolutionState.graceEnd = now + RESOLUTION_GRACE_PERIOD;
|
|
918
944
|
recoveryState.inProgress = false;
|
|
@@ -947,7 +973,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
947
973
|
resolutionState.graceEnd = now + RESOLUTION_GRACE_PERIOD;
|
|
948
974
|
return true;
|
|
949
975
|
}
|
|
950
|
-
// Tab replacement not available
|
|
976
|
+
// Tab replacement not available - skip directly to acceptance.
|
|
951
977
|
resolutionState.recoveryAttempt = 2;
|
|
952
978
|
}
|
|
953
979
|
// Acceptance: resolution still degraded after both recovery attempts. Log once and stop retrying.
|
|
@@ -994,12 +1020,12 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
994
1020
|
const elapsedHours = (elapsedMs / 3600000).toFixed(1);
|
|
995
1021
|
LOG.info("Proactive reload after %sh of continuous playback (site limit: %sh). Reloading page to prevent stream cutoff.", elapsedHours, String(profile.maxContinuousPlayback));
|
|
996
1022
|
recoveryState.inProgress = true;
|
|
997
|
-
// Check page reload rate limit. Proactive reload is best-effort maintenance
|
|
1023
|
+
// Check page reload rate limit. Proactive reload is best-effort maintenance - if the reload budget is exhausted from recent error recoveries, we gracefully
|
|
998
1024
|
// yield. If the site eventually cuts the stream, normal error recovery handles it.
|
|
999
1025
|
if (!isPageReloadAllowed()) {
|
|
1000
|
-
LOG.warn("Proactive reload deferred
|
|
1026
|
+
LOG.warn("Proactive reload deferred - page navigation rate limit reached (%s in %s minutes).", CONFIG.playback.maxPageReloads, Math.round(CONFIG.playback.pageReloadWindow / 60000));
|
|
1001
1027
|
// Set a grace period to prevent this deferral from re-triggering every 2 seconds while the rate limit remains in effect. We set recoveryState.graceUntil
|
|
1002
|
-
// directly rather than calling setRecoveryGracePeriod() because no recovery action was performed
|
|
1028
|
+
// directly rather than calling setRecoveryGracePeriod() because no recovery action was performed - the window state is unchanged and pendingReMinimize
|
|
1003
1029
|
// should not be set.
|
|
1004
1030
|
recoveryState.graceUntil = now + recoveryGracePeriods[3];
|
|
1005
1031
|
recoveryState.inProgress = false;
|
|
@@ -1043,7 +1069,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
1043
1069
|
// Segment production stall handling. When segments stopped flowing after L2/L3 recovery, the capture pipeline is dead and normal recovery won't help. Skip the
|
|
1044
1070
|
// escalation ladder and go directly to tab replacement if available.
|
|
1045
1071
|
if (segmentState.productionStalled && onTabReplacement) {
|
|
1046
|
-
LOG.warn("Capture pipeline still stalled
|
|
1072
|
+
LOG.warn("Capture pipeline still stalled - escalating to %s.", RECOVERY_METHODS.tabReplacement);
|
|
1047
1073
|
await executeTabReplacement("capture pipeline stalled");
|
|
1048
1074
|
return true;
|
|
1049
1075
|
}
|
|
@@ -1051,7 +1077,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
1051
1077
|
const cbResult = checkCircuitBreaker(circuitBreaker, now);
|
|
1052
1078
|
if (cbResult.shouldTrip) {
|
|
1053
1079
|
const elapsedSeconds = circuitBreaker.firstFailureTime ? Math.round((now - circuitBreaker.firstFailureTime) / 1000) : 0;
|
|
1054
|
-
LOG.error("Recovery failed after %s attempts in %ss
|
|
1080
|
+
LOG.error("Recovery failed after %s attempts in %ss - terminating stream.", cbResult.totalCount, elapsedSeconds);
|
|
1055
1081
|
clearInterval(interval);
|
|
1056
1082
|
onCircuitBreak();
|
|
1057
1083
|
return true;
|
|
@@ -1084,11 +1110,11 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
1084
1110
|
lastIssueTime = now;
|
|
1085
1111
|
// If a previous recovery was pending (L1 or L2 that didn't result in healthy playback), log that it was unsuccessful before starting the new attempt.
|
|
1086
1112
|
if (metrics.currentRecoveryMethod !== null) {
|
|
1087
|
-
LOG.warn("%s did not resolve the issue
|
|
1113
|
+
LOG.warn("%s did not resolve the issue - escalating to %s.", capitalize(metrics.currentRecoveryMethod), recoveryMethod);
|
|
1088
1114
|
}
|
|
1089
1115
|
else {
|
|
1090
1116
|
const issueDesc = getIssueDescription(issueCategory);
|
|
1091
|
-
LOG.warn("Playback %s
|
|
1117
|
+
LOG.warn("Playback %s - recovering via %s.", issueDesc, recoveryMethod);
|
|
1092
1118
|
}
|
|
1093
1119
|
recordRecoveryAttempt(metrics, recoveryMethod);
|
|
1094
1120
|
// For L2/L3 recovery, record the current segment index so we can verify segments are flowing after recovery completes.
|
|
@@ -1107,21 +1133,21 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
1107
1133
|
recoveryState.sourceReloadAttempted = true;
|
|
1108
1134
|
markStreamDiscontinuity();
|
|
1109
1135
|
}
|
|
1110
|
-
recoveryState.graceUntil = now + recoveryGracePeriods[recoveryState.escalationLevel];
|
|
1136
|
+
recoveryState.graceUntil = now + (recoveryGracePeriods[recoveryState.escalationLevel] ?? 0);
|
|
1111
1137
|
resetResolutionState();
|
|
1112
1138
|
}
|
|
1113
1139
|
else {
|
|
1114
1140
|
// Level 3: Page navigation recovery.
|
|
1115
1141
|
// Safety check: If page navigation has failed twice consecutively, fall back to source reload.
|
|
1116
1142
|
if (consecutiveNavigationFailures >= 2) {
|
|
1117
|
-
LOG.warn("Page navigation has failed %s consecutive times
|
|
1143
|
+
LOG.warn("Page navigation has failed %s consecutive times - falling back to source reload.", consecutiveNavigationFailures);
|
|
1118
1144
|
recoveryState.escalationLevel = 2;
|
|
1119
1145
|
consecutiveNavigationFailures = 0;
|
|
1120
1146
|
recoveryState.sourceReloadAttempted = false;
|
|
1121
1147
|
}
|
|
1122
1148
|
else {
|
|
1123
1149
|
if (!isPageReloadAllowed()) {
|
|
1124
|
-
LOG.warn("Page navigation rate limit reached (%s in %s minutes)
|
|
1150
|
+
LOG.warn("Page navigation rate limit reached (%s in %s minutes) - falling back to source reload.", CONFIG.playback.maxPageReloads, Math.round(CONFIG.playback.pageReloadWindow / 60000));
|
|
1125
1151
|
recoveryState.escalationLevel = 2;
|
|
1126
1152
|
recoveryState.sourceReloadAttempted = false;
|
|
1127
1153
|
}
|
|
@@ -1156,14 +1182,14 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
1156
1182
|
}
|
|
1157
1183
|
/* Main monitoring interval. This runs every MONITOR_INTERVAL milliseconds to check video state and trigger recovery when needed.
|
|
1158
1184
|
*
|
|
1159
|
-
*
|
|
1185
|
+
* Every early return must call emitStatusUpdate() before returning (except when the stream is terminating, e.g., page closed or circuit breaker tripped). This
|
|
1160
1186
|
* ensures SSE clients always have current status data (duration, memory, health) even during recovery, buffering, or video search periods. Without this, the
|
|
1161
1187
|
* streamStatuses map becomes stale and new SSE connections receive outdated snapshots.
|
|
1162
1188
|
*
|
|
1163
|
-
*
|
|
1164
|
-
* closed while the handler creates a new page
|
|
1165
|
-
* updates to stop permanently. The sequence is: (1) intervalCleared for explicit cleanup, (2) recoveryState.inProgress to continue during
|
|
1166
|
-
* for unexpected page termination outside of recovery.
|
|
1189
|
+
* Check ordering: the recoveryState.inProgress check must come before the currentPage.isClosed() check. During tab replacement, the old page is intentionally
|
|
1190
|
+
* closed while the handler creates a new page; if we checked isClosed() first, we would terminate the interval while recovery is still in progress, causing
|
|
1191
|
+
* status updates to stop permanently. The required sequence is: (1) intervalCleared for explicit cleanup, (2) recoveryState.inProgress to continue during
|
|
1192
|
+
* recovery, (3) isClosed() for unexpected page termination outside of recovery.
|
|
1167
1193
|
*/
|
|
1168
1194
|
const interval = setInterval(() => {
|
|
1169
1195
|
// Stop monitoring if cleanup was requested.
|
|
@@ -1298,7 +1324,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
1298
1324
|
await resizeAndMinimizeWindow(currentPage);
|
|
1299
1325
|
}
|
|
1300
1326
|
/* Fullscreen reinforcement. Some streaming sites (notably Hulu) revert the video to a mini-player or PiP layout in response to browser state changes such as
|
|
1301
|
-
* window minimization or visibility events. Because the video continues playing normally in the smaller frame, no existing recovery condition is triggered
|
|
1327
|
+
* window minimization or visibility events. Because the video continues playing normally in the smaller frame, no existing recovery condition is triggered - the
|
|
1302
1328
|
* health monitor sees healthy, progressing playback while the captured frame shows a small video in the corner of the viewport. We verify that the video fills
|
|
1303
1329
|
* the viewport on every healthy tick and re-apply CSS fullscreen styling when it shrinks. The response is graduated: basic CSS first (sufficient for
|
|
1304
1330
|
* well-behaved sites like Hulu), escalating to !important priority only if basic styles don't hold by the next tick. The readyState guard prevents false
|
|
@@ -1411,7 +1437,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
1411
1437
|
lastIssueTime = Date.now();
|
|
1412
1438
|
// After 3 consecutive timeouts, attempt tab replacement if the callback is available.
|
|
1413
1439
|
if ((consecutiveTimeouts >= 3) && onTabReplacement) {
|
|
1414
|
-
LOG.warn("Tab unresponsive
|
|
1440
|
+
LOG.warn("Tab unresponsive - recovering via %s.", RECOVERY_METHODS.tabReplacement);
|
|
1415
1441
|
await executeTabReplacement("tab unresponsive");
|
|
1416
1442
|
return;
|
|
1417
1443
|
}
|