prismcast 1.13.0 → 1.14.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 +88 -6
- package/dist/app.js +287 -154
- package/dist/app.js.map +1 -1
- package/dist/browser/cdp.d.ts +6 -17
- package/dist/browser/cdp.js +6 -57
- package/dist/browser/cdp.js.map +1 -1
- package/dist/browser/index.d.ts +106 -23
- package/dist/browser/index.js +292 -225
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/precaching.d.ts +25 -11
- package/dist/browser/precaching.js +245 -97
- package/dist/browser/precaching.js.map +1 -1
- package/dist/browser/tabCapture.js +3 -10
- package/dist/browser/tabCapture.js.map +1 -1
- package/dist/browser/tuning/comcastPolymer.js +13 -14
- package/dist/browser/tuning/comcastPolymer.js.map +1 -1
- package/dist/browser/tuning/hbo.js +34 -17
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/video.d.ts +1 -1
- package/dist/browser/video.js +3 -3
- package/dist/channels/index.js +5 -5
- package/dist/channels/index.js.map +1 -1
- package/dist/cliHelp.d.ts +11 -0
- package/dist/cliHelp.js +148 -0
- package/dist/cliHelp.js.map +1 -0
- package/dist/config/channelForm.js +5 -4
- package/dist/config/channelForm.js.map +1 -1
- package/dist/config/consistencyProbe.d.ts +2 -4
- package/dist/config/consistencyProbe.js +8 -51
- package/dist/config/consistencyProbe.js.map +1 -1
- package/dist/config/index.d.ts +107 -55
- package/dist/config/index.js +499 -267
- package/dist/config/index.js.map +1 -1
- package/dist/config/paths.d.ts +13 -8
- package/dist/config/paths.js +21 -25
- package/dist/config/paths.js.map +1 -1
- package/dist/config/persistence.d.ts +57 -11
- package/dist/config/persistence.js +127 -44
- package/dist/config/persistence.js.map +1 -1
- package/dist/config/reactivity.d.ts +60 -36
- package/dist/config/reactivity.js +89 -116
- package/dist/config/reactivity.js.map +1 -1
- package/dist/config/services.d.ts +61 -26
- package/dist/config/services.js +137 -68
- package/dist/config/services.js.map +1 -1
- package/dist/config/streamSettings.d.ts +24 -0
- package/dist/config/streamSettings.js +17 -0
- package/dist/config/streamSettings.js.map +1 -0
- package/dist/config/userChannels.d.ts +60 -52
- package/dist/config/userChannels.js +138 -188
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +224 -61
- package/dist/config/userConfig.js +351 -134
- package/dist/config/userConfig.js.map +1 -1
- package/dist/hdhr/discover.d.ts +7 -1
- package/dist/hdhr/discover.js +12 -6
- package/dist/hdhr/discover.js.map +1 -1
- package/dist/hdhr/index.d.ts +51 -10
- package/dist/hdhr/index.js +108 -112
- package/dist/hdhr/index.js.map +1 -1
- package/dist/hdhr/udp.d.ts +11 -4
- package/dist/hdhr/udp.js +25 -20
- package/dist/hdhr/udp.js.map +1 -1
- package/dist/index.js +16 -143
- package/dist/index.js.map +1 -1
- package/dist/native/index.d.ts +1 -1
- package/dist/native/index.js +4 -3
- package/dist/native/index.js.map +1 -1
- package/dist/native/proxy.d.ts +1 -1
- package/dist/native/proxy.js +36 -28
- package/dist/native/proxy.js.map +1 -1
- package/dist/routes/cdp.d.ts +7 -6
- package/dist/routes/cdp.js +9 -8
- package/dist/routes/cdp.js.map +1 -1
- package/dist/routes/components.d.ts +23 -5
- package/dist/routes/components.js +21 -4
- package/dist/routes/components.js.map +1 -1
- package/dist/routes/config/channels/endpoints/browse.js +39 -29
- package/dist/routes/config/channels/endpoints/browse.js.map +1 -1
- package/dist/routes/config/channels/endpoints/bulk.js +2 -6
- package/dist/routes/config/channels/endpoints/bulk.js.map +1 -1
- package/dist/routes/config/channels/endpoints/predefined.js +5 -15
- package/dist/routes/config/channels/endpoints/predefined.js.map +1 -1
- package/dist/routes/config/channels/endpoints/prefs.js +7 -6
- package/dist/routes/config/channels/endpoints/prefs.js.map +1 -1
- package/dist/routes/config/channels/endpoints/service.js +6 -5
- package/dist/routes/config/channels/endpoints/service.js.map +1 -1
- package/dist/routes/config/channels/endpoints/tags.js +4 -16
- package/dist/routes/config/channels/endpoints/tags.js.map +1 -1
- package/dist/routes/config/channels/table.d.ts +2 -1
- package/dist/routes/config/channels/table.js +11 -13
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/http/envelope.d.ts +6 -3
- package/dist/routes/config/http/envelope.js +18 -5
- package/dist/routes/config/http/envelope.js.map +1 -1
- package/dist/routes/config/index.d.ts +21 -13
- package/dist/routes/config/index.js +47 -34
- package/dist/routes/config/index.js.map +1 -1
- package/dist/routes/config/settings.d.ts +43 -6
- package/dist/routes/config/settings.js +225 -125
- package/dist/routes/config/settings.js.map +1 -1
- package/dist/routes/config/vocabulary.d.ts +24 -0
- package/dist/routes/config/vocabulary.js +33 -0
- package/dist/routes/config/vocabulary.js.map +1 -0
- package/dist/routes/debug.js +28 -13
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/root/content.js +33 -26
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/scripts/channels.js +23 -11
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/config.d.ts +1 -1
- package/dist/routes/root/scripts/config.js +127 -79
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/shared.js +34 -23
- package/dist/routes/root/scripts/shared.js.map +1 -1
- package/dist/routes/root/scripts/status.handlers.d.ts +8 -13
- package/dist/routes/root/scripts/status.handlers.js +35 -21
- package/dist/routes/root/scripts/status.handlers.js.map +1 -1
- package/dist/routes/root/styles.js +13 -16
- package/dist/routes/root/styles.js.map +1 -1
- package/dist/routes/services.js +12 -16
- package/dist/routes/services.js.map +1 -1
- package/dist/routes/theme.js +12 -12
- package/dist/routes/theme.js.map +1 -1
- package/dist/routes/ui.d.ts +4 -3
- package/dist/routes/ui.js +17 -13
- package/dist/routes/ui.js.map +1 -1
- package/dist/service/generators.js +12 -9
- package/dist/service/generators.js.map +1 -1
- package/dist/streaming/captureSession.d.ts +4 -4
- package/dist/streaming/captureSession.js +6 -7
- package/dist/streaming/captureSession.js.map +1 -1
- package/dist/streaming/codec.d.ts +12 -2
- package/dist/streaming/codec.js +40 -28
- package/dist/streaming/codec.js.map +1 -1
- package/dist/streaming/fmp4Segmenter.d.ts +14 -4
- package/dist/streaming/fmp4Segmenter.js +99 -79
- package/dist/streaming/fmp4Segmenter.js.map +1 -1
- package/dist/streaming/hls.d.ts +21 -14
- package/dist/streaming/hls.js +148 -122
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +36 -13
- package/dist/streaming/hlsResume.js +68 -37
- package/dist/streaming/hlsResume.js.map +1 -1
- package/dist/streaming/monitor.d.ts +5 -3
- package/dist/streaming/monitor.js +47 -37
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/mp4Parser.d.ts +29 -4
- package/dist/streaming/mp4Parser.js +49 -7
- package/dist/streaming/mp4Parser.js.map +1 -1
- package/dist/streaming/playlistBuilder.d.ts +1 -2
- package/dist/streaming/playlistBuilder.js +2 -3
- package/dist/streaming/playlistBuilder.js.map +1 -1
- package/dist/streaming/preroll.d.ts +43 -16
- package/dist/streaming/preroll.js +112 -62
- package/dist/streaming/preroll.js.map +1 -1
- package/dist/streaming/pretune.d.ts +2 -2
- package/dist/streaming/pretune.js +11 -7
- package/dist/streaming/pretune.js.map +1 -1
- package/dist/streaming/recovery.d.ts +29 -2
- package/dist/streaming/recovery.js +43 -4
- package/dist/streaming/recovery.js.map +1 -1
- package/dist/streaming/registry.d.ts +13 -10
- package/dist/streaming/registry.js +6 -5
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +29 -20
- package/dist/streaming/setup.js +181 -170
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +24 -12
- package/dist/streaming/showInfo.js +120 -113
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +15 -10
- package/dist/streaming/statusEmitter.js +24 -20
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/config.d.ts +24 -13
- package/dist/types/config.js +5 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.js +2 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/profiles.d.ts +2 -2
- package/dist/types/selection.d.ts +12 -10
- package/dist/types/streaming.d.ts +12 -5
- package/dist/types/streaming.js +12 -5
- package/dist/types/streaming.js.map +1 -1
- package/dist/utils/debugFilter.d.ts +3 -2
- package/dist/utils/debugFilter.js +7 -5
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/{hdhr → utils}/deviceId.d.ts +1 -1
- package/dist/{hdhr → utils}/deviceId.js +11 -29
- package/dist/utils/deviceId.js.map +1 -0
- package/dist/utils/errors.d.ts +7 -0
- package/dist/utils/errors.js +12 -3
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/fileLogger.d.ts +23 -12
- package/dist/utils/fileLogger.js +73 -41
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/httpErrors.d.ts +13 -0
- package/dist/utils/httpErrors.js +22 -0
- package/dist/utils/httpErrors.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.d.ts +9 -8
- package/dist/utils/logger.js +64 -20
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/markup.js +8 -3
- package/dist/utils/markup.js.map +1 -1
- package/dist/utils/plainObject.d.ts +6 -0
- package/dist/utils/plainObject.js +18 -0
- package/dist/utils/plainObject.js.map +1 -0
- package/dist/utils/runtimeIdentity.context.js +1 -29
- package/dist/utils/runtimeIdentity.context.js.map +1 -1
- package/dist/utils/runtimeIdentity.d.ts +17 -6
- package/dist/utils/runtimeIdentity.js +50 -17
- package/dist/utils/runtimeIdentity.js.map +1 -1
- package/package.json +5 -5
- package/dist/hdhr/deviceId.js.map +0 -1
package/dist/streaming/setup.js
CHANGED
|
@@ -50,11 +50,11 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
50
50
|
var e = new Error(message);
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
|
-
import { BrowserCaptureImpairedError, BrowserSupersededError, BrowserUnavailableError, acquireCaptureStream, confirmSharedWindowPlacement, emulateCaptureSurface, emulateLayoutSurface, getBrowserInstance, getCaptureImpairment, getCurrentBrowser, installActivationHeal, noteBrowserCaptureImpaired, registerManagedPage, resolveSharedWindowCarrier, setCaptureProbe, syncWindowVisibility, unregisterManagedPage } from "../browser/index.js";
|
|
53
|
+
import { BrowserCaptureImpairedError, BrowserSupersededError, BrowserUnavailableError, CaptureLaunchError, acquireCaptureStream, confirmSharedWindowPlacement, emulateCaptureSurface, emulateLayoutSurface, getBrowserInstance, getCaptureImpairment, getCurrentBrowser, installActivationHeal, noteBrowserCaptureImpaired, reaffirmCaptureSurface, registerManagedPage, resolveSharedWindowCarrier, setCaptureProbe, syncWindowVisibility, unregisterManagedPage } from "../browser/index.js";
|
|
54
54
|
import { CAPTURE_SOURCE_UNAVAILABLE_MESSAGE, isChannelSelectionProfile } from "../types/index.js";
|
|
55
55
|
import { CaptureAbandonedError, CaptureTurnTimeoutError, createCaptureLock } from "./captureLock.js";
|
|
56
56
|
import { FINALIZE_SETTLE_DELAY, installManifestInterceptor } from "../browser/manifestInterceptor.js";
|
|
57
|
-
import { LOG, chromeFetch,
|
|
57
|
+
import { LOG, chromeFetch, extractDomain, formatError, getStreamContext, maxRetryDuration, registerAbortController, resolveFFmpegPath, retryOperation, runWithStreamContext, spawnFFmpeg, startTimer, timeoutSignal, toError, waitWithTimeout } from "../utils/index.js";
|
|
58
58
|
import { getAuthDomainForChannel, getServiceDisplayName, resolveServiceKey } from "../config/services.js";
|
|
59
59
|
import { getDomainConfig, getProfileForChannel, getProfileForUrl, resolveProfile } from "../config/profiles.js";
|
|
60
60
|
import { getProviderByStrategy, invalidateDirectUrl, resolveDirectUrl } from "../browser/channelSelection.js";
|
|
@@ -65,17 +65,15 @@ import { getBuiltinProfile } from "../config/sites.js";
|
|
|
65
65
|
import { getCachedEncryption } from "../native/probe.js";
|
|
66
66
|
import { getCaptureMimeType } from "./codec.js";
|
|
67
67
|
import { getDomainAuthState } from "../config/health.js";
|
|
68
|
-
import { getNextStreamId } from "./registry.js";
|
|
69
68
|
import { getUserProfiles } from "../config/userProfiles.js";
|
|
70
69
|
import { isCaptureInfrastructureError } from "./recovery.js";
|
|
71
70
|
import { monitorPlaybackHealth } from "./monitor.js";
|
|
72
71
|
import { mutateChannels } from "../config/userChannels.js";
|
|
73
72
|
import { openSharedWindowTab } from "../browser/tabSelection.js";
|
|
74
73
|
import { pipeline } from "node:stream/promises";
|
|
75
|
-
import { reaffirmCaptureSurface } from "../browser/cdp.js";
|
|
76
74
|
import { startOverlayHandling } from "../browser/consent.js";
|
|
77
75
|
import { systemClock } from "homebridge-plugin-utils";
|
|
78
|
-
/* This module contains the common stream setup logic for HLS streaming.
|
|
76
|
+
/* This module contains the common stream setup logic for HLS streaming. At its core are createPageWithCapture() and setupStream():
|
|
79
77
|
*
|
|
80
78
|
* 1. createPageWithCapture(): Creates a browser page, starts media capture, navigates to the URL, and sets up video playback. This is the reusable core that both
|
|
81
79
|
* initial stream setup and tab replacement recovery use.
|
|
@@ -83,12 +81,16 @@ import { systemClock } from "homebridge-plugin-utils";
|
|
|
83
81
|
* 2. setupStream(): Orchestrates stream creation by calling createPageWithCapture(), then starting the health monitor. This is the
|
|
84
82
|
* entry point for new stream requests.
|
|
85
83
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
84
|
+
* Beside them, the module holds the shared channel playback establishment (establishChannelPlayback), native manifest re-establishment
|
|
85
|
+
* (reestablishChannelManifest), the capture-system verification (verifyCaptureSystem), and the passive mid-life capture detector.
|
|
86
|
+
*
|
|
87
|
+
* The separation allows tab replacement recovery, built in hls.ts and invoked by the monitor, to reuse the capture setup logic without duplicating code. When a
|
|
88
|
+
* browser tab becomes unresponsive, the recovery handler calls createPageWithCapture() to establish a fresh page while the existing capture keeps serving, then
|
|
89
|
+
* swaps to the new page under the same stream ID.
|
|
88
90
|
*
|
|
89
91
|
* createPageWithCapture() handles:
|
|
90
92
|
* - Browser page creation with CSP bypass
|
|
91
|
-
* - Media stream initialization (
|
|
93
|
+
* - Media stream initialization (a Matroska capture that FFmpeg remuxes to fMP4)
|
|
92
94
|
* - Navigation with retry
|
|
93
95
|
* - Video element detection and playback setup
|
|
94
96
|
*
|
|
@@ -97,8 +99,6 @@ import { systemClock } from "homebridge-plugin-utils";
|
|
|
97
99
|
* - Health monitor startup
|
|
98
100
|
* - Cleanup function creation
|
|
99
101
|
*/
|
|
100
|
-
// Native fMP4 capture uses MP4/AAC for direct HLS segmentation without transcoding.
|
|
101
|
-
const NATIVE_FMP4_MIME_TYPE = "video/mp4;codecs=avc1,mp4a.40.2";
|
|
102
102
|
// Capture initialization is serialized through a task-scoped lock. Chrome's tabCapture extension can only initialize one capture at a time - a second start while
|
|
103
103
|
// one is in flight fails with "Cannot capture a tab with an active stream" - so every acquisition runs as a task on this one process-wide lock, which holds the
|
|
104
104
|
// turn until the task's promise settles. Once a capture is established it runs concurrently with other captures without issue.
|
|
@@ -129,9 +129,9 @@ const PROBE_TEARDOWN_ALLOWANCE_MS = STOP_RECORDING_CEILING_MS + PAGE_CLOSE_MARGI
|
|
|
129
129
|
// that retry while still preventing a pathological hang if multiple internal timeouts chain sequentially. Consumed by both the phase-2 race and the interception
|
|
130
130
|
// budget below.
|
|
131
131
|
const PLAYBACK_INIT_TIMEOUT = 45000;
|
|
132
|
-
// Margin folded into the interception budget beyond the phases it explicitly sizes: the small span between phase-2 finishing and finalize firing (the
|
|
133
|
-
//
|
|
134
|
-
// latency bound - no healthy path waits on it - so its generosity costs nothing.
|
|
132
|
+
// Margin folded into the interception budget beyond the phases it explicitly sizes: the small span between phase-2 finishing and finalize firing (the
|
|
133
|
+
// capture-surface re-affirmation and the window-visibility sync, setupStream's pre-verification work) plus true slack. The interception window is a leak bound for
|
|
134
|
+
// a tune that dies without unwinding, not a latency bound - no healthy path waits on it - so its generosity costs nothing.
|
|
135
135
|
const INTERCEPTION_BUDGET_MARGIN_MS = 5000;
|
|
136
136
|
// The caller-visible capture-timeout messages, each defined once so the lock's deadline errors and any assertion test read the exact text. Both match
|
|
137
137
|
// isCaptureInfrastructureError via its "timed out" substring, so they are exported for the classification assertion that locks that contract.
|
|
@@ -294,8 +294,8 @@ export function validateStreamUrl(url) {
|
|
|
294
294
|
// Page and Capture Creation.
|
|
295
295
|
/**
|
|
296
296
|
* Disposes a browser page acquired during stream setup: unregisters it from managed-page tracking and closes it if still open. The close is fire-and-forget with a
|
|
297
|
-
* debug log on error,
|
|
298
|
-
* setupStream, and by the setup-result cleanup closure, so every setup-phase page teardown flows through one definition.
|
|
297
|
+
* debug log on error, because a page that fails to close during setup cleanup has no caller left to report to. Used as the DisposableStack disposer for pages in
|
|
298
|
+
* createPageWithCapture and setupStream, and by the setup-result cleanup closure, so every setup-phase page teardown flows through one definition.
|
|
299
299
|
* @param page - The page to dispose.
|
|
300
300
|
*/
|
|
301
301
|
function disposePage(page) {
|
|
@@ -307,18 +307,18 @@ function disposePage(page) {
|
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
const defaultCreatePageWithCaptureDeps = { acquireCaptureStream, awaitCaptureVerdict: noteClassifiedCaptureFailure,
|
|
310
|
-
emulateCaptureSurface, getCurrentBrowser, installActivationHeal, openSharedWindowTab, reaffirmCaptureSurface,
|
|
311
|
-
syncWindowVisibility };
|
|
310
|
+
emulateCaptureSurface, getCurrentBrowser, installActivationHeal, openSharedWindowTab, reaffirmCaptureSurface, resolveFFmpegPath, spawnFFmpeg,
|
|
311
|
+
startOverlayHandling, syncWindowVisibility };
|
|
312
312
|
/* The window-topology answers the open primitive needs and cannot reach for itself: tabSelection.ts speaks to the capture extension alone, so the CDP-side carrier
|
|
313
|
-
* resolution and placement confirmation arrive from here, where both modules are already in view. One record, referenced by
|
|
314
|
-
*
|
|
313
|
+
* resolution and placement confirmation arrive from here, where both modules are already in view. One record, referenced by every call site, because every call
|
|
314
|
+
* site wants the identical pair.
|
|
315
315
|
*/
|
|
316
316
|
const SHARED_WINDOW_TOPOLOGY = { confirmPlacement: confirmSharedWindowPlacement, resolveCarrier: resolveSharedWindowCarrier };
|
|
317
317
|
/**
|
|
318
318
|
* Creates a browser page with media capture and navigates to the URL. This is the reusable core function used by both initial stream setup and tab replacement
|
|
319
319
|
* recovery. It handles:
|
|
320
320
|
* - Creating a new browser page with CSP bypass
|
|
321
|
-
* - Initializing media capture (
|
|
321
|
+
* - Initializing media capture (a Matroska capture that FFmpeg remuxes to fMP4)
|
|
322
322
|
* - Navigating to the URL with retry
|
|
323
323
|
* - Setting up video playback via navigateToPage() + initializePlayback()
|
|
324
324
|
* - Re-entering itself once, on a fresh page through a freshly acquired browser, when Chrome refuses the capture start and the mid-life probe's verdict says a
|
|
@@ -328,19 +328,22 @@ const SHARED_WINDOW_TOPOLOGY = { confirmPlacement: confirmSharedWindowPlacement,
|
|
|
328
328
|
* - Creating the segmenter and attaching it to the capture session via captureSession.attachSegmenter()
|
|
329
329
|
* - Registering/updating the stream in the registry
|
|
330
330
|
* - Starting/updating the health monitor
|
|
331
|
-
*
|
|
331
|
+
*
|
|
332
|
+
* On failure the function disposes everything it acquired, so the caller owns only what a successful return hands over.
|
|
332
333
|
*
|
|
333
334
|
* @param options - Options for page and capture creation.
|
|
334
|
-
* @param deps - The injected browser
|
|
335
|
-
*
|
|
336
|
-
* @returns The page, context, and capture session (which owns the raw capture stream and
|
|
337
|
-
* @throws
|
|
335
|
+
* @param deps - The injected browser-boundary collaborators (see CreatePageWithCaptureDeps); defaults to defaultCreatePageWithCaptureDeps, so a test can drive this
|
|
336
|
+
* function without a live Chrome.
|
|
337
|
+
* @returns The page, context, and capture session (which owns the raw capture stream and its FFmpeg child).
|
|
338
|
+
* @throws DirectUrlEstablishmentError when a resolved direct watch URL failed on evidence against it (the hint is already evicted); the browser supervisor's own
|
|
339
|
+
* refusal errors from the browser accessor unchanged; a plain Error when Chrome crashed during capture initialization more times than the retry cap allows;
|
|
340
|
+
* otherwise the page-creation, capture-initialization, or navigation failure as it was raised.
|
|
338
341
|
*/
|
|
339
342
|
export async function createPageWithCapture(options, deps = defaultCreatePageWithCaptureDeps) {
|
|
340
343
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
341
344
|
try {
|
|
342
345
|
const captureElapsed = startTimer();
|
|
343
|
-
const { comment, onFFmpegError, profile, streamId, url } = options;
|
|
346
|
+
const { comment, onFFmpegError, profile, settings, streamId, url } = options;
|
|
344
347
|
/* Bring the window on screen before anything else. Capture reads the compositor's output for the shared window, and that output is only composed correctly for a
|
|
345
348
|
* window the desktop is presenting, so the sync has to land ahead of capture acquisition rather than alongside it. The pending registry entry is already in
|
|
346
349
|
* capture mode by the time any caller reaches here, on both request paths, so the policy reads capture-active and this resolves to a window on screen.
|
|
@@ -349,7 +352,7 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
|
|
|
349
352
|
// Acquire every resource on a DisposableStack so that any throw - in capture initialization, navigation, or playback setup - disposes them structurally as the
|
|
350
353
|
// function unwinds, in last-acquired-first order (capture session before page, so the capture stream is destroyed and STOP_RECORDING fires while the browser is
|
|
351
354
|
// still connected, before the page closes). On success we move() the stack to disarm it and transfer ownership to the caller. This centralizes teardown that
|
|
352
|
-
// would otherwise be repeated in each failure path, and
|
|
355
|
+
// would otherwise be repeated in each failure path, and keeps the manifest interceptor inside the same teardown on every exit.
|
|
353
356
|
const resources = __addDisposableResource(env_1, new DisposableStack(), false);
|
|
354
357
|
/* Create browser page. The establishment goes on to start a capture, so a browser that can no longer start one refuses here, before a page exists. The page is
|
|
355
358
|
* opened as a tab of the shared window rather than wherever Chrome would place it, because a capture tab rooted in a discovery window keeps that window alive
|
|
@@ -373,40 +376,38 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
|
|
|
373
376
|
// Install the tab-activation heal alongside it, before capture is acquired, so the page is enrolled and carries its listener from its first document onward:
|
|
374
377
|
// whichever way this tab becomes the selected one, the capture's composition moves back to the emulated surface about a second later.
|
|
375
378
|
await deps.installActivationHeal(page);
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
const useFFmpeg = CONFIG.streaming.captureMode === "ffmpeg";
|
|
379
|
-
const captureMimeType = useFFmpeg ? getCaptureMimeType() : NATIVE_FMP4_MIME_TYPE;
|
|
379
|
+
// The capture's Matroska MIME type carries the codec decision, which is the codec module's: it weighs the user's allowlist against the GPU's hardware encoders.
|
|
380
|
+
const captureMimeType = getCaptureMimeType();
|
|
380
381
|
// Resolve the FFmpeg binary path up front, before the raw capture stream is acquired below. resolveFFmpegPath is a memoized resolver that can sticky-reject; if its
|
|
381
382
|
// await sat between acquiring the raw capture stream and wrapping it in a CaptureSession, a rejection would strand the stream undestroyed (STOP_RECORDING never
|
|
382
383
|
// fires, leaving chrome.tabCapture active). Resolving it here keeps any rejection on a path with no capture resource yet acquired, so the CaptureSession remains
|
|
383
|
-
// the single owner from the instant the stream exists. Falls back to "ffmpeg" so spawn() defers to PATH lookup
|
|
384
|
-
const ffmpegBin =
|
|
385
|
-
// The capture-pipeline composite, assigned once the raw capture stream and
|
|
384
|
+
// the single owner from the instant the stream exists. Falls back to "ffmpeg" so spawn() defers to PATH lookup.
|
|
385
|
+
const ffmpegBin = (await deps.resolveFFmpegPath()) ?? "ffmpeg";
|
|
386
|
+
// The capture-pipeline composite, assigned once the raw capture stream and its FFmpeg child exist and registered on the DisposableStack the moment it is built.
|
|
386
387
|
let captureSession;
|
|
387
388
|
// Initialize media stream capture. The whole capture-init phase runs inside one try so a browser crash detected at turn grant drives the closed-page recursion,
|
|
388
389
|
// while every other failure unwinds through the DisposableStack.
|
|
389
390
|
try {
|
|
390
391
|
const streamOptions = {
|
|
391
392
|
audio: true,
|
|
392
|
-
audioBitsPerSecond:
|
|
393
|
+
audioBitsPerSecond: settings.audioBitsPerSecond,
|
|
393
394
|
mimeType: captureMimeType,
|
|
394
395
|
video: true,
|
|
395
|
-
videoBitsPerSecond:
|
|
396
|
+
videoBitsPerSecond: settings.videoBitsPerSecond,
|
|
396
397
|
/* The dimension bounds hold the track to exactly the surface emulateCaptureSurface declared on this page, rather than to a second read of the configured
|
|
397
398
|
* preset, so a preset saved mid-establishment cannot leave the encoder constrained to dimensions the page was never emulated at.
|
|
398
399
|
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
* tabCapture API level before encoding matters, so a representative-but-minimal constraint set suffices there.
|
|
400
|
+
* Both frame-rate bounds hold the stream's frame rate, so the track is held to exactly the rate the stream started with, on its first capture and on every
|
|
401
|
+
* tab replacement alike. The configuration load keeps that rate inside the range the setting's metadata declares, 60 being the live-TV ceiling and 30 the
|
|
402
|
+
* floor below which motion judders. The readiness probe (attemptCaptureProbe) instead holds both bounds to a flat 30 because its acquisition fails or
|
|
403
|
+
* succeeds at the tabCapture API level before encoding matters, so a representative-but-minimal constraint set suffices there.
|
|
403
404
|
*/
|
|
404
405
|
videoConstraints: {
|
|
405
406
|
mandatory: {
|
|
406
|
-
maxFrameRate:
|
|
407
|
+
maxFrameRate: settings.frameRate,
|
|
407
408
|
maxHeight: surface.height,
|
|
408
409
|
maxWidth: surface.width,
|
|
409
|
-
minFrameRate:
|
|
410
|
+
minFrameRate: settings.frameRate,
|
|
410
411
|
minHeight: surface.height,
|
|
411
412
|
minWidth: surface.width
|
|
412
413
|
}
|
|
@@ -444,61 +445,57 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
|
|
|
444
445
|
},
|
|
445
446
|
turnWaitMs: CONFIG.streaming.navigationTimeout
|
|
446
447
|
});
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
else {
|
|
475
|
-
LOG.error("FFmpeg stdout pipe error: %s.", errorMessage);
|
|
476
|
-
ffmpeg.kill();
|
|
477
|
-
if (onFFmpegError) {
|
|
478
|
-
onFFmpegError(error);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
// Pipe the Matroska capture stream to FFmpeg's stdin using pipeline() for proper cleanup. When FFmpeg is killed during tab replacement, pipeline() automatically
|
|
483
|
-
// destroys the source stream, preventing "write after end" errors that would occur with .pipe().
|
|
484
|
-
pipeline(stream, ffmpeg.stdin).catch((error) => {
|
|
485
|
-
const errorMessage = formatError(error);
|
|
486
|
-
// EPIPE, "write after end", and "Premature close" errors are expected during cleanup when FFmpeg is killed or the capture stream is destroyed, and a
|
|
487
|
-
// pipeline whose FFmpeg is being torn down produces whatever it produces.
|
|
488
|
-
if (errorMessage.includes("EPIPE") || errorMessage.includes("write after end") || errorMessage.includes("Premature close") || isTornDown()) {
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
// Unexpected pipeline errors require cleanup.
|
|
492
|
-
LOG.error("Capture pipeline error: %s.", errorMessage);
|
|
448
|
+
// Spawn FFmpeg to remux the Matroska capture to fMP4: it copies the captured video and transcodes the Opus audio to AAC. The binary path was resolved up front
|
|
449
|
+
// (above) so no throwable await sits between acquiring the raw capture stream and wrapping it in the CaptureSession that owns it. The spawn and pipeline wiring
|
|
450
|
+
// below are synchronous, so the stream is owned the instant it exists.
|
|
451
|
+
const ffmpeg = deps.spawnFFmpeg(ffmpegBin, settings.audioBitsPerSecond, (error) => {
|
|
452
|
+
LOG.error("FFmpeg process error: %s.", formatError(error));
|
|
453
|
+
if (onFFmpegError) {
|
|
454
|
+
onFFmpegError(error);
|
|
455
|
+
}
|
|
456
|
+
}, streamId, comment);
|
|
457
|
+
/* A disposed pipeline must never fire its error callback. The listeners below sit on the process's own streams rather than inside the spawn wrapper, so
|
|
458
|
+
* they are the places a post-kill teardown event can still reach a caller, and each gates on the same teardown flag the wrapper's own handlers use.
|
|
459
|
+
*
|
|
460
|
+
* The gate is what keeps a tab replacement's discard silent. A replacement disposes the outgoing pipeline in the same synchronous frame it installs the
|
|
461
|
+
* incoming one, so a stray event from the killed FFmpeg lands a tick later - against a registry that already holds a healthy new pipeline - and this
|
|
462
|
+
* closure's callback would break the circuit on the very stream the replacement just saved. String filters cannot cover that: they name the errors
|
|
463
|
+
* teardown is known to produce, and the point here is every error a killed pipeline produces.
|
|
464
|
+
*/
|
|
465
|
+
const isTornDown = () => ffmpeg.isShuttingDown();
|
|
466
|
+
// Handle pipe errors on stdout. Stdin errors are handled by pipeline() below.
|
|
467
|
+
ffmpeg.stdout.on("error", (error) => {
|
|
468
|
+
const errorMessage = formatError(error);
|
|
469
|
+
if (errorMessage.includes("EPIPE") || isTornDown()) {
|
|
470
|
+
LOG.debug("streaming:ffmpeg", "FFmpeg stdout pipe closed: %s.", errorMessage);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
LOG.error("FFmpeg stdout pipe error: %s.", errorMessage);
|
|
493
474
|
ffmpeg.kill();
|
|
494
475
|
if (onFFmpegError) {
|
|
495
|
-
onFFmpegError(error
|
|
476
|
+
onFFmpegError(error);
|
|
496
477
|
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
// Pipe the Matroska capture stream to FFmpeg's stdin using pipeline() for proper cleanup. When FFmpeg is killed during tab replacement, pipeline() automatically
|
|
481
|
+
// destroys the source stream, preventing "write after end" errors that would occur with .pipe().
|
|
482
|
+
pipeline(stream, ffmpeg.stdin).catch((error) => {
|
|
483
|
+
const errorMessage = formatError(error);
|
|
484
|
+
// EPIPE, "write after end", and "Premature close" errors are expected during cleanup when FFmpeg is killed or the capture stream is destroyed, and a
|
|
485
|
+
// pipeline whose FFmpeg is being torn down produces whatever it produces.
|
|
486
|
+
if (errorMessage.includes("EPIPE") || errorMessage.includes("write after end") || errorMessage.includes("Premature close") || isTornDown()) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
// Unexpected pipeline errors require cleanup.
|
|
490
|
+
LOG.error("Capture pipeline error: %s.", errorMessage);
|
|
491
|
+
ffmpeg.kill();
|
|
492
|
+
if (onFFmpegError) {
|
|
493
|
+
onFFmpegError(toError(error));
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
// Wrap the raw capture stream and its FFmpeg child as one self-disposing pipeline unit, and register it for structural teardown. The session feeds the segmenter
|
|
497
|
+
// from FFmpeg's fMP4 stdout; the caller attaches the segmenter once it is created.
|
|
498
|
+
captureSession = createCaptureSession({ ffmpegProcess: ffmpeg, rawCaptureStream: stream });
|
|
502
499
|
resources.use(captureSession);
|
|
503
500
|
}
|
|
504
501
|
catch (error) {
|
|
@@ -540,7 +537,7 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
|
|
|
540
537
|
}
|
|
541
538
|
// Every other rejection - a caller-deadline CaptureDeadlineError, a refusal the one retry has already been spent on, or any other capture-init failure - just
|
|
542
539
|
// unwinds. The verdict still gets the failure, because a browser that cannot start captures is worth marking whoever asked; nothing waits on it here.
|
|
543
|
-
// Resource teardown (page,
|
|
540
|
+
// Resource teardown (the page, and the capture session once built) is handled by the DisposableStack as this throw unwinds the function scope.
|
|
544
541
|
void deps.awaitCaptureVerdict(error);
|
|
545
542
|
throw error;
|
|
546
543
|
}
|
|
@@ -583,8 +580,8 @@ export async function createPageWithCapture(options, deps = defaultCreatePageWit
|
|
|
583
580
|
*/
|
|
584
581
|
const tuneResult = await establishChannelPlayback(page, profile, manifestInterception, {
|
|
585
582
|
initOptions: { persistResolution: options.persistResolution, requestedUrl: navigationUrl, skipChannelSelection: usedDirectUrl },
|
|
586
|
-
// Navigate with retry. The
|
|
587
|
-
//
|
|
583
|
+
// Navigate with retry. The configured navigationTimeout bounds each page load, and retryOperation reloads the page on genuine navigation failures. The
|
|
584
|
+
// retry ladder is this path's own policy, kept separate from channel selection so its timeout does not race the internal click retry loops
|
|
588
585
|
// in channel selection strategies (guideGrid can take 15-20 seconds for binary search + click retries).
|
|
589
586
|
navigate: async () => {
|
|
590
587
|
await retryOperation({
|
|
@@ -771,7 +768,8 @@ const defaultEstablishChannelPlaybackDeps = { initializePlayback };
|
|
|
771
768
|
* only per-path policy differs.
|
|
772
769
|
* @param page - The page being established.
|
|
773
770
|
* @param profile - The resolved site profile whose strategy drives navigation and channel selection.
|
|
774
|
-
* @param handle - The interceptor observing this establishment, or null when nothing is observing it (a tab replacement,
|
|
771
|
+
* @param handle - The interceptor observing this establishment, or null when nothing is observing it (a tab replacement, a skipped interception for a
|
|
772
|
+
* capture-locked or DRM-cached channel, or an install that failed).
|
|
775
773
|
* @param options - The caller's navigation policy, its initialization options, and an optional settlement hook. See EstablishChannelPlaybackOptions.
|
|
776
774
|
* @param deps - Injected browser-boundary collaborator; defaults to the real playback initializer in production.
|
|
777
775
|
* @returns The result playback initialization produced: the video context for subsequent monitoring, and the strategy's direct-tune flag where it resolved one.
|
|
@@ -820,20 +818,17 @@ export async function adjudicateChannelSelection(handle, profile, directTune) {
|
|
|
820
818
|
*
|
|
821
819
|
* @param options - Stream configuration options.
|
|
822
820
|
* @param onCircuitBreak - Callback invoked when the circuit breaker trips (stream unrecoverable).
|
|
823
|
-
* @param deps - The injected browser
|
|
824
|
-
* defaultCreatePageWithCaptureDeps. Threaded for the same reason createPageWithCapture takes them: a test drives
|
|
825
|
-
*
|
|
821
|
+
* @param deps - The injected browser-boundary collaborators (see CreatePageWithCaptureDeps), forwarded to every establishment attempt this function makes and
|
|
822
|
+
* read directly by its cleanup closure; defaults to defaultCreatePageWithCaptureDeps. Threaded for the same reason createPageWithCapture takes them: a test drives
|
|
823
|
+
* the establishment sequence - including its guide fallback - without a live Chrome.
|
|
826
824
|
* @returns Setup result with capture session, cleanup function, and metadata.
|
|
827
825
|
* @throws StreamSetupError if setup fails with appropriate status code and message.
|
|
828
826
|
*/
|
|
829
827
|
export async function setupStream(options, onCircuitBreak, deps = defaultCreatePageWithCaptureDeps) {
|
|
830
|
-
const { channel, channelName, channelSelector, clickSelector, clickToPlay, onTabReplacementFactory, probeIdentity, profileOverride, staticCapture, url } = options;
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
const
|
|
834
|
-
const numericStreamId = options.numericStreamId ?? getNextStreamId();
|
|
835
|
-
// Stream setup is a composition point - it builds the capture lock, the health monitor, and the page - so it reads the system clock at its own boundary.
|
|
836
|
-
const startTime = systemClock.now();
|
|
828
|
+
const { channel, channelName, channelSelector, clickSelector, clickToPlay, numericStreamId, onTabReplacementFactory, probeIdentity, profileOverride, settings, staticCapture, streamId, url } = options;
|
|
829
|
+
// The stream's start instant is its registry entry's, taken at registration, so the health monitor's status and the tune-time line report the one instant the
|
|
830
|
+
// registry holds.
|
|
831
|
+
const { startTime } = options;
|
|
837
832
|
// Create and register the AbortController for this stream. This allows pending evaluate calls to be cancelled immediately when the stream is terminated.
|
|
838
833
|
const abortController = new AbortController();
|
|
839
834
|
registerAbortController(streamId, abortController);
|
|
@@ -916,8 +911,8 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
|
|
|
916
911
|
try {
|
|
917
912
|
/* Skip CDP manifest interception when the channel is locked to screen capture, or when the probe cache already knows this stream's binding resolves to
|
|
918
913
|
* DRM. The per-channel override short-circuits first, so a forced channel never installs the interceptor: nothing intercepts a manifest, no native
|
|
919
|
-
* attempt runs, no probe fires, and the encryption cache stays untouched by that stream. The cache half avoids creating a CDP
|
|
920
|
-
*
|
|
914
|
+
* attempt runs, no probe fires, and the encryption cache stays untouched by that stream. The cache half avoids creating a CDP observer that can produce
|
|
915
|
+
* nothing a DRM stream could use; every stream carries an identity, ad-hoc URLs included, so that lookup needs no guard.
|
|
921
916
|
*/
|
|
922
917
|
const skipInterception = (channel?.forceCapture === true) || (getCachedEncryption(probeIdentity, systemClock.now()) === "drm");
|
|
923
918
|
// Build the persistResolution closure for the active channel. When the resolution layer in selectChannel() converts a category selector to a concrete call
|
|
@@ -932,6 +927,7 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
|
|
|
932
927
|
onFFmpegError: onCircuitBreak,
|
|
933
928
|
persistResolution,
|
|
934
929
|
profile,
|
|
930
|
+
settings,
|
|
935
931
|
skipManifestInterception: skipInterception,
|
|
936
932
|
streamId,
|
|
937
933
|
url
|
|
@@ -977,7 +973,8 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
|
|
|
977
973
|
throw new StreamSetupError("Browser temporarily unavailable.", 503, "The browser can no longer start captures and will relaunch as soon as nothing is " +
|
|
978
974
|
"using it. Please retry shortly.", { cause: error });
|
|
979
975
|
}
|
|
980
|
-
// createPageWithCapture
|
|
976
|
+
// createPageWithCapture disposes everything it acquired on failure (the capture session with its FFmpeg child, the interceptor, and the page), so nothing is
|
|
977
|
+
// left here to clean up.
|
|
981
978
|
const errorMessage = formatError(error);
|
|
982
979
|
const lowerMessage = errorMessage.toLowerCase();
|
|
983
980
|
const benignPatterns = ["abort", "session closed"];
|
|
@@ -985,14 +982,16 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
|
|
|
985
982
|
if (!isBenign) {
|
|
986
983
|
LOG.error("Stream setup failed for %s: %s.", url, errorMessage);
|
|
987
984
|
}
|
|
988
|
-
/* Capture infrastructure errors should return 503 to signal Channels DVR to back off. These include
|
|
989
|
-
* timeouts, and stream initialization failures. Using 503 with Retry-After prevents retry storms when
|
|
990
|
-
*
|
|
985
|
+
/* Capture infrastructure errors should return 503 to signal Channels DVR to back off. These include a browser whose capture system failed a launch gate,
|
|
986
|
+
* Chrome capture state issues, capture-lock turn-wait timeouts, and stream initialization failures. Using 503 with Retry-After prevents retry storms when
|
|
987
|
+
* there's a systemic issue. A launch-gate failure is answered by its type, a CaptureLaunchError, before the classifier reads any message, so the gate's
|
|
988
|
+
* messages are free to state what happened. isCaptureInfrastructureError (recovery.ts) is the single source of truth for every other failure, shared with the
|
|
989
|
+
* browser supervisor's readiness detection.
|
|
991
990
|
*
|
|
992
991
|
* The read here is purely for the client-facing status. The side effect that judgment also drives - handing the failure to the passive mid-life capture
|
|
993
992
|
* detector - fires inside createPageWithCapture's own catches, where every caller of the acquisition reaches it rather than only this one.
|
|
994
993
|
*/
|
|
995
|
-
const isCaptureError = isCaptureInfrastructureError(errorMessage);
|
|
994
|
+
const isCaptureError = (error instanceof CaptureLaunchError) || isCaptureInfrastructureError(errorMessage);
|
|
996
995
|
// A failed tune on a service currently marked needs-sign-in most likely failed AT the auth wall, so the user-facing message leads with the remedy.
|
|
997
996
|
throw new StreamSetupError("Stream error.", isCaptureError ? 503 : 500, withSignInGuidance("Failed to start stream.", channelName, serviceName), { cause: error });
|
|
998
997
|
}
|
|
@@ -1011,10 +1010,10 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
|
|
|
1011
1010
|
// Tune verification. The shared adjudication stage finalizes the manifest interceptor and confirms the captured master manifest URL belongs to the channel
|
|
1012
1011
|
// that was just tuned. This step makes setupStream "verified by construction" - every consumer of StreamSetupResult (HLS preroll, HLS blocking, MPEG-TS,
|
|
1013
1012
|
// native proxy, capture mode) receives a stream guaranteed to be on the requested channel without having to opt in or coordinate. Streams with no manifest
|
|
1014
|
-
// interception at all (DRM-cached
|
|
1015
|
-
// the remaining ones it can speak to. A failure reason throws StreamSetupError so the existing failure path
|
|
1016
|
-
// registry entry, and surfaces a clear error - never silently delivers the wrong channel. The scope guard disposes
|
|
1017
|
-
// as the throw unwinds.
|
|
1013
|
+
// interception at all (a channel locked to screen capture, a DRM-cached binding, or an install that failed) have nothing to adjudicate and skip the step
|
|
1014
|
+
// entirely; verifyManifestSelection owns which of the remaining ones it can speak to. A failure reason throws StreamSetupError so the existing failure path
|
|
1015
|
+
// marks channel health, terminates the pending registry entry, and surfaces a clear error - never silently delivers the wrong channel. The scope guard disposes
|
|
1016
|
+
// the capture session, interceptor, and page as the throw unwinds.
|
|
1018
1017
|
if (manifestInterception) {
|
|
1019
1018
|
const verifyError = await adjudicateChannelSelection(manifestInterception, profile, directTune);
|
|
1020
1019
|
if (verifyError) {
|
|
@@ -1022,12 +1021,13 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
|
|
|
1022
1021
|
throw new StreamSetupError("Tune verification failed: " + verifyError, 502, withSignInGuidance("Tune verification failed for " + failureLabel + ". " + verifyError, channelName, serviceName));
|
|
1023
1022
|
}
|
|
1024
1023
|
}
|
|
1025
|
-
// Monitor stream info for status updates. The serviceTag enables service-specific monitoring flags (e.g.,
|
|
1024
|
+
// Monitor stream info for status updates. The serviceTag enables service-specific monitoring flags (e.g., tinySegmentEvidenceSeconds).
|
|
1026
1025
|
const monitorStreamInfo = {
|
|
1027
1026
|
channelName: channel?.name ?? null,
|
|
1028
1027
|
numericStreamId,
|
|
1029
1028
|
serviceName,
|
|
1030
1029
|
serviceTag: getDomainConfig(url)?.serviceTag,
|
|
1030
|
+
settings,
|
|
1031
1031
|
startTime
|
|
1032
1032
|
};
|
|
1033
1033
|
// Start the health monitor for this stream.
|
|
@@ -1049,7 +1049,7 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
|
|
|
1049
1049
|
await deps.syncWindowVisibility();
|
|
1050
1050
|
};
|
|
1051
1051
|
// Success: transfer ownership out of the scope guard. The cleanup closure and, once the session is installed on the registry entry, terminateStream become
|
|
1052
|
-
// responsible for disposing the page and capture session; the interceptor continues into
|
|
1052
|
+
// responsible for disposing the page and capture session; the interceptor, already finalized and verified above, continues into the native upgrade.
|
|
1053
1053
|
owned.move();
|
|
1054
1054
|
// Return the setup result.
|
|
1055
1055
|
return {
|
|
@@ -1065,7 +1065,6 @@ export async function setupStream(options, onCircuitBreak, deps = defaultCreateP
|
|
|
1065
1065
|
profile,
|
|
1066
1066
|
profileName,
|
|
1067
1067
|
serviceName,
|
|
1068
|
-
startTime,
|
|
1069
1068
|
streamId,
|
|
1070
1069
|
url
|
|
1071
1070
|
};
|
|
@@ -1181,15 +1180,17 @@ export async function reestablishChannelManifest(options) {
|
|
|
1181
1180
|
* times with a delay between attempts, giving the system time to settle before giving up. At boot this prevents a rapid restart storm where the service manager
|
|
1182
1181
|
* relaunches PrismCast repeatedly, each attempt orphaning a Chrome process; at relaunch it provides in-launch settling before the supervisor counts a launch failure.
|
|
1183
1182
|
* @param browser - The Chrome instance to verify (the local instance being launched, passed in rather than re-acquired to avoid re-entering the launch in flight).
|
|
1183
|
+
* @param clock - The clock the wait between attempts and each attempt's own timing run on. Defaults to the system clock.
|
|
1184
|
+
* @throws CaptureLaunchError when every attempt failed.
|
|
1184
1185
|
*/
|
|
1185
|
-
export async function verifyCaptureSystem(browser) {
|
|
1186
|
+
export async function verifyCaptureSystem(browser, clock = systemClock) {
|
|
1186
1187
|
const PROBE_MAX_ATTEMPTS = 3;
|
|
1187
1188
|
const PROBE_RETRY_DELAY = 5000;
|
|
1188
1189
|
for (let attempt = 1; attempt <= PROBE_MAX_ATTEMPTS; attempt++) {
|
|
1189
1190
|
// The launch gate runs its capture probe OFF the capture lock, deliberately: it fires pre-publish, when supervisor.current() is null, so a wedged gate task would
|
|
1190
1191
|
// have no recovery target and would jam the shared lock. GATE mode keeps the acquisition bounded internally instead.
|
|
1191
1192
|
// eslint-disable-next-line no-await-in-loop -- Sequential retries are intentional; each probe must complete before deciding whether to retry.
|
|
1192
|
-
const result = await attemptCaptureProbe(browser, { boundMs: CAPTURE_PROBE_TIMEOUT_MS, kind: "gate" });
|
|
1193
|
+
const result = await attemptCaptureProbe(browser, { boundMs: CAPTURE_PROBE_TIMEOUT_MS, kind: "gate" }, clock);
|
|
1193
1194
|
// Probe succeeded.
|
|
1194
1195
|
if (result === null) {
|
|
1195
1196
|
return;
|
|
@@ -1198,57 +1199,62 @@ export async function verifyCaptureSystem(browser) {
|
|
|
1198
1199
|
if (attempt < PROBE_MAX_ATTEMPTS) {
|
|
1199
1200
|
LOG.warn("Capture probe attempt %d of %d failed: %s. Retrying in %ds.", attempt, PROBE_MAX_ATTEMPTS, result, PROBE_RETRY_DELAY / 1000);
|
|
1200
1201
|
// eslint-disable-next-line no-await-in-loop -- Deliberate delay between sequential retry attempts.
|
|
1201
|
-
await delay(PROBE_RETRY_DELAY);
|
|
1202
|
+
await clock.delay(PROBE_RETRY_DELAY);
|
|
1202
1203
|
}
|
|
1203
1204
|
else {
|
|
1204
1205
|
/* On Windows the probe's own error rarely names the cause, and two conditions account for nearly every failure there: a virtualization layer sitting between
|
|
1205
1206
|
* Chrome and the display, and a profile that will not load an unpacked extension. Naming both in the thrown message saves the user a support round trip.
|
|
1206
|
-
* The message is unchanged on every other platform, and the hint
|
|
1207
|
-
*
|
|
1207
|
+
* The message is unchanged on every other platform, and the hint follows the probe's own text so the cause the probe named still leads. The failure's
|
|
1208
|
+
* status comes from its type rather than from any wording here, so the hint is free to read as plain advice.
|
|
1208
1209
|
*/
|
|
1209
1210
|
const windowsHint = (process.platform === "win32") ? " On Windows, Hyper-V or WSL can interfere with Chrome's capture pipeline, and Chrome refuses to " +
|
|
1210
1211
|
"load the capture extension unless the profile has extension developer mode enabled." : "";
|
|
1211
1212
|
// The probe's message is a diagnostic fragment that may or may not end in a period, so we terminate it before appending a sentence of our own.
|
|
1212
1213
|
const detail = ((windowsHint.length > 0) && !result.endsWith(".")) ? (result + ".") : result;
|
|
1213
|
-
throw new
|
|
1214
|
+
throw new CaptureLaunchError("Capture system verification failed after " + String(PROBE_MAX_ATTEMPTS) + " attempts: " + detail + windowsHint);
|
|
1214
1215
|
}
|
|
1215
1216
|
}
|
|
1216
1217
|
}
|
|
1217
1218
|
/**
|
|
1218
|
-
* Executes a single capture probe attempt. Creates a temporary page on the given browser, tries to start a capture stream, and tears everything down cleanly.
|
|
1219
|
-
*
|
|
1220
|
-
* the acquisition is
|
|
1219
|
+
* Executes a single capture probe attempt. Creates a temporary page on the given browser, tries to start a capture stream, and tears everything down cleanly. A
|
|
1220
|
+
* failed attempt never throws in any mode: it returns null on success or an error-message string on failure, so callers branch on the string. Opening the page
|
|
1221
|
+
* and emulating its layout surface run inside the same try as the acquisition, so a rejection from either is reported the same way, releasing the page when one
|
|
1222
|
+
* was opened, and the gate's retry loop goes on to its next attempt. The modes differ only in how the acquisition is bounded - see CaptureProbeMode.
|
|
1221
1223
|
* @param browser - The Chrome instance to probe.
|
|
1222
1224
|
* @param mode - The operating mode: gate (internal acquisition race) or midlife (self-timed acquisition on the lock).
|
|
1223
1225
|
* @param clock - Clock used for the mid-life self-timing and the teardown confirmation. Defaults to the system clock.
|
|
1224
|
-
* @returns Null on success, or an error message string on
|
|
1226
|
+
* @returns Null on success, or an error message string on a failed attempt.
|
|
1225
1227
|
*/
|
|
1226
1228
|
async function attemptCaptureProbe(browser, mode, clock = systemClock) {
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
* gains nothing from queuing behind the selection executor to prove it.
|
|
1231
|
-
*/
|
|
1232
|
-
const page = (mode.kind === "midlife") ? await openSharedWindowTab(browser, { deps: SHARED_WINDOW_TOPOLOGY }) : await browser.newPage({ background: true });
|
|
1233
|
-
registerManagedPage(page);
|
|
1234
|
-
// The probe page carries the preset-sized layout at the display's density, the surface a capture page starts from before its own declaration, so the probe's
|
|
1235
|
-
// acquisition keeps the shape the field validated.
|
|
1236
|
-
const surface = await emulateLayoutSurface(page);
|
|
1237
|
-
// Tears the probe page down cleanly: retire the raw capture stream (destroy plus the stop confirmation) while the browser is still connected, unregister the
|
|
1238
|
-
// managed page, then close it. Shared by every success and self-timed-failure path in both modes.
|
|
1239
|
-
const teardown = async (stream) => {
|
|
1240
|
-
await retireRawStream(stream, clock);
|
|
1241
|
-
unregisterManagedPage(page);
|
|
1242
|
-
if (!page.isClosed()) {
|
|
1243
|
-
await page.close();
|
|
1244
|
-
}
|
|
1245
|
-
};
|
|
1229
|
+
// The page the attempt opened, held outside the try so the catch can release it. It stays null until the open resolves, so an open that rejects leaves the catch
|
|
1230
|
+
// no page to close.
|
|
1231
|
+
let openedPage = null;
|
|
1246
1232
|
try {
|
|
1233
|
+
/* The mid-life probe runs against a browser that is serving streams, so its page is opened as a tab of the shared window for the reason a stream's page is: a
|
|
1234
|
+
* probe tab rooted in a discovery window would hold that window open for as long as the probe took. The launch gate keeps the plain create: it runs
|
|
1235
|
+
* pre-publish, when the browser has exactly one window and no discovery window can exist yet, so its tab has no wrong window to land in - and a gate that has
|
|
1236
|
+
* to stay bounded gains nothing from queuing behind the selection executor to prove it.
|
|
1237
|
+
*/
|
|
1238
|
+
const page = (mode.kind === "midlife") ? await openSharedWindowTab(browser, { deps: SHARED_WINDOW_TOPOLOGY }) : await browser.newPage({ background: true });
|
|
1239
|
+
openedPage = page;
|
|
1240
|
+
registerManagedPage(page);
|
|
1241
|
+
// The probe page carries the preset-sized layout at the display's density, the surface a capture page starts from before its own declaration, so the probe's
|
|
1242
|
+
// acquisition keeps the shape the field validated.
|
|
1243
|
+
const surface = await emulateLayoutSurface(page);
|
|
1244
|
+
// Tears the probe page down cleanly: retire the raw capture stream (destroy plus the stop confirmation) while the browser is still connected, unregister the
|
|
1245
|
+
// managed page, then close it. The launch gate, which races its acquisition internally, calls it on success; the mid-life probe, the only one that self-times
|
|
1246
|
+
// its acquisition, calls it before judging that latency, so the page is torn down whether the acquisition lands inside the bound or past it.
|
|
1247
|
+
const teardown = async (stream) => {
|
|
1248
|
+
await retireRawStream(stream, clock);
|
|
1249
|
+
unregisterManagedPage(page);
|
|
1250
|
+
if (!page.isClosed()) {
|
|
1251
|
+
await page.close();
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1247
1254
|
// Use the same capture MIME type and surface as the runtime. The stale state error occurs at the tabCapture API level before encoding matters, so matching
|
|
1248
1255
|
// those runtime constraints ensures the probe exercises a representative acquisition. The constraints are held to the dimensions the declaration above
|
|
1249
1256
|
// returned, so the probe holds its track to the surface the page actually carries rather than to a second read of the preset.
|
|
1250
|
-
const
|
|
1251
|
-
const captureMimeType = useFFmpeg ? getCaptureMimeType() : NATIVE_FMP4_MIME_TYPE;
|
|
1257
|
+
const captureMimeType = getCaptureMimeType();
|
|
1252
1258
|
const streamOptions = {
|
|
1253
1259
|
audio: true,
|
|
1254
1260
|
mimeType: captureMimeType,
|
|
@@ -1295,8 +1301,9 @@ async function attemptCaptureProbe(browser, mode, clock = systemClock) {
|
|
|
1295
1301
|
const stream = await acquireCaptureStream(page, streamOptions);
|
|
1296
1302
|
const elapsed = clock.now() - startedAt;
|
|
1297
1303
|
await teardown(stream);
|
|
1298
|
-
// Report failure when the caller abandoned this probe at the lock's outer deadline (unobservable in practice - the lock already rejected the caller - but it
|
|
1299
|
-
// the
|
|
1304
|
+
// Report failure when the caller abandoned this probe at the lock's outer deadline (unobservable in practice - the lock already rejected the caller - but it
|
|
1305
|
+
// reports the abandonment as a returned string, the way every failed capture here is reported, and prevents stranding a capture), or when the acquisition's
|
|
1306
|
+
// own latency exceeded the criterion bound.
|
|
1300
1307
|
if (mode.signal.aborted || (elapsed > mode.boundMs)) {
|
|
1301
1308
|
return CAPTURE_PROBE_TIMEOUT_MESSAGE;
|
|
1302
1309
|
}
|
|
@@ -1305,10 +1312,12 @@ async function attemptCaptureProbe(browser, mode, clock = systemClock) {
|
|
|
1305
1312
|
}
|
|
1306
1313
|
catch (error) {
|
|
1307
1314
|
const errorMessage = formatError(error);
|
|
1308
|
-
// Clean up the test page.
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1315
|
+
// Clean up the test page, when the attempt got as far as opening one.
|
|
1316
|
+
if (openedPage) {
|
|
1317
|
+
unregisterManagedPage(openedPage);
|
|
1318
|
+
if (!openedPage.isClosed()) {
|
|
1319
|
+
openedPage.close().catch(() => { });
|
|
1320
|
+
}
|
|
1312
1321
|
}
|
|
1313
1322
|
return errorMessage;
|
|
1314
1323
|
}
|
|
@@ -1342,8 +1351,8 @@ let captureVerdictInFlight = null;
|
|
|
1342
1351
|
* Classifies a throw from the probe's run on the capture lock into a verdict or the absence of one. A turn-wait timeout means the probe never got to run: with
|
|
1343
1352
|
* other streams tuning at the same moment it queued behind their legitimate acquisitions, and a lock that is busy says something about load, not about the browser
|
|
1344
1353
|
* - a holder that is genuinely hung marks the browser through its own wedge. Every other value, the outer deadline above all, means the probe held its turn and its
|
|
1345
|
-
* own acquisition did not settle, which is exactly the failure the mark exists for. Pure and total, so the distinction the detector rests on
|
|
1346
|
-
* browser.
|
|
1354
|
+
* own acquisition did not settle, which is exactly the failure the mark exists for. Pure and total, so a test can assert the distinction the detector rests on
|
|
1355
|
+
* without a browser.
|
|
1347
1356
|
* @param error - The value the lock run threw.
|
|
1348
1357
|
* @returns The outcome the detector acts on.
|
|
1349
1358
|
*/
|
|
@@ -1369,7 +1378,8 @@ async function probeCaptureSerialized(browser, timeout) {
|
|
|
1369
1378
|
deadlineMessage: CAPTURE_PROBE_TIMEOUT_MESSAGE,
|
|
1370
1379
|
deadlineMs: timeout + PROBE_TEARDOWN_ALLOWANCE_MS,
|
|
1371
1380
|
// The probe's wedge is a loud warning only, never a second verdict: by wedge time the detector has already marked this browser from the returned failure
|
|
1372
|
-
// (the outer deadline fires
|
|
1381
|
+
// (the outer deadline always fires first, because the lock derives the wedge bound to land strictly after the deadline), so a mark raised here would land
|
|
1382
|
+
// on an instance that already carries one.
|
|
1373
1383
|
onWedge: () => {
|
|
1374
1384
|
LOG.warn("A mid-life capture probe has wedged past the recovery bound; the browser was already marked when the probe's deadline fired, so this wedge " +
|
|
1375
1385
|
"is informational.");
|
|
@@ -1386,17 +1396,18 @@ async function probeCaptureSerialized(browser, timeout) {
|
|
|
1386
1396
|
* Classifies an establishment failure and, when it carries a capture-infrastructure signature, hands it to the passive mid-life detector.
|
|
1387
1397
|
*
|
|
1388
1398
|
* This sits at the acquisition chokepoint rather than in one caller's catch, so every path that establishes a capture gets the detection: a fresh stream's setup,
|
|
1389
|
-
* a tab replacement, and anything added later.
|
|
1399
|
+
* a tab replacement, and anything added later. Each of createPageWithCapture's own catch blocks calls it, which is what extends the coverage across phases - the
|
|
1390
1400
|
* acquisition catch sees a refused capture start, the establishment catch sees the playback-initialization timeout the pattern list also names.
|
|
1391
1401
|
*
|
|
1392
|
-
*
|
|
1393
|
-
*
|
|
1394
|
-
*
|
|
1395
|
-
* browser
|
|
1402
|
+
* Classification is skipped for the typed direct-URL failure: a failure blamed on a direct watch URL leaves createPageWithCapture wrapped for a retry and
|
|
1403
|
+
* classifies nothing, and the standing attempt that follows it classifies for both. The refusal retry is the deliberate exception - the acquisition catch classifies the
|
|
1404
|
+
* first refusal and waits on its verdict, because waiting on that verdict is the retry policy. A retry that is refused again, or whose navigation then fails,
|
|
1405
|
+
* classifies a second time: the shared in-flight slot or the impairment already recorded against the browser absorbs that call when the first verdict is still
|
|
1406
|
+
* pending or marked the browser, and otherwise it is a fresh question about a browser that has failed again since its last verdict.
|
|
1396
1407
|
*
|
|
1397
|
-
* DirectUrlEstablishmentError is excluded here as well, as the backstop that keeps the
|
|
1398
|
-
*
|
|
1399
|
-
* the classification line is reached.
|
|
1408
|
+
* DirectUrlEstablishmentError is excluded here as well, as the backstop that keeps the skip true of a call site added later rather than as a filter the existing
|
|
1409
|
+
* call sites depend on: none of them can reach this with one, because the type exists only to leave createPageWithCapture, and the path that wraps into it throws
|
|
1410
|
+
* before the classification line is reached.
|
|
1400
1411
|
* @param error - The failure the establishment is about to rethrow.
|
|
1401
1412
|
* @returns The verdict the detector reached, or null when this failure called for none.
|
|
1402
1413
|
*/
|