prismcast 1.5.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -28
- package/dist/app.d.ts +5 -0
- package/dist/app.js +63 -5
- package/dist/app.js.map +1 -1
- package/dist/browser/channelSelection.js +14 -2
- package/dist/browser/channelSelection.js.map +1 -1
- package/dist/browser/index.d.ts +23 -9
- package/dist/browser/index.js +205 -88
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/precaching.js +3 -3
- package/dist/browser/precaching.js.map +1 -1
- package/dist/browser/tuning/directv.js +13 -0
- package/dist/browser/tuning/directv.js.map +1 -1
- package/dist/browser/tuning/fox.js +12 -0
- package/dist/browser/tuning/fox.js.map +1 -1
- package/dist/browser/tuning/hbo.js +11 -0
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.js +23 -0
- package/dist/browser/tuning/hulu.js.map +1 -1
- package/dist/browser/tuning/sling.js +34 -4
- package/dist/browser/tuning/sling.js.map +1 -1
- package/dist/browser/tuning/spectrum.js +12 -0
- package/dist/browser/tuning/spectrum.js.map +1 -1
- package/dist/browser/tuning/xfinity.d.ts +2 -0
- package/dist/browser/tuning/xfinity.js +607 -0
- package/dist/browser/tuning/xfinity.js.map +1 -0
- package/dist/browser/tuning/youtubeTv.js +14 -0
- package/dist/browser/tuning/youtubeTv.js.map +1 -1
- package/dist/browser/video.d.ts +2 -0
- package/dist/browser/video.js +99 -17
- package/dist/browser/video.js.map +1 -1
- package/dist/channels/index.js +173 -56
- package/dist/channels/index.js.map +1 -1
- package/dist/config/health.d.ts +28 -28
- package/dist/config/health.js +52 -52
- package/dist/config/health.js.map +1 -1
- package/dist/config/paths.d.ts +12 -0
- package/dist/config/paths.js +16 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/profiles.d.ts +3 -2
- package/dist/config/profiles.js +45 -34
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/providers.d.ts +7 -0
- package/dist/config/providers.js +14 -0
- package/dist/config/providers.js.map +1 -1
- package/dist/config/sites.d.ts +28 -0
- package/dist/config/sites.js +93 -122
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +3 -0
- package/dist/config/userChannels.js +10 -3
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +1 -0
- package/dist/config/userConfig.js +4 -0
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +0 -1
- package/dist/config/userProfiles.js +18 -10
- package/dist/config/userProfiles.js.map +1 -1
- package/dist/index.js +16 -7
- package/dist/index.js.map +1 -1
- package/dist/native/decrypt.d.ts +32 -0
- package/dist/native/decrypt.js +85 -0
- package/dist/native/decrypt.js.map +1 -0
- package/dist/native/index.d.ts +58 -0
- package/dist/native/index.js +341 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/intercept.d.ts +35 -0
- package/dist/native/intercept.js +184 -0
- package/dist/native/intercept.js.map +1 -0
- package/dist/native/probe.d.ts +48 -0
- package/dist/native/probe.js +270 -0
- package/dist/native/probe.js.map +1 -0
- package/dist/native/proxy.d.ts +52 -0
- package/dist/native/proxy.js +904 -0
- package/dist/native/proxy.js.map +1 -0
- package/dist/routes/config/channels/table.js +19 -25
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/providers.js +4 -4
- package/dist/routes/config/providers.js.map +1 -1
- package/dist/routes/debug.js +5 -5
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/hls.js +12 -2
- package/dist/routes/hls.js.map +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.js +3 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/root/content.js +80 -20
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/scripts/status.js +14 -13
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/root/styles.js +4 -2
- package/dist/routes/root/styles.js.map +1 -1
- package/dist/streaming/fmp4Segmenter.d.ts +3 -0
- package/dist/streaming/fmp4Segmenter.js +143 -42
- package/dist/streaming/fmp4Segmenter.js.map +1 -1
- package/dist/streaming/hls.d.ts +24 -19
- package/dist/streaming/hls.js +486 -261
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +7 -0
- package/dist/streaming/hlsResume.js +22 -2
- package/dist/streaming/hlsResume.js.map +1 -1
- package/dist/streaming/hlsSegments.d.ts +42 -2
- package/dist/streaming/hlsSegments.js +109 -8
- package/dist/streaming/hlsSegments.js.map +1 -1
- package/dist/streaming/lifecycle.js +20 -2
- package/dist/streaming/lifecycle.js.map +1 -1
- package/dist/streaming/monitor.d.ts +1 -0
- package/dist/streaming/monitor.js +369 -21
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/mp4Parser.d.ts +52 -6
- package/dist/streaming/mp4Parser.js +156 -30
- package/dist/streaming/mp4Parser.js.map +1 -1
- package/dist/streaming/mpegts.d.ts +2 -2
- package/dist/streaming/mpegts.js +156 -109
- package/dist/streaming/mpegts.js.map +1 -1
- package/dist/streaming/playlistBuilder.d.ts +38 -0
- package/dist/streaming/playlistBuilder.js +66 -0
- package/dist/streaming/playlistBuilder.js.map +1 -0
- package/dist/streaming/preroll.d.ts +106 -0
- package/dist/streaming/preroll.js +373 -0
- package/dist/streaming/preroll.js.map +1 -0
- package/dist/streaming/pretune.d.ts +8 -0
- package/dist/streaming/pretune.js +226 -0
- package/dist/streaming/pretune.js.map +1 -0
- package/dist/streaming/registry.d.ts +24 -3
- package/dist/streaming/registry.js +23 -1
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +7 -0
- package/dist/streaming/setup.js +27 -6
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +24 -0
- package/dist/streaming/showInfo.js +55 -3
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +6 -4
- package/dist/streaming/statusEmitter.js +9 -3
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/channels.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/profiles.d.ts +8 -1
- package/dist/types/selection.d.ts +4 -1
- package/dist/types/streaming.d.ts +7 -0
- package/dist/utils/chromeFetch.d.ts +20 -0
- package/dist/utils/chromeFetch.js +43 -0
- package/dist/utils/chromeFetch.js.map +1 -0
- package/dist/utils/debugFilter.js +15 -0
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/utils/delay.d.ts +16 -0
- package/dist/utils/delay.js +15 -1
- package/dist/utils/delay.js.map +1 -1
- package/dist/utils/ffmpeg.d.ts +1 -7
- package/dist/utils/ffmpeg.js +86 -127
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/pid.d.ts +40 -0
- package/dist/utils/pid.js +83 -0
- package/dist/utils/pid.js.map +1 -0
- package/package.json +5 -5
package/dist/streaming/mpegts.js
CHANGED
|
@@ -1,33 +1,30 @@
|
|
|
1
|
+
/* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* mpegts.ts: MPEG-TS streaming handler for PrismCast.
|
|
4
|
+
*/
|
|
1
5
|
import { LOG, formatError, spawnMpegTsRemuxer } from "../utils/index.js";
|
|
2
|
-
import { awaitStreamReadySilent, initializeStream, sendValidationError, validateChannel } from "./hls.js";
|
|
3
6
|
import { getStream, updateLastAccess } from "./registry.js";
|
|
7
|
+
import { initializeStream, sendValidationError, validateChannel } from "./hls.js";
|
|
4
8
|
import { registerClient, unregisterClient } from "./clients.js";
|
|
5
9
|
import { CONFIG } from "../config/index.js";
|
|
6
10
|
import { StreamSetupError } from "./setup.js";
|
|
7
11
|
import { getChannelStreamId } from "./lifecycle.js";
|
|
8
12
|
import { waitForInitSegment } from "./hlsSegments.js";
|
|
9
|
-
/* This module provides a continuous MPEG-TS byte stream
|
|
10
|
-
*
|
|
11
|
-
*
|
|
13
|
+
/* This module provides a continuous MPEG-TS byte stream for HDHomeRun-compatible clients (such as Plex) that expect raw MPEG-TS when tuning a channel. Two delivery
|
|
14
|
+
* modes are supported:
|
|
15
|
+
*
|
|
16
|
+
* Capture mode (fMP4 → MPEG-TS): The capture pipeline produces fMP4 segments. Each MPEG-TS client gets its own FFmpeg remuxer that converts fMP4 to MPEG-TS with
|
|
17
|
+
* codec copy (no transcoding). FFmpeg reads the init segment + media segments from stdin and outputs a continuous MPEG-TS stream on stdout, piped to the HTTP response.
|
|
12
18
|
*
|
|
13
|
-
*
|
|
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
|
|
19
|
+
* Native mode (MPEG-TS pass-through): The native HLS proxy already produces MPEG-TS segments. These are written directly to the HTTP response without any remuxing.
|
|
23
20
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* Both modes share the same client lifecycle via connectMpegTsClient(): register the client, subscribe to segment events for real-time delivery, write existing
|
|
22
|
+
* segments for catchup, and clean up on disconnect. The header flush for new streams prevents client timeouts during the 4-10+ second startup sequence.
|
|
26
23
|
*/
|
|
27
24
|
// Public Endpoint Handler.
|
|
28
25
|
/**
|
|
29
|
-
* Handles MPEG-TS stream requests. Validates the channel, flushes HTTP headers early for new streams, then ensures a
|
|
30
|
-
*
|
|
26
|
+
* Handles MPEG-TS stream requests. Validates the channel, flushes HTTP headers early for new streams, then ensures a stream is running and delegates to the
|
|
27
|
+
* mode-appropriate serving path (FFmpeg remuxer for capture mode, direct pass-through for native mode).
|
|
31
28
|
*
|
|
32
29
|
* 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
30
|
* 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.
|
|
@@ -45,21 +42,17 @@ export async function handleMpegTsStream(req, res) {
|
|
|
45
42
|
}
|
|
46
43
|
// Check for an existing stream first. If one exists, we can skip validation and header flushing.
|
|
47
44
|
const existingStreamId = getChannelStreamId(channelName);
|
|
48
|
-
// Fast path: a
|
|
49
|
-
if (
|
|
45
|
+
// Fast path: a stream already exists (either fully set up or a pending entry from an HLS request). Serve it directly.
|
|
46
|
+
if (existingStreamId !== undefined) {
|
|
50
47
|
await serveMpegTsStream(existingStreamId, channelName, req, res);
|
|
51
48
|
return;
|
|
52
49
|
}
|
|
53
|
-
//
|
|
54
|
-
// channels,
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
sendValidationError(validation, res);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
validatedChannel = validation.channel;
|
|
50
|
+
// No existing stream — validate the channel before flushing headers. This ensures we can still return proper error responses for invalid channels, disabled
|
|
51
|
+
// channels, and login mode.
|
|
52
|
+
const validation = validateChannel(channelName);
|
|
53
|
+
if (!validation.valid) {
|
|
54
|
+
sendValidationError(validation, res);
|
|
55
|
+
return;
|
|
63
56
|
}
|
|
64
57
|
// 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
58
|
// failures will close the connection with no data.
|
|
@@ -68,55 +61,40 @@ export async function handleMpegTsStream(req, res) {
|
|
|
68
61
|
res.setHeader("Content-Type", "video/mpeg");
|
|
69
62
|
res.setHeader("transferMode.dlna.org", "Streaming");
|
|
70
63
|
res.flushHeaders();
|
|
71
|
-
//
|
|
64
|
+
// Start a new stream directly. initializeStream blocks until setup completes (no preroll for MPEG-TS clients). Since headers are already flushed, errors are logged
|
|
65
|
+
// and the connection is closed.
|
|
72
66
|
let streamId;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
67
|
+
try {
|
|
68
|
+
streamId = await initializeStream({
|
|
69
|
+
channel: validation.channel,
|
|
70
|
+
channelName,
|
|
71
|
+
clientAddress: req.ip ?? req.socket.remoteAddress ?? null,
|
|
72
|
+
mpegTsClient: true,
|
|
73
|
+
profileOverride: req.query.profile,
|
|
74
|
+
url: validation.channel.url
|
|
75
|
+
});
|
|
81
76
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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;
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof StreamSetupError) {
|
|
79
|
+
LOG.warn("MPEG-TS stream startup failed for %s: %s.", channelName, error.userMessage);
|
|
107
80
|
}
|
|
108
|
-
|
|
109
|
-
LOG.warn("MPEG-TS stream startup failed for %s
|
|
110
|
-
res.end();
|
|
111
|
-
return;
|
|
81
|
+
else {
|
|
82
|
+
LOG.warn("MPEG-TS stream startup failed for %s: %s.", channelName, formatError(error));
|
|
112
83
|
}
|
|
84
|
+
res.end();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (streamId === null) {
|
|
88
|
+
LOG.warn("MPEG-TS stream startup failed for %s (terminated during setup).", channelName);
|
|
89
|
+
res.end();
|
|
90
|
+
return;
|
|
113
91
|
}
|
|
114
92
|
await serveMpegTsStream(streamId, channelName, req, res);
|
|
115
93
|
}
|
|
116
94
|
// Internal Helpers.
|
|
117
95
|
/**
|
|
118
|
-
* Serves the MPEG-TS stream once a stream ID is available. Waits for
|
|
119
|
-
*
|
|
96
|
+
* Serves the MPEG-TS stream once a stream ID is available. Waits for init segment readiness, then delegates to mode-specific serving: capture mode spawns an FFmpeg
|
|
97
|
+
* remuxer to convert fMP4 to MPEG-TS, while native mode passes .ts segments directly to the response. Both paths share client lifecycle via connectMpegTsClient().
|
|
120
98
|
*
|
|
121
99
|
* @param streamId - The numeric stream ID.
|
|
122
100
|
* @param channelName - The channel name for logging.
|
|
@@ -124,7 +102,8 @@ export async function handleMpegTsStream(req, res) {
|
|
|
124
102
|
* @param res - Express response object.
|
|
125
103
|
*/
|
|
126
104
|
async function serveMpegTsStream(streamId, channelName, req, res) {
|
|
127
|
-
// Wait for the init segment to be available.
|
|
105
|
+
// Wait for the init segment to be available. For capture-mode streams, this waits for the fMP4 init segment (ftyp+moov). For native-mode streams,
|
|
106
|
+
// signalInitSegmentReady() was called immediately during setup, so this returns instantly.
|
|
128
107
|
const initReady = await waitForInitSegment(streamId, CONFIG.streaming.navigationTimeout);
|
|
129
108
|
if (!initReady) {
|
|
130
109
|
if (!res.headersSent) {
|
|
@@ -137,9 +116,40 @@ async function serveMpegTsStream(streamId, channelName, req, res) {
|
|
|
137
116
|
}
|
|
138
117
|
return;
|
|
139
118
|
}
|
|
140
|
-
// Get the stream from the registry
|
|
119
|
+
// Get the stream from the registry.
|
|
141
120
|
const stream = getStream(streamId);
|
|
142
|
-
if (!stream
|
|
121
|
+
if (!stream) {
|
|
122
|
+
if (!res.headersSent) {
|
|
123
|
+
res.status(500).send("Stream no longer available.");
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
res.end();
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// For native-mode streams, segments are already MPEG-TS — write them directly to the response without FFmpeg.
|
|
131
|
+
if (stream.streamingMode === "native") {
|
|
132
|
+
connectMpegTsClient({
|
|
133
|
+
logLabel: "Native MPEG-TS",
|
|
134
|
+
onStreamTerminated: () => {
|
|
135
|
+
if (!res.writableEnded) {
|
|
136
|
+
res.end();
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
req,
|
|
140
|
+
res,
|
|
141
|
+
stream,
|
|
142
|
+
streamId,
|
|
143
|
+
writeSegment: (data) => {
|
|
144
|
+
if (!res.writableEnded) {
|
|
145
|
+
res.write(data);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// For capture-mode streams, verify the init segment is available (required by FFmpeg).
|
|
152
|
+
if (!stream.hls.initSegment) {
|
|
143
153
|
if (!res.headersSent) {
|
|
144
154
|
res.status(500).send("Stream no longer available.");
|
|
145
155
|
}
|
|
@@ -148,23 +158,11 @@ async function serveMpegTsStream(streamId, channelName, req, res) {
|
|
|
148
158
|
}
|
|
149
159
|
return;
|
|
150
160
|
}
|
|
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
161
|
// 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.
|
|
162
|
+
// MPEG-TS stream on stdout. Video (H264) and audio (AAC) are copied without transcoding. We declare cleanup as a let so the error callback can reference it before
|
|
163
|
+
// connectMpegTsClient assigns the real implementation — all assignments happen synchronously before any async events can fire.
|
|
164
|
+
let cleanup = () => { };
|
|
165
|
+
const streamLog = LOG.withStreamId(stream.streamIdStr);
|
|
168
166
|
const remuxer = spawnMpegTsRemuxer((error) => {
|
|
169
167
|
streamLog.debug("streaming:mpegts", "MPEG-TS remuxer error: %s.", formatError(error));
|
|
170
168
|
cleanup();
|
|
@@ -172,29 +170,79 @@ async function serveMpegTsStream(streamId, channelName, req, res) {
|
|
|
172
170
|
res.end();
|
|
173
171
|
}
|
|
174
172
|
}, stream.streamIdStr);
|
|
175
|
-
//
|
|
173
|
+
// Suppress errors from writing to a closed FFmpeg stdin. This can happen during cleanup when the capture stream closes before we stop writing.
|
|
174
|
+
remuxer.stdin.on("error", () => {
|
|
175
|
+
cleanup();
|
|
176
|
+
});
|
|
177
|
+
cleanup = connectMpegTsClient({
|
|
178
|
+
beforeCatchup: () => {
|
|
179
|
+
// Pipe FFmpeg stdout to the HTTP response. When FFmpeg exits (either from stdin ending or being killed), stdout closes and the response ends automatically.
|
|
180
|
+
remuxer.stdout.pipe(res);
|
|
181
|
+
// Write the init segment first — FFmpeg needs the ftyp and moov boxes before it can process any media segments.
|
|
182
|
+
remuxer.stdin.write(stream.hls.initSegment);
|
|
183
|
+
},
|
|
184
|
+
extraCleanup: () => {
|
|
185
|
+
remuxer.kill();
|
|
186
|
+
},
|
|
187
|
+
logLabel: "MPEG-TS",
|
|
188
|
+
onStreamTerminated: () => {
|
|
189
|
+
remuxer.stdin.end();
|
|
190
|
+
},
|
|
191
|
+
req,
|
|
192
|
+
res,
|
|
193
|
+
stream,
|
|
194
|
+
streamId,
|
|
195
|
+
writeSegment: (data) => {
|
|
196
|
+
remuxer.stdin.write(data);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Sets up a single MPEG-TS client session with shared lifecycle management. Handles client registration, segment event subscription, response headers, catchup
|
|
202
|
+
* delivery of existing segments, and cleanup on disconnect. The caller provides mode-specific callbacks for segment writing and stream termination.
|
|
203
|
+
*
|
|
204
|
+
* @param options.beforeCatchup - Optional callback invoked after event subscription and headers but before writing existing segments. Used by the capture path to
|
|
205
|
+
* pipe FFmpeg output and write the init segment before catchup begins.
|
|
206
|
+
* @param options.extraCleanup - Optional callback invoked during cleanup for mode-specific teardown (e.g., killing the FFmpeg remuxer).
|
|
207
|
+
* @param options.logLabel - Label for connect/disconnect debug messages (e.g., "MPEG-TS", "Native MPEG-TS").
|
|
208
|
+
* @param options.onStreamTerminated - Callback invoked when the stream emits a "terminated" event. Capture mode ends FFmpeg stdin; native mode ends the response.
|
|
209
|
+
* @param options.req - Express request object.
|
|
210
|
+
* @param options.res - Express response object.
|
|
211
|
+
* @param options.stream - The stream registry entry.
|
|
212
|
+
* @param options.streamId - The numeric stream ID.
|
|
213
|
+
* @param options.writeSegment - Callback to write segment data to the output target (FFmpeg stdin or HTTP response).
|
|
214
|
+
* @returns Cleanup function. The capture path wires this to the FFmpeg error handler; the native path does not need it.
|
|
215
|
+
*/
|
|
216
|
+
function connectMpegTsClient({ beforeCatchup, extraCleanup, logLabel, onStreamTerminated, req, res, stream, streamId, writeSegment }) {
|
|
217
|
+
const clientAddress = req.ip ?? req.socket.remoteAddress ?? "unknown";
|
|
218
|
+
// Increment the MPEG-TS client counter to prevent idle timeout while this client is connected.
|
|
219
|
+
stream.mpegTsClientCount++;
|
|
220
|
+
updateLastAccess(streamId);
|
|
221
|
+
registerClient(streamId, clientAddress, "mpegts");
|
|
222
|
+
const streamLog = LOG.withStreamId(stream.streamIdStr);
|
|
223
|
+
// Track which segments have been written to avoid duplicates during the catchup phase. When we subscribe to segment events and then write existing segments, a new
|
|
224
|
+
// segment could arrive via the event that we also encounter in the existing segment iteration. The Set prevents writing it twice.
|
|
225
|
+
const sentSegments = new Set();
|
|
226
|
+
let cleanedUp = false;
|
|
227
|
+
// Handler for new media segments. Writes each segment to the output target and updates the last access timestamp to prevent idle timeout.
|
|
176
228
|
const onSegment = (filename, data) => {
|
|
177
229
|
if (cleanedUp || sentSegments.has(filename)) {
|
|
178
230
|
return;
|
|
179
231
|
}
|
|
180
232
|
sentSegments.add(filename);
|
|
181
|
-
|
|
233
|
+
writeSegment(data);
|
|
182
234
|
updateLastAccess(streamId);
|
|
183
235
|
};
|
|
184
|
-
// Handler for stream termination.
|
|
236
|
+
// Handler for stream termination.
|
|
185
237
|
const onTerminated = () => {
|
|
186
238
|
if (cleanedUp) {
|
|
187
239
|
return;
|
|
188
240
|
}
|
|
189
|
-
|
|
241
|
+
onStreamTerminated();
|
|
190
242
|
};
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
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 = () => {
|
|
243
|
+
// Idempotent cleanup function. The cleanedUp flag ensures it runs only once regardless of which event triggers it first (client disconnect, stream termination, or
|
|
244
|
+
// output error).
|
|
245
|
+
const cleanup = () => {
|
|
198
246
|
if (cleanedUp) {
|
|
199
247
|
return;
|
|
200
248
|
}
|
|
@@ -212,10 +260,10 @@ async function serveMpegTsStream(streamId, channelName, req, res) {
|
|
|
212
260
|
unregisterClient(streamId, clientAddress, "mpegts");
|
|
213
261
|
stream.hls.segmentEmitter.off("segment", onSegment);
|
|
214
262
|
stream.hls.segmentEmitter.off("terminated", onTerminated);
|
|
215
|
-
|
|
216
|
-
streamLog.debug("streaming:mpegts", "
|
|
263
|
+
extraCleanup?.();
|
|
264
|
+
streamLog.debug("streaming:mpegts", "%s client disconnected.", logLabel);
|
|
217
265
|
};
|
|
218
|
-
// Clean up when the client disconnects.
|
|
266
|
+
// Clean up when the client disconnects.
|
|
219
267
|
req.on("close", () => {
|
|
220
268
|
cleanup();
|
|
221
269
|
});
|
|
@@ -229,10 +277,8 @@ async function serveMpegTsStream(streamId, channelName, req, res) {
|
|
|
229
277
|
res.setHeader("Content-Type", "video/mpeg");
|
|
230
278
|
res.setHeader("transferMode.dlna.org", "Streaming");
|
|
231
279
|
}
|
|
232
|
-
//
|
|
233
|
-
|
|
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);
|
|
280
|
+
// Run any mode-specific initialization before writing catchup segments.
|
|
281
|
+
beforeCatchup?.();
|
|
236
282
|
// Write all existing media segments to provide immediate playback catchup. The sentSegments Set deduplicates against any segments received via the event handler
|
|
237
283
|
// during this iteration.
|
|
238
284
|
for (const [filename, data] of stream.hls.segments) {
|
|
@@ -241,8 +287,9 @@ async function serveMpegTsStream(streamId, channelName, req, res) {
|
|
|
241
287
|
break;
|
|
242
288
|
}
|
|
243
289
|
sentSegments.add(filename);
|
|
244
|
-
|
|
290
|
+
writeSegment(data);
|
|
245
291
|
}
|
|
246
|
-
streamLog.debug("streaming:mpegts", "
|
|
292
|
+
streamLog.debug("streaming:mpegts", "%s client connected.", logLabel);
|
|
293
|
+
return cleanup;
|
|
247
294
|
}
|
|
248
295
|
//# sourceMappingURL=mpegts.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mpegts.js","sourceRoot":"","sources":["../../src/streaming/mpegts.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mpegts.js","sourceRoot":"","sources":["../../src/streaming/mpegts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single segment entry in the playlist. Each entry carries everything the builder needs to emit that segment's tags. The caller constructs entries from its own data
|
|
3
|
+
* source (fMP4 segmenter state, native proxy metadata, or preroll segment cache) and passes them to buildPlaylist(). Properties are listed alphabetically per project
|
|
4
|
+
* convention.
|
|
5
|
+
*/
|
|
6
|
+
export interface PlaylistSegmentEntry {
|
|
7
|
+
cueIn?: boolean;
|
|
8
|
+
cueOut?: string;
|
|
9
|
+
cueOutCont?: string;
|
|
10
|
+
discontinuity?: boolean;
|
|
11
|
+
duration: number;
|
|
12
|
+
mapUri?: string;
|
|
13
|
+
programDateTime?: string;
|
|
14
|
+
url: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Playlist-level options for buildPlaylist(). Properties are listed alphabetically per project convention.
|
|
18
|
+
*/
|
|
19
|
+
export interface PlaylistOptions {
|
|
20
|
+
discontinuitySequence?: number;
|
|
21
|
+
initialMapUri?: string;
|
|
22
|
+
mediaSequence: number;
|
|
23
|
+
targetDuration?: number;
|
|
24
|
+
version: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Builds an HLS playlist string from playlist options and an ordered array of segment entries. This is the single formatting function used by all playlist generators
|
|
28
|
+
* in PrismCast — the capture-mode fMP4 segmenter, the native HLS proxy, and the standalone preroll playlist. The builder is stateless and pure: it iterates the
|
|
29
|
+
* entries, emits the appropriate HLS tags for each, and returns the formatted m3u8 string.
|
|
30
|
+
*
|
|
31
|
+
* Per-segment tag ordering follows the HLS spec and preserves the ordering used by both existing generators: DISCONTINUITY, MAP, PROGRAM-DATE-TIME, CUE-IN,
|
|
32
|
+
* CUE-OUT, CUE-OUT-CONT, EXTINF, then the segment URL.
|
|
33
|
+
*
|
|
34
|
+
* @param options - Playlist-level configuration (version, sequence numbers, init segment, target duration).
|
|
35
|
+
* @param entries - Ordered array of segment entries to format.
|
|
36
|
+
* @returns The complete m3u8 playlist string with a trailing newline.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildPlaylist(options: PlaylistOptions, entries: PlaylistSegmentEntry[]): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* playlistBuilder.ts: Shared HLS playlist builder for PrismCast.
|
|
4
|
+
*/
|
|
5
|
+
// Playlist Builder.
|
|
6
|
+
/**
|
|
7
|
+
* Builds an HLS playlist string from playlist options and an ordered array of segment entries. This is the single formatting function used by all playlist generators
|
|
8
|
+
* in PrismCast — the capture-mode fMP4 segmenter, the native HLS proxy, and the standalone preroll playlist. The builder is stateless and pure: it iterates the
|
|
9
|
+
* entries, emits the appropriate HLS tags for each, and returns the formatted m3u8 string.
|
|
10
|
+
*
|
|
11
|
+
* Per-segment tag ordering follows the HLS spec and preserves the ordering used by both existing generators: DISCONTINUITY, MAP, PROGRAM-DATE-TIME, CUE-IN,
|
|
12
|
+
* CUE-OUT, CUE-OUT-CONT, EXTINF, then the segment URL.
|
|
13
|
+
*
|
|
14
|
+
* @param options - Playlist-level configuration (version, sequence numbers, init segment, target duration).
|
|
15
|
+
* @param entries - Ordered array of segment entries to format.
|
|
16
|
+
* @returns The complete m3u8 playlist string with a trailing newline.
|
|
17
|
+
*/
|
|
18
|
+
export function buildPlaylist(options, entries) {
|
|
19
|
+
// Compute TARGETDURATION as the maximum of the provided floor value and the maximum entry duration, rounded up to the nearest integer per RFC 8216 Section 4.3.3.1.
|
|
20
|
+
let maxDuration = options.targetDuration ?? 0;
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
if (entry.duration > maxDuration) {
|
|
23
|
+
maxDuration = entry.duration;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const lines = [
|
|
27
|
+
"#EXTM3U",
|
|
28
|
+
"#EXT-X-VERSION:" + String(options.version),
|
|
29
|
+
"#EXT-X-TARGETDURATION:" + String(Math.ceil(maxDuration)),
|
|
30
|
+
"#EXT-X-MEDIA-SEQUENCE:" + String(options.mediaSequence)
|
|
31
|
+
];
|
|
32
|
+
// Emit DISCONTINUITY-SEQUENCE when the caller provides a value. The distinction between undefined (no discontinuities in the stream's history) and 0
|
|
33
|
+
// (discontinuities exist but none have scrolled off the window) is meaningful for spec compliance — both capture and native paths control this independently.
|
|
34
|
+
if (options.discontinuitySequence !== undefined) {
|
|
35
|
+
lines.push("#EXT-X-DISCONTINUITY-SEQUENCE:" + String(options.discontinuitySequence));
|
|
36
|
+
}
|
|
37
|
+
// Emit the initial EXT-X-MAP for fMP4 streams. This applies to all segments until a per-segment mapUri overrides it at a discontinuity boundary.
|
|
38
|
+
if (options.initialMapUri) {
|
|
39
|
+
lines.push("#EXT-X-MAP:URI=\"" + options.initialMapUri + "\"");
|
|
40
|
+
}
|
|
41
|
+
// Emit per-segment tags and EXTINF for each entry.
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (entry.discontinuity) {
|
|
44
|
+
lines.push("#EXT-X-DISCONTINUITY");
|
|
45
|
+
}
|
|
46
|
+
if (entry.mapUri) {
|
|
47
|
+
lines.push("#EXT-X-MAP:URI=\"" + entry.mapUri + "\"");
|
|
48
|
+
}
|
|
49
|
+
if (entry.programDateTime) {
|
|
50
|
+
lines.push("#EXT-X-PROGRAM-DATE-TIME:" + entry.programDateTime);
|
|
51
|
+
}
|
|
52
|
+
if (entry.cueIn) {
|
|
53
|
+
lines.push("#EXT-X-CUE-IN");
|
|
54
|
+
}
|
|
55
|
+
if (entry.cueOut !== undefined) {
|
|
56
|
+
lines.push((entry.cueOut.length > 0) ? "#EXT-X-CUE-OUT:" + entry.cueOut : "#EXT-X-CUE-OUT");
|
|
57
|
+
}
|
|
58
|
+
if (entry.cueOutCont !== undefined) {
|
|
59
|
+
lines.push("#EXT-X-CUE-OUT-CONT:" + entry.cueOutCont);
|
|
60
|
+
}
|
|
61
|
+
lines.push("#EXTINF:" + entry.duration.toFixed(3) + ",");
|
|
62
|
+
lines.push(entry.url);
|
|
63
|
+
}
|
|
64
|
+
return lines.join("\n") + "\n";
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=playlistBuilder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playlistBuilder.js","sourceRoot":"","sources":["../../src/streaming/playlistBuilder.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAqEH,oBAAoB;AAEpB;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,OAAwB,EAAE,OAA+B;IAErF,oKAAoK;IACpK,IAAI,WAAW,GAAG,OAAO,CAAC,cAAc,IAAI,CAAC,CAAC;IAE9C,KAAI,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAE3B,IAAG,KAAK,CAAC,QAAQ,GAAG,WAAW,EAAE,CAAC;YAEhC,WAAW,GAAG,KAAK,CAAC,QAAQ,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAa;QACtB,SAAS;QACT,iBAAiB,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;QAC3C,wBAAwB,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACzD,wBAAwB,GAAG,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC;KACzD,CAAC;IAEF,qJAAqJ;IACrJ,8JAA8J;IAC9J,IAAG,OAAO,CAAC,qBAAqB,KAAK,SAAS,EAAE,CAAC;QAE/C,KAAK,CAAC,IAAI,CAAC,gCAAgC,GAAG,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC;IACvF,CAAC;IAED,iJAAiJ;IACjJ,IAAG,OAAO,CAAC,aAAa,EAAE,CAAC;QAEzB,KAAK,CAAC,IAAI,CAAC,mBAAmB,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IACjE,CAAC;IAED,mDAAmD;IACnD,KAAI,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAE3B,IAAG,KAAK,CAAC,aAAa,EAAE,CAAC;YAEvB,KAAK,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;QACrC,CAAC;QAED,IAAG,KAAK,CAAC,MAAM,EAAE,CAAC;YAEhB,KAAK,CAAC,IAAI,CAAC,mBAAmB,GAAG,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;QACxD,CAAC;QAED,IAAG,KAAK,CAAC,eAAe,EAAE,CAAC;YAEzB,KAAK,CAAC,IAAI,CAAC,2BAA2B,GAAG,KAAK,CAAC,eAAe,CAAC,CAAC;QAClE,CAAC;QAED,IAAG,KAAK,CAAC,KAAK,EAAE,CAAC;YAEf,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAC9B,CAAC;QAED,IAAG,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAE9B,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC;QAC9F,CAAC;QAED,IAAG,KAAK,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YAElC,KAAK,CAAC,IAAI,CAAC,sBAAsB,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;QACxD,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;QACzD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import type { PlaylistSegmentEntry } from "./playlistBuilder.js";
|
|
3
|
+
/**
|
|
4
|
+
* Returns true when the preroll init segment and at least one media segment have been generated and are ready to serve.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isPrerollReady(): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Returns the number of preroll segments available. Used by the segmenter to determine the preroll-to-real segment index boundary.
|
|
9
|
+
* @returns The count of preroll media segments.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getPrerollSegmentCount(): number;
|
|
12
|
+
/**
|
|
13
|
+
* Returns the duration of a specific preroll segment in seconds. Used by the segmenter's generatePlaylist() for EXTINF values when preroll entries are in the sliding
|
|
14
|
+
* window.
|
|
15
|
+
* @param index - The zero-based segment index.
|
|
16
|
+
* @returns The segment duration in seconds.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getPrerollSegmentDuration(index: number): number;
|
|
19
|
+
/**
|
|
20
|
+
* Returns the total duration of all preroll segments in seconds. Used by the fmp4Segmenter to compute PTS offsets that make Chrome's real content timestamps continue
|
|
21
|
+
* from where the preroll ended, eliminating the PTS discontinuity at the preroll-to-live boundary.
|
|
22
|
+
* @returns The sum of all preroll segment durations in seconds.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getPrerollTotalDurationSec(): number;
|
|
25
|
+
/**
|
|
26
|
+
* Returns the maximum duration across all preroll segments, rounded up to the nearest integer. Used for TARGETDURATION computation in the composite playlist.
|
|
27
|
+
* @returns The ceiling of the maximum preroll segment duration.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getPrerollMaxDuration(): number;
|
|
30
|
+
/**
|
|
31
|
+
* Generates the preroll fMP4 at startup. Spawns FFmpeg to create PREROLL_TOTAL_DURATION seconds of black frame + silence as fragmented MP4, then splits
|
|
32
|
+
* the output into an init segment (ftyp + moov) and individual media segments (moof + mdat pairs) using the MP4 box parser. Each segment has naturally monotonic PTS
|
|
33
|
+
* because it comes from a continuous FFmpeg encode. If FFmpeg is unavailable or fails, the preroll is left uninitialized and the system degrades gracefully — the
|
|
34
|
+
* blocking stream setup path is used instead.
|
|
35
|
+
*/
|
|
36
|
+
export declare function generatePreroll(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Options for computing the composite playlist sliding window when preroll is active.
|
|
39
|
+
*/
|
|
40
|
+
export interface PrerollWindowOptions {
|
|
41
|
+
currentSegmentIndex: number;
|
|
42
|
+
maxSegments: number;
|
|
43
|
+
prerollSegmentCount: number;
|
|
44
|
+
realSegmentCount: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Computes the sliding window start index for a composite playlist containing both preroll and real segments. The start index is determined by three constraints:
|
|
48
|
+
* (1) never negative, (2) the standard sliding window rule (current index minus window size), and (3) the preroll cap that limits how many preroll entries appear in
|
|
49
|
+
* the window. The third constraint is the key addition — it prevents clients from playing through many seconds of remaining preroll before reaching live content.
|
|
50
|
+
*
|
|
51
|
+
* @param options - Window computation parameters.
|
|
52
|
+
* @returns The start index for the composite playlist window.
|
|
53
|
+
*/
|
|
54
|
+
export declare function computePrerollWindow(options: PrerollWindowOptions): number;
|
|
55
|
+
/**
|
|
56
|
+
* Options for building preroll segment entries.
|
|
57
|
+
*/
|
|
58
|
+
export interface PrerollEntryOptions {
|
|
59
|
+
baseUrl: string;
|
|
60
|
+
extension: string;
|
|
61
|
+
prerollSegmentCount: number;
|
|
62
|
+
startIndex: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Builds an array of playlist segment entries for preroll segments within the given index range. Each entry has an absolute URL pointing to the global /preroll/ route
|
|
66
|
+
* and the duration extracted from the preroll segment cache. No per-segment metadata tags (DISCONTINUITY, PROGRAM-DATE-TIME, etc.) are set — preroll is synthetic
|
|
67
|
+
* placeholder content.
|
|
68
|
+
*
|
|
69
|
+
* @param options - Entry construction parameters.
|
|
70
|
+
* @returns Ordered array of preroll segment entries for the builder.
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildPrerollEntries(options: PrerollEntryOptions): PlaylistSegmentEntry[];
|
|
73
|
+
/**
|
|
74
|
+
* Computes how many preroll segments should be visible based on elapsed wall-clock time since the preroll started. The initial window (PREROLL_INITIAL_WINDOW segments)
|
|
75
|
+
* is revealed immediately so the client has enough content to begin playback per the HLS 3-from-end rule. Additional segments are revealed one at a time as wall-clock
|
|
76
|
+
* time passes — each new segment appears when enough time has elapsed for the client to have consumed the prior content beyond the initial window.
|
|
77
|
+
*
|
|
78
|
+
* @param prerollStartTime - Wall-clock time when the preroll began.
|
|
79
|
+
* @returns The number of preroll segments to reveal.
|
|
80
|
+
*/
|
|
81
|
+
export declare function computeProgressiveReveal(prerollStartTime: Date): number;
|
|
82
|
+
/**
|
|
83
|
+
* Generates a progressive HLS playlist referencing the global preroll segments. The playlist simulates a live stream by revealing segments based on elapsed wall-clock
|
|
84
|
+
* time since the preroll started. On each client poll, more segments become visible — the client sees new content appear and keeps playing without stalling.
|
|
85
|
+
*
|
|
86
|
+
* The playlist uses absolute URLs because preroll segments are served at global routes (/preroll/*) while the playlist itself is served under /hls/:name/. Without
|
|
87
|
+
* absolute URLs, the client would request /hls/:name/preroll/segment0.m4s which does not exist. The playlist omits #EXT-X-ENDLIST so it behaves as a live playlist —
|
|
88
|
+
* the client polls for updates and receives the real content playlist when ready.
|
|
89
|
+
*
|
|
90
|
+
* PROGRAM-DATE-TIME is intentionally omitted from the preroll playlist. Preroll is synthetic placeholder content — assigning it wall-clock timestamps would be
|
|
91
|
+
* misleading and would create a backward time jump at the preroll-to-live boundary (preroll PDT would overshoot real content PDT because the preroll covers 30
|
|
92
|
+
* seconds of content but real content typically arrives in ~15 seconds). PDT is emitted only on real segments once the segmenter produces them.
|
|
93
|
+
*
|
|
94
|
+
* @param baseUrl - The server's external URL (e.g., "http://192.168.1.100:5589").
|
|
95
|
+
* @param startingSequence - The MEDIA-SEQUENCE offset. Zero for fresh starts. For resume streams, this is set to the saved segment index so the preroll
|
|
96
|
+
* playlist continues from the prior session's sequence range rather than restarting at 0.
|
|
97
|
+
* @param prerollStartTime - Wall-clock time when the preroll began. Used to compute elapsed time and determine how many segments to reveal.
|
|
98
|
+
* @returns The complete HLS playlist string, or an empty string if the preroll is not ready.
|
|
99
|
+
*/
|
|
100
|
+
export declare function generatePrerollPlaylist(baseUrl: string, startingSequence: number, prerollStartTime: Date): string;
|
|
101
|
+
/**
|
|
102
|
+
* Registers the preroll segment routes on the Express application. These serve the cached preroll buffers with immutable cache headers since the content never
|
|
103
|
+
* changes after generation.
|
|
104
|
+
* @param app - The Express application.
|
|
105
|
+
*/
|
|
106
|
+
export declare function setupPrerollRoutes(app: Express): void;
|