prismcast 1.2.1 → 1.3.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.
Files changed (128) hide show
  1. package/README.md +26 -1
  2. package/dist/app.js +29 -2
  3. package/dist/app.js.map +1 -1
  4. package/dist/browser/cdp.js +1 -1
  5. package/dist/browser/cdp.js.map +1 -1
  6. package/dist/browser/channelSelection.d.ts +11 -1
  7. package/dist/browser/channelSelection.js +45 -980
  8. package/dist/browser/channelSelection.js.map +1 -1
  9. package/dist/browser/index.d.ts +18 -4
  10. package/dist/browser/index.js +202 -36
  11. package/dist/browser/index.js.map +1 -1
  12. package/dist/browser/tuning/fox.d.ts +19 -0
  13. package/dist/browser/tuning/fox.js +61 -0
  14. package/dist/browser/tuning/fox.js.map +1 -0
  15. package/dist/browser/tuning/hbo.d.ts +22 -0
  16. package/dist/browser/tuning/hbo.js +182 -0
  17. package/dist/browser/tuning/hbo.js.map +1 -0
  18. package/dist/browser/tuning/hulu.d.ts +30 -0
  19. package/dist/browser/tuning/hulu.js +514 -0
  20. package/dist/browser/tuning/hulu.js.map +1 -0
  21. package/dist/browser/tuning/sling.d.ts +22 -0
  22. package/dist/browser/tuning/sling.js +287 -0
  23. package/dist/browser/tuning/sling.js.map +1 -0
  24. package/dist/browser/tuning/thumbnailRow.d.ts +18 -0
  25. package/dist/browser/tuning/thumbnailRow.js +107 -0
  26. package/dist/browser/tuning/thumbnailRow.js.map +1 -0
  27. package/dist/browser/tuning/tileClick.d.ts +18 -0
  28. package/dist/browser/tuning/tileClick.js +102 -0
  29. package/dist/browser/tuning/tileClick.js.map +1 -0
  30. package/dist/browser/tuning/youtubeTv.d.ts +17 -0
  31. package/dist/browser/tuning/youtubeTv.js +99 -0
  32. package/dist/browser/tuning/youtubeTv.js.map +1 -0
  33. package/dist/browser/video.d.ts +2 -2
  34. package/dist/browser/video.js +18 -8
  35. package/dist/browser/video.js.map +1 -1
  36. package/dist/channels/index.js +53 -7
  37. package/dist/channels/index.js.map +1 -1
  38. package/dist/config/profiles.js +11 -0
  39. package/dist/config/profiles.js.map +1 -1
  40. package/dist/config/providers.d.ts +46 -1
  41. package/dist/config/providers.js +178 -13
  42. package/dist/config/providers.js.map +1 -1
  43. package/dist/config/sites.d.ts +2 -0
  44. package/dist/config/sites.js +43 -14
  45. package/dist/config/sites.js.map +1 -1
  46. package/dist/config/userChannels.js +21 -3
  47. package/dist/config/userChannels.js.map +1 -1
  48. package/dist/config/userConfig.d.ts +2 -1
  49. package/dist/config/userConfig.js +12 -4
  50. package/dist/config/userConfig.js.map +1 -1
  51. package/dist/index.js +10 -4
  52. package/dist/index.js.map +1 -1
  53. package/dist/routes/auth.js +6 -0
  54. package/dist/routes/auth.js.map +1 -1
  55. package/dist/routes/config.d.ts +5 -0
  56. package/dist/routes/config.js +273 -35
  57. package/dist/routes/config.js.map +1 -1
  58. package/dist/routes/debug.d.ts +6 -0
  59. package/dist/routes/debug.js +274 -0
  60. package/dist/routes/debug.js.map +1 -0
  61. package/dist/routes/index.d.ts +1 -0
  62. package/dist/routes/index.js +3 -0
  63. package/dist/routes/index.js.map +1 -1
  64. package/dist/routes/logs.js +17 -5
  65. package/dist/routes/logs.js.map +1 -1
  66. package/dist/routes/root.js +506 -127
  67. package/dist/routes/root.js.map +1 -1
  68. package/dist/routes/theme.js +7 -0
  69. package/dist/routes/theme.js.map +1 -1
  70. package/dist/service/commands.js +8 -8
  71. package/dist/service/commands.js.map +1 -1
  72. package/dist/service/generators.js +22 -16
  73. package/dist/service/generators.js.map +1 -1
  74. package/dist/streaming/fmp4Segmenter.d.ts +7 -0
  75. package/dist/streaming/fmp4Segmenter.js +124 -15
  76. package/dist/streaming/fmp4Segmenter.js.map +1 -1
  77. package/dist/streaming/hls.d.ts +74 -6
  78. package/dist/streaming/hls.js +210 -129
  79. package/dist/streaming/hls.js.map +1 -1
  80. package/dist/streaming/hlsSegments.js +11 -4
  81. package/dist/streaming/hlsSegments.js.map +1 -1
  82. package/dist/streaming/lifecycle.js +1 -1
  83. package/dist/streaming/lifecycle.js.map +1 -1
  84. package/dist/streaming/monitor.d.ts +1 -1
  85. package/dist/streaming/monitor.js +120 -49
  86. package/dist/streaming/monitor.js.map +1 -1
  87. package/dist/streaming/mp4Parser.d.ts +29 -1
  88. package/dist/streaming/mp4Parser.js +243 -18
  89. package/dist/streaming/mp4Parser.js.map +1 -1
  90. package/dist/streaming/mpegts.d.ts +5 -2
  91. package/dist/streaming/mpegts.js +122 -29
  92. package/dist/streaming/mpegts.js.map +1 -1
  93. package/dist/streaming/registry.d.ts +1 -1
  94. package/dist/streaming/registry.js.map +1 -1
  95. package/dist/streaming/setup.d.ts +13 -1
  96. package/dist/streaming/setup.js +136 -36
  97. package/dist/streaming/setup.js.map +1 -1
  98. package/dist/streaming/showInfo.js +9 -9
  99. package/dist/streaming/showInfo.js.map +1 -1
  100. package/dist/streaming/statusEmitter.js +5 -6
  101. package/dist/streaming/statusEmitter.js.map +1 -1
  102. package/dist/types/index.d.ts +12 -3
  103. package/dist/types/index.js +5 -1
  104. package/dist/types/index.js.map +1 -1
  105. package/dist/utils/debugFilter.d.ts +38 -0
  106. package/dist/utils/debugFilter.js +157 -0
  107. package/dist/utils/debugFilter.js.map +1 -0
  108. package/dist/utils/evaluate.js.map +1 -1
  109. package/dist/utils/ffmpeg.d.ts +5 -0
  110. package/dist/utils/ffmpeg.js +13 -2
  111. package/dist/utils/ffmpeg.js.map +1 -1
  112. package/dist/utils/fileLogger.d.ts +2 -1
  113. package/dist/utils/fileLogger.js +4 -2
  114. package/dist/utils/fileLogger.js.map +1 -1
  115. package/dist/utils/index.d.ts +1 -0
  116. package/dist/utils/index.js +1 -0
  117. package/dist/utils/index.js.map +1 -1
  118. package/dist/utils/logEmitter.d.ts +1 -0
  119. package/dist/utils/logEmitter.js.map +1 -1
  120. package/dist/utils/logger.d.ts +9 -8
  121. package/dist/utils/logger.js +52 -28
  122. package/dist/utils/logger.js.map +1 -1
  123. package/dist/utils/retry.d.ts +1 -1
  124. package/dist/utils/retry.js +3 -3
  125. package/dist/utils/retry.js.map +1 -1
  126. package/dist/utils/version.js +4 -9
  127. package/dist/utils/version.js.map +1 -1
  128. package/package.json +6 -6
