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.
Files changed (158) hide show
  1. package/README.md +44 -28
  2. package/dist/app.d.ts +5 -0
  3. package/dist/app.js +63 -5
  4. package/dist/app.js.map +1 -1
  5. package/dist/browser/channelSelection.js +14 -2
  6. package/dist/browser/channelSelection.js.map +1 -1
  7. package/dist/browser/index.d.ts +23 -9
  8. package/dist/browser/index.js +205 -88
  9. package/dist/browser/index.js.map +1 -1
  10. package/dist/browser/precaching.js +3 -3
  11. package/dist/browser/precaching.js.map +1 -1
  12. package/dist/browser/tuning/directv.js +13 -0
  13. package/dist/browser/tuning/directv.js.map +1 -1
  14. package/dist/browser/tuning/fox.js +12 -0
  15. package/dist/browser/tuning/fox.js.map +1 -1
  16. package/dist/browser/tuning/hbo.js +11 -0
  17. package/dist/browser/tuning/hbo.js.map +1 -1
  18. package/dist/browser/tuning/hulu.js +23 -0
  19. package/dist/browser/tuning/hulu.js.map +1 -1
  20. package/dist/browser/tuning/sling.js +34 -4
  21. package/dist/browser/tuning/sling.js.map +1 -1
  22. package/dist/browser/tuning/spectrum.js +12 -0
  23. package/dist/browser/tuning/spectrum.js.map +1 -1
  24. package/dist/browser/tuning/xfinity.d.ts +2 -0
  25. package/dist/browser/tuning/xfinity.js +607 -0
  26. package/dist/browser/tuning/xfinity.js.map +1 -0
  27. package/dist/browser/tuning/youtubeTv.js +14 -0
  28. package/dist/browser/tuning/youtubeTv.js.map +1 -1
  29. package/dist/browser/video.d.ts +2 -0
  30. package/dist/browser/video.js +99 -17
  31. package/dist/browser/video.js.map +1 -1
  32. package/dist/channels/index.js +173 -56
  33. package/dist/channels/index.js.map +1 -1
  34. package/dist/config/health.d.ts +28 -28
  35. package/dist/config/health.js +52 -52
  36. package/dist/config/health.js.map +1 -1
  37. package/dist/config/paths.d.ts +12 -0
  38. package/dist/config/paths.js +16 -0
  39. package/dist/config/paths.js.map +1 -1
  40. package/dist/config/profiles.d.ts +3 -2
  41. package/dist/config/profiles.js +45 -34
  42. package/dist/config/profiles.js.map +1 -1
  43. package/dist/config/providers.d.ts +7 -0
  44. package/dist/config/providers.js +14 -0
  45. package/dist/config/providers.js.map +1 -1
  46. package/dist/config/sites.d.ts +28 -0
  47. package/dist/config/sites.js +93 -122
  48. package/dist/config/sites.js.map +1 -1
  49. package/dist/config/userChannels.d.ts +3 -0
  50. package/dist/config/userChannels.js +10 -3
  51. package/dist/config/userChannels.js.map +1 -1
  52. package/dist/config/userConfig.d.ts +1 -0
  53. package/dist/config/userConfig.js +4 -0
  54. package/dist/config/userConfig.js.map +1 -1
  55. package/dist/config/userProfiles.d.ts +0 -1
  56. package/dist/config/userProfiles.js +18 -10
  57. package/dist/config/userProfiles.js.map +1 -1
  58. package/dist/index.js +16 -7
  59. package/dist/index.js.map +1 -1
  60. package/dist/native/decrypt.d.ts +32 -0
  61. package/dist/native/decrypt.js +85 -0
  62. package/dist/native/decrypt.js.map +1 -0
  63. package/dist/native/index.d.ts +58 -0
  64. package/dist/native/index.js +341 -0
  65. package/dist/native/index.js.map +1 -0
  66. package/dist/native/intercept.d.ts +35 -0
  67. package/dist/native/intercept.js +184 -0
  68. package/dist/native/intercept.js.map +1 -0
  69. package/dist/native/probe.d.ts +48 -0
  70. package/dist/native/probe.js +270 -0
  71. package/dist/native/probe.js.map +1 -0
  72. package/dist/native/proxy.d.ts +52 -0
  73. package/dist/native/proxy.js +904 -0
  74. package/dist/native/proxy.js.map +1 -0
  75. package/dist/routes/config/channels/table.js +19 -25
  76. package/dist/routes/config/channels/table.js.map +1 -1
  77. package/dist/routes/config/providers.js +4 -4
  78. package/dist/routes/config/providers.js.map +1 -1
  79. package/dist/routes/debug.js +5 -5
  80. package/dist/routes/debug.js.map +1 -1
  81. package/dist/routes/hls.js +12 -2
  82. package/dist/routes/hls.js.map +1 -1
  83. package/dist/routes/index.d.ts +1 -0
  84. package/dist/routes/index.js +3 -0
  85. package/dist/routes/index.js.map +1 -1
  86. package/dist/routes/root/content.js +80 -20
  87. package/dist/routes/root/content.js.map +1 -1
  88. package/dist/routes/root/scripts/status.js +14 -13
  89. package/dist/routes/root/scripts/status.js.map +1 -1
  90. package/dist/routes/root/styles.js +4 -2
  91. package/dist/routes/root/styles.js.map +1 -1
  92. package/dist/streaming/fmp4Segmenter.d.ts +3 -0
  93. package/dist/streaming/fmp4Segmenter.js +143 -42
  94. package/dist/streaming/fmp4Segmenter.js.map +1 -1
  95. package/dist/streaming/hls.d.ts +24 -19
  96. package/dist/streaming/hls.js +486 -261
  97. package/dist/streaming/hls.js.map +1 -1
  98. package/dist/streaming/hlsResume.d.ts +7 -0
  99. package/dist/streaming/hlsResume.js +22 -2
  100. package/dist/streaming/hlsResume.js.map +1 -1
  101. package/dist/streaming/hlsSegments.d.ts +42 -2
  102. package/dist/streaming/hlsSegments.js +109 -8
  103. package/dist/streaming/hlsSegments.js.map +1 -1
  104. package/dist/streaming/lifecycle.js +20 -2
  105. package/dist/streaming/lifecycle.js.map +1 -1
  106. package/dist/streaming/monitor.d.ts +1 -0
  107. package/dist/streaming/monitor.js +369 -21
  108. package/dist/streaming/monitor.js.map +1 -1
  109. package/dist/streaming/mp4Parser.d.ts +52 -6
  110. package/dist/streaming/mp4Parser.js +156 -30
  111. package/dist/streaming/mp4Parser.js.map +1 -1
  112. package/dist/streaming/mpegts.d.ts +2 -2
  113. package/dist/streaming/mpegts.js +156 -109
  114. package/dist/streaming/mpegts.js.map +1 -1
  115. package/dist/streaming/playlistBuilder.d.ts +38 -0
  116. package/dist/streaming/playlistBuilder.js +66 -0
  117. package/dist/streaming/playlistBuilder.js.map +1 -0
  118. package/dist/streaming/preroll.d.ts +106 -0
  119. package/dist/streaming/preroll.js +373 -0
  120. package/dist/streaming/preroll.js.map +1 -0
  121. package/dist/streaming/pretune.d.ts +8 -0
  122. package/dist/streaming/pretune.js +226 -0
  123. package/dist/streaming/pretune.js.map +1 -0
  124. package/dist/streaming/registry.d.ts +24 -3
  125. package/dist/streaming/registry.js +23 -1
  126. package/dist/streaming/registry.js.map +1 -1
  127. package/dist/streaming/setup.d.ts +7 -0
  128. package/dist/streaming/setup.js +27 -6
  129. package/dist/streaming/setup.js.map +1 -1
  130. package/dist/streaming/showInfo.d.ts +24 -0
  131. package/dist/streaming/showInfo.js +55 -3
  132. package/dist/streaming/showInfo.js.map +1 -1
  133. package/dist/streaming/statusEmitter.d.ts +6 -4
  134. package/dist/streaming/statusEmitter.js +9 -3
  135. package/dist/streaming/statusEmitter.js.map +1 -1
  136. package/dist/types/channels.d.ts +1 -0
  137. package/dist/types/index.d.ts +1 -1
  138. package/dist/types/profiles.d.ts +8 -1
  139. package/dist/types/selection.d.ts +4 -1
  140. package/dist/types/streaming.d.ts +7 -0
  141. package/dist/utils/chromeFetch.d.ts +20 -0
  142. package/dist/utils/chromeFetch.js +43 -0
  143. package/dist/utils/chromeFetch.js.map +1 -0
  144. package/dist/utils/debugFilter.js +15 -0
  145. package/dist/utils/debugFilter.js.map +1 -1
  146. package/dist/utils/delay.d.ts +16 -0
  147. package/dist/utils/delay.js +15 -1
  148. package/dist/utils/delay.js.map +1 -1
  149. package/dist/utils/ffmpeg.d.ts +1 -7
  150. package/dist/utils/ffmpeg.js +86 -127
  151. package/dist/utils/ffmpeg.js.map +1 -1
  152. package/dist/utils/index.d.ts +2 -0
  153. package/dist/utils/index.js +2 -0
  154. package/dist/utils/index.js.map +1 -1
  155. package/dist/utils/pid.d.ts +40 -0
  156. package/dist/utils/pid.js +83 -0
  157. package/dist/utils/pid.js.map +1 -0
  158. package/package.json +5 -5
