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
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { LOG, resolveFFmpegPath } from "../utils/index.js";
|
|
2
|
+
import { createMP4BoxParser, offsetMoofTimestamps, parseMoovTrackInfo } from "./mp4Parser.js";
|
|
3
|
+
import { CONFIG } from "../config/index.js";
|
|
4
|
+
import { buildPlaylist } from "./playlistBuilder.js";
|
|
5
|
+
import { getEffectiveViewport } from "../config/presets.js";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
/* When a client requests an HLS playlist for a channel that is still starting up, PrismCast returns a startup playlist immediately to prevent HTTP timeouts. Channels
|
|
8
|
+
* DVR requires at least one segment in the playlist — an empty playlist is rejected with "Playlist had no segments." This module generates PREROLL_TOTAL_DURATION
|
|
9
|
+
* seconds of black+silence fMP4 at server startup, splits it into individual segments with naturally monotonic PTS, and serves them via global routes so the startup
|
|
10
|
+
* playlist can reference valid media content.
|
|
11
|
+
*
|
|
12
|
+
* The preroll playlist is served progressively — segments are revealed over time based on elapsed wall-clock time, simulating a live stream. The client polls for
|
|
13
|
+
* updates, sees new segments appear, and keeps playing without stalling for the full duration of the tune. When real content arrives, the fMP4 segmenter's composite
|
|
14
|
+
* playlist takes over, including preroll entries in its sliding window during the transition. As real segments accumulate, preroll entries fall off the window and the
|
|
15
|
+
* playlist becomes purely live content.
|
|
16
|
+
*/
|
|
17
|
+
// Total duration of preroll content in seconds. FFmpeg generates this much continuous black+silence fMP4, which is then split into individual segments at keyframe
|
|
18
|
+
// boundaries. The configured duration provides ample coverage for the slowest providers with the progressive playlist delivering content in real time for the full
|
|
19
|
+
// tune duration.
|
|
20
|
+
const PREROLL_TOTAL_DURATION = 30;
|
|
21
|
+
// Number of segments to reveal immediately on the first preroll playlist request. The HLS spec (RFC 8216 Section 6.3.3) says a client should start playback 3 target
|
|
22
|
+
// durations from the end of a live playlist, so 4 segments gives the client 3 playable segments plus one buffer.
|
|
23
|
+
const PREROLL_INITIAL_WINDOW = 4;
|
|
24
|
+
// Cached preroll buffers. Generated once at startup, reused for all streams.
|
|
25
|
+
let prerollInitSegment = null;
|
|
26
|
+
// Individual preroll media segment buffers. Each entry is one moof+mdat pair with naturally monotonic PTS from the continuous FFmpeg output. Unlike the previous
|
|
27
|
+
// implementation that served the same buffer N times (causing PTS restarts), each segment has distinct timestamps.
|
|
28
|
+
const prerollMediaSegments = [];
|
|
29
|
+
// Per-segment durations in seconds. Derived from trun sample durations and moov timescales during splitPrerollBuffers(). Used for EXTINF values in the playlist.
|
|
30
|
+
const prerollSegmentDurations = [];
|
|
31
|
+
// Public Accessors.
|
|
32
|
+
/**
|
|
33
|
+
* Returns true when the preroll init segment and at least one media segment have been generated and are ready to serve.
|
|
34
|
+
*/
|
|
35
|
+
export function isPrerollReady() {
|
|
36
|
+
return (prerollInitSegment !== null) && (prerollMediaSegments.length > 0);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Returns the number of preroll segments available. Used by the segmenter to determine the preroll-to-real segment index boundary.
|
|
40
|
+
* @returns The count of preroll media segments.
|
|
41
|
+
*/
|
|
42
|
+
export function getPrerollSegmentCount() {
|
|
43
|
+
return prerollMediaSegments.length;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 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
|
|
47
|
+
* window.
|
|
48
|
+
* @param index - The zero-based segment index.
|
|
49
|
+
* @returns The segment duration in seconds.
|
|
50
|
+
*/
|
|
51
|
+
export function getPrerollSegmentDuration(index) {
|
|
52
|
+
return prerollSegmentDurations[index] ?? 2;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 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
|
|
56
|
+
* from where the preroll ended, eliminating the PTS discontinuity at the preroll-to-live boundary.
|
|
57
|
+
* @returns The sum of all preroll segment durations in seconds.
|
|
58
|
+
*/
|
|
59
|
+
export function getPrerollTotalDurationSec() {
|
|
60
|
+
let total = 0;
|
|
61
|
+
for (const duration of prerollSegmentDurations) {
|
|
62
|
+
total += duration;
|
|
63
|
+
}
|
|
64
|
+
return total;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Returns the maximum duration across all preroll segments, rounded up to the nearest integer. Used for TARGETDURATION computation in the composite playlist.
|
|
68
|
+
* @returns The ceiling of the maximum preroll segment duration.
|
|
69
|
+
*/
|
|
70
|
+
export function getPrerollMaxDuration() {
|
|
71
|
+
if (prerollSegmentDurations.length === 0) {
|
|
72
|
+
return 2;
|
|
73
|
+
}
|
|
74
|
+
let max = 0;
|
|
75
|
+
for (const duration of prerollSegmentDurations) {
|
|
76
|
+
if (duration > max) {
|
|
77
|
+
max = duration;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return Math.ceil(max);
|
|
81
|
+
}
|
|
82
|
+
// Preroll Generation.
|
|
83
|
+
/**
|
|
84
|
+
* Generates the preroll fMP4 at startup. Spawns FFmpeg to create PREROLL_TOTAL_DURATION seconds of black frame + silence as fragmented MP4, then splits
|
|
85
|
+
* 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
|
|
86
|
+
* 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
|
|
87
|
+
* blocking stream setup path is used instead.
|
|
88
|
+
*/
|
|
89
|
+
export async function generatePreroll() {
|
|
90
|
+
const ffmpegPath = await resolveFFmpegPath();
|
|
91
|
+
if (!ffmpegPath) {
|
|
92
|
+
LOG.warn("FFmpeg is not available. Preroll generation skipped — startup playlists will have no segments.");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Use the effective viewport to match the resolution Chrome MediaRecorder will produce. This prevents a resolution mismatch at the preroll→live discontinuity
|
|
96
|
+
// boundary. Baseline profile and level 3.1 match Chrome's MediaRecorder output (confirmed via parseMoovCodecConfig telemetry). Slow preset and CRF 18 produce
|
|
97
|
+
// higher quality — acceptable for a short duration of simple content generated once at startup.
|
|
98
|
+
const viewport = getEffectiveViewport(CONFIG);
|
|
99
|
+
const size = String(viewport.width) + "x" + String(viewport.height);
|
|
100
|
+
const args = [
|
|
101
|
+
"-hide_banner", "-loglevel", "warning",
|
|
102
|
+
"-f", "lavfi", "-i", "color=black:size=" + size + ":rate=30:duration=" + String(PREROLL_TOTAL_DURATION),
|
|
103
|
+
"-f", "lavfi", "-i", "anullsrc=r=48000:cl=stereo",
|
|
104
|
+
"-t", String(PREROLL_TOTAL_DURATION),
|
|
105
|
+
"-c:v", "libx264", "-preset", "slow", "-tune", "stillimage", "-profile:v", "baseline", "-level", "3.1", "-pix_fmt", "yuv420p",
|
|
106
|
+
"-crf", "18",
|
|
107
|
+
"-g", "60",
|
|
108
|
+
"-c:a", "aac", "-b:a", "128000",
|
|
109
|
+
"-f", "mp4",
|
|
110
|
+
"-movflags", "frag_keyframe+empty_moov+default_base_moof+skip_sidx+skip_trailer",
|
|
111
|
+
"-flush_packets", "1",
|
|
112
|
+
"pipe:1"
|
|
113
|
+
];
|
|
114
|
+
try {
|
|
115
|
+
const output = await spawnAndCollect(ffmpegPath, args);
|
|
116
|
+
splitPrerollBuffers(output);
|
|
117
|
+
const init = prerollInitSegment;
|
|
118
|
+
if (init && (prerollMediaSegments.length > 0)) {
|
|
119
|
+
const totalSize = prerollMediaSegments.reduce((sum, seg) => sum + seg.length, 0);
|
|
120
|
+
LOG.debug("streaming:preroll", "Preroll ready: %d segments, init=%d bytes, media=%d bytes total.", prerollMediaSegments.length, init.length, totalSize);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
LOG.warn("Preroll generation produced incomplete output — startup playlists will have no segments.");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
LOG.warn("Preroll generation failed: %s.", error instanceof Error ? error.message : String(error));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Internal Helpers.
|
|
132
|
+
/**
|
|
133
|
+
* Spawns FFmpeg with the given arguments and collects all stdout output into a single Buffer. Used for one-shot operations like preroll generation where the entire
|
|
134
|
+
* output fits in memory.
|
|
135
|
+
* @param ffmpegBin - Path to the FFmpeg executable.
|
|
136
|
+
* @param args - FFmpeg command-line arguments.
|
|
137
|
+
* @returns Promise resolving to the complete stdout output as a Buffer.
|
|
138
|
+
*/
|
|
139
|
+
async function spawnAndCollect(ffmpegBin, args) {
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
const chunks = [];
|
|
142
|
+
const ffmpeg = spawn(ffmpegBin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
143
|
+
ffmpeg.stdout.on("data", (chunk) => {
|
|
144
|
+
chunks.push(chunk);
|
|
145
|
+
});
|
|
146
|
+
ffmpeg.stderr.on("data", (data) => {
|
|
147
|
+
const message = data.toString().trim();
|
|
148
|
+
if (message.length > 0) {
|
|
149
|
+
LOG.debug("streaming:preroll", "FFmpeg: %s", message);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
ffmpeg.on("error", (error) => {
|
|
153
|
+
reject(error);
|
|
154
|
+
});
|
|
155
|
+
ffmpeg.on("exit", (code) => {
|
|
156
|
+
if (code !== 0) {
|
|
157
|
+
reject(new Error("FFmpeg exited with code " + String(code) + "."));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
resolve(Buffer.concat(chunks));
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Splits an fMP4 buffer into an init segment (ftyp + moov) and individual media segments (moof + mdat pairs) using the MP4 box parser. The init segment contains codec
|
|
166
|
+
* configuration needed by the HLS client to initialize its decoder. Each moof+mdat pair becomes a distinct segment buffer with naturally monotonic PTS timestamps.
|
|
167
|
+
*
|
|
168
|
+
* Duration extraction: the moov box is parsed with parseMoovTrackInfo() to get per-track timescales. Each moof is then parsed with offsetMoofTimestamps() (with an
|
|
169
|
+
* empty offset map for pure pass-through) to extract per-track trun durations. The maximum duration across tracks (converted to seconds via timescale) becomes the
|
|
170
|
+
* segment's EXTINF duration.
|
|
171
|
+
*
|
|
172
|
+
* @param data - The complete fMP4 output from FFmpeg.
|
|
173
|
+
*/
|
|
174
|
+
function splitPrerollBuffers(data) {
|
|
175
|
+
const initBoxes = [];
|
|
176
|
+
let moovBox = null;
|
|
177
|
+
// Accumulator for the current moof+mdat pair. A segment is finalized when the mdat following a moof arrives.
|
|
178
|
+
let pendingMoof = null;
|
|
179
|
+
const parser = createMP4BoxParser((box) => {
|
|
180
|
+
if ((box.type === "ftyp") || (box.type === "moov")) {
|
|
181
|
+
initBoxes.push(box.data);
|
|
182
|
+
if (box.type === "moov") {
|
|
183
|
+
moovBox = box.data;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else if (box.type === "moof") {
|
|
187
|
+
// Start a new segment. If there's an orphaned moof without a following mdat (shouldn't happen with well-formed fMP4), it gets overwritten.
|
|
188
|
+
pendingMoof = box.data;
|
|
189
|
+
}
|
|
190
|
+
else if (box.type === "mdat") {
|
|
191
|
+
// Finalize the current segment by combining the pending moof with this mdat.
|
|
192
|
+
if (pendingMoof) {
|
|
193
|
+
prerollMediaSegments.push(Buffer.concat([pendingMoof, box.data]));
|
|
194
|
+
pendingMoof = null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
parser.push(data);
|
|
199
|
+
parser.flush();
|
|
200
|
+
if (initBoxes.length > 0) {
|
|
201
|
+
prerollInitSegment = Buffer.concat(initBoxes);
|
|
202
|
+
}
|
|
203
|
+
// Extract per-segment durations from the moov timescales and moof trun sample durations. The moov provides the timescale for converting raw sample durations to
|
|
204
|
+
// seconds. Each moof's traf/trun boxes contain the accumulated sample durations for that fragment.
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- moovBox is set inside the parser callback; TS can't track closure mutations.
|
|
206
|
+
if (moovBox && (prerollMediaSegments.length > 0)) {
|
|
207
|
+
const trackInfoMap = parseMoovTrackInfo(moovBox);
|
|
208
|
+
// Build a timescale lookup from trackId to timescale.
|
|
209
|
+
const timescales = new Map();
|
|
210
|
+
for (const [trackId, info] of trackInfoMap) {
|
|
211
|
+
timescales.set(trackId, info.timescale);
|
|
212
|
+
}
|
|
213
|
+
// Extract duration from each segment's moof box. The offsetMoofTimestamps function with an empty offset map acts as a pure reader — it returns per-track
|
|
214
|
+
// durations without modifying the buffer (it writes back the same tfdt values it read since the offset is 0). A fresh map is created per segment to prevent
|
|
215
|
+
// state accumulation across iterations.
|
|
216
|
+
for (const segment of prerollMediaSegments) {
|
|
217
|
+
// The segment buffer is moof+mdat concatenated. The moof is the first box — parse it to find its size.
|
|
218
|
+
if (segment.length < 8) {
|
|
219
|
+
prerollSegmentDurations.push(2);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const moofSize = segment.readUInt32BE(0);
|
|
223
|
+
const moofData = segment.subarray(0, moofSize);
|
|
224
|
+
// Extract per-track durations from the moof. Take the maximum across tracks and convert to seconds.
|
|
225
|
+
const trackResults = offsetMoofTimestamps(moofData, new Map());
|
|
226
|
+
let maxDurationSec = 0;
|
|
227
|
+
for (const [trackId, result] of trackResults) {
|
|
228
|
+
const timescale = timescales.get(trackId);
|
|
229
|
+
if (timescale && (result.duration > 0n)) {
|
|
230
|
+
const seconds = Number(result.duration) / timescale;
|
|
231
|
+
if (seconds > maxDurationSec) {
|
|
232
|
+
maxDurationSec = seconds;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Fall back to 2 seconds if duration extraction failed (shouldn't happen with well-formed FFmpeg output).
|
|
237
|
+
prerollSegmentDurations.push(maxDurationSec > 0 ? maxDurationSec : 2);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Preroll Compositor.
|
|
242
|
+
// Maximum number of preroll entries allowed in a composite playlist window. When the composite playlist first appears, the client may be mid-stream in the progressive
|
|
243
|
+
// preroll playlist at a low segment index. Limiting preroll entries forces MEDIA-SEQUENCE past the client's current position, causing it to seek forward to near the
|
|
244
|
+
// live edge rather than playing through many seconds of remaining preroll before reaching real content.
|
|
245
|
+
const MAX_PREROLL_IN_WINDOW = 3;
|
|
246
|
+
/**
|
|
247
|
+
* Computes the sliding window start index for a composite playlist containing both preroll and real segments. The start index is determined by three constraints:
|
|
248
|
+
* (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
|
|
249
|
+
* the window. The third constraint is the key addition — it prevents clients from playing through many seconds of remaining preroll before reaching live content.
|
|
250
|
+
*
|
|
251
|
+
* @param options - Window computation parameters.
|
|
252
|
+
* @returns The start index for the composite playlist window.
|
|
253
|
+
*/
|
|
254
|
+
export function computePrerollWindow(options) {
|
|
255
|
+
const totalAvailable = options.prerollSegmentCount + options.realSegmentCount;
|
|
256
|
+
const windowSize = Math.min(totalAvailable, options.maxSegments);
|
|
257
|
+
return Math.max(0, options.currentSegmentIndex - windowSize, options.prerollSegmentCount - MAX_PREROLL_IN_WINDOW);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 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
|
|
261
|
+
* and the duration extracted from the preroll segment cache. No per-segment metadata tags (DISCONTINUITY, PROGRAM-DATE-TIME, etc.) are set — preroll is synthetic
|
|
262
|
+
* placeholder content.
|
|
263
|
+
*
|
|
264
|
+
* @param options - Entry construction parameters.
|
|
265
|
+
* @returns Ordered array of preroll segment entries for the builder.
|
|
266
|
+
*/
|
|
267
|
+
export function buildPrerollEntries(options) {
|
|
268
|
+
const entries = [];
|
|
269
|
+
for (let i = options.startIndex; i < options.prerollSegmentCount; i++) {
|
|
270
|
+
entries.push({
|
|
271
|
+
duration: getPrerollSegmentDuration(i),
|
|
272
|
+
url: options.baseUrl + "/preroll/segment" + String(i) + options.extension
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return entries;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 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)
|
|
279
|
+
* 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
|
|
280
|
+
* time passes — each new segment appears when enough time has elapsed for the client to have consumed the prior content beyond the initial window.
|
|
281
|
+
*
|
|
282
|
+
* @param prerollStartTime - Wall-clock time when the preroll began.
|
|
283
|
+
* @returns The number of preroll segments to reveal.
|
|
284
|
+
*/
|
|
285
|
+
export function computeProgressiveReveal(prerollStartTime) {
|
|
286
|
+
const totalSegments = prerollMediaSegments.length;
|
|
287
|
+
const elapsedSec = (Date.now() - prerollStartTime.getTime()) / 1000;
|
|
288
|
+
let revealCount = Math.min(PREROLL_INITIAL_WINDOW, totalSegments);
|
|
289
|
+
// Compute the total duration of the initial window. Segments beyond this threshold are revealed progressively — each one becomes visible when elapsed time
|
|
290
|
+
// exceeds the cumulative duration of all segments before it, minus the initial window's duration (since those were available from the start).
|
|
291
|
+
let initialWindowDuration = 0;
|
|
292
|
+
for (let i = 0; i < Math.min(PREROLL_INITIAL_WINDOW, totalSegments); i++) {
|
|
293
|
+
initialWindowDuration += prerollSegmentDurations[i] ?? 2;
|
|
294
|
+
}
|
|
295
|
+
let cumulativeDuration = initialWindowDuration;
|
|
296
|
+
for (let i = PREROLL_INITIAL_WINDOW; i < totalSegments; i++) {
|
|
297
|
+
cumulativeDuration += prerollSegmentDurations[i] ?? 2;
|
|
298
|
+
if (elapsedSec < (cumulativeDuration - initialWindowDuration)) {
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
revealCount = i + 1;
|
|
302
|
+
}
|
|
303
|
+
return revealCount;
|
|
304
|
+
}
|
|
305
|
+
// Playlist Generation.
|
|
306
|
+
/**
|
|
307
|
+
* Generates a progressive HLS playlist referencing the global preroll segments. The playlist simulates a live stream by revealing segments based on elapsed wall-clock
|
|
308
|
+
* time since the preroll started. On each client poll, more segments become visible — the client sees new content appear and keeps playing without stalling.
|
|
309
|
+
*
|
|
310
|
+
* The playlist uses absolute URLs because preroll segments are served at global routes (/preroll/*) while the playlist itself is served under /hls/:name/. Without
|
|
311
|
+
* 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 —
|
|
312
|
+
* the client polls for updates and receives the real content playlist when ready.
|
|
313
|
+
*
|
|
314
|
+
* PROGRAM-DATE-TIME is intentionally omitted from the preroll playlist. Preroll is synthetic placeholder content — assigning it wall-clock timestamps would be
|
|
315
|
+
* 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
|
|
316
|
+
* seconds of content but real content typically arrives in ~15 seconds). PDT is emitted only on real segments once the segmenter produces them.
|
|
317
|
+
*
|
|
318
|
+
* @param baseUrl - The server's external URL (e.g., "http://192.168.1.100:5589").
|
|
319
|
+
* @param startingSequence - The MEDIA-SEQUENCE offset. Zero for fresh starts. For resume streams, this is set to the saved segment index so the preroll
|
|
320
|
+
* playlist continues from the prior session's sequence range rather than restarting at 0.
|
|
321
|
+
* @param prerollStartTime - Wall-clock time when the preroll began. Used to compute elapsed time and determine how many segments to reveal.
|
|
322
|
+
* @returns The complete HLS playlist string, or an empty string if the preroll is not ready.
|
|
323
|
+
*/
|
|
324
|
+
export function generatePrerollPlaylist(baseUrl, startingSequence, prerollStartTime) {
|
|
325
|
+
if (!isPrerollReady()) {
|
|
326
|
+
return "";
|
|
327
|
+
}
|
|
328
|
+
const revealCount = computeProgressiveReveal(prerollStartTime);
|
|
329
|
+
const entries = buildPrerollEntries({ baseUrl, extension: ".m4s", prerollSegmentCount: revealCount, startIndex: 0 });
|
|
330
|
+
return buildPlaylist({
|
|
331
|
+
initialMapUri: baseUrl + "/preroll/init.mp4",
|
|
332
|
+
mediaSequence: startingSequence,
|
|
333
|
+
targetDuration: getPrerollMaxDuration(),
|
|
334
|
+
version: 7
|
|
335
|
+
}, entries);
|
|
336
|
+
}
|
|
337
|
+
// Route Setup.
|
|
338
|
+
/**
|
|
339
|
+
* Registers the preroll segment routes on the Express application. These serve the cached preroll buffers with immutable cache headers since the content never
|
|
340
|
+
* changes after generation.
|
|
341
|
+
* @param app - The Express application.
|
|
342
|
+
*/
|
|
343
|
+
export function setupPrerollRoutes(app) {
|
|
344
|
+
app.get("/preroll/init.mp4", (_req, res) => {
|
|
345
|
+
if (!prerollInitSegment) {
|
|
346
|
+
res.status(404).send("Preroll not available.");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
res.setHeader("Cache-Control", "public, max-age=86400");
|
|
350
|
+
res.setHeader("Content-Type", "video/mp4");
|
|
351
|
+
res.send(prerollInitSegment);
|
|
352
|
+
});
|
|
353
|
+
// Parameterized route serves individual preroll segment buffers. Each segment contains a distinct moof+mdat pair with unique PTS values from the continuous FFmpeg
|
|
354
|
+
// output. The init.mp4 exact route above takes priority over this catch-all for init segment requests.
|
|
355
|
+
app.get("/preroll/:segment", (req, res) => {
|
|
356
|
+
// Extract the segment index from the filename (e.g., "segment5.m4s" → 5).
|
|
357
|
+
const segmentParam = req.params.segment;
|
|
358
|
+
const match = (typeof segmentParam === "string") ? /^segment(\d+)\.m4s$/.exec(segmentParam) : null;
|
|
359
|
+
if (!match) {
|
|
360
|
+
res.status(404).send("Preroll not available.");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const index = parseInt(match[1], 10);
|
|
364
|
+
if ((index < 0) || (index >= prerollMediaSegments.length)) {
|
|
365
|
+
res.status(404).send("Preroll segment not found.");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
res.setHeader("Cache-Control", "public, max-age=86400");
|
|
369
|
+
res.setHeader("Content-Type", "video/mp4");
|
|
370
|
+
res.send(prerollMediaSegments[index]);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
//# sourceMappingURL=preroll.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preroll.js","sourceRoot":"","sources":["../../src/streaming/preroll.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starts the pretune polling loop. Should be called on server startup after show info polling is initialized.
|
|
3
|
+
*/
|
|
4
|
+
export declare function startPretunePolling(): void;
|
|
5
|
+
/**
|
|
6
|
+
* Stops the pretune polling loop and clears all pending timers. Should be called on server shutdown.
|
|
7
|
+
*/
|
|
8
|
+
export declare function stopPretunePolling(): void;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* pretune.ts: Predictive channel pretuning from Channels DVR schedule.
|
|
4
|
+
*/
|
|
5
|
+
import { LOG, delay, formatError } from "../utils/index.js";
|
|
6
|
+
import { fetchFromDvr, getDeviceMappings, getDvrHost } from "./showInfo.js";
|
|
7
|
+
import { getChannelStreamId, terminateStream } from "./lifecycle.js";
|
|
8
|
+
import { initializeStream, validateChannel } from "./hls.js";
|
|
9
|
+
import { emitCurrentSystemStatus } from "../browser/index.js";
|
|
10
|
+
import { getStream } from "./registry.js";
|
|
11
|
+
/* This module polls the Channels DVR schedule API to discover upcoming recordings and pretunes channels 30 seconds before they start. When the DVR requests the
|
|
12
|
+
* stream, it's already live with buffered segments — achieving near-instant tuning instead of 3-7 second cold starts.
|
|
13
|
+
*
|
|
14
|
+
* The polling loop runs every 60 seconds, checking for jobs starting within a 5-minute scheduling horizon. For each eligible job (PrismCast channel as the DVR's
|
|
15
|
+
* preferred source), a precise setTimeout is scheduled for 30 seconds before the recording start time. When the timer fires, the module validates the channel,
|
|
16
|
+
* checks for conflicts, and calls initializeStream() with the preTuned flag. A safety timeout tears down unclaimed streams 90 seconds after the scheduled start.
|
|
17
|
+
*
|
|
18
|
+
* Key design decisions:
|
|
19
|
+
* - Only pretune when a PrismCast guide number is the FIRST entry in the job's channels array (DVR's preferred source).
|
|
20
|
+
* - Skip channels that are already streaming — no duplicate streams for the same channel.
|
|
21
|
+
* - Pretune alongside existing streams freely; capacity enforcement is handled by initializeStream().
|
|
22
|
+
* - Retry up to 5 times within the pretune window on failure.
|
|
23
|
+
* - Safety timeout at start_time + 90s handles cancelled jobs or missed client connections.
|
|
24
|
+
*/
|
|
25
|
+
// Constants.
|
|
26
|
+
// How often to poll for upcoming jobs (60 seconds).
|
|
27
|
+
const POLL_INTERVAL_MS = 60000;
|
|
28
|
+
// How far ahead to look for upcoming jobs (5 minutes).
|
|
29
|
+
const SCHEDULING_HORIZON_MS = 300000;
|
|
30
|
+
// How far before the recording start to begin pretuning (30 seconds).
|
|
31
|
+
const PRETUNE_LEAD_MS = 30000;
|
|
32
|
+
// Safety timeout for unclaimed pretuned streams (90 seconds past scheduled start).
|
|
33
|
+
const SAFETY_TIMEOUT_MS = 90000;
|
|
34
|
+
// Maximum retry attempts for failed pretune.
|
|
35
|
+
const MAX_RETRIES = 5;
|
|
36
|
+
// Delay between retry attempts (5 seconds).
|
|
37
|
+
const RETRY_DELAY_MS = 5000;
|
|
38
|
+
// State.
|
|
39
|
+
// Interval handle for periodic job polling.
|
|
40
|
+
let pollInterval = null;
|
|
41
|
+
// Active pretune timers keyed by job ID. Used to avoid duplicate scheduling and to clear timers when jobs disappear.
|
|
42
|
+
const activeTimers = new Map();
|
|
43
|
+
// Safety timers keyed by stream ID. Used to tear down unclaimed pretuned streams after the scheduled start time.
|
|
44
|
+
const safetyTimers = new Map();
|
|
45
|
+
// Public API.
|
|
46
|
+
/**
|
|
47
|
+
* Starts the pretune polling loop. Should be called on server startup after show info polling is initialized.
|
|
48
|
+
*/
|
|
49
|
+
export function startPretunePolling() {
|
|
50
|
+
if (pollInterval) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Run the first poll after a short delay to allow the persisted DVR host to load.
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
void pollForUpcomingJobs();
|
|
56
|
+
}, 5000);
|
|
57
|
+
pollInterval = setInterval(() => {
|
|
58
|
+
void pollForUpcomingJobs();
|
|
59
|
+
}, POLL_INTERVAL_MS);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Stops the pretune polling loop and clears all pending timers. Should be called on server shutdown.
|
|
63
|
+
*/
|
|
64
|
+
export function stopPretunePolling() {
|
|
65
|
+
if (pollInterval) {
|
|
66
|
+
clearInterval(pollInterval);
|
|
67
|
+
pollInterval = null;
|
|
68
|
+
}
|
|
69
|
+
// Clear all pending pretune timers.
|
|
70
|
+
for (const timer of activeTimers.values()) {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
}
|
|
73
|
+
activeTimers.clear();
|
|
74
|
+
// Clear all safety timers.
|
|
75
|
+
for (const timer of safetyTimers.values()) {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
}
|
|
78
|
+
safetyTimers.clear();
|
|
79
|
+
}
|
|
80
|
+
// Internal Functions.
|
|
81
|
+
/**
|
|
82
|
+
* Polls the Channels DVR API for upcoming scheduled recordings and schedules pretune timers for eligible jobs.
|
|
83
|
+
*/
|
|
84
|
+
async function pollForUpcomingJobs() {
|
|
85
|
+
const host = getDvrHost();
|
|
86
|
+
if (!host) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const jobs = await fetchFromDvr(host, "/api/v1/jobs");
|
|
90
|
+
if (jobs.length === 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
const horizon = now + SCHEDULING_HORIZON_MS;
|
|
95
|
+
// Track which job IDs we see this poll cycle so we can clear timers for removed jobs.
|
|
96
|
+
const seenJobIds = new Set();
|
|
97
|
+
// Get the device mappings once for resolving guide numbers. The cache ensures this is fast on repeated calls.
|
|
98
|
+
const mappings = await getDeviceMappings(host);
|
|
99
|
+
if (mappings.size === 0) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const job of jobs) {
|
|
103
|
+
// Skip cancelled or skipped jobs.
|
|
104
|
+
if (job.item.cancelled || job.skipped) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const startMs = job.start_time * 1000;
|
|
108
|
+
// Skip jobs outside our scheduling horizon or already started.
|
|
109
|
+
if ((startMs > horizon) || (startMs <= now)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Only consider jobs where the first (preferred) channel is a PrismCast channel.
|
|
113
|
+
if (job.channels.length === 0) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const guideNumber = job.channels[0];
|
|
117
|
+
const channelId = resolveGuideNumber(mappings, guideNumber);
|
|
118
|
+
if (!channelId) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
seenJobIds.add(job.id);
|
|
122
|
+
// Skip if a timer is already scheduled for this job.
|
|
123
|
+
if (activeTimers.has(job.id)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// Calculate when to pretune. If the pretune time has already passed but the start time hasn't, pretune immediately.
|
|
127
|
+
const pretuneTime = startMs - PRETUNE_LEAD_MS;
|
|
128
|
+
const effectiveDelay = Math.max(0, pretuneTime - now);
|
|
129
|
+
LOG.debug("streaming:pretune", "Scheduling pretune for '%s' (%s) in %ds.", job.name, channelId, Math.round(effectiveDelay / 1000));
|
|
130
|
+
const timer = setTimeout(() => {
|
|
131
|
+
activeTimers.delete(job.id);
|
|
132
|
+
void pretuneChannel(channelId, job.name, startMs);
|
|
133
|
+
}, effectiveDelay);
|
|
134
|
+
activeTimers.set(job.id, timer);
|
|
135
|
+
}
|
|
136
|
+
// Clear timers for jobs that disappeared from the API (cancelled, rescheduled, or moved outside the horizon).
|
|
137
|
+
for (const [jobId, timer] of activeTimers) {
|
|
138
|
+
if (!seenJobIds.has(jobId)) {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
activeTimers.delete(jobId);
|
|
141
|
+
LOG.debug("streaming:pretune", "Cleared timer for removed job %s.", jobId);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Resolves a DVR guide number to a PrismCast channel ID by checking all device mappings.
|
|
147
|
+
* @param mappings - Map of DeviceID to (Map of GuideNumber to channel ID).
|
|
148
|
+
* @param guideNumber - The guide number to resolve (e.g., "7220").
|
|
149
|
+
* @returns The PrismCast channel ID if the guide number maps to a PrismCast channel, undefined otherwise.
|
|
150
|
+
*/
|
|
151
|
+
function resolveGuideNumber(mappings, guideNumber) {
|
|
152
|
+
for (const deviceMappings of mappings.values()) {
|
|
153
|
+
const channelId = deviceMappings.get(guideNumber);
|
|
154
|
+
if (channelId) {
|
|
155
|
+
return channelId;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Pretunes a channel ahead of a scheduled recording. Validates the channel, checks for conflicts, initializes the stream with the preTuned flag, and sets up a
|
|
162
|
+
* safety timeout for unclaimed streams. Retries on failure up to MAX_RETRIES times within the pretune window.
|
|
163
|
+
* @param channelId - The PrismCast channel key (e.g., "cnn").
|
|
164
|
+
* @param jobName - The program title for logging (e.g., "Anderson Cooper 360").
|
|
165
|
+
* @param startTimeMs - The recording start time in milliseconds.
|
|
166
|
+
*/
|
|
167
|
+
async function pretuneChannel(channelId, jobName, startTimeMs) {
|
|
168
|
+
// Check if the channel is already streaming.
|
|
169
|
+
const existingStreamId = getChannelStreamId(channelId);
|
|
170
|
+
if (existingStreamId !== undefined) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Validate the channel.
|
|
174
|
+
const validation = validateChannel(channelId);
|
|
175
|
+
if (!validation.valid) {
|
|
176
|
+
LOG.debug("streaming:pretune", "Pretune validation failed for %s: %s.", channelId, typeof validation.body === "string" ? validation.body : "validation error");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const displayName = validation.channel.name ?? channelId;
|
|
180
|
+
let attempts = 0;
|
|
181
|
+
while (attempts < MAX_RETRIES) {
|
|
182
|
+
attempts++;
|
|
183
|
+
try {
|
|
184
|
+
// eslint-disable-next-line no-await-in-loop
|
|
185
|
+
const streamId = await initializeStream({
|
|
186
|
+
channel: validation.channel,
|
|
187
|
+
channelName: channelId,
|
|
188
|
+
clientAddress: null,
|
|
189
|
+
preTuned: true,
|
|
190
|
+
url: validation.channel.url
|
|
191
|
+
});
|
|
192
|
+
if (streamId !== null) {
|
|
193
|
+
const leadSeconds = Math.round((startTimeMs - Date.now()) / 1000);
|
|
194
|
+
LOG.info("Pretuned %s for %s (starts in %ds).", displayName, jobName, leadSeconds);
|
|
195
|
+
// Set a safety timeout to tear down the stream if no real client connects within 90 seconds of the scheduled start.
|
|
196
|
+
const safetyDelay = Math.max(0, (startTimeMs + SAFETY_TIMEOUT_MS) - Date.now());
|
|
197
|
+
const safetyTimer = setTimeout(() => {
|
|
198
|
+
safetyTimers.delete(streamId);
|
|
199
|
+
const stream = getStream(streamId);
|
|
200
|
+
if (stream?.preTuned) {
|
|
201
|
+
LOG.info("No client connected for pretuned %s. Ending stream.", displayName);
|
|
202
|
+
terminateStream(streamId, channelId, "pretune safety timeout");
|
|
203
|
+
void emitCurrentSystemStatus();
|
|
204
|
+
}
|
|
205
|
+
}, safetyDelay);
|
|
206
|
+
safetyTimers.set(streamId, safetyTimer);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
LOG.warn("Pretune attempt %d/%d failed for %s: %s.", attempts, MAX_RETRIES, displayName, formatError(error));
|
|
212
|
+
}
|
|
213
|
+
// Stop retrying if we're past the scheduled start time.
|
|
214
|
+
if (Date.now() >= startTimeMs) {
|
|
215
|
+
LOG.debug("streaming:pretune", "Past start time for %s. Stopping pretune attempts.", channelId);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Brief delay before retry.
|
|
219
|
+
if (attempts < MAX_RETRIES) {
|
|
220
|
+
// eslint-disable-next-line no-await-in-loop
|
|
221
|
+
await delay(RETRY_DELAY_MS);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
LOG.warn("All %d pretune attempts failed for %s.", MAX_RETRIES, displayName);
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=pretune.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pretune.js","sourceRoot":"","sources":["../../src/streaming/pretune.ts"],"names":[],"mappings":"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"}
|