prismcast 1.5.2 → 1.7.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/cdp.js +1 -1
- package/dist/browser/cdp.js.map +1 -1
- package/dist/browser/channelSelection.d.ts +10 -34
- package/dist/browser/channelSelection.js +31 -96
- package/dist/browser/channelSelection.js.map +1 -1
- package/dist/browser/display.d.ts +19 -0
- package/dist/browser/display.js +16 -11
- package/dist/browser/display.js.map +1 -1
- package/dist/browser/index.d.ts +31 -54
- package/dist/browser/index.js +334 -272
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/login.d.ts +65 -0
- package/dist/browser/login.js +193 -0
- package/dist/browser/login.js.map +1 -0
- package/dist/browser/precaching.js +15 -3
- package/dist/browser/precaching.js.map +1 -1
- package/dist/browser/tuning/comcastPolymer.d.ts +23 -0
- package/dist/browser/tuning/comcastPolymer.js +655 -0
- package/dist/browser/tuning/comcastPolymer.js.map +1 -0
- package/dist/browser/tuning/cox.d.ts +2 -0
- package/dist/browser/tuning/cox.js +14 -0
- package/dist/browser/tuning/cox.js.map +1 -0
- package/dist/browser/tuning/directv.js +14 -1
- package/dist/browser/tuning/directv.js.map +1 -1
- package/dist/browser/tuning/fox.js +14 -2
- package/dist/browser/tuning/fox.js.map +1 -1
- package/dist/browser/tuning/hbo.js +12 -1
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.js +24 -1
- package/dist/browser/tuning/hulu.js.map +1 -1
- package/dist/browser/tuning/shared.d.ts +46 -0
- package/dist/browser/tuning/shared.js +100 -0
- package/dist/browser/tuning/shared.js.map +1 -0
- package/dist/browser/tuning/sling.js +35 -5
- package/dist/browser/tuning/sling.js.map +1 -1
- package/dist/browser/tuning/spectrum.js +13 -1
- package/dist/browser/tuning/spectrum.js.map +1 -1
- package/dist/browser/tuning/thumbnailRow.js +1 -1
- package/dist/browser/tuning/thumbnailRow.js.map +1 -1
- package/dist/browser/tuning/tileClick.js +1 -1
- package/dist/browser/tuning/tileClick.js.map +1 -1
- package/dist/browser/tuning/xfinity.d.ts +2 -0
- package/dist/browser/tuning/xfinity.js +14 -0
- package/dist/browser/tuning/xfinity.js.map +1 -0
- package/dist/browser/tuning/youtubeTv.js +15 -1
- package/dist/browser/tuning/youtubeTv.js.map +1 -1
- package/dist/browser/video.d.ts +20 -0
- package/dist/browser/video.js +168 -124
- package/dist/browser/video.js.map +1 -1
- package/dist/channels/index.d.ts +2 -2
- package/dist/channels/index.js +2075 -683
- package/dist/channels/index.js.map +1 -1
- package/dist/config/health.d.ts +28 -28
- package/dist/config/health.js +72 -54
- package/dist/config/health.js.map +1 -1
- package/dist/config/index.js +29 -64
- package/dist/config/index.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/providerPacks.js +4 -1
- package/dist/config/providerPacks.js.map +1 -1
- package/dist/config/providers.d.ts +19 -5
- package/dist/config/providers.js +123 -93
- package/dist/config/providers.js.map +1 -1
- package/dist/config/sites.d.ts +28 -0
- package/dist/config/sites.js +104 -127
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +53 -2
- package/dist/config/userChannels.js +222 -13
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +2 -1
- package/dist/config/userConfig.js +6 -15
- 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/hdhr/channelMap.d.ts +1 -1
- package/dist/hdhr/channelMap.js +4 -4
- package/dist/hdhr/channelMap.js.map +1 -1
- package/dist/index.js +25 -22
- 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 +61 -0
- package/dist/native/index.js +342 -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 +49 -0
- package/dist/native/probe.js +294 -0
- package/dist/native/probe.js.map +1 -0
- package/dist/native/proxy.d.ts +54 -0
- package/dist/native/proxy.js +947 -0
- package/dist/native/proxy.js.map +1 -0
- package/dist/routes/components.d.ts +43 -0
- package/dist/routes/components.js +122 -0
- package/dist/routes/components.js.map +1 -1
- package/dist/routes/config/channels/index.d.ts +2 -2
- package/dist/routes/config/channels/index.js +1 -1
- package/dist/routes/config/channels/index.js.map +1 -1
- package/dist/routes/config/channels/routes.js +443 -65
- package/dist/routes/config/channels/routes.js.map +1 -1
- package/dist/routes/config/channels/table.d.ts +76 -0
- package/dist/routes/config/channels/table.js +369 -89
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/index.d.ts +1 -1
- package/dist/routes/config/index.js +1 -1
- package/dist/routes/config/index.js.map +1 -1
- package/dist/routes/config/providers.d.ts +6 -6
- package/dist/routes/config/providers.js +73 -99
- 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 +0 -15
- package/dist/routes/index.js +2 -16
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/providers.js +106 -3
- package/dist/routes/providers.js.map +1 -1
- package/dist/routes/root/content.js +124 -47
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/index.js +2 -1
- package/dist/routes/root/index.js.map +1 -1
- package/dist/routes/root/scripts/channels.js +772 -312
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/config.js +596 -544
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/index.d.ts +1 -0
- package/dist/routes/root/scripts/index.js +1 -0
- package/dist/routes/root/scripts/index.js.map +1 -1
- package/dist/routes/root/scripts/shared.d.ts +6 -0
- package/dist/routes/root/scripts/shared.js +391 -0
- package/dist/routes/root/scripts/shared.js.map +1 -0
- package/dist/routes/root/scripts/status.js +49 -33
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/root/styles.js +117 -10
- package/dist/routes/root/styles.js.map +1 -1
- package/dist/service/commands.js +2 -1
- package/dist/service/commands.js.map +1 -1
- package/dist/streaming/fmp4Segmenter.d.ts +5 -0
- package/dist/streaming/fmp4Segmenter.js +146 -42
- package/dist/streaming/fmp4Segmenter.js.map +1 -1
- package/dist/streaming/hls.d.ts +24 -19
- package/dist/streaming/hls.js +603 -297
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +16 -3
- package/dist/streaming/hlsResume.js +35 -8
- package/dist/streaming/hlsResume.js.map +1 -1
- package/dist/streaming/hlsSegments.d.ts +41 -2
- package/dist/streaming/hlsSegments.js +131 -28
- package/dist/streaming/hlsSegments.js.map +1 -1
- package/dist/streaming/lifecycle.js +27 -13
- package/dist/streaming/lifecycle.js.map +1 -1
- package/dist/streaming/monitor.d.ts +1 -0
- package/dist/streaming/monitor.js +862 -474
- 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 +126 -0
- package/dist/streaming/preroll.js +434 -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/recovery.d.ts +0 -6
- package/dist/streaming/recovery.js +0 -8
- package/dist/streaming/recovery.js.map +1 -1
- package/dist/streaming/registry.d.ts +57 -9
- package/dist/streaming/registry.js +26 -7
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +8 -1
- package/dist/streaming/setup.js +71 -27
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +29 -4
- package/dist/streaming/showInfo.js +210 -38
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +20 -6
- package/dist/streaming/statusEmitter.js +31 -7
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/channels.d.ts +32 -0
- package/dist/types/config.d.ts +2 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js.map +1 -1
- package/dist/types/profiles.d.ts +13 -1
- package/dist/types/selection.d.ts +5 -1
- package/dist/types/shared.d.ts +1 -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.d.ts +0 -5
- package/dist/utils/debugFilter.js +20 -11
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/utils/delay.d.ts +28 -0
- package/dist/utils/delay.js +35 -1
- package/dist/utils/delay.js.map +1 -1
- package/dist/utils/evaluate.js +6 -11
- package/dist/utils/evaluate.js.map +1 -1
- package/dist/utils/ffmpeg.d.ts +14 -12
- package/dist/utils/ffmpeg.js +114 -134
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.js +8 -0
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logEmitter.js +1 -1
- package/dist/utils/logEmitter.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/dist/utils/retry.d.ts +15 -9
- package/dist/utils/retry.js +7 -23
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/timing.d.ts +5 -0
- package/dist/utils/timing.js +13 -0
- package/dist/utils/timing.js.map +1 -0
- package/dist/utils/version.js +2 -2
- package/dist/utils/version.js.map +1 -1
- package/package.json +7 -7
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"}
|