@@ -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 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).
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
- * 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
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
- * 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.
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 capture is running, waits for the init segment,
30
- * spawns a per-client FFmpeg remuxer, and streams the output.
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 real stream already exists. No early flush needed — the stream data will flow quickly.
49
- if ((existingStreamId !== undefined) && (existingStreamId !== -1)) {
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
- // 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;
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
- // Acquire the stream. If a startup is in progress (another request started it), poll silently. Otherwise, start a new stream via initializeStream().
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
- 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
- }
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
- 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;
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
- if (streamId === null) {
109
- LOG.warn("MPEG-TS stream startup failed for %s (terminated during setup).", channelName);
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 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).
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. The init segment contains codec configuration (ftyp+moov boxes) that FFmpeg needs before it can process media segments.
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 and verify it's still alive with a valid init segment.
119
+ // Get the stream from the registry.
141
120
  const stream = getStream(streamId);
142
- if (!stream?.hls.initSegment) {
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
- // Handler for new media segments. Writes each segment to FFmpeg stdin and updates the last access timestamp to prevent idle timeout.
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
- remuxer.stdin.write(data);
233
+ writeSegment(data);
182
234
  updateLastAccess(streamId);
183
235
  };
184
- // Handler for stream termination. Ends FFmpeg stdin gracefully so it can flush remaining data and exit cleanly.
236
+ // Handler for stream termination.
185
237
  const onTerminated = () => {
186
238
  if (cleanedUp) {
187
239
  return;
188
240
  }
189
- remuxer.stdin.end();
241
+ onStreamTerminated();
190
242
  };
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 = () => {
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
- remuxer.kill();
216
- streamLog.debug("streaming:mpegts", "MPEG-TS client disconnected.");
263
+ extraCleanup?.();
264
+ streamLog.debug("streaming:mpegts", "%s client disconnected.", logLabel);
217
265
  };