@@ -1,30 +1,36 @@
1
- /* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
- *
3
- * mpegts.ts: MPEG-TS streaming handler for PrismCast.
4
- */
5
1
  import { LOG, formatError, spawnMpegTsRemuxer } from "../utils/index.js";
2
+ import { awaitStreamReadySilent, initializeStream, sendValidationError, validateChannel } from "./hls.js";
6
3
  import { getStream, updateLastAccess } from "./registry.js";
7
4
  import { registerClient, unregisterClient } from "./clients.js";
8
5
  import { CONFIG } from "../config/index.js";
9
- import { ensureChannelStream } from "./hls.js";
6
+ import { StreamSetupError } from "./setup.js";
7
+ import { getChannelStreamId } from "./lifecycle.js";
10
8
  import { waitForInitSegment } from "./hlsSegments.js";
11
9
  /* This module provides a continuous MPEG-TS byte stream from the same capture pipeline used for HLS. It is designed for HDHomeRun-compatible clients (such as Plex)
12
10
  * that expect raw MPEG-TS when tuning a channel. The existing capture → segmenter → HLS segments flow is unchanged. Each MPEG-TS client gets its own FFmpeg remuxer
13
11
  * that converts stored fMP4 segments to MPEG-TS with codec copy (no transcoding).
14
12
  *
15
13
  * Data flow per client:
16
- * 1. ensureChannelStream() starts or reuses an HLS capture
17
- * 2. Wait for the init segment (ftyp+moov codec configuration)
18
- * 3. Spawn FFmpeg: -f mp4 -i pipe:0 -c copy -f mpegts pipe:1
19
- * 4. Write init segment + existing media segments to FFmpeg stdin
20
- * 5. Subscribe to segment events for new segments in real time
21
- * 6. Pipe FFmpeg stdout to the HTTP response as video/mp2t
22
- * 7. On client disconnect or stream termination, kill FFmpeg and clean up
14
+ * 1. Validate channel and check for existing stream
15
+ * 2. If new stream needed, flush HTTP 200 headers immediately (so the client sees "connection accepted")
16
+ * 3. initializeStream() starts the capture, or awaitStreamReadySilent() waits for an in-progress startup
17
+ * 4. Wait for the init segment (ftyp+moov codec configuration)
18
+ * 5. Spawn FFmpeg: -f mp4 -i pipe:0 -c copy -f mpegts pipe:1
19
+ * 6. Write init segment + existing media segments to FFmpeg stdin
20
+ * 7. Subscribe to segment events for new segments in real time
21
+ * 8. Pipe FFmpeg stdout to the HTTP response as video/mp2t
22
+ * 9. On client disconnect or stream termination, kill FFmpeg and clean up
23
+ *
24
+ * The header flush in step 2 prevents client timeouts. Without it, the client receives zero bytes until the entire stream setup completes (4-10+ seconds), which may
25
+ * exceed the client's connection timeout.
23
26
  */
