@bnhf/prismcast 1.3.4-2026.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +7 -0
- package/README.md +347 -0
- package/bin/prismcast +6 -0
- package/dist/app.d.ts +6 -0
- package/dist/app.js +315 -0
- package/dist/app.js.map +1 -0
- package/dist/browser/cdp.d.ts +38 -0
- package/dist/browser/cdp.js +155 -0
- package/dist/browser/cdp.js.map +1 -0
- package/dist/browser/channelSelection.d.ts +65 -0
- package/dist/browser/channelSelection.js +202 -0
- package/dist/browser/channelSelection.js.map +1 -0
- package/dist/browser/display.d.ts +34 -0
- package/dist/browser/display.js +54 -0
- package/dist/browser/display.js.map +1 -0
- package/dist/browser/index.d.ts +205 -0
- package/dist/browser/index.js +1205 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/tuning/fox.d.ts +2 -0
- package/dist/browser/tuning/fox.js +83 -0
- package/dist/browser/tuning/fox.js.map +1 -0
- package/dist/browser/tuning/hbo.d.ts +2 -0
- package/dist/browser/tuning/hbo.js +237 -0
- package/dist/browser/tuning/hbo.js.map +1 -0
- package/dist/browser/tuning/hulu.d.ts +2 -0
- package/dist/browser/tuning/hulu.js +550 -0
- package/dist/browser/tuning/hulu.js.map +1 -0
- package/dist/browser/tuning/sling.d.ts +2 -0
- package/dist/browser/tuning/sling.js +518 -0
- package/dist/browser/tuning/sling.js.map +1 -0
- package/dist/browser/tuning/thumbnailRow.d.ts +2 -0
- package/dist/browser/tuning/thumbnailRow.js +108 -0
- package/dist/browser/tuning/thumbnailRow.js.map +1 -0
- package/dist/browser/tuning/tileClick.d.ts +2 -0
- package/dist/browser/tuning/tileClick.js +103 -0
- package/dist/browser/tuning/tileClick.js.map +1 -0
- package/dist/browser/tuning/youtubeTv.d.ts +2 -0
- package/dist/browser/tuning/youtubeTv.js +182 -0
- package/dist/browser/tuning/youtubeTv.js.map +1 -0
- package/dist/browser/video.d.ts +289 -0
- package/dist/browser/video.js +996 -0
- package/dist/browser/video.js.map +1 -0
- package/dist/channels/index.d.ts +3 -0
- package/dist/channels/index.js +392 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/config/index.d.ts +53 -0
- package/dist/config/index.js +233 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/presets.d.ts +98 -0
- package/dist/config/presets.js +241 -0
- package/dist/config/presets.js.map +1 -0
- package/dist/config/profiles.d.ts +79 -0
- package/dist/config/profiles.js +245 -0
- package/dist/config/profiles.js.map +1 -0
- package/dist/config/providers.d.ts +120 -0
- package/dist/config/providers.js +450 -0
- package/dist/config/providers.js.map +1 -0
- package/dist/config/sites.d.ts +22 -0
- package/dist/config/sites.js +377 -0
- package/dist/config/sites.js.map +1 -0
- package/dist/config/userChannels.d.ts +178 -0
- package/dist/config/userChannels.js +543 -0
- package/dist/config/userChannels.js.map +1 -0
- package/dist/config/userConfig.d.ts +235 -0
- package/dist/config/userConfig.js +913 -0
- package/dist/config/userConfig.js.map +1 -0
- package/dist/hdhr/channelMap.d.ts +21 -0
- package/dist/hdhr/channelMap.js +82 -0
- package/dist/hdhr/channelMap.js.map +1 -0
- package/dist/hdhr/deviceId.d.ts +11 -0
- package/dist/hdhr/deviceId.js +84 -0
- package/dist/hdhr/deviceId.js.map +1 -0
- package/dist/hdhr/discover.d.ts +6 -0
- package/dist/hdhr/discover.js +155 -0
- package/dist/hdhr/discover.js.map +1 -0
- package/dist/hdhr/index.d.ts +9 -0
- package/dist/hdhr/index.js +87 -0
- package/dist/hdhr/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +144 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/assets.d.ts +6 -0
- package/dist/routes/assets.js +79 -0
- package/dist/routes/assets.js.map +1 -0
- package/dist/routes/auth.d.ts +6 -0
- package/dist/routes/auth.js +77 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/channels.d.ts +6 -0
- package/dist/routes/channels.js +40 -0
- package/dist/routes/channels.js.map +1 -0
- package/dist/routes/components.d.ts +138 -0
- package/dist/routes/components.js +210 -0
- package/dist/routes/components.js.map +1 -0
- package/dist/routes/config.d.ts +72 -0
- package/dist/routes/config.js +1977 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/debug.d.ts +6 -0
- package/dist/routes/debug.js +274 -0
- package/dist/routes/debug.js.map +1 -0
- package/dist/routes/health.d.ts +6 -0
- package/dist/routes/health.js +85 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/hls.d.ts +6 -0
- package/dist/routes/hls.js +25 -0
- package/dist/routes/hls.js.map +1 -0
- package/dist/routes/index.d.ts +19 -0
- package/dist/routes/index.js +49 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/logs.d.ts +6 -0
- package/dist/routes/logs.js +164 -0
- package/dist/routes/logs.js.map +1 -0
- package/dist/routes/mpegts.d.ts +6 -0
- package/dist/routes/mpegts.js +19 -0
- package/dist/routes/mpegts.js.map +1 -0
- package/dist/routes/play.d.ts +6 -0
- package/dist/routes/play.js +18 -0
- package/dist/routes/play.js.map +1 -0
- package/dist/routes/playlist.d.ts +36 -0
- package/dist/routes/playlist.js +134 -0
- package/dist/routes/playlist.js.map +1 -0
- package/dist/routes/root.d.ts +6 -0
- package/dist/routes/root.js +2920 -0
- package/dist/routes/root.js.map +1 -0
- package/dist/routes/streams.d.ts +6 -0
- package/dist/routes/streams.js +88 -0
- package/dist/routes/streams.js.map +1 -0
- package/dist/routes/theme.d.ts +15 -0
- package/dist/routes/theme.js +275 -0
- package/dist/routes/theme.js.map +1 -0
- package/dist/routes/ui.d.ts +56 -0
- package/dist/routes/ui.js +354 -0
- package/dist/routes/ui.js.map +1 -0
- package/dist/service/commands.d.ts +41 -0
- package/dist/service/commands.js +391 -0
- package/dist/service/commands.js.map +1 -0
- package/dist/service/generators.d.ts +33 -0
- package/dist/service/generators.js +432 -0
- package/dist/service/generators.js.map +1 -0
- package/dist/service/index.d.ts +2 -0
- package/dist/service/index.js +7 -0
- package/dist/service/index.js.map +1 -0
- package/dist/streaming/clients.d.ts +48 -0
- package/dist/streaming/clients.js +114 -0
- package/dist/streaming/clients.js.map +1 -0
- package/dist/streaming/fmp4Segmenter.d.ts +61 -0
- package/dist/streaming/fmp4Segmenter.js +461 -0
- package/dist/streaming/fmp4Segmenter.js.map +1 -0
- package/dist/streaming/hls.d.ts +120 -0
- package/dist/streaming/hls.js +722 -0
- package/dist/streaming/hls.js.map +1 -0
- package/dist/streaming/hlsSegments.d.ts +54 -0
- package/dist/streaming/hlsSegments.js +162 -0
- package/dist/streaming/hlsSegments.js.map +1 -0
- package/dist/streaming/lifecycle.d.ts +33 -0
- package/dist/streaming/lifecycle.js +185 -0
- package/dist/streaming/lifecycle.js.map +1 -0
- package/dist/streaming/monitor.d.ts +74 -0
- package/dist/streaming/monitor.js +1310 -0
- package/dist/streaming/monitor.js.map +1 -0
- package/dist/streaming/mp4Parser.d.ts +74 -0
- package/dist/streaming/mp4Parser.js +566 -0
- package/dist/streaming/mp4Parser.js.map +1 -0
- package/dist/streaming/mpegts.d.ts +14 -0
- package/dist/streaming/mpegts.js +248 -0
- package/dist/streaming/mpegts.js.map +1 -0
- package/dist/streaming/registry.d.ts +119 -0
- package/dist/streaming/registry.js +127 -0
- package/dist/streaming/registry.js.map +1 -0
- package/dist/streaming/setup.d.ts +135 -0
- package/dist/streaming/setup.js +670 -0
- package/dist/streaming/setup.js.map +1 -0
- package/dist/streaming/showInfo.d.ts +30 -0
- package/dist/streaming/showInfo.js +362 -0
- package/dist/streaming/showInfo.js.map +1 -0
- package/dist/streaming/statusEmitter.d.ts +125 -0
- package/dist/streaming/statusEmitter.js +139 -0
- package/dist/streaming/statusEmitter.js.map +1 -0
- package/dist/types/index.d.ts +403 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/debugFilter.d.ts +38 -0
- package/dist/utils/debugFilter.js +157 -0
- package/dist/utils/debugFilter.js.map +1 -0
- package/dist/utils/delay.d.ts +6 -0
- package/dist/utils/delay.js +15 -0
- package/dist/utils/delay.js.map +1 -0
- package/dist/utils/errors.d.ts +15 -0
- package/dist/utils/errors.js +40 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/evaluate.d.ts +51 -0
- package/dist/utils/evaluate.js +124 -0
- package/dist/utils/evaluate.js.map +1 -0
- package/dist/utils/ffmpeg.d.ts +65 -0
- package/dist/utils/ffmpeg.js +317 -0
- package/dist/utils/ffmpeg.js.map +1 -0
- package/dist/utils/fileLogger.d.ts +25 -0
- package/dist/utils/fileLogger.js +248 -0
- package/dist/utils/fileLogger.js.map +1 -0
- package/dist/utils/format.d.ts +16 -0
- package/dist/utils/format.js +46 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/html.d.ts +6 -0
- package/dist/utils/html.js +24 -0
- package/dist/utils/html.js.map +1 -0
- package/dist/utils/index.d.ts +15 -0
- package/dist/utils/index.js +20 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logEmitter.d.ts +17 -0
- package/dist/utils/logEmitter.js +30 -0
- package/dist/utils/logEmitter.js.map +1 -0
- package/dist/utils/logger.d.ts +82 -0
- package/dist/utils/logger.js +219 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/m3u.d.ts +32 -0
- package/dist/utils/m3u.js +148 -0
- package/dist/utils/m3u.js.map +1 -0
- package/dist/utils/morganStream.d.ts +7 -0
- package/dist/utils/morganStream.js +33 -0
- package/dist/utils/morganStream.js.map +1 -0
- package/dist/utils/platform.d.ts +64 -0
- package/dist/utils/platform.js +157 -0
- package/dist/utils/platform.js.map +1 -0
- package/dist/utils/retry.d.ts +15 -0
- package/dist/utils/retry.js +82 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/streamContext.d.ts +28 -0
- package/dist/utils/streamContext.js +33 -0
- package/dist/utils/streamContext.js.map +1 -0
- package/dist/utils/version.d.ts +37 -0
- package/dist/utils/version.js +228 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +92 -0
- package/prismcast.png +0 -0
- package/prismcast.svg +74 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { LOG } from "./logger.js";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import ffmpegForHomebridge from "ffmpeg-for-homebridge";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
// The ffmpeg-for-homebridge package has incorrect type definitions (declares named export but JS uses default export). Cast to the correct type.
|
|
8
|
+
const ffmpegPath = ffmpegForHomebridge;
|
|
9
|
+
/* When using WebM capture mode, Chrome's MediaRecorder outputs WebM container with H264 video and Opus audio. For HLS compatibility, we need fMP4 container with
|
|
10
|
+
* H264 video and AAC audio. FFmpeg handles this conversion:
|
|
11
|
+
*
|
|
12
|
+
* - Video: Passed through unchanged (copy codec) - no quality loss, minimal CPU
|
|
13
|
+
* - Audio: Transcoded from Opus to AAC - lightweight operation
|
|
14
|
+
* - Container: Converted from WebM to fragmented MP4 with streaming-friendly flags
|
|
15
|
+
*
|
|
16
|
+
* The FFmpeg process runs for the lifetime of the stream, reading WebM from stdin and writing fMP4 to stdout. This output feeds directly into the existing fMP4
|
|
17
|
+
* segmenter.
|
|
18
|
+
*/
|
|
19
|
+
/* FFmpeg can be located in several places depending on how it was installed. We check in order of preference:
|
|
20
|
+
* 1. Channels DVR bundled FFmpeg:
|
|
21
|
+
* - macOS: ~/Library/Application Support/ChannelsDVR/latest/ffmpeg
|
|
22
|
+
* - Windows: C:\ProgramData\channelsdvr\latest\ffmpeg.exe
|
|
23
|
+
* - Linux: ~/channels-dvr/latest/ffmpeg, /usr/local/channels-dvr/latest/ffmpeg, /opt/channels-dvr/latest/ffmpeg
|
|
24
|
+
* 2. Bundled FFmpeg from ffmpeg-for-homebridge package
|
|
25
|
+
* 3. System PATH (standard installation via package manager or manual install)
|
|
26
|
+
*
|
|
27
|
+
* The resolved path is cached after the first successful lookup to avoid repeated filesystem checks.
|
|
28
|
+
*/
|
|
29
|
+
// Cached FFmpeg path after resolution. Null means not yet resolved, undefined means not found.
|
|
30
|
+
let cachedFFmpegPath = null;
|
|
31
|
+
/**
|
|
32
|
+
* Checks if FFmpeg exists at a specific path by attempting to run it.
|
|
33
|
+
* @param pathToCheck - Full path to the FFmpeg executable.
|
|
34
|
+
* @returns Promise resolving to true if FFmpeg runs successfully at this path.
|
|
35
|
+
*/
|
|
36
|
+
async function checkFFmpegAtPath(pathToCheck) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const ffmpeg = spawn(pathToCheck, ["-version"], {
|
|
39
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
40
|
+
});
|
|
41
|
+
ffmpeg.on("error", () => {
|
|
42
|
+
resolve(false);
|
|
43
|
+
});
|
|
44
|
+
ffmpeg.on("exit", (code) => {
|
|
45
|
+
resolve(code === 0);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolves the FFmpeg executable path. Checks Channels DVR (macOS, Windows, Linux), then the bundled ffmpeg-for-homebridge, then system PATH. The resolved path is
|
|
51
|
+
* cached for subsequent calls.
|
|
52
|
+
* @returns Promise resolving to the FFmpeg path if found, or undefined if not available.
|
|
53
|
+
*/
|
|
54
|
+
export async function resolveFFmpegPath() {
|
|
55
|
+
// Return cached result if already resolved.
|
|
56
|
+
if (cachedFFmpegPath !== null) {
|
|
57
|
+
return cachedFFmpegPath;
|
|
58
|
+
}
|
|
59
|
+
// On macOS, check Channels DVR bundled FFmpeg first. Users of PrismCast with Channels DVR likely have this available.
|
|
60
|
+
if (process.platform === "darwin") {
|
|
61
|
+
const channelsDvrPath = join(homedir(), "Library", "Application Support", "ChannelsDVR", "latest", "ffmpeg");
|
|
62
|
+
if (existsSync(channelsDvrPath) && (await checkFFmpegAtPath(channelsDvrPath))) {
|
|
63
|
+
cachedFFmpegPath = channelsDvrPath;
|
|
64
|
+
return cachedFFmpegPath;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// On Windows, check Channels DVR bundled FFmpeg. Users of PrismCast with Channels DVR likely have this available.
|
|
68
|
+
if (process.platform === "win32") {
|
|
69
|
+
const channelsDvrPath = join("C:", "ProgramData", "channelsdvr", "latest", "ffmpeg.exe");
|
|
70
|
+
if (existsSync(channelsDvrPath) && (await checkFFmpegAtPath(channelsDvrPath))) {
|
|
71
|
+
cachedFFmpegPath = channelsDvrPath;
|
|
72
|
+
return cachedFFmpegPath;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// On Linux, check common Channels DVR installation paths. The Channels DVR setup script creates a channels-dvr directory in the current working directory when
|
|
76
|
+
// run. The official recommendation is ~/channels-dvr, but users also install to /usr/local/channels-dvr and /opt/channels-dvr.
|
|
77
|
+
if (process.platform === "linux") {
|
|
78
|
+
const linuxChannelsDvrPaths = [
|
|
79
|
+
join(homedir(), "channels-dvr", "latest", "ffmpeg"),
|
|
80
|
+
join("/usr", "local", "channels-dvr", "latest", "ffmpeg"),
|
|
81
|
+
join("/opt", "channels-dvr", "latest", "ffmpeg")
|
|
82
|
+
];
|
|
83
|
+
for (const channelsDvrPath of linuxChannelsDvrPaths) {
|
|
84
|
+
// eslint-disable-next-line no-await-in-loop
|
|
85
|
+
if (existsSync(channelsDvrPath) && (await checkFFmpegAtPath(channelsDvrPath))) {
|
|
86
|
+
cachedFFmpegPath = channelsDvrPath;
|
|
87
|
+
return cachedFFmpegPath;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Check ffmpeg-for-homebridge bundled FFmpeg. This provides a reliable fallback without requiring manual FFmpeg installation.
|
|
92
|
+
if (ffmpegPath && existsSync(ffmpegPath) && (await checkFFmpegAtPath(ffmpegPath))) {
|
|
93
|
+
cachedFFmpegPath = ffmpegPath;
|
|
94
|
+
return cachedFFmpegPath;
|
|
95
|
+
}
|
|
96
|
+
// Finally, check if ffmpeg is available in the system PATH.
|
|
97
|
+
if (await checkFFmpegAtPath("ffmpeg")) {
|
|
98
|
+
cachedFFmpegPath = "ffmpeg";
|
|
99
|
+
return cachedFFmpegPath;
|
|
100
|
+
}
|
|
101
|
+
// FFmpeg not found anywhere.
|
|
102
|
+
cachedFFmpegPath = undefined;
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Spawns an FFmpeg process configured to transcode WebM (H264+Opus) to fMP4 (H264+AAC). The process reads from stdin and writes to stdout, allowing it to be
|
|
107
|
+
* integrated into a Node.js stream pipeline. Video is passed through unchanged; audio is transcoded from Opus to AAC for HLS compatibility.
|
|
108
|
+
*
|
|
109
|
+
* FFmpeg arguments:
|
|
110
|
+
* - `-hide_banner -loglevel warning`: Reduce noise, only show warnings/errors
|
|
111
|
+
* - `-probesize 16384`: Limit input probing to 16KB (Chrome's WebM header fits well under this) to minimize startup delay
|
|
112
|
+
* - `-i pipe:0`: Read input from stdin
|
|
113
|
+
* - `-c:v copy`: Copy video stream without re-encoding (H264 passthrough)
|
|
114
|
+
* - `-c:a aac -b:a <bitrate>`: Transcode audio to AAC at specified bitrate
|
|
115
|
+
* - `-f mp4`: Output MP4 container format
|
|
116
|
+
* - `-movflags frag_keyframe+empty_moov+default_base_moof`: Streaming-friendly fMP4 flags
|
|
117
|
+
* - `-flush_packets 1`: Flush output immediately after each packet to minimize latency
|
|
118
|
+
* - `pipe:1`: Write output to stdout
|
|
119
|
+
* @param audioBitrate - Audio bitrate in bits per second (e.g., 256000 for 256 kbps).
|
|
120
|
+
* @param onError - Callback invoked when FFmpeg exits unexpectedly or encounters an error.
|
|
121
|
+
* @param streamId - Stream identifier for logging.
|
|
122
|
+
* @param comment - Optional comment metadata (channel name or domain) to embed in the output.
|
|
123
|
+
* @returns FFmpeg process wrapper with stdin, stdout, and kill function.
|
|
124
|
+
*/
|
|
125
|
+
export function spawnFFmpeg(audioBitrate, onError, streamId, comment) {
|
|
126
|
+
// Use the cached FFmpeg path from resolveFFmpegPath(). This should always be set because isFFmpegAvailable() is called during startup, which populates the cache.
|
|
127
|
+
// If somehow not set, fall back to "ffmpeg" and let spawn handle the error.
|
|
128
|
+
const ffmpegPath = cachedFFmpegPath ?? "ffmpeg";
|
|
129
|
+
// Use Apple's AudioToolbox AAC encoder on macOS for better quality and performance. Fall back to FFmpeg's built-in AAC encoder on other platforms.
|
|
130
|
+
const aacEncoder = process.platform === "darwin" ? "aac_at" : "aac";
|
|
131
|
+
const ffmpegArgs = [
|
|
132
|
+
"-hide_banner",
|
|
133
|
+
"-loglevel", "warning",
|
|
134
|
+
"-probesize", "16384",
|
|
135
|
+
"-i", "pipe:0",
|
|
136
|
+
"-c:v", "copy",
|
|
137
|
+
"-c:a", aacEncoder,
|
|
138
|
+
"-b:a", String(audioBitrate),
|
|
139
|
+
"-f", "mp4",
|
|
140
|
+
"-movflags", "frag_keyframe+empty_moov+default_base_moof+skip_sidx+skip_trailer",
|
|
141
|
+
"-flush_packets", "1"
|
|
142
|
+
];
|
|
143
|
+
// Add metadata comment if provided. This embeds "PrismCast - <channel>" in the output for identification.
|
|
144
|
+
if (comment) {
|
|
145
|
+
ffmpegArgs.push("-metadata", "comment=PrismCast - " + comment);
|
|
146
|
+
}
|
|
147
|
+
ffmpegArgs.push("pipe:1");
|
|
148
|
+
const ffmpeg = spawn(ffmpegPath, ffmpegArgs, {
|
|
149
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
150
|
+
});
|
|
151
|
+
const logPrefix = streamId ? "[" + streamId + "] " : "";
|
|
152
|
+
// Track whether graceful shutdown has been initiated. When true, we suppress error callbacks because any exit (whether from SIGTERM or stdin close) is expected.
|
|
153
|
+
let shuttingDown = false;
|
|
154
|
+
// Log FFmpeg stderr output (warnings and errors). stderr is guaranteed to be a Readable since we set stdio: ["pipe", "pipe", "pipe"].
|
|
155
|
+
ffmpeg.stderr.on("data", (data) => {
|
|
156
|
+
// Suppress warnings during shutdown - truncated input warnings are expected when the capture stream closes.
|
|
157
|
+
if (shuttingDown) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const message = data.toString().trim();
|
|
161
|
+
// Filter out common noise that isn't actionable.
|
|
162
|
+
const noisePatterns = ["Press [q] to stop", "frame=", "size=", "time=", "bitrate=", "speed="];
|
|
163
|
+
if (noisePatterns.some((pattern) => message.includes(pattern))) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (message.length > 0) {
|
|
167
|
+
LOG.debug("streaming:ffmpeg", "%sFFmpeg: %s", logPrefix, message);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
// Handle FFmpeg process exit.
|
|
171
|
+
ffmpeg.on("exit", (code, signal) => {
|
|
172
|
+
// During graceful shutdown, any exit is expected (whether from SIGTERM or stdin closing).
|
|
173
|
+
if (shuttingDown) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (signal === "SIGTERM") {
|
|
177
|
+
// Normal termination via kill() - don't treat as error.
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if ((code !== null) && (code !== 0)) {
|
|
181
|
+
onError(new Error("FFmpeg exited with code " + String(code) + "."));
|
|
182
|
+
}
|
|
183
|
+
else if (signal) {
|
|
184
|
+
onError(new Error("FFmpeg killed by signal " + signal + "."));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
// Handle spawn errors (e.g., FFmpeg not found).
|
|
188
|
+
ffmpeg.on("error", (error) => {
|
|
189
|
+
// During graceful shutdown, suppress errors from stdin pipe closing.
|
|
190
|
+
if (shuttingDown) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
onError(error);
|
|
194
|
+
});
|
|
195
|
+
// Kill function for graceful shutdown. Sets the shuttingDown flag before sending SIGTERM so that any exit (whether from SIGTERM or stdin closing due to capture
|
|
196
|
+
// stream ending) is treated as normal termination.
|
|
197
|
+
const kill = () => {
|
|
198
|
+
shuttingDown = true;
|
|
199
|
+
if (!ffmpeg.killed) {
|
|
200
|
+
ffmpeg.kill("SIGTERM");
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
kill,
|
|
205
|
+
process: ffmpeg,
|
|
206
|
+
stdin: ffmpeg.stdin,
|
|
207
|
+
stdout: ffmpeg.stdout
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Spawns an FFmpeg process configured to remux fMP4 input to MPEG-TS output with codec copy. The process reads a continuous fMP4 stream (init segment followed by
|
|
212
|
+
* media segments) from stdin and writes MPEG-TS to stdout. No transcoding occurs — both video (H264) and audio (AAC) are copied unchanged — so CPU usage is minimal.
|
|
213
|
+
*
|
|
214
|
+
* FFmpeg arguments:
|
|
215
|
+
* - `-hide_banner -loglevel warning`: Reduce noise, only show warnings/errors
|
|
216
|
+
* - `-probesize 16384`: Limit input probing to 16KB (fMP4 init segment is ~1.3KB) to minimize startup delay
|
|
217
|
+
* - `-f mp4 -i pipe:0`: Read fragmented MP4 from stdin
|
|
218
|
+
* - `-c copy`: Copy both video and audio codecs without transcoding
|
|
219
|
+
* - `-f mpegts`: Output MPEG-TS container format
|
|
220
|
+
* - `-mpegts_pmt_start_pid 0x0020`: Use ATSC-conventional PMT PID range instead of FFmpeg's default (0x1000). Minimum allowed value is 0x0020 (32).
|
|
221
|
+
* - `-mpegts_start_pid 0x0031`: Use ATSC-conventional elementary stream PIDs instead of FFmpeg's defaults (0x100+)
|
|
222
|
+
* - `-mpegts_service_type digital_tv`: Label the service as digital TV in the PMT service descriptor
|
|
223
|
+
* - `-pat_period 0.1`: Repeat PAT/PMT tables every 100ms, matching ATSC broadcast frequency
|
|
224
|
+
* - `-pcr_period 40`: Insert PCR timestamps every 40ms, matching ATSC broadcast convention
|
|
225
|
+
* - `-flush_packets 1`: Flush output immediately after each packet to minimize latency
|
|
226
|
+
* - `pipe:1`: Write output to stdout
|
|
227
|
+
* @param onError - Callback invoked when FFmpeg exits unexpectedly or encounters an error.
|
|
228
|
+
* @param streamId - Optional stream identifier for logging.
|
|
229
|
+
* @returns FFmpeg process wrapper with stdin, stdout, and kill function.
|
|
230
|
+
*/
|
|
231
|
+
export function spawnMpegTsRemuxer(onError, streamId) {
|
|
232
|
+
const ffmpegBin = cachedFFmpegPath ?? "ffmpeg";
|
|
233
|
+
// MPEG-TS muxer flags are tuned to produce output resembling a real HDHomeRun CONNECT DUO (HDTC-2US) ATSC transport stream. Plex's transcoder may make
|
|
234
|
+
// assumptions about stream structure based on the reported device model (PID assignments, PAT/PMT frequency). Using ATSC-conventional values avoids "Invalid
|
|
235
|
+
// argument" failures when Plex tries to transcode the live session for remote clients. These are pure container metadata changes — the actual A/V data is
|
|
236
|
+
// untouched by -c copy.
|
|
237
|
+
const ffmpegArgs = [
|
|
238
|
+
"-hide_banner",
|
|
239
|
+
"-loglevel", "warning",
|
|
240
|
+
"-probesize", "16384",
|
|
241
|
+
"-f", "mp4",
|
|
242
|
+
"-i", "pipe:0",
|
|
243
|
+
"-c", "copy",
|
|
244
|
+
"-f", "mpegts",
|
|
245
|
+
"-mpegts_pmt_start_pid", "0x0020",
|
|
246
|
+
"-mpegts_start_pid", "0x0031",
|
|
247
|
+
"-mpegts_service_type", "digital_tv",
|
|
248
|
+
"-pat_period", "0.1",
|
|
249
|
+
"-pcr_period", "40",
|
|
250
|
+
"-flush_packets", "1",
|
|
251
|
+
"pipe:1"
|
|
252
|
+
];
|
|
253
|
+
const ffmpeg = spawn(ffmpegBin, ffmpegArgs, {
|
|
254
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
255
|
+
});
|
|
256
|
+
const logPrefix = streamId ? "[" + streamId + "] " : "";
|
|
257
|
+
// Track whether graceful shutdown has been initiated. When true, we suppress error callbacks because any exit is expected.
|
|
258
|
+
let shuttingDown = false;
|
|
259
|
+
// Log FFmpeg stderr output (warnings and errors).
|
|
260
|
+
ffmpeg.stderr.on("data", (data) => {
|
|
261
|
+
if (shuttingDown) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const message = data.toString().trim();
|
|
265
|
+
const noisePatterns = ["Press [q] to stop", "frame=", "size=", "time=", "bitrate=", "speed="];
|
|
266
|
+
if (noisePatterns.some((pattern) => message.includes(pattern))) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (message.length > 0) {
|
|
270
|
+
LOG.debug("streaming:ffmpeg", "%sMPEG-TS remuxer: %s", logPrefix, message);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
// Handle FFmpeg process exit.
|
|
274
|
+
ffmpeg.on("exit", (code, signal) => {
|
|
275
|
+
if (shuttingDown) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (signal === "SIGTERM") {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if ((code !== null) && (code !== 0)) {
|
|
282
|
+
onError(new Error("MPEG-TS remuxer exited with code " + String(code) + "."));
|
|
283
|
+
}
|
|
284
|
+
else if (signal) {
|
|
285
|
+
onError(new Error("MPEG-TS remuxer killed by signal " + signal + "."));
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
// Handle spawn errors (e.g., FFmpeg not found).
|
|
289
|
+
ffmpeg.on("error", (error) => {
|
|
290
|
+
if (shuttingDown) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
onError(error);
|
|
294
|
+
});
|
|
295
|
+
// Kill function for graceful shutdown.
|
|
296
|
+
const kill = () => {
|
|
297
|
+
shuttingDown = true;
|
|
298
|
+
if (!ffmpeg.killed) {
|
|
299
|
+
ffmpeg.kill("SIGTERM");
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
return {
|
|
303
|
+
kill,
|
|
304
|
+
process: ffmpeg,
|
|
305
|
+
stdin: ffmpeg.stdin,
|
|
306
|
+
stdout: ffmpeg.stdout
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Checks if FFmpeg is available on the system. This resolves the FFmpeg path and caches it for use by spawnFFmpeg().
|
|
311
|
+
* @returns Promise resolving to true if FFmpeg is available, false otherwise.
|
|
312
|
+
*/
|
|
313
|
+
export async function isFFmpegAvailable() {
|
|
314
|
+
const ffmpegPath = await resolveFFmpegPath();
|
|
315
|
+
return ffmpegPath !== undefined;
|
|
316
|
+
}
|
|
317
|
+
//# sourceMappingURL=ffmpeg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg.js","sourceRoot":"","sources":["../../src/utils/ffmpeg.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initializes the file logger. Creates the log file if it does not exist. Must be called after the data directory is ensured to exist.
|
|
3
|
+
* @param maxSize - Maximum log file size in bytes from CONFIG.logging.maxSize.
|
|
4
|
+
*/
|
|
5
|
+
export declare function initializeFileLogger(maxSize: number): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Writes a log entry to the buffer. Entries are flushed to disk periodically.
|
|
8
|
+
* @param level - Log level ("info", "warn", "error", "debug").
|
|
9
|
+
* @param message - The formatted log message.
|
|
10
|
+
* @param color - Optional ANSI color code to apply to the level prefix and message.
|
|
11
|
+
* @param categoryTag - Optional debug category tag (e.g., "recovery:tab"). Appended to the level prefix as [DEBUG:category].
|
|
12
|
+
*/
|
|
13
|
+
export declare function writeLogEntry(level: string, message: string, color?: string, categoryTag?: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* Flushes the write buffer to disk asynchronously. Called periodically by the flush timer.
|
|
16
|
+
*/
|
|
17
|
+
export declare function flushLogBuffer(): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Flushes the write buffer to disk synchronously. Used during shutdown to ensure final logs are written.
|
|
20
|
+
*/
|
|
21
|
+
export declare function flushLogBufferSync(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Shuts down the file logger, flushing any remaining buffer synchronously.
|
|
24
|
+
*/
|
|
25
|
+
export declare function shutdownFileLogger(): void;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import df from "dateformat";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
const { promises: fsPromises } = fs;
|
|
6
|
+
/* The file logger provides persistent logging to ~/.prismcast/prismcast.log with automatic size-based trimming. When the log file exceeds the configured maximum
|
|
7
|
+
* size, it is trimmed to half the maximum size, keeping only complete lines (the most recent logs are preserved). This approach prevents unbounded log growth while
|
|
8
|
+
* maintaining recent history for troubleshooting.
|
|
9
|
+
*
|
|
10
|
+
* Design decisions:
|
|
11
|
+
*
|
|
12
|
+
* 1. Asynchronous buffered writes - Logs are collected in a buffer and flushed periodically to avoid blocking the event loop during high-frequency logging.
|
|
13
|
+
* 2. Periodic size checking - File size is checked every N writes rather than on each write to minimize syscall overhead.
|
|
14
|
+
* 3. Atomic trim operations - Trimming writes to a temp file then renames, preventing data loss if the process crashes during trim.
|
|
15
|
+
* 4. Timestamps - Uses the same format as console-stamp for consistency: yyyy/mm/dd HH:MM:ss.l
|
|
16
|
+
*/
|
|
17
|
+
/* The file logger maintains state for the log file path, write buffer, and size tracking. State is initialized when initializeFileLogger() is called during server
|
|
18
|
+
* startup.
|
|
19
|
+
*/
|
|
20
|
+
// Path to the log file, set during initialization.
|
|
21
|
+
let logFilePath = null;
|
|
22
|
+
// Buffer for collecting log entries before flushing to disk.
|
|
23
|
+
let writeBuffer = [];
|
|
24
|
+
// Approximate file size tracked in memory between actual file size checks.
|
|
25
|
+
let approximateSize = 0;
|
|
26
|
+
// Counter for tracking writes since last file size check.
|
|
27
|
+
let writeCount = 0;
|
|
28
|
+
// Timer for periodic buffer flushing.
|
|
29
|
+
let flushTimer = null;
|
|
30
|
+
// Flag indicating whether the file logger is initialized and operational.
|
|
31
|
+
let isInitialized = false;
|
|
32
|
+
// Flag to temporarily disable logging on write errors, preventing error cascades.
|
|
33
|
+
let isDisabled = false;
|
|
34
|
+
// Timestamp when logging was disabled due to error, for retry timing.
|
|
35
|
+
let disabledAt = 0;
|
|
36
|
+
// Maximum log file size, set during initialization.
|
|
37
|
+
let maxLogSize = 1048576;
|
|
38
|
+
// Configuration Constants.
|
|
39
|
+
// Interval in milliseconds between buffer flushes.
|
|
40
|
+
const FLUSH_INTERVAL_MS = 1000;
|
|
41
|
+
// Number of writes between file size checks.
|
|
42
|
+
const SIZE_CHECK_FREQUENCY = 100;
|
|
43
|
+
// Duration in milliseconds to disable logging after a write error before retrying.
|
|
44
|
+
const ERROR_RETRY_DELAY_MS = 60000;
|
|
45
|
+
/* Terminal color codes for log file output. These match the colors used in console mode so that viewing the log file with terminal commands (tail -f, less -R, cat)
|
|
46
|
+
* shows the same color scheme as console output.
|
|
47
|
+
*/
|
|
48
|
+
const ANSI_RESET = "\x1b[0m";
|
|
49
|
+
// Initialization.
|
|
50
|
+
/**
|
|
51
|
+
* Initializes the file logger. Creates the log file if it does not exist. Must be called after the data directory is ensured to exist.
|
|
52
|
+
* @param maxSize - Maximum log file size in bytes from CONFIG.logging.maxSize.
|
|
53
|
+
*/
|
|
54
|
+
export async function initializeFileLogger(maxSize) {
|
|
55
|
+
const dataDir = path.join(os.homedir(), ".prismcast");
|
|
56
|
+
logFilePath = path.join(dataDir, "prismcast.log");
|
|
57
|
+
maxLogSize = maxSize;
|
|
58
|
+
try {
|
|
59
|
+
// Ensure the data directory exists.
|
|
60
|
+
await fsPromises.mkdir(dataDir, { recursive: true });
|
|
61
|
+
// Check if log file exists and get its size.
|
|
62
|
+
try {
|
|
63
|
+
const stats = await fsPromises.stat(logFilePath);
|
|
64
|
+
approximateSize = stats.size;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// File does not exist, create it.
|
|
68
|
+
if (error.code === "ENOENT") {
|
|
69
|
+
await fsPromises.writeFile(logFilePath, "", "utf-8");
|
|
70
|
+
approximateSize = 0;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Start the periodic flush timer.
|
|
77
|
+
flushTimer = setInterval(() => {
|
|
78
|
+
void flushLogBuffer();
|
|
79
|
+
}, FLUSH_INTERVAL_MS);
|
|
80
|
+
isInitialized = true;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
// Log to console since file logging failed, but do not throw - file logging is a best-effort feature.
|
|
84
|
+
// eslint-disable-next-line no-console
|
|
85
|
+
console.error("Failed to initialize file logger: %s. File logging disabled.", (error instanceof Error) ? error.message : String(error));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Log Entry Writing.
|
|
89
|
+
/**
|
|
90
|
+
* Writes a log entry to the buffer. Entries are flushed to disk periodically.
|
|
91
|
+
* @param level - Log level ("info", "warn", "error", "debug").
|
|
92
|
+
* @param message - The formatted log message.
|
|
93
|
+
* @param color - Optional ANSI color code to apply to the level prefix and message.
|
|
94
|
+
* @param categoryTag - Optional debug category tag (e.g., "recovery:tab"). Appended to the level prefix as [DEBUG:category].
|
|
95
|
+
*/
|
|
96
|
+
export function writeLogEntry(level, message, color, categoryTag) {
|
|
97
|
+
if (!isInitialized || !logFilePath) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Check if logging is disabled due to previous error and whether retry delay has passed.
|
|
101
|
+
if (isDisabled) {
|
|
102
|
+
if ((Date.now() - disabledAt) < ERROR_RETRY_DELAY_MS) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Re-enable logging and try again.
|
|
106
|
+
isDisabled = false;
|
|
107
|
+
}
|
|
108
|
+
// Format the log entry with timestamp and level. Apply ANSI color if provided.
|
|
109
|
+
const timestamp = df(new Date(), "yyyy/mm/dd HH:MM:ss.l");
|
|
110
|
+
const levelTag = categoryTag ? [level.toUpperCase(), ":", categoryTag].join("") : level.toUpperCase();
|
|
111
|
+
const levelPrefix = (level === "info") ? "" : ["[", levelTag, "] "].join("");
|
|
112
|
+
const colorStart = color ?? "";
|
|
113
|
+
const colorEnd = color ? ANSI_RESET : "";
|
|
114
|
+
const entry = ["[", timestamp, "] ", colorStart, levelPrefix, message, colorEnd, "\n"].join("");
|
|
115
|
+
// Add to buffer.
|
|
116
|
+
writeBuffer.push(entry);
|
|
117
|
+
approximateSize += entry.length;
|
|
118
|
+
writeCount++;
|
|
119
|
+
// Check if we should verify actual file size.
|
|
120
|
+
if ((writeCount % SIZE_CHECK_FREQUENCY) === 0) {
|
|
121
|
+
void checkAndTrimFile();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Buffer Flushing.
|
|
125
|
+
/**
|
|
126
|
+
* Flushes the write buffer to disk asynchronously. Called periodically by the flush timer.
|
|
127
|
+
*/
|
|
128
|
+
export async function flushLogBuffer() {
|
|
129
|
+
if (!isInitialized || !logFilePath || (writeBuffer.length === 0)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Take the current buffer and reset.
|
|
133
|
+
const entries = writeBuffer;
|
|
134
|
+
writeBuffer = [];
|
|
135
|
+
const content = entries.join("");
|
|
136
|
+
try {
|
|
137
|
+
await fsPromises.appendFile(logFilePath, content, "utf-8");
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
// Disable logging temporarily to prevent error cascade.
|
|
141
|
+
isDisabled = true;
|
|
142
|
+
disabledAt = Date.now();
|
|
143
|
+
// Log to console as fallback.
|
|
144
|
+
// eslint-disable-next-line no-console
|
|
145
|
+
console.error("Failed to write to log file: %s. File logging disabled for %s seconds.", (error instanceof Error) ? error.message : String(error), ERROR_RETRY_DELAY_MS / 1000);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Flushes the write buffer to disk synchronously. Used during shutdown to ensure final logs are written.
|
|
150
|
+
*/
|
|
151
|
+
export function flushLogBufferSync() {
|
|
152
|
+
if (!isInitialized || !logFilePath || (writeBuffer.length === 0)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const content = writeBuffer.join("");
|
|
156
|
+
writeBuffer = [];
|
|
157
|
+
try {
|
|
158
|
+
fs.appendFileSync(logFilePath, content, "utf-8");
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
// Log to console as fallback.
|
|
162
|
+
// eslint-disable-next-line no-console
|
|
163
|
+
console.error("Failed to write final log entries: %s.", (error instanceof Error) ? error.message : String(error));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Size Management.
|
|
167
|
+
/**
|
|
168
|
+
* Checks the actual file size and trims if it exceeds the maximum.
|
|
169
|
+
*/
|
|
170
|
+
async function checkAndTrimFile() {
|
|
171
|
+
if (!logFilePath) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const stats = await fsPromises.stat(logFilePath);
|
|
176
|
+
approximateSize = stats.size;
|
|
177
|
+
if (approximateSize > maxLogSize) {
|
|
178
|
+
await trimLogFile();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
// File might have been deleted externally - reset size tracking.
|
|
183
|
+
if (error.code === "ENOENT") {
|
|
184
|
+
approximateSize = 0;
|
|
185
|
+
}
|
|
186
|
+
// Log to console but continue operating.
|
|
187
|
+
// eslint-disable-next-line no-console
|
|
188
|
+
console.warn("Error checking log file size: %s.", (error instanceof Error) ? error.message : String(error));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Trims the log file to half the maximum size, keeping only complete lines. The most recent logs are preserved.
|
|
193
|
+
*/
|
|
194
|
+
async function trimLogFile() {
|
|
195
|
+
if (!logFilePath) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const content = await fsPromises.readFile(logFilePath, "utf-8");
|
|
200
|
+
// Calculate target size (half of max).
|
|
201
|
+
const targetSize = Math.floor(maxLogSize / 2);
|
|
202
|
+
// We want to keep the END of the file (most recent logs). Find where to cut.
|
|
203
|
+
const cutPosition = content.length - targetSize;
|
|
204
|
+
if (cutPosition <= 0) {
|
|
205
|
+
// File is smaller than target, no trimming needed.
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// Find the next newline after the cut position to keep complete lines.
|
|
209
|
+
let lineStart = content.indexOf("\n", cutPosition);
|
|
210
|
+
if (lineStart === -1) {
|
|
211
|
+
// No newline found after cut position, keep from cut position.
|
|
212
|
+
lineStart = cutPosition;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
// Start after the newline.
|
|
216
|
+
lineStart += 1;
|
|
217
|
+
}
|
|
218
|
+
const trimmedContent = content.substring(lineStart);
|
|
219
|
+
// Write to temp file, then rename (atomic replace).
|
|
220
|
+
const tempPath = logFilePath + ".tmp";
|
|
221
|
+
await fsPromises.writeFile(tempPath, trimmedContent, "utf-8");
|
|
222
|
+
await fsPromises.rename(tempPath, logFilePath);
|
|
223
|
+
approximateSize = trimmedContent.length;
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
// Log to console but continue operating - trim will be retried on next check.
|
|
227
|
+
// eslint-disable-next-line no-console
|
|
228
|
+
console.warn("Error trimming log file: %s.", (error instanceof Error) ? error.message : String(error));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Shutdown.
|
|
232
|
+
/**
|
|
233
|
+
* Shuts down the file logger, flushing any remaining buffer synchronously.
|
|
234
|
+
*/
|
|
235
|
+
export function shutdownFileLogger() {
|
|
236
|
+
if (!isInitialized) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Stop the flush timer.
|
|
240
|
+
if (flushTimer) {
|
|
241
|
+
clearInterval(flushTimer);
|
|
242
|
+
flushTimer = null;
|
|
243
|
+
}
|
|
244
|
+
// Flush remaining buffer synchronously.
|
|
245
|
+
flushLogBufferSync();
|
|
246
|
+
isInitialized = false;
|
|
247
|
+
}
|
|
248
|
+
//# sourceMappingURL=fileLogger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileLogger.js","sourceRoot":"","sources":["../../src/utils/fileLogger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a duration in milliseconds as a human-readable string. The format varies based on duration length:
|
|
3
|
+
* - Less than 60 seconds: "17s"
|
|
4
|
+
* - Less than 1 hour: "6m 39s"
|
|
5
|
+
* - 1 hour or more: "1h 23m"
|
|
6
|
+
* @param ms - Duration in milliseconds.
|
|
7
|
+
* @returns Formatted duration string.
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatDuration(ms: number): string;
|
|
10
|
+
/**
|
|
11
|
+
* Extracts a concise domain from a URL by keeping only the last two portions of the hostname (e.g., "watch.foodnetwork.com" becomes "foodnetwork.com",
|
|
12
|
+
* "www.hulu.com" becomes "hulu.com"). Used as a standard domain key for DOMAIN_CONFIG lookups and as a display fallback when no provider name is configured.
|
|
13
|
+
* @param url - The URL to extract the domain from.
|
|
14
|
+
* @returns The concise domain, or the original URL if parsing fails.
|
|
15
|
+
*/
|
|
16
|
+
export declare function extractDomain(url: string): string;
|