218
- // Clean up when the client disconnects. Registered immediately after cleanup is assigned to minimize the window where a disconnect could be missed.
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
- // 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);
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
- remuxer.stdin.write(data);
290
+ writeSegment(data);
245
291
  }
246
- streamLog.debug("streaming:mpegts", "MPEG-TS client connected.");
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":"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"}
1
+ {"version":3,"file":"mpegts.js","sourceRoot":"","sources":["../../src/streaming/mpegts.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEzE,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAClF,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAG5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtD;;;;;;;;;;GAUG;AAEH,2BAA2B;AAE3B;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,GAAY,EAAE,GAAa;IAElE,MAAM,WAAW,GAAI,GAAG,CAAC,MAA4B,CAAC,IAAI,CAAC;IAE3D,IAAG,CAAC,WAAW,EAAE,CAAC;QAEhB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;QAElD,OAAO;IACT,CAAC;IAED,iGAAiG;IACjG,MAAM,gBAAgB,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC;IAEzD,sHAAsH;IACtH,IAAG,gBAAgB,KAAK,SAAS,EAAE,CAAC;QAElC,MAAM,iBAAiB,CAAC,gBAAgB,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;QAEjE,OAAO;IACT,CAAC;IAED,4JAA4J;IAC5J,4BAA4B;IAC5B,MAAM,UAAU,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAEhD,IAAG,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QAErB,mBAAmB,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;QAErC,OAAO;IACT,CAAC;IAED,oKAAoK;IACpK,mDAAmD;IACnD,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;IAC3C,GAAG,CAAC,SAAS,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACrC,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;IAC5C,GAAG,CAAC,SAAS,CAAC,uBAAuB,EAAE,WAAW,CAAC,CAAC;IACpD,GAAG,CAAC,YAAY,EAAE,CAAC;IAEnB,oKAAoK;IACpK,gCAAgC;IAChC,IAAI,QAA0B,CAAC;IAE/B,IAAI,CAAC;QAEH,QAAQ,GAAG,MAAM,gBAAgB,CAAC;YAEhC,OAAO,EAAE,UAAU,CAAC,OAAO;YAC3B,WAAW;YACX,aAAa,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,IAAI;YACzD,YAAY,EAAE,IAAI;YAClB,eAAe,EAAE,GAAG,CAAC,KAAK,CAAC,OAA6B;YACxD,GAAG,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG;SAC5B,CAAC,CAAC;IACL,CAAC;IAAC,OAAM,KAAK,EAAE,CAAC;QAEd,IAAG,KAAK,YAAY,gBAAgB,EAAE,CAAC;YAErC,GAAG,CAAC,IAAI,CAAC,2CAA2C,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;QACxF,CAAC;aAAM,CAAC;YAEN,GAAG,CAAC,IAAI,CAAC,2CAA2C,EAAE,WAAW,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;QACzF,CAAC;QAED,GAAG,CAAC,GAAG,EAAE,CAAC;QAEV,OAAO;IACT,CAAC;IAED,IAAG,QAAQ,KAAK,IAAI,EAAE,CAAC;QAErB,GAAG,CAAC,IAAI,CAAC,iEAAiE,EAAE,WAAW,CAAC,CAAC;QACzF,GAAG,CAAC,GAAG,EAAE,CAAC;QAEV,OAAO;IACT,CAAC;IAED,MAAM,iBAAiB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAC3D,CAAC;AAED,oBAAoB;AAEpB;;;;;;;;GAQG;AACH,KAAK,UAAU,iBAAiB,CAAC,QAAgB,EAAE,WAAmB,EAAE,GAAY,EAAE,GAAa;IAEjG,kJAAkJ;IAClJ,2FAA2F;IAC3F,MAAM,SAAS,GAAG,MAAM,kBAAkB,CAAC,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;IAEzF,IAAG,CAAC,SAAS,EAAE,CAAC;QAEd,IAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;YAEpB,GAAG,CAAC,SAAS,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;YAClC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;QAC5D,CAAC;aAAM,CAAC;YAEN,GAAG,CAAC,IAAI,CAAC,sCAAsC,EAAE,WAAW,CAAC,CAAC;YAC9D,GAAG,CAAC,GAAG,EAAE,CAAC;QACZ,CAAC;QAED,OAAO;IACT,CAAC;IAED,oCAAoC;IACpC,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;IAEnC,IAAG,CAAC,MAAM,EAAE,CAAC;QAEX,IAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;YAEpB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QACtD,CAAC;aAAM,CAAC;YAEN,GAAG,CAAC,GAAG,EAAE,CAAC;QACZ,CAAC;QAED,OAAO;IACT,CAAC;IAED,8GAA8G;IAC9G,IAAG,MAAM,CAAC,aAAa,KAAK,QAAQ,EAAE,CAAC;QAErC,mBAAmB,CAAC;YAElB,QAAQ,EAAE,gBAAgB;YAC1B,kBAAkB,EAAE,GAAG,EAAE;gBAEvB,IAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;oBAEtB,GAAG,CAAC,GAAG,EAAE,CAAC;gBACZ,CAAC;YACH,CAAC;YACD,GAAG;YACH,GAAG;YACH,MAAM;YACN,QAAQ;YACR,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE;gBAErB,IAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;oBAEtB,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAClB,CAAC;YACH,CAAC;SACF,CAAC,CAAC;QAEH,OAAO;IACT,CAAC;IAED,uFAAuF;IACvF,IAAG,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;QAE3B,IAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;YAEpB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QACtD,CAAC;aAAM,CAAC;YAEN,GAAG,CAAC,GAAG,EAAE,CAAC;QACZ,CAAC;QAED,OAAO;IACT,CAAC;IAED,4JAA4J;IAC5J,mKAAmK;IACnK,+HAA+H;IAC/H,IAAI,OAAO,GAAe,GAAG,EAAE,GAAyE,CAAC,CAAC;IAE1G,MAAM,SAAS,GAAG,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAEvD,MAAM,OAAO,GAAG,kBAAkB,CAAC,CAAC,KAAK,EAAE,EAAE;QAE3C,SAAS,CAAC,KAAK,CAAC,kBAAkB,EAAE,4BAA4B,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;QACtF,OAAO,EAAE,CAAC;QAEV,IAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;YAEtB,GAAG,CAAC,GAAG,EAAE,CAAC;QACZ,CAAC;IACH,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IAEvB,+IAA+I;IAC/I,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QAE7B,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,mBAAmB,CAAC;QAE5B,aAAa,EAAE,GAAG,EAAE;YAElB,4JAA4J;YAC5J,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAEzB,gHAAgH;YAChH,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC9C,CAAC;QACD,YAAY,EAAE,GAAG,EAAE;YAEjB,OAAO,CAAC,IAAI,EAAE,CAAC;QACjB,CAAC;QACD,QAAQ,EAAE,SAAS;QACnB,kBAAkB,EAAE,GAAG,EAAE;YAEvB,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QACtB,CAAC;QACD,GAAG;QACH,GAAG;QACH,MAAM;QACN,QAAQ;QACR,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE;YAErB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5B,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,SAAS,mBAAmB,CAAC,EAAE,aAAa,EAAE,YAAY,EAAE,QAAQ,EAAE,kBAAkB,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAUjI;IAEC,MAAM,aAAa,GAAG,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,SAAS,CAAC;IAEtE,+FAA+F;IAC/F,MAAM,CAAC,iBAAiB,EAAE,CAAC;IAC3B,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAC3B,cAAc,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,CAAC;IAElD,MAAM,SAAS,GAAG,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAEvD,mKAAmK;IACnK,kIAAkI;IAClI,MAAM,YAAY,GAAG,IAAI,GAAG,EAAU,CAAC;IACvC,IAAI,SAAS,GAAG,KAAK,CAAC;IAEtB,0IAA0I;IAC1I,MAAM,SAAS,GAAG,CAAC,QAAgB,EAAE,IAAY,EAAQ,EAAE;QAEzD,IAAG,SAAS,IAAI,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAE3C,OAAO;QACT,CAAC;QAED,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC3B,YAAY,CAAC,IAAI,CAAC,CAAC;QACnB,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAC7B,CAAC,CAAC;IAEF,kCAAkC;IAClC,MAAM,YAAY,GAAG,GAAS,EAAE;QAE9B,IAAG,SAAS,EAAE,CAAC;YAEb,OAAO;QACT,CAAC;QAED,kBAAkB,EAAE,CAAC;IACvB,CAAC,CAAC;IAEF,mKAAmK;IACnK,iBAAiB;IACjB,MAAM,OAAO,GAAG,GAAS,EAAE;QAEzB,IAAG,SAAS,EAAE,CAAC;YAEb,OAAO;QACT,CAAC;QAED,SAAS,GAAG,IAAI,CAAC;QAEjB,oIAAoI;QACpI,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;QAE1C,IAAG,aAAa,EAAE,CAAC;YAEjB,aAAa,CAAC,iBAAiB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,aAAa,CAAC,iBAAiB,GAAG,CAAC,CAAC,CAAC;YAEnF,sJAAsJ;YACtJ,4FAA4F;YAC5F,IAAG,aAAa,CAAC,iBAAiB,KAAK,CAAC,EAAE,CAAC;gBAEzC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;QAED,gBAAgB,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,CAAC;QAEpD,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACpD,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;QAC1D,YAAY,EAAE,EAAE,CAAC;QAEjB,SAAS,CAAC,KAAK,CAAC,kBAAkB,EAAE,yBAAyB,EAAE,QAAQ,CAAC,CAAC;IAC3E,CAAC,CAAC;IAEF,wCAAwC;IACxC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QAEnB,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC,CAAC;IAEH,6HAA6H;IAC7H,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IACnD,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;IAEzD,0FAA0F;IAC1F,IAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;QAEpB,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;QAC3C,GAAG,CAAC,SAAS,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QACrC,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;QAC5C,GAAG,CAAC,SAAS,CAAC,uBAAuB,EAAE,WAAW,CAAC,CAAC;IACtD,CAAC;IAED,wEAAwE;IACxE,aAAa,EAAE,EAAE,CAAC;IAElB,iKAAiK;IACjK,yBAAyB;IACzB,KAAI,MAAM,CAAE,QAAQ,EAAE,IAAI,CAAE,IAAI,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAEpD,uEAAuE;QACvE,IAAG,SAAS,EAAE,CAAC;YAEb,MAAM;QACR,CAAC;QAED,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC3B,YAAY,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IAED,SAAS,CAAC,KAAK,CAAC,kBAAkB,EAAE,sBAAsB,EAAE,QAAQ,CAAC,CAAC;IAEtE,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -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;