@bnhf/prismcast 1.3.4-2026.2.19
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/LICENSE.md +7 -0
- package/README.md +347 -0
- package/bin/prismcast +6 -0
- package/dist/app.d.ts +6 -0
- package/dist/app.js +315 -0
- package/dist/app.js.map +1 -0
- package/dist/browser/cdp.d.ts +38 -0
- package/dist/browser/cdp.js +155 -0
- package/dist/browser/cdp.js.map +1 -0
- package/dist/browser/channelSelection.d.ts +65 -0
- package/dist/browser/channelSelection.js +202 -0
- package/dist/browser/channelSelection.js.map +1 -0
- package/dist/browser/display.d.ts +34 -0
- package/dist/browser/display.js +54 -0
- package/dist/browser/display.js.map +1 -0
- package/dist/browser/index.d.ts +205 -0
- package/dist/browser/index.js +1205 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/tuning/fox.d.ts +2 -0
- package/dist/browser/tuning/fox.js +83 -0
- package/dist/browser/tuning/fox.js.map +1 -0
- package/dist/browser/tuning/hbo.d.ts +2 -0
- package/dist/browser/tuning/hbo.js +237 -0
- package/dist/browser/tuning/hbo.js.map +1 -0
- package/dist/browser/tuning/hulu.d.ts +2 -0
- package/dist/browser/tuning/hulu.js +550 -0
- package/dist/browser/tuning/hulu.js.map +1 -0
- package/dist/browser/tuning/sling.d.ts +2 -0
- package/dist/browser/tuning/sling.js +518 -0
- package/dist/browser/tuning/sling.js.map +1 -0
- package/dist/browser/tuning/thumbnailRow.d.ts +2 -0
- package/dist/browser/tuning/thumbnailRow.js +108 -0
- package/dist/browser/tuning/thumbnailRow.js.map +1 -0
- package/dist/browser/tuning/tileClick.d.ts +2 -0
- package/dist/browser/tuning/tileClick.js +103 -0
- package/dist/browser/tuning/tileClick.js.map +1 -0
- package/dist/browser/tuning/youtubeTv.d.ts +2 -0
- package/dist/browser/tuning/youtubeTv.js +182 -0
- package/dist/browser/tuning/youtubeTv.js.map +1 -0
- package/dist/browser/video.d.ts +289 -0
- package/dist/browser/video.js +996 -0
- package/dist/browser/video.js.map +1 -0
- package/dist/channels/index.d.ts +3 -0
- package/dist/channels/index.js +392 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/config/index.d.ts +53 -0
- package/dist/config/index.js +233 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/presets.d.ts +98 -0
- package/dist/config/presets.js +241 -0
- package/dist/config/presets.js.map +1 -0
- package/dist/config/profiles.d.ts +79 -0
- package/dist/config/profiles.js +245 -0
- package/dist/config/profiles.js.map +1 -0
- package/dist/config/providers.d.ts +120 -0
- package/dist/config/providers.js +450 -0
- package/dist/config/providers.js.map +1 -0
- package/dist/config/sites.d.ts +22 -0
- package/dist/config/sites.js +377 -0
- package/dist/config/sites.js.map +1 -0
- package/dist/config/userChannels.d.ts +178 -0
- package/dist/config/userChannels.js +543 -0
- package/dist/config/userChannels.js.map +1 -0
- package/dist/config/userConfig.d.ts +235 -0
- package/dist/config/userConfig.js +913 -0
- package/dist/config/userConfig.js.map +1 -0
- package/dist/hdhr/channelMap.d.ts +21 -0
- package/dist/hdhr/channelMap.js +82 -0
- package/dist/hdhr/channelMap.js.map +1 -0
- package/dist/hdhr/deviceId.d.ts +11 -0
- package/dist/hdhr/deviceId.js +84 -0
- package/dist/hdhr/deviceId.js.map +1 -0
- package/dist/hdhr/discover.d.ts +6 -0
- package/dist/hdhr/discover.js +155 -0
- package/dist/hdhr/discover.js.map +1 -0
- package/dist/hdhr/index.d.ts +9 -0
- package/dist/hdhr/index.js +87 -0
- package/dist/hdhr/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +144 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/assets.d.ts +6 -0
- package/dist/routes/assets.js +79 -0
- package/dist/routes/assets.js.map +1 -0
- package/dist/routes/auth.d.ts +6 -0
- package/dist/routes/auth.js +77 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/channels.d.ts +6 -0
- package/dist/routes/channels.js +40 -0
- package/dist/routes/channels.js.map +1 -0
- package/dist/routes/components.d.ts +138 -0
- package/dist/routes/components.js +210 -0
- package/dist/routes/components.js.map +1 -0
- package/dist/routes/config.d.ts +72 -0
- package/dist/routes/config.js +1977 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/debug.d.ts +6 -0
- package/dist/routes/debug.js +274 -0
- package/dist/routes/debug.js.map +1 -0
- package/dist/routes/health.d.ts +6 -0
- package/dist/routes/health.js +85 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/hls.d.ts +6 -0
- package/dist/routes/hls.js +25 -0
- package/dist/routes/hls.js.map +1 -0
- package/dist/routes/index.d.ts +19 -0
- package/dist/routes/index.js +49 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/logs.d.ts +6 -0
- package/dist/routes/logs.js +164 -0
- package/dist/routes/logs.js.map +1 -0
- package/dist/routes/mpegts.d.ts +6 -0
- package/dist/routes/mpegts.js +19 -0
- package/dist/routes/mpegts.js.map +1 -0
- package/dist/routes/play.d.ts +6 -0
- package/dist/routes/play.js +18 -0
- package/dist/routes/play.js.map +1 -0
- package/dist/routes/playlist.d.ts +36 -0
- package/dist/routes/playlist.js +134 -0
- package/dist/routes/playlist.js.map +1 -0
- package/dist/routes/root.d.ts +6 -0
- package/dist/routes/root.js +2920 -0
- package/dist/routes/root.js.map +1 -0
- package/dist/routes/streams.d.ts +6 -0
- package/dist/routes/streams.js +88 -0
- package/dist/routes/streams.js.map +1 -0
- package/dist/routes/theme.d.ts +15 -0
- package/dist/routes/theme.js +275 -0
- package/dist/routes/theme.js.map +1 -0
- package/dist/routes/ui.d.ts +56 -0
- package/dist/routes/ui.js +354 -0
- package/dist/routes/ui.js.map +1 -0
- package/dist/service/commands.d.ts +41 -0
- package/dist/service/commands.js +391 -0
- package/dist/service/commands.js.map +1 -0
- package/dist/service/generators.d.ts +33 -0
- package/dist/service/generators.js +432 -0
- package/dist/service/generators.js.map +1 -0
- package/dist/service/index.d.ts +2 -0
- package/dist/service/index.js +7 -0
- package/dist/service/index.js.map +1 -0
- package/dist/streaming/clients.d.ts +48 -0
- package/dist/streaming/clients.js +114 -0
- package/dist/streaming/clients.js.map +1 -0
- package/dist/streaming/fmp4Segmenter.d.ts +61 -0
- package/dist/streaming/fmp4Segmenter.js +461 -0
- package/dist/streaming/fmp4Segmenter.js.map +1 -0
- package/dist/streaming/hls.d.ts +120 -0
- package/dist/streaming/hls.js +722 -0
- package/dist/streaming/hls.js.map +1 -0
- package/dist/streaming/hlsSegments.d.ts +54 -0
- package/dist/streaming/hlsSegments.js +162 -0
- package/dist/streaming/hlsSegments.js.map +1 -0
- package/dist/streaming/lifecycle.d.ts +33 -0
- package/dist/streaming/lifecycle.js +185 -0
- package/dist/streaming/lifecycle.js.map +1 -0
- package/dist/streaming/monitor.d.ts +74 -0
- package/dist/streaming/monitor.js +1310 -0
- package/dist/streaming/monitor.js.map +1 -0
- package/dist/streaming/mp4Parser.d.ts +74 -0
- package/dist/streaming/mp4Parser.js +566 -0
- package/dist/streaming/mp4Parser.js.map +1 -0
- package/dist/streaming/mpegts.d.ts +14 -0
- package/dist/streaming/mpegts.js +248 -0
- package/dist/streaming/mpegts.js.map +1 -0
- package/dist/streaming/registry.d.ts +119 -0
- package/dist/streaming/registry.js +127 -0
- package/dist/streaming/registry.js.map +1 -0
- package/dist/streaming/setup.d.ts +135 -0
- package/dist/streaming/setup.js +670 -0
- package/dist/streaming/setup.js.map +1 -0
- package/dist/streaming/showInfo.d.ts +30 -0
- package/dist/streaming/showInfo.js +362 -0
- package/dist/streaming/showInfo.js.map +1 -0
- package/dist/streaming/statusEmitter.d.ts +125 -0
- package/dist/streaming/statusEmitter.js +139 -0
- package/dist/streaming/statusEmitter.js.map +1 -0
- package/dist/types/index.d.ts +403 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/debugFilter.d.ts +38 -0
- package/dist/utils/debugFilter.js +157 -0
- package/dist/utils/debugFilter.js.map +1 -0
- package/dist/utils/delay.d.ts +6 -0
- package/dist/utils/delay.js +15 -0
- package/dist/utils/delay.js.map +1 -0
- package/dist/utils/errors.d.ts +15 -0
- package/dist/utils/errors.js +40 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/evaluate.d.ts +51 -0
- package/dist/utils/evaluate.js +124 -0
- package/dist/utils/evaluate.js.map +1 -0
- package/dist/utils/ffmpeg.d.ts +65 -0
- package/dist/utils/ffmpeg.js +317 -0
- package/dist/utils/ffmpeg.js.map +1 -0
- package/dist/utils/fileLogger.d.ts +25 -0
- package/dist/utils/fileLogger.js +248 -0
- package/dist/utils/fileLogger.js.map +1 -0
- package/dist/utils/format.d.ts +16 -0
- package/dist/utils/format.js +46 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/html.d.ts +6 -0
- package/dist/utils/html.js +24 -0
- package/dist/utils/html.js.map +1 -0
- package/dist/utils/index.d.ts +15 -0
- package/dist/utils/index.js +20 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logEmitter.d.ts +17 -0
- package/dist/utils/logEmitter.js +30 -0
- package/dist/utils/logEmitter.js.map +1 -0
- package/dist/utils/logger.d.ts +82 -0
- package/dist/utils/logger.js +219 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/m3u.d.ts +32 -0
- package/dist/utils/m3u.js +148 -0
- package/dist/utils/m3u.js.map +1 -0
- package/dist/utils/morganStream.d.ts +7 -0
- package/dist/utils/morganStream.js +33 -0
- package/dist/utils/morganStream.js.map +1 -0
- package/dist/utils/platform.d.ts +64 -0
- package/dist/utils/platform.js +157 -0
- package/dist/utils/platform.js.map +1 -0
- package/dist/utils/retry.d.ts +15 -0
- package/dist/utils/retry.js +82 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/streamContext.d.ts +28 -0
- package/dist/utils/streamContext.js +33 -0
- package/dist/utils/streamContext.js.map +1 -0
- package/dist/utils/version.d.ts +37 -0
- package/dist/utils/version.js +228 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +92 -0
- package/prismcast.png +0 -0
- package/prismcast.svg +74 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { LOG, formatError, spawnMpegTsRemuxer } from "../utils/index.js";
|
|
2
|
+
import { awaitStreamReadySilent, initializeStream, sendValidationError, validateChannel } from "./hls.js";
|
|
3
|
+
import { getStream, updateLastAccess } from "./registry.js";
|
|
4
|
+
import { registerClient, unregisterClient } from "./clients.js";
|
|
5
|
+
import { CONFIG } from "../config/index.js";
|
|
6
|
+
import { StreamSetupError } from "./setup.js";
|
|
7
|
+
import { getChannelStreamId } from "./lifecycle.js";
|
|
8
|
+
import { waitForInitSegment } from "./hlsSegments.js";
|
|
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)
|
|
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
|
|
11
|
+
* that converts stored fMP4 segments to MPEG-TS with codec copy (no transcoding).
|
|
12
|
+
*
|
|
13
|
+
* Data flow per client:
|
|
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.
|
|
26
|
+
*/
|
|
27
|
+
// Public Endpoint Handler.
|
|
28
|
+
/**
|
|
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.
|
|
34
|
+
*
|
|
35
|
+
* Route: GET /stream/:name
|
|
36
|
+
*
|
|
37
|
+
* @param req - Express request object.
|
|
38
|
+
* @param res - Express response object.
|
|
39
|
+
*/
|
|
40
|
+
export async function handleMpegTsStream(req, res) {
|
|
41
|
+
const channelName = req.params.name;
|
|
42
|
+
if (!channelName) {
|
|
43
|
+
res.status(400).send("Channel name is required.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
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);
|
|
51
|
+
return;
|
|
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/mpeg");
|
|
69
|
+
res.setHeader("transferMode.dlna.org", "Streaming");
|
|
70
|
+
res.flushHeaders();
|
|
71
|
+
// Acquire the stream. If a startup is in progress (another request started it), poll silently. Otherwise, start a new stream via initializeStream().
|
|
72
|
+
let streamId;
|
|
73
|
+
if (existingStreamId === -1) {
|
|
74
|
+
// Another request is already starting this stream. Wait silently (no error responses possible after flush).
|
|
75
|
+
streamId = await awaitStreamReadySilent(channelName);
|
|
76
|
+
if (streamId === null) {
|
|
77
|
+
LOG.warn("MPEG-TS stream startup failed for %s (startup did not complete).", channelName);
|
|
78
|
+
res.end();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// Start a new stream directly. validatedChannel is guaranteed set: this branch runs only when existingStreamId === undefined, which requires successful
|
|
84
|
+
// validation above. Since headers are already flushed, errors are logged and the connection is closed.
|
|
85
|
+
if (!validatedChannel) {
|
|
86
|
+
res.end();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
streamId = await initializeStream({
|
|
91
|
+
channel: validatedChannel,
|
|
92
|
+
channelName,
|
|
93
|
+
clientAddress: req.ip ?? req.socket.remoteAddress ?? null,
|
|
94
|
+
profileOverride: req.query.profile,
|
|
95
|
+
url: validatedChannel.url
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (error instanceof StreamSetupError) {
|
|
100
|
+
LOG.warn("MPEG-TS stream startup failed for %s: %s.", channelName, error.userMessage);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
LOG.warn("MPEG-TS stream startup failed for %s: %s.", channelName, formatError(error));
|
|
104
|
+
}
|
|
105
|
+
res.end();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (streamId === null) {
|
|
109
|
+
LOG.warn("MPEG-TS stream startup failed for %s (terminated during setup).", channelName);
|
|
110
|
+
res.end();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
await serveMpegTsStream(streamId, channelName, req, res);
|
|
115
|
+
}
|
|
116
|
+
// Internal Helpers.
|
|
117
|
+
/**
|
|
118
|
+
* 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
|
|
119
|
+
* shared implementation used by both the fast path (existing stream) and the flush path (new stream).
|
|
120
|
+
*
|
|
121
|
+
* @param streamId - The numeric stream ID.
|
|
122
|
+
* @param channelName - The channel name for logging.
|
|
123
|
+
* @param req - Express request object.
|
|
124
|
+
* @param res - Express response object.
|
|
125
|
+
*/
|
|
126
|
+
async function serveMpegTsStream(streamId, channelName, req, res) {
|
|
127
|
+
// 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.
|
|
128
|
+
const initReady = await waitForInitSegment(streamId, CONFIG.streaming.navigationTimeout);
|
|
129
|
+
if (!initReady) {
|
|
130
|
+
if (!res.headersSent) {
|
|
131
|
+
res.setHeader("Retry-After", "5");
|
|
132
|
+
res.status(503).send("Stream is starting. Please retry.");
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
LOG.warn("MPEG-TS init segment timeout for %s.", channelName);
|
|
136
|
+
res.end();
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Get the stream from the registry and verify it's still alive with a valid init segment.
|
|
141
|
+
const stream = getStream(streamId);
|
|
142
|
+
if (!stream?.hls.initSegment) {
|
|
143
|
+
if (!res.headersSent) {
|
|
144
|
+
res.status(500).send("Stream no longer available.");
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
res.end();
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Capture client address for client tracking. Captured before any async operations so it remains consistent in the cleanup closure, even if the request object
|
|
152
|
+
// becomes unreliable after disconnect.
|
|
153
|
+
const clientAddress = req.ip ?? req.socket.remoteAddress ?? "unknown";
|
|
154
|
+
// Increment the MPEG-TS client counter to prevent idle timeout while this client is connected.
|
|
155
|
+
stream.mpegTsClientCount++;
|
|
156
|
+
updateLastAccess(streamId);
|
|
157
|
+
registerClient(streamId, clientAddress, "mpegts");
|
|
158
|
+
const streamLog = LOG.withStreamId(stream.streamIdStr);
|
|
159
|
+
// Track which segments have been written to FFmpeg stdin to avoid duplicates during the catchup phase. When we subscribe to segment events and then write
|
|
160
|
+
// existing segments, a new segment could arrive via the event that we also encounter in the existing segment iteration. The Set prevents writing it twice.
|
|
161
|
+
const sentSegments = new Set();
|
|
162
|
+
let cleanedUp = false;
|
|
163
|
+
// We declare cleanup as a let initialized to a no-op so the error callback and stdin error handler can reference it before the real implementation is assigned. It
|
|
164
|
+
// is reassigned to the real cleanup function immediately after all handlers are defined, before any asynchronous events can fire.
|
|
165
|
+
let cleanup = () => { };
|
|
166
|
+
// 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
|
|
167
|
+
// MPEG-TS stream on stdout. Video (H264) and audio (AAC) are copied without transcoding.
|
|
168
|
+
const remuxer = spawnMpegTsRemuxer((error) => {
|
|
169
|
+
streamLog.debug("streaming:mpegts", "MPEG-TS remuxer error: %s.", formatError(error));
|
|
170
|
+
cleanup();
|
|
171
|
+
if (!res.writableEnded) {
|
|
172
|
+
res.end();
|
|
173
|
+
}
|
|
174
|
+
}, stream.streamIdStr);
|
|
175
|
+
// Handler for new media segments. Writes each segment to FFmpeg stdin and updates the last access timestamp to prevent idle timeout.
|
|
176
|
+
const onSegment = (filename, data) => {
|
|
177
|
+
if (cleanedUp || sentSegments.has(filename)) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
sentSegments.add(filename);
|
|
181
|
+
remuxer.stdin.write(data);
|
|
182
|
+
updateLastAccess(streamId);
|
|
183
|
+
};
|
|
184
|
+
// Handler for stream termination. Ends FFmpeg stdin gracefully so it can flush remaining data and exit cleanly.
|
|
185
|
+
const onTerminated = () => {
|
|
186
|
+
if (cleanedUp) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
remuxer.stdin.end();
|
|
190
|
+
};
|
|
191
|
+
// Suppress errors from writing to a closed FFmpeg stdin. This can happen during cleanup when the capture stream closes before we stop writing.
|
|
192
|
+
remuxer.stdin.on("error", () => {
|
|
193
|
+
cleanup();
|
|
194
|
+
});
|
|
195
|
+
// Assign the real cleanup function. This is idempotent — the cleanedUp flag ensures it only runs once regardless of which event triggers it first (client
|
|
196
|
+
// disconnect, stream termination, or FFmpeg error).
|
|
197
|
+
cleanup = () => {
|
|
198
|
+
if (cleanedUp) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
cleanedUp = true;
|
|
202
|
+
// Decrement the client counter. Re-read the stream from the registry since it may have been unregistered during stream termination.
|
|
203
|
+
const currentStream = getStream(streamId);
|
|
204
|
+
if (currentStream) {
|
|
205
|
+
currentStream.mpegTsClientCount = Math.max(0, currentStream.mpegTsClientCount - 1);
|
|
206
|
+
// When the last MPEG-TS client disconnects, reset the idle timer so the stream gets the standard idle timeout grace period before cleanup. This gives
|
|
207
|
+
// channel-surfing users time to switch back without the stream being torn down immediately.
|
|
208
|
+
if (currentStream.mpegTsClientCount === 0) {
|
|
209
|
+
updateLastAccess(streamId);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
unregisterClient(streamId, clientAddress, "mpegts");
|
|
213
|
+
stream.hls.segmentEmitter.off("segment", onSegment);
|
|
214
|
+
stream.hls.segmentEmitter.off("terminated", onTerminated);
|
|
215
|
+
remuxer.kill();
|
|
216
|
+
streamLog.debug("streaming:mpegts", "MPEG-TS client disconnected.");
|
|
217
|
+
};
|
|
218
|
+
// Clean up when the client disconnects. Registered immediately after cleanup is assigned to minimize the window where a disconnect could be missed.
|
|
219
|
+
req.on("close", () => {
|
|
220
|
+
cleanup();
|
|
221
|
+
});
|
|
222
|
+
// Subscribe to segment events BEFORE writing existing segments to avoid missing any segments added during the catchup phase.
|
|
223
|
+
stream.hls.segmentEmitter.on("segment", onSegment);
|
|
224
|
+
stream.hls.segmentEmitter.on("terminated", onTerminated);
|
|
225
|
+
// Set response headers if they haven't been flushed yet (fast path for existing streams).
|
|
226
|
+
if (!res.headersSent) {
|
|
227
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
228
|
+
res.setHeader("Connection", "close");
|
|
229
|
+
res.setHeader("Content-Type", "video/mpeg");
|
|
230
|
+
res.setHeader("transferMode.dlna.org", "Streaming");
|
|
231
|
+
}
|
|
232
|
+
// Pipe FFmpeg stdout to the HTTP response. When FFmpeg exits (either from stdin ending or being killed), stdout closes and the response ends automatically.
|
|
233
|
+
remuxer.stdout.pipe(res);
|
|
234
|
+
// Write the init segment first — FFmpeg needs the ftyp and moov boxes before it can process any media segments.
|
|
235
|
+
remuxer.stdin.write(stream.hls.initSegment);
|
|
236
|
+
// Write all existing media segments to provide immediate playback catchup. The sentSegments Set deduplicates against any segments received via the event handler
|
|
237
|
+
// during this iteration.
|
|
238
|
+
for (const [filename, data] of stream.hls.segments) {
|
|
239
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
240
|
+
if (cleanedUp) {
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
sentSegments.add(filename);
|
|
244
|
+
remuxer.stdin.write(data);
|
|
245
|
+
}
|
|
246
|
+
streamLog.debug("streaming:mpegts", "MPEG-TS client connected.");
|
|
247
|
+
}
|
|
248
|
+
//# sourceMappingURL=mpegts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mpegts.js","sourceRoot":"","sources":["../../src/streaming/mpegts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Nullable, ResolvedSiteProfile } from "../types/index.js";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import type { FFmpegProcess } from "../utils/index.js";
|
|
4
|
+
import type { FMP4SegmenterResult } from "./fmp4Segmenter.js";
|
|
5
|
+
import type { Page } from "puppeteer-core";
|
|
6
|
+
import type { Readable } from "node:stream";
|
|
7
|
+
import type { RecoveryMetrics } from "./monitor.js";
|
|
8
|
+
/**
|
|
9
|
+
* HLS segment and playlist storage for a stream. This includes the fMP4 initialization segment (codec configuration), media segments (.m4s files), and the current
|
|
10
|
+
* playlist content. The playlistReady promise allows callers to wait for the first playlist to be generated.
|
|
11
|
+
*
|
|
12
|
+
* Note: HLSState is co-located with the registry because it is part of StreamRegistryEntry. Moving it to hlsSegments.ts would create a circular dependency since
|
|
13
|
+
* hlsSegments.ts imports getStream from registry.ts.
|
|
14
|
+
*/
|
|
15
|
+
export interface HLSState {
|
|
16
|
+
initSegment: Nullable<Buffer>;
|
|
17
|
+
initSegmentReady: Promise<void>;
|
|
18
|
+
playlist: string;
|
|
19
|
+
playlistReady: Promise<void>;
|
|
20
|
+
segmentEmitter: EventEmitter;
|
|
21
|
+
segments: Map<string, Buffer>;
|
|
22
|
+
signalInitSegmentReady: () => void;
|
|
23
|
+
signalPlaylistReady: () => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Stream-specific information for idle detection.
|
|
27
|
+
*/
|
|
28
|
+
export interface StreamInfo {
|
|
29
|
+
lastPlaylistRequest: number;
|
|
30
|
+
storeKey: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Registry entry for an active stream. This is the single source of truth for all stream data, including browser state, HLS segments, and the segmenter reference.
|
|
34
|
+
*/
|
|
35
|
+
export interface StreamRegistryEntry {
|
|
36
|
+
channelName: Nullable<string>;
|
|
37
|
+
clientAddress: Nullable<string>;
|
|
38
|
+
ffmpegProcess: Nullable<FFmpegProcess>;
|
|
39
|
+
hls: HLSState;
|
|
40
|
+
id: number;
|
|
41
|
+
mpegTsClientCount: number;
|
|
42
|
+
info: StreamInfo;
|
|
43
|
+
page: Page;
|
|
44
|
+
profile: ResolvedSiteProfile;
|
|
45
|
+
rawCaptureStream: Nullable<Readable>;
|
|
46
|
+
segmenter: Nullable<FMP4SegmenterResult>;
|
|
47
|
+
startTime: Date;
|
|
48
|
+
stopMonitor: Nullable<() => RecoveryMetrics>;
|
|
49
|
+
streamIdStr: string;
|
|
50
|
+
url: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Gets the next unique stream ID by incrementing the counter. Each call returns a new, higher ID that has never been used before in this process lifetime.
|
|
54
|
+
* @returns The next unique stream ID.
|
|
55
|
+
*/
|
|
56
|
+
export declare function getNextStreamId(): number;
|
|
57
|
+
/**
|
|
58
|
+
* Registers a stream in the registry. This should be called after stream setup is complete and the stream is ready to serve data.
|
|
59
|
+
* @param entry - The stream registry entry to add.
|
|
60
|
+
*/
|
|
61
|
+
export declare function registerStream(entry: StreamRegistryEntry): void;
|
|
62
|
+
/**
|
|
63
|
+
* Unregisters a stream from the registry. This should be called during stream cleanup to remove the stream from tracking.
|
|
64
|
+
* @param id - The numeric stream ID to remove.
|
|
65
|
+
*/
|
|
66
|
+
export declare function unregisterStream(id: number): void;
|
|
67
|
+
/**
|
|
68
|
+
* Gets a stream entry by its ID.
|
|
69
|
+
* @param id - The numeric stream ID to look up.
|
|
70
|
+
* @returns The stream entry if found, undefined otherwise.
|
|
71
|
+
*/
|
|
72
|
+
export declare function getStream(id: number): StreamRegistryEntry | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* Gets all stream entries in the registry.
|
|
75
|
+
* @returns Array of all stream registry entries.
|
|
76
|
+
*/
|
|
77
|
+
export declare function getAllStreams(): StreamRegistryEntry[];
|
|
78
|
+
/**
|
|
79
|
+
* Gets the total number of streams in the registry.
|
|
80
|
+
* @returns The number of active streams.
|
|
81
|
+
*/
|
|
82
|
+
export declare function getStreamCount(): number;
|
|
83
|
+
/**
|
|
84
|
+
* Updates the last playlist request timestamp for a stream. This should be called whenever a playlist or segment is requested to keep the idle timeout accurate.
|
|
85
|
+
* @param id - The numeric stream ID.
|
|
86
|
+
*/
|
|
87
|
+
export declare function updateLastAccess(id: number): void;
|
|
88
|
+
/**
|
|
89
|
+
* Creates the initial HLS state for a new stream. This sets up empty segment storage and the playlist readiness signaling mechanism.
|
|
90
|
+
* @returns A new HLSState object ready to receive segments.
|
|
91
|
+
*/
|
|
92
|
+
export declare function createHLSState(): HLSState;
|
|
93
|
+
/**
|
|
94
|
+
* Memory usage breakdown for a stream's HLS segment storage.
|
|
95
|
+
*/
|
|
96
|
+
export interface StreamMemoryUsage {
|
|
97
|
+
initSegment: number;
|
|
98
|
+
segments: number;
|
|
99
|
+
total: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Calculates the memory usage for a single stream's HLS segment storage. This measures the Buffer sizes of the init segment and all media segments currently retained
|
|
103
|
+
* in memory.
|
|
104
|
+
* @param entry - The stream registry entry to measure.
|
|
105
|
+
* @returns Memory usage breakdown in bytes.
|
|
106
|
+
*/
|
|
107
|
+
export declare function getStreamMemoryUsage(entry: StreamRegistryEntry): StreamMemoryUsage;
|
|
108
|
+
/**
|
|
109
|
+
* Calculates the total segment memory usage across all active streams. This is useful for monitoring overall memory consumption by HLS buffers.
|
|
110
|
+
* @returns Total memory usage in bytes across all streams.
|
|
111
|
+
*/
|
|
112
|
+
export declare function getTotalSegmentMemory(): number;
|
|
113
|
+
/**
|
|
114
|
+
* Gets the size in bytes of the last segment stored for a stream. Used by the monitor to detect dead capture pipelines that produce empty segments (18 bytes observed)
|
|
115
|
+
* while the video element appears healthy.
|
|
116
|
+
* @param entry - The stream registry entry to query.
|
|
117
|
+
* @returns Segment size in bytes, or null if no segmenter exists.
|
|
118
|
+
*/
|
|
119
|
+
export declare function getLastSegmentSize(entry: StreamRegistryEntry): Nullable<number>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
// State.
|
|
3
|
+
// The unified stream registry. Maps numeric stream IDs to stream entries.
|
|
4
|
+
const streamRegistry = new Map();
|
|
5
|
+
// Counter for generating unique stream IDs. Incremented for each new stream.
|
|
6
|
+
let streamIdCounter = 0;
|
|
7
|
+
// Public API.
|
|
8
|
+
/**
|
|
9
|
+
* Gets the next unique stream ID by incrementing the counter. Each call returns a new, higher ID that has never been used before in this process lifetime.
|
|
10
|
+
* @returns The next unique stream ID.
|
|
11
|
+
*/
|
|
12
|
+
export function getNextStreamId() {
|
|
13
|
+
return ++streamIdCounter;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Registers a stream in the registry. This should be called after stream setup is complete and the stream is ready to serve data.
|
|
17
|
+
* @param entry - The stream registry entry to add.
|
|
18
|
+
*/
|
|
19
|
+
export function registerStream(entry) {
|
|
20
|
+
streamRegistry.set(entry.id, entry);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Unregisters a stream from the registry. This should be called during stream cleanup to remove the stream from tracking.
|
|
24
|
+
* @param id - The numeric stream ID to remove.
|
|
25
|
+
*/
|
|
26
|
+
export function unregisterStream(id) {
|
|
27
|
+
streamRegistry.delete(id);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Gets a stream entry by its ID.
|
|
31
|
+
* @param id - The numeric stream ID to look up.
|
|
32
|
+
* @returns The stream entry if found, undefined otherwise.
|
|
33
|
+
*/
|
|
34
|
+
export function getStream(id) {
|
|
35
|
+
return streamRegistry.get(id);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Gets all stream entries in the registry.
|
|
39
|
+
* @returns Array of all stream registry entries.
|
|
40
|
+
*/
|
|
41
|
+
export function getAllStreams() {
|
|
42
|
+
return Array.from(streamRegistry.values());
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Gets the total number of streams in the registry.
|
|
46
|
+
* @returns The number of active streams.
|
|
47
|
+
*/
|
|
48
|
+
export function getStreamCount() {
|
|
49
|
+
return streamRegistry.size;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Updates the last playlist request timestamp for a stream. This should be called whenever a playlist or segment is requested to keep the idle timeout accurate.
|
|
53
|
+
* @param id - The numeric stream ID.
|
|
54
|
+
*/
|
|
55
|
+
export function updateLastAccess(id) {
|
|
56
|
+
const entry = streamRegistry.get(id);
|
|
57
|
+
if (entry) {
|
|
58
|
+
entry.info.lastPlaylistRequest = Date.now();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Creates the initial HLS state for a new stream. This sets up empty segment storage and the playlist readiness signaling mechanism.
|
|
63
|
+
* @returns A new HLSState object ready to receive segments.
|
|
64
|
+
*/
|
|
65
|
+
export function createHLSState() {
|
|
66
|
+
let signalInitSegmentReady = () => { };
|
|
67
|
+
let signalPlaylistReady = () => { };
|
|
68
|
+
const initSegmentReady = new Promise((resolve) => {
|
|
69
|
+
signalInitSegmentReady = resolve;
|
|
70
|
+
});
|
|
71
|
+
const playlistReady = new Promise((resolve) => {
|
|
72
|
+
signalPlaylistReady = resolve;
|
|
73
|
+
});
|
|
74
|
+
const segmentEmitter = new EventEmitter();
|
|
75
|
+
// Allow up to 20 listeners per event to support multiple concurrent MPEG-TS clients consuming the same stream.
|
|
76
|
+
segmentEmitter.setMaxListeners(20);
|
|
77
|
+
return {
|
|
78
|
+
initSegment: null,
|
|
79
|
+
initSegmentReady,
|
|
80
|
+
playlist: "",
|
|
81
|
+
playlistReady,
|
|
82
|
+
segmentEmitter,
|
|
83
|
+
segments: new Map(),
|
|
84
|
+
signalInitSegmentReady,
|
|
85
|
+
signalPlaylistReady
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Calculates the memory usage for a single stream's HLS segment storage. This measures the Buffer sizes of the init segment and all media segments currently retained
|
|
90
|
+
* in memory.
|
|
91
|
+
* @param entry - The stream registry entry to measure.
|
|
92
|
+
* @returns Memory usage breakdown in bytes.
|
|
93
|
+
*/
|
|
94
|
+
export function getStreamMemoryUsage(entry) {
|
|
95
|
+
const initSegmentSize = entry.hls.initSegment?.length ?? 0;
|
|
96
|
+
let segmentsSize = 0;
|
|
97
|
+
for (const segment of entry.hls.segments.values()) {
|
|
98
|
+
segmentsSize += segment.length;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
initSegment: initSegmentSize,
|
|
102
|
+
segments: segmentsSize,
|
|
103
|
+
total: initSegmentSize + segmentsSize
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Calculates the total segment memory usage across all active streams. This is useful for monitoring overall memory consumption by HLS buffers.
|
|
108
|
+
* @returns Total memory usage in bytes across all streams.
|
|
109
|
+
*/
|
|
110
|
+
export function getTotalSegmentMemory() {
|
|
111
|
+
let total = 0;
|
|
112
|
+
for (const entry of streamRegistry.values()) {
|
|
113
|
+
total += getStreamMemoryUsage(entry).total;
|
|
114
|
+
}
|
|
115
|
+
return total;
|
|
116
|
+
}
|
|
117
|
+
// Segment Health.
|
|
118
|
+
/**
|
|
119
|
+
* Gets the size in bytes of the last segment stored for a stream. Used by the monitor to detect dead capture pipelines that produce empty segments (18 bytes observed)
|
|
120
|
+
* while the video element appears healthy.
|
|
121
|
+
* @param entry - The stream registry entry to query.
|
|
122
|
+
* @returns Segment size in bytes, or null if no segmenter exists.
|
|
123
|
+
*/
|
|
124
|
+
export function getLastSegmentSize(entry) {
|
|
125
|
+
return entry.segmenter?.getLastSegmentSize() ?? null;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/streaming/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { Channel, Nullable, ResolvedSiteProfile, UrlValidation } from "../types/index.js";
|
|
2
|
+
import type { Frame, Page } from "puppeteer-core";
|
|
3
|
+
import type { RecoveryMetrics, TabReplacementResult } from "./monitor.js";
|
|
4
|
+
import type { FFmpegProcess } from "../utils/index.js";
|
|
5
|
+
import type { Readable } from "node:stream";
|
|
6
|
+
/**
|
|
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
|
+
* create a handler with access to all necessary context.
|
|
9
|
+
*/
|
|
10
|
+
export type TabReplacementHandlerFactory = (numericStreamId: number, streamId: string, profile: ResolvedSiteProfile, metadataComment: string | undefined) => () => Promise<Nullable<TabReplacementResult>>;
|
|
11
|
+
/**
|
|
12
|
+
* Options for setting up a stream.
|
|
13
|
+
*/
|
|
14
|
+
export interface StreamSetupOptions {
|
|
15
|
+
channel?: Channel;
|
|
16
|
+
channelName?: string;
|
|
17
|
+
channelSelector?: string;
|
|
18
|
+
clickSelector?: string;
|
|
19
|
+
clickToPlay?: boolean;
|
|
20
|
+
noVideo?: boolean;
|
|
21
|
+
onTabReplacementFactory?: TabReplacementHandlerFactory;
|
|
22
|
+
profileOverride?: string;
|
|
23
|
+
url: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Result from setting up a stream.
|
|
27
|
+
*/
|
|
28
|
+
export interface StreamSetupResult {
|
|
29
|
+
captureStream: Readable;
|
|
30
|
+
channelName: Nullable<string>;
|
|
31
|
+
cleanup: () => Promise<void>;
|
|
32
|
+
ffmpegProcess: Nullable<FFmpegProcess>;
|
|
33
|
+
numericStreamId: number;
|
|
34
|
+
page: Page;
|
|
35
|
+
profile: ResolvedSiteProfile;
|
|
36
|
+
profileName: string;
|
|
37
|
+
providerName: string;
|
|
38
|
+
rawCaptureStream: Readable;
|
|
39
|
+
startTime: Date;
|
|
40
|
+
stopMonitor: () => RecoveryMetrics;
|
|
41
|
+
streamId: string;
|
|
42
|
+
url: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Error thrown when stream setup fails. Includes HTTP status code and user-friendly message for the response.
|
|
46
|
+
*/
|
|
47
|
+
export declare class StreamSetupError extends Error {
|
|
48
|
+
readonly statusCode: number;
|
|
49
|
+
readonly userMessage: string;
|
|
50
|
+
constructor(message: string, statusCode: number, userMessage: string);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Options for creating a page with capture.
|
|
54
|
+
*/
|
|
55
|
+
export interface CreatePageWithCaptureOptions {
|
|
56
|
+
comment?: string;
|
|
57
|
+
onFFmpegError?: (error: Error) => void;
|
|
58
|
+
profile: ResolvedSiteProfile;
|
|
59
|
+
streamId: string;
|
|
60
|
+
url: string;
|
|
61
|
+
_pageClosedRetries?: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Result from creating a page with capture. Contains everything needed to create a segmenter and continue with stream setup.
|
|
65
|
+
*/
|
|
66
|
+
export interface CreatePageWithCaptureResult {
|
|
67
|
+
captureStream: Readable;
|
|
68
|
+
context: Frame | Page;
|
|
69
|
+
ffmpegProcess: Nullable<FFmpegProcess>;
|
|
70
|
+
page: Page;
|
|
71
|
+
rawCaptureStream: Readable;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Generates a concise stream identifier for logging purposes. The identifier combines the channel name or hostname with a unique request ID, making it easy to
|
|
75
|
+
* trace related log messages. We prefer the channel name when available because it's more meaningful than a hostname.
|
|
76
|
+
* @param channelName - The channel name if streaming a named channel.
|
|
77
|
+
* @param url - The URL being streamed.
|
|
78
|
+
* @returns A concise stream identifier.
|
|
79
|
+
*/
|
|
80
|
+
export declare function generateStreamId(channelName: string | undefined, url: string | undefined): string;
|
|
81
|
+
/**
|
|
82
|
+
* Validates a URL before attempting to navigate to it. This function checks for supported protocols, prevents local file access, and ensures the URL is properly
|
|
83
|
+
* formatted. Validating URLs before navigation prevents security issues and provides clear error messages.
|
|
84
|
+
* @param url - The URL to validate.
|
|
85
|
+
* @returns Validation result with optional reason for failure.
|
|
86
|
+
*/
|
|
87
|
+
export declare function validateStreamUrl(url: string | undefined): UrlValidation;
|
|
88
|
+
/**
|
|
89
|
+
* 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
|
|
90
|
+
* recovery. It handles:
|
|
91
|
+
* - Creating a new browser page with CSP bypass
|
|
92
|
+
* - Initializing media capture (native fMP4 or WebM+FFmpeg)
|
|
93
|
+
* - Navigating to the URL with retry
|
|
94
|
+
* - Setting up video playback via navigateToPage() + initializePlayback()
|
|
95
|
+
*
|
|
96
|
+
* The caller is responsible for:
|
|
97
|
+
* - Creating the segmenter and piping captureStream to it
|
|
98
|
+
* - Registering/updating the stream in the registry
|
|
99
|
+
* - Starting/updating the health monitor
|
|
100
|
+
* - Handling cleanup on failure
|
|
101
|
+
*
|
|
102
|
+
* @param options - Options for page and capture creation.
|
|
103
|
+
* @returns The page, context, capture stream, and FFmpeg process (if any).
|
|
104
|
+
* @throws Error if page creation, capture initialization, or navigation fails.
|
|
105
|
+
*/
|
|
106
|
+
export declare function createPageWithCapture(options: CreatePageWithCaptureOptions): Promise<CreatePageWithCaptureResult>;
|
|
107
|
+
/**
|
|
108
|
+
* Sets up a stream: validates input, creates browser page, initializes capture, navigates to URL, and starts health monitoring.
|
|
109
|
+
*
|
|
110
|
+
* This function handles all common stream setup logic. The caller is responsible for:
|
|
111
|
+
* - Connecting the returned captureStream to the appropriate output (HTTP response, FFmpeg, etc.)
|
|
112
|
+
* - Registering the stream in the registry
|
|
113
|
+
* - Triggering cleanup when the stream ends
|
|
114
|
+
*
|
|
115
|
+
* @param options - Stream configuration options.
|
|
116
|
+
* @param onCircuitBreak - Callback invoked when the circuit breaker trips (stream unrecoverable).
|
|
117
|
+
* @returns Setup result with capture stream, cleanup function, and metadata.
|
|
118
|
+
* @throws StreamSetupError if setup fails with appropriate status code and message.
|
|
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
|
+
* After a system reboot, Chrome's display stack or capture extension may not be ready when the service manager starts PrismCast. The probe retries up to
|
|
130
|
+
* PROBE_MAX_ATTEMPTS times with a delay between attempts, giving the system time to settle before giving up. This prevents a rapid restart storm where the service
|
|
131
|
+
* manager relaunches PrismCast repeatedly, each attempt orphaning a Chrome process and degrading the environment further.
|
|
132
|
+
*
|
|
133
|
+
* If stale capture state is detected, the process exits immediately — Chrome restart cannot fix the leaked mutex, only a fresh process can.
|
|
134
|
+
*/
|
|
135
|
+
export declare function verifyCaptureSystem(): Promise<void>;
|