24
27
  // Public Endpoint Handler.
25
28
  /**
26
- * Handles MPEG-TS stream requests. Ensures a capture is running for the requested channel, waits for the init segment, spawns a per-client FFmpeg remuxer to convert
27
- * fMP4 to MPEG-TS, and streams the output to the HTTP response.
29
+ * Handles MPEG-TS stream requests. Validates the channel, flushes HTTP headers early for new streams, then ensures a capture is running, waits for the init segment,
30
+ * spawns a per-client FFmpeg remuxer, and streams the output.
31
+ *
32
+ * For new streams, headers are flushed before stream setup begins so the client sees an immediate 200 response. This prevents timeout failures during the 4-10+
33
+ * second startup sequence. The trade-off is that error responses cannot be sent after the flush — failures are logged server-side and the connection is closed.
28
34
  *
29
35
  * Route: GET /stream/:name
30
36
  *
@@ -37,23 +43,108 @@ export async function handleMpegTsStream(req, res) {
37
43
  res.status(400).send("Channel name is required.");
38
44
  return;
39
45
  }
40
- // Ensure a stream is running for this channel. This validates the channel, starts a stream if needed, and handles concurrent startup deduplication.
41
- const streamId = await ensureChannelStream(channelName, req, res);
42
- if (streamId === null) {
43
- // Error response already sent by ensureChannelStream.
46
+ // Check for an existing stream first. If one exists, we can skip validation and header flushing.
47
+ const existingStreamId = getChannelStreamId(channelName);
48
+ // Fast path: a real stream already exists. No early flush needed — the stream data will flow quickly.
49
+ if ((existingStreamId !== undefined) && (existingStreamId !== -1)) {
50
+ await serveMpegTsStream(existingStreamId, channelName, req, res);
44
51
  return;
45
52
  }
53
+ // If no existing stream or startup in progress, validate the channel before flushing headers. This ensures we can still return proper error responses for invalid
54
+ // channels, disabled channels, and login mode. Store the validated channel for use during stream initialization below.
55
+ let validatedChannel;
56
+ if (existingStreamId === undefined) {
57
+ const validation = validateChannel(channelName);
58
+ if (!validation.valid) {
59
+ sendValidationError(validation, res);
60
+ return;
61
+ }
62
+ validatedChannel = validation.channel;
63
+ }
64
+ // Flush HTTP 200 headers immediately. The client sees "connection accepted, data coming" and waits patiently. After this point, we cannot send error status codes —
65
+ // failures will close the connection with no data.
66
+ res.setHeader("Cache-Control", "no-cache");
67
+ res.setHeader("Connection", "close");
68
+ res.setHeader("Content-Type", "video/mp2t");
69
+ res.flushHeaders();
70
+ // Acquire the stream. If a startup is in progress (another request started it), poll silently. Otherwise, start a new stream via initializeStream().
71
+ let streamId;
72
+ if (existingStreamId === -1) {
73
+ // Another request is already starting this stream. Wait silently (no error responses possible after flush).
74
+ streamId = await awaitStreamReadySilent(channelName);
75
+ if (streamId === null) {
76
+ LOG.warn("MPEG-TS stream startup failed for %s (startup did not complete).", channelName);
77
+ res.end();
78
+ return;
79
+ }
80
+ }
81
+ else {
82
+ // Start a new stream directly. validatedChannel is guaranteed set: this branch runs only when existingStreamId === undefined, which requires successful
83
+ // validation above. Since headers are already flushed, errors are logged and the connection is closed.
84
+ if (!validatedChannel) {
85
+ res.end();
86
+ return;
87
+ }
88
+ try {
89
+ streamId = await initializeStream({
90
+ channel: validatedChannel,
91
+ channelName,
92
+ clientAddress: req.ip ?? req.socket.remoteAddress ?? null,
93
+ profileOverride: req.query.profile,
94
+ url: validatedChannel.url
95
+ });
96
+ }
97
+ catch (error) {
98
+ if (error instanceof StreamSetupError) {
99
+ LOG.warn("MPEG-TS stream startup failed for %s: %s.", channelName, error.userMessage);
100
+ }
101
+ else {
102
+ LOG.warn("MPEG-TS stream startup failed for %s: %s.", channelName, formatError(error));
103
+ }
104
+ res.end();
105
+ return;
106
+ }
107
+ if (streamId === null) {
108
+ LOG.warn("MPEG-TS stream startup failed for %s (terminated during setup).", channelName);
109
+ res.end();
110
+ return;
111
+ }
112
+ }
113
+ await serveMpegTsStream(streamId, channelName, req, res);
114
+ }
115
+ // Internal Helpers.
116
+ /**
117
+ * Serves the MPEG-TS stream once a stream ID is available. Waits for the init segment, spawns the FFmpeg remuxer, and pipes the output to the response. This is the
118
+ * shared implementation used by both the fast path (existing stream) and the flush path (new stream).
119
+ *
120
+ * @param streamId - The numeric stream ID.
121
+ * @param channelName - The channel name for logging.
122
+ * @param req - Express request object.
123
+ * @param res - Express response object.
124
+ */
125
+ async function serveMpegTsStream(streamId, channelName, req, res) {
46
126
  // Wait for the init segment to be available. The init segment contains codec configuration (ftyp+moov boxes) that FFmpeg needs before it can process media segments.
47
127
  const initReady = await waitForInitSegment(streamId, CONFIG.streaming.navigationTimeout);
48
128
  if (!initReady) {
49
- res.setHeader("Retry-After", "5");
50
- res.status(503).send("Stream is starting. Please retry.");
129
+ if (!res.headersSent) {
130
+ res.setHeader("Retry-After", "5");
131
+ res.status(503).send("Stream is starting. Please retry.");
132
+ }
133
+ else {
134
+ LOG.warn("MPEG-TS init segment timeout for %s.", channelName);
135
+ res.end();
136
+ }
51
137
  return;
52
138
  }
53
139
  // Get the stream from the registry and verify it's still alive with a valid init segment.
54
140
  const stream = getStream(streamId);
55
- if (!stream || !stream.hls.initSegment) {
56
- res.status(500).send("Stream no longer available.");
141
+ if (!stream?.hls.initSegment) {
142
+ if (!res.headersSent) {
143
+ res.status(500).send("Stream no longer available.");
144
+ }
145
+ else {
146
+ res.end();
147
+ }
57
148
  return;
58
149
  }
59
150
  // Capture client address for client tracking. Captured before any async operations so it remains consistent in the cleanup closure, even if the request object
@@ -74,7 +165,7 @@ export async function handleMpegTsStream(req, res) {
74
165
  // Spawn an FFmpeg process to remux fMP4 to MPEG-TS. The process reads concatenated fMP4 (init segment + media segments) from stdin and outputs a continuous
75
166
  // MPEG-TS stream on stdout. Video (H264) and audio (AAC) are copied without transcoding.
76
167
  const remuxer = spawnMpegTsRemuxer((error) => {
77
- streamLog.debug("MPEG-TS remuxer error: %s.", formatError(error));
168
+ streamLog.debug("streaming:mpegts", "MPEG-TS remuxer error: %s.", formatError(error));
78
169
  cleanup();
79
170
  if (!res.writableEnded) {
80
171
  res.end();
@@ -121,7 +212,7 @@ export async function handleMpegTsStream(req, res) {
121
212
  stream.hls.segmentEmitter.off("segment", onSegment);
122
213
  stream.hls.segmentEmitter.off("terminated", onTerminated);
123
214
  remuxer.kill();
124
- streamLog.debug("MPEG-TS client disconnected.");
215
+ streamLog.debug("streaming:mpegts", "MPEG-TS client disconnected.");
125
216
  };
126
217
  // Clean up when the client disconnects. Registered immediately after cleanup is assigned to minimize the window where a disconnect could be missed.
127
218
  req.on("close", () => {
@@ -130,10 +221,12 @@ export async function handleMpegTsStream(req, res) {
130
221
  // Subscribe to segment events BEFORE writing existing segments to avoid missing any segments added during the catchup phase.
131
222
  stream.hls.segmentEmitter.on("segment", onSegment);
132
223
  stream.hls.segmentEmitter.on("terminated", onTerminated);
133
- // Set response headers for MPEG-TS streaming.
134
- res.setHeader("Cache-Control", "no-cache");
135
- res.setHeader("Connection", "close");
136
- res.setHeader("Content-Type", "video/mp2t");
224
+ // Set response headers if they haven't been flushed yet (fast path for existing streams).
225
+ if (!res.headersSent) {
226
+ res.setHeader("Cache-Control", "no-cache");
227
+ res.setHeader("Connection", "close");
228
+ res.setHeader("Content-Type", "video/mp2t");
229
+ }
137
230
  // Pipe FFmpeg stdout to the HTTP response. When FFmpeg exits (either from stdin ending or being killed), stdout closes and the response ends automatically.
138
231
  remuxer.stdout.pipe(res);
139
232
  // Write the init segment first — FFmpeg needs the ftyp and moov boxes before it can process any media segments.
@@ -148,6 +241,6 @@ export async function handleMpegTsStream(req, res) {
148
241
  sentSegments.add(filename);
149
242
  remuxer.stdin.write(data);
150
243
  }
151
- streamLog.debug("MPEG-TS client connected.");
244
+ streamLog.debug("streaming:mpegts", "MPEG-TS client connected.");
152
245
  }
153
246
  //# sourceMappingURL=mpegts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"mpegts.js","sourceRoot":"","sources":["../../src/streaming/mpegts.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mpegts.js","sourceRoot":"","sources":["../../src/streaming/mpegts.ts"],"names":[],"mappings":"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"}
@@ -116,4 +116,4 @@ export declare function getTotalSegmentMemory(): number;
116
116
  * @param entry - The stream registry entry to query.
117
117
  * @returns Segment size in bytes, or null if no segmenter exists.
118
118
  */
119
- export declare function getLastSegmentSize(entry: StreamRegistryEntry): number | null;
119
+ export declare function getLastSegmentSize(entry: StreamRegistryEntry): Nullable<number>;
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/streaming/registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/streaming/registry.ts"],"names":[],"mappings":"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"}
@@ -7,7 +7,7 @@ import type { Readable } from "node:stream";
7
7
  * Factory function type for creating tab replacement handlers. Called by setupStream after generating stream IDs and resolving the profile, allowing the caller to
8
8
  * create a handler with access to all necessary context.
9
9
  */
10
- export type TabReplacementHandlerFactory = (numericStreamId: number, streamId: string, profile: ResolvedSiteProfile, metadataComment: string | undefined) => () => Promise<TabReplacementResult | null>;
10
+ export type TabReplacementHandlerFactory = (numericStreamId: number, streamId: string, profile: ResolvedSiteProfile, metadataComment: string | undefined) => () => Promise<Nullable<TabReplacementResult>>;
11
11
  /**
12
12
  * Options for setting up a stream.
13
13
  */
@@ -58,6 +58,7 @@ export interface CreatePageWithCaptureOptions {
58
58
  profile: ResolvedSiteProfile;
59
59
  streamId: string;
60
60
  url: string;
61
+ _pageClosedRetries?: number;
61
62
  }
62
63
  /**
63
64
  * Result from creating a page with capture. Contains everything needed to create a segmenter and continue with stream setup.
@@ -117,3 +118,14 @@ export declare function createPageWithCapture(options: CreatePageWithCaptureOpti
117
118
  * @throws StreamSetupError if setup fails with appropriate status code and message.
118
119
  */
119
120
  export declare function setupStream(options: StreamSetupOptions, onCircuitBreak: () => void): Promise<StreamSetupResult>;
121
+ /**
122
+ * Verifies that Chrome's capture system is functional before the server starts accepting requests. This detects stale tabCapture state left over from a previous
123
+ * Chrome process — common during quick service restarts where the old process hasn't fully exited before the new one launches. Without this probe, the first stream
124
+ * request would trigger the runtime stale capture handler, which exits the process because the puppeteer-stream mutex is permanently leaked.
125
+ *
126
+ * The probe creates a temporary page, attempts a short capture, and tears down both cleanly. A 500ms delay after destroying the capture stream allows
127
+ * puppeteer-stream's fire-and-forget STOP_RECORDING chain to complete before closing the page, preventing the stale capture cascade on the first real request.
128
+ *
129
+ * If stale capture state is detected, the process exits immediately — Chrome restart cannot fix the leaked mutex, only a fresh process can.
130
+ */
131
+ export declare function verifyCaptureSystem(): Promise<void>;
@@ -1,5 +1,5 @@
1
- import { LOG, delay, extractDomain, formatError, registerAbortController, retryOperation, runWithStreamContext, spawnFFmpeg } from "../utils/index.js";
2
- import { closeBrowser, getCurrentBrowser, getStream, minimizeBrowserWindow, registerManagedPage, unregisterManagedPage } from "../browser/index.js";
1
+ import { LOG, delay, extractDomain, formatError, registerAbortController, retryOperation, runWithStreamContext, spawnFFmpeg, startTimer } from "../utils/index.js";
2
+ import { getCurrentBrowser, getStream, minimizeBrowserWindow, registerManagedPage, unregisterManagedPage } from "../browser/index.js";
3
3
  import { getNextStreamId, getStreamCount } from "./registry.js";
4
4
  import { getProfileForChannel, getProfileForUrl, getProfiles, resolveProfile } from "../config/profiles.js";
5
5
  import { initializePlayback, navigateToPage } from "../browser/video.js";
@@ -41,10 +41,9 @@ const WEBM_FFMPEG_MIME_TYPE = "video/webm;codecs=h264,opus";
41
41
  // tab with an active stream." We serialize capture initialization using a promise chain so requests execute sequentially. Once a capture is established, it runs
42
42
  // concurrently with other captures without issue.
43
43
  let captureQueue = Promise.resolve();
44
- // Stale capture recovery. Chrome's tabCapture can retain stale state after a crash, causing "Cannot capture a tab with an active stream" errors on fresh launches.
45
- // When detected, we restart Chrome entirely to clear the state. Limited to 3 attempts per server lifetime to prevent infinite loops.
46
- const MAX_STALE_CAPTURE_RECOVERY_ATTEMPTS = 3;
47
- let staleCaptureRecoveryAttempts = 0;
44
+ // Maximum number of times createPageWithCapture() will retry when it detects that the page was closed while waiting in the capture queue (e.g., due to a browser
45
+ // crash). An explicit guard prevents unbounded recursion.
46
+ const MAX_PAGE_CLOSED_RETRIES = 3;
48
47
  /**
49
48
  * Error thrown when stream setup fails. Includes HTTP status code and user-friendly message for the response.
50
49
  */
@@ -144,6 +143,7 @@ export function validateStreamUrl(url) {
144
143
  * @throws Error if page creation, capture initialization, or navigation fails.
145
144
  */
146
145
  export async function createPageWithCapture(options) {
146
+ const captureElapsed = startTimer();
147
147
  const { comment, onFFmpegError, profile, streamId, url } = options;
148
148
  // Create browser page.
149
149
  const browser = await getCurrentBrowser();
@@ -158,6 +158,16 @@ export async function createPageWithCapture(options) {
158
158
  let outputStream;
159
159
  let rawCaptureStream = null;
160
160
  let ffmpegProcess = null;
161
+ // Capture queue release function, hoisted here so both the try and catch blocks can access it. Initialized in the try block when the queue entry is created.
162
+ // The once-guard prevents double-releasing from multiple code paths (success handler, catch block, timeout).
163
+ let captureQueueReleased = false;
164
+ let releaseCaptureQueue = () => { };
165
+ const releaseCaptureOnce = () => {
166
+ if (!captureQueueReleased) {
167
+ captureQueueReleased = true;
168
+ releaseCaptureQueue();
169
+ }
170
+ };
161
171
  // Initialize media stream capture.
162
172
  try {
163
173
  const streamOptions = {
@@ -171,17 +181,15 @@ export async function createPageWithCapture(options) {
171
181
  maxFrameRate: 60,
172
182
  maxHeight: getEffectiveViewport(CONFIG).height,
173
183
  maxWidth: getEffectiveViewport(CONFIG).width,
174
- minFrameRate: CONFIG.streaming.frameRate,
184
+ minFrameRate: Math.max(30, Math.min(60, CONFIG.streaming.frameRate)),
175
185
  minHeight: getEffectiveViewport(CONFIG).height,
176
186
  minWidth: getEffectiveViewport(CONFIG).width
177
187
  }
178
188
  }
179
189
  };
180
190
  // Serialize capture initialization. Wait for any previous capture to finish before calling getStream(), because Chrome's tabCapture extension rejects
181
- // concurrent initialization attempts. The lock is released when getStream() resolves or rejects (not when our timeout fires), because Chrome's extension state
182
- // is what matters. If our timeout fires while getStream() is still in-flight, the catch block closes the page, which causes the pending getStream() to reject,
183
- // which releases the lock and allows the next queued capture to proceed.
184
- let releaseCaptureQueue = () => { };
191
+ // concurrent initialization attempts. On success, the lock is released immediately so the next caller can proceed. On failure, the lock is held until the
192
+ // catch block decides what to do — the catch block releases the lock after handling the error.
185
193
  const previousCapture = captureQueue;
186
194
  captureQueue = new Promise((resolve) => {
187
195
  releaseCaptureQueue = resolve;
@@ -200,12 +208,24 @@ export async function createPageWithCapture(options) {
200
208
  }
201
209
  catch (error) {
202
210
  // Release our queue position so subsequent captures aren't blocked by our failure.
203
- releaseCaptureQueue();
211
+ releaseCaptureOnce();
204
212
  throw error;
205
213
  }
214
+ // After the queue wait, verify our page is still connected. If Chrome crashed while we were waiting, our page is dead and we need to start over with a
215
+ // fresh page on the new browser. Release our queue position first so subsequent callers aren't blocked.
216
+ if (page.isClosed()) {
217
+ releaseCaptureOnce();
218
+ unregisterManagedPage(page);
219
+ const retryCount = options._pageClosedRetries ?? 0;
220
+ if (retryCount >= MAX_PAGE_CLOSED_RETRIES) {
221
+ throw new Error("Browser crashed too many times during capture initialization.");
222
+ }
223
+ return await createPageWithCapture({ ...options, _pageClosedRetries: retryCount + 1 });
224
+ }
206
225
  const streamPromise = getStream(page, streamOptions);
207
- // Release the queue when getStream() completes, regardless of success or failure. This is independent of our application timeout below.
208
- void streamPromise.then(() => releaseCaptureQueue(), () => releaseCaptureQueue());
226
+ // Release the queue on success only. On failure, the catch block handles the release. The rejection handler is a no-op to suppress unhandled rejection
227
+ // warnings; the actual error handling happens in the catch block below.
228
+ void streamPromise.then(() => { releaseCaptureOnce(); }, () => { });
209
229
  const timeoutPromise = new Promise((_, reject) => {
210
230
  setTimeout(() => {
211
231
  reject(new Error("Stream initialization timed out."));
@@ -227,7 +247,7 @@ export async function createPageWithCapture(options) {
227
247
  ffmpeg.stdout.on("error", (error) => {
228
248
  const errorMessage = formatError(error);
229
249
  if (errorMessage.includes("EPIPE")) {
230
- LOG.debug("FFmpeg stdout pipe closed: %s.", errorMessage);
250
+ LOG.debug("streaming:ffmpeg", "FFmpeg stdout pipe closed: %s.", errorMessage);
231
251
  }
232
252
  else {
233
253
  LOG.error("FFmpeg stdout pipe error: %s.", errorMessage);
@@ -249,7 +269,7 @@ export async function createPageWithCapture(options) {
249
269
  LOG.error("Capture pipeline error: %s.", errorMessage);
250
270
  ffmpeg.kill();
251
271
  if (onFFmpegError) {
252
- onFFmpegError(error);
272
+ onFFmpegError(error instanceof Error ? error : new Error(String(error)));
253
273
  }
254
274
  });
255
275
  // Use FFmpeg's stdout (fMP4 output) as the output stream for segmentation.
@@ -269,27 +289,25 @@ export async function createPageWithCapture(options) {
269
289
  if (!page.isClosed()) {
270
290
  page.close().catch(() => { });
271
291
  }
272
- // Check for stale capture state error. This occurs after a crash when Chrome's tabCapture retains state from the previous process. Restarting Chrome clears
273
- // the stale state. We limit recovery attempts to prevent infinite loops if the issue persists.
274
292
  const errorMessage = formatError(error);
275
- if (errorMessage.includes("Cannot capture a tab with an active stream") && (staleCaptureRecoveryAttempts < MAX_STALE_CAPTURE_RECOVERY_ATTEMPTS)) {
276
- staleCaptureRecoveryAttempts++;
277
- LOG.warn("Stale capture state detected (attempt %d/%d). Restarting Chrome to clear state.", staleCaptureRecoveryAttempts, MAX_STALE_CAPTURE_RECOVERY_ATTEMPTS);
278
- // Kill Chrome entirely to clear all tabCapture state.
279
- await closeBrowser();
280
- // Wait for Chrome to fully exit before relaunching.
281
- await delay(1500);
282
- // Relaunch Chrome. getCurrentBrowser() creates a fresh instance since closeBrowser() cleared the reference.
283
- await getCurrentBrowser();
284
- // Retry the capture with a fresh browser.
285
- return createPageWithCapture(options);
293
+ // Stale capture state is unrecoverable. The "Cannot capture a tab with an active stream" error occurs inside puppeteer-stream's second lock section, which
294
+ // has no try/finally. The internal mutex is permanently leaked — all subsequent getStream() calls will hang on it. Chrome restart cannot fix module-level
295
+ // state, so the only recourse is a full process restart. Release the capture queue so other callers aren't left hanging, then exit.
296
+ if (errorMessage.includes("Cannot capture a tab with an active stream")) {
297
+ LOG.error("Stale capture state detected. puppeteer-stream's internal capture mutex is now permanently locked. The capture system is unrecoverable. " +
298
+ "Exiting so the service manager can restart with a clean module state.");
299
+ releaseCaptureOnce();
300
+ setTimeout(() => process.exit(1), 100);
301
+ throw error;
286
302
  }
303
+ // For non-stale errors, release the capture queue so subsequent callers can proceed.
304
+ releaseCaptureOnce();
287
305
  throw error;
288
306
  }
289
307
  // Navigate and set up playback. For noVideo profiles, just navigate without video setup.
290
308
  let context;
291
309
  try {
292
- if (profile.noVideo === false) {
310
+ if (!profile.noVideo) {
293
311
  // Phase 1: Navigate to the page with retry. The 10-second navigationTimeout is appropriate for page loads, and retryOperation correctly reloads the page on
294
312
  // genuine navigation failures. Navigation is wrapped in retryOperation separately from channel selection so the timeout does not race with the internal click
295
313
  // retry loops in channel selection strategies (guideGrid can take 15-20 seconds for binary search + click retries).
@@ -297,9 +315,10 @@ export async function createPageWithCapture(options) {
297
315
  await navigateToPage(page, url, profile);
298
316
  }, CONFIG.streaming.maxNavigationRetries, CONFIG.streaming.navigationTimeout, "page navigation for " + url, undefined, () => page.isClosed());
299
317
  // Phase 2: Channel selection + video setup. Runs after navigation succeeds with no outer timeout racing against internal click retries. Each sub-step
300
- // (selectChannel, waitForVideoReady, etc.) has its own internal timeout via videoTimeout and click retry constants. A 30-second safety-net timeout prevents
301
- // pathological hangs if multiple internal timeouts chain sequentially.
302
- const PLAYBACK_INIT_TIMEOUT = 30000;
318
+ // (selectChannel, waitForVideoReady, etc.) has its own internal timeout via videoTimeout and click retry constants. For guideGrid strategies, a channel
319
+ // selection failure triggers an overlay dismiss and retry, which doubles the channel selection time budget. The 45-second safety-net timeout accommodates
320
+ // this retry while still preventing pathological hangs if multiple internal timeouts chain sequentially.
321
+ const PLAYBACK_INIT_TIMEOUT = 45000;
303
322
  const tuneResult = await Promise.race([
304
323
  initializePlayback(page, profile),
305
324
  new Promise((_, reject) => {
@@ -327,10 +346,14 @@ export async function createPageWithCapture(options) {
327
346
  if (!page.isClosed()) {
328
347
  page.close().catch(() => { });
329
348
  }
349
+ // Re-minimize the browser window. Navigation may have un-minimized it (new tab activation on macOS), and without this the window stays visible after the
350
+ // failed attempt. Fire-and-forget since we're about to throw.
351
+ minimizeBrowserWindow().catch(() => { });
330
352
  throw error;
331
353
  }
332
354
  // Resize and minimize window.
333
355
  await resizeAndMinimizeWindow(page, !profile.noVideo);
356
+ LOG.debug("timing:startup", "Page with capture ready. Total: %sms.", captureElapsed());
334
357
  return {
335
358
  captureStream: outputStream,
336
359
  context,
@@ -392,7 +415,7 @@ export async function setupStream(options, onCircuitBreak) {
392
415
  const redirectResult = getProfileForUrl(resolvedUrl);
393
416
  if (redirectResult.profileName !== "default") {
394
417
  profileResult = redirectResult;
395
- LOG.info("Resolved redirect for profile detection: %s → %s (%s).", urlToResolve, resolvedUrl, redirectResult.profileName);
418
+ LOG.debug("streaming:setup", "Resolved redirect for profile detection: %s → %s (%s).", urlToResolve, resolvedUrl, redirectResult.profileName);
396
419
  }
397
420
  }
398
421
  }
@@ -407,7 +430,7 @@ export async function setupStream(options, onCircuitBreak) {
407
430
  if (validProfiles.includes(profileOverride)) {
408
431
  profile = resolveProfile(profileOverride);
409
432
  profileName = profileOverride;
410
- LOG.info("Profile overridden to '%s' via query parameter.", profileOverride);
433
+ LOG.debug("streaming:setup", "Profile overridden to '%s' via query parameter.", profileOverride);
411
434
  }
412
435
  else {
413
436
  LOG.warn("Unknown profile override '%s', using resolved profile.", profileOverride);
@@ -501,7 +524,7 @@ export async function setupStream(options, onCircuitBreak) {
501
524
  // Close the browser page (fire-and-forget to avoid blocking on stuck pages).
502
525
  if (!page.isClosed()) {
503
526
  page.close().catch((error) => {
504
- LOG.warn("Page close error during cleanup: %s.", formatError(error));
527
+ LOG.debug("streaming:setup", "Page close error during cleanup: %s.", formatError(error));
505
528
  });
506
529
  }
507
530
  // Re-minimize the browser window.
@@ -526,4 +549,81 @@ export async function setupStream(options, onCircuitBreak) {
526
549
  };
527
550
  });
528
551
  }
552
+ // Startup Capture Verification.
553
+ /**
554
+ * Verifies that Chrome's capture system is functional before the server starts accepting requests. This detects stale tabCapture state left over from a previous
555
+ * Chrome process — common during quick service restarts where the old process hasn't fully exited before the new one launches. Without this probe, the first stream
556
+ * request would trigger the runtime stale capture handler, which exits the process because the puppeteer-stream mutex is permanently leaked.
557
+ *
558
+ * The probe creates a temporary page, attempts a short capture, and tears down both cleanly. A 500ms delay after destroying the capture stream allows
559
+ * puppeteer-stream's fire-and-forget STOP_RECORDING chain to complete before closing the page, preventing the stale capture cascade on the first real request.
560
+ *
561
+ * If stale capture state is detected, the process exits immediately — Chrome restart cannot fix the leaked mutex, only a fresh process can.
562
+ */
563
+ export async function verifyCaptureSystem() {
564
+ const PROBE_TIMEOUT = 5000;
565
+ const browser = await getCurrentBrowser();
566
+ const page = await browser.newPage();
567
+ registerManagedPage(page);
568
+ try {
569
+ // Use the same capture MIME type and viewport constraints as the runtime. The stale state error occurs at the tabCapture API level before encoding matters,
570
+ // but matching the runtime configuration ensures the probe exercises the exact same getStream() parameters.
571
+ const useFFmpeg = CONFIG.streaming.captureMode === "ffmpeg";
572
+ const captureMimeType = useFFmpeg ? WEBM_FFMPEG_MIME_TYPE : NATIVE_FMP4_MIME_TYPE;
573
+ const streamOptions = {
574
+ audio: true,
575
+ mimeType: captureMimeType,
576
+ video: true,
577
+ videoConstraints: {
578
+ mandatory: {
579
+ maxFrameRate: 30,
580
+ maxHeight: getEffectiveViewport(CONFIG).height,
581
+ maxWidth: getEffectiveViewport(CONFIG).width,
582
+ minFrameRate: 30,
583
+ minHeight: getEffectiveViewport(CONFIG).height,
584
+ minWidth: getEffectiveViewport(CONFIG).width
585
+ }
586
+ }
587
+ };
588
+ const stream = await Promise.race([
589
+ getStream(page, streamOptions),
590
+ new Promise((_, reject) => {
591
+ setTimeout(() => {
592
+ reject(new Error("Capture probe timed out."));
593
+ }, PROBE_TIMEOUT);
594
+ })
595
+ ]);
596
+ // Capture succeeded — the system is functional. Destroy the stream before closing the page to ensure chrome.tabCapture releases the capture cleanly.
597
+ const readable = stream;
598
+ readable.destroy();
599
+ // Wait for puppeteer-stream's capture cleanup chain to complete. readable.destroy() triggers STOP_RECORDING via the close handler, but the call is
600
+ // fire-and-forget. The async chain (STOP_RECORDING → recorder.stop() → onstop → track.stop()) must finish before closing the page, or Chrome's tabCapture
601
+ // state may linger and cause "Cannot capture a tab with an active stream" errors on the first real stream request.
602
+ await delay(500);
603
+ unregisterManagedPage(page);
604
+ if (!page.isClosed()) {
605
+ await page.close();
606
+ }
607
+ LOG.info("Capture system verified successfully.");
608
+ }
609
+ catch (error) {
610
+ const errorMessage = formatError(error);
611
+ // Clean up the test page.
612
+ unregisterManagedPage(page);
613
+ if (!page.isClosed()) {
614
+ page.close().catch(() => { });
615
+ }
616
+ // Stale capture state is unrecoverable. The error occurs inside puppeteer-stream's second lock section, which has no try/finally — the internal mutex is
617
+ // permanently leaked. All subsequent getStream() calls will hang on it. Chrome restart cannot fix module-level state, so exit and let the service manager
618
+ // restart with a clean process.
619
+ if (errorMessage.includes("Cannot capture a tab with an active stream")) {
620
+ LOG.error("Startup probe detected stale capture state. puppeteer-stream's internal capture mutex is now permanently locked. Exiting so the service " +
621
+ "manager can restart with a clean module state.");
622
+ process.exit(1);
623
+ }
624
+ // Non-stale error. Fail immediately — retrying without restarting Chrome won't change anything, and a non-stale getStream() failure at startup indicates a
625
+ // fundamental problem (extension not loaded, Chrome misconfigured, etc.).
626
+ throw new Error("Capture system verification failed: " + errorMessage);
627
+ }
628
+ }
529
629
  //# sourceMappingURL=setup.js.map