@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.
Files changed (234) hide show
  1. package/LICENSE.md +7 -0
  2. package/README.md +347 -0
  3. package/bin/prismcast +6 -0
  4. package/dist/app.d.ts +6 -0
  5. package/dist/app.js +315 -0
  6. package/dist/app.js.map +1 -0
  7. package/dist/browser/cdp.d.ts +38 -0
  8. package/dist/browser/cdp.js +155 -0
  9. package/dist/browser/cdp.js.map +1 -0
  10. package/dist/browser/channelSelection.d.ts +65 -0
  11. package/dist/browser/channelSelection.js +202 -0
  12. package/dist/browser/channelSelection.js.map +1 -0
  13. package/dist/browser/display.d.ts +34 -0
  14. package/dist/browser/display.js +54 -0
  15. package/dist/browser/display.js.map +1 -0
  16. package/dist/browser/index.d.ts +205 -0
  17. package/dist/browser/index.js +1205 -0
  18. package/dist/browser/index.js.map +1 -0
  19. package/dist/browser/tuning/fox.d.ts +2 -0
  20. package/dist/browser/tuning/fox.js +83 -0
  21. package/dist/browser/tuning/fox.js.map +1 -0
  22. package/dist/browser/tuning/hbo.d.ts +2 -0
  23. package/dist/browser/tuning/hbo.js +237 -0
  24. package/dist/browser/tuning/hbo.js.map +1 -0
  25. package/dist/browser/tuning/hulu.d.ts +2 -0
  26. package/dist/browser/tuning/hulu.js +550 -0
  27. package/dist/browser/tuning/hulu.js.map +1 -0
  28. package/dist/browser/tuning/sling.d.ts +2 -0
  29. package/dist/browser/tuning/sling.js +518 -0
  30. package/dist/browser/tuning/sling.js.map +1 -0
  31. package/dist/browser/tuning/thumbnailRow.d.ts +2 -0
  32. package/dist/browser/tuning/thumbnailRow.js +108 -0
  33. package/dist/browser/tuning/thumbnailRow.js.map +1 -0
  34. package/dist/browser/tuning/tileClick.d.ts +2 -0
  35. package/dist/browser/tuning/tileClick.js +103 -0
  36. package/dist/browser/tuning/tileClick.js.map +1 -0
  37. package/dist/browser/tuning/youtubeTv.d.ts +2 -0
  38. package/dist/browser/tuning/youtubeTv.js +182 -0
  39. package/dist/browser/tuning/youtubeTv.js.map +1 -0
  40. package/dist/browser/video.d.ts +289 -0
  41. package/dist/browser/video.js +996 -0
  42. package/dist/browser/video.js.map +1 -0
  43. package/dist/channels/index.d.ts +3 -0
  44. package/dist/channels/index.js +392 -0
  45. package/dist/channels/index.js.map +1 -0
  46. package/dist/config/index.d.ts +53 -0
  47. package/dist/config/index.js +233 -0
  48. package/dist/config/index.js.map +1 -0
  49. package/dist/config/presets.d.ts +98 -0
  50. package/dist/config/presets.js +241 -0
  51. package/dist/config/presets.js.map +1 -0
  52. package/dist/config/profiles.d.ts +79 -0
  53. package/dist/config/profiles.js +245 -0
  54. package/dist/config/profiles.js.map +1 -0
  55. package/dist/config/providers.d.ts +120 -0
  56. package/dist/config/providers.js +450 -0
  57. package/dist/config/providers.js.map +1 -0
  58. package/dist/config/sites.d.ts +22 -0
  59. package/dist/config/sites.js +377 -0
  60. package/dist/config/sites.js.map +1 -0
  61. package/dist/config/userChannels.d.ts +178 -0
  62. package/dist/config/userChannels.js +543 -0
  63. package/dist/config/userChannels.js.map +1 -0
  64. package/dist/config/userConfig.d.ts +235 -0
  65. package/dist/config/userConfig.js +913 -0
  66. package/dist/config/userConfig.js.map +1 -0
  67. package/dist/hdhr/channelMap.d.ts +21 -0
  68. package/dist/hdhr/channelMap.js +82 -0
  69. package/dist/hdhr/channelMap.js.map +1 -0
  70. package/dist/hdhr/deviceId.d.ts +11 -0
  71. package/dist/hdhr/deviceId.js +84 -0
  72. package/dist/hdhr/deviceId.js.map +1 -0
  73. package/dist/hdhr/discover.d.ts +6 -0
  74. package/dist/hdhr/discover.js +155 -0
  75. package/dist/hdhr/discover.js.map +1 -0
  76. package/dist/hdhr/index.d.ts +9 -0
  77. package/dist/hdhr/index.js +87 -0
  78. package/dist/hdhr/index.js.map +1 -0
  79. package/dist/index.d.ts +1 -0
  80. package/dist/index.js +144 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/routes/assets.d.ts +6 -0
  83. package/dist/routes/assets.js +79 -0
  84. package/dist/routes/assets.js.map +1 -0
  85. package/dist/routes/auth.d.ts +6 -0
  86. package/dist/routes/auth.js +77 -0
  87. package/dist/routes/auth.js.map +1 -0
  88. package/dist/routes/channels.d.ts +6 -0
  89. package/dist/routes/channels.js +40 -0
  90. package/dist/routes/channels.js.map +1 -0
  91. package/dist/routes/components.d.ts +138 -0
  92. package/dist/routes/components.js +210 -0
  93. package/dist/routes/components.js.map +1 -0
  94. package/dist/routes/config.d.ts +72 -0
  95. package/dist/routes/config.js +1977 -0
  96. package/dist/routes/config.js.map +1 -0
  97. package/dist/routes/debug.d.ts +6 -0
  98. package/dist/routes/debug.js +274 -0
  99. package/dist/routes/debug.js.map +1 -0
  100. package/dist/routes/health.d.ts +6 -0
  101. package/dist/routes/health.js +85 -0
  102. package/dist/routes/health.js.map +1 -0
  103. package/dist/routes/hls.d.ts +6 -0
  104. package/dist/routes/hls.js +25 -0
  105. package/dist/routes/hls.js.map +1 -0
  106. package/dist/routes/index.d.ts +19 -0
  107. package/dist/routes/index.js +49 -0
  108. package/dist/routes/index.js.map +1 -0
  109. package/dist/routes/logs.d.ts +6 -0
  110. package/dist/routes/logs.js +164 -0
  111. package/dist/routes/logs.js.map +1 -0
  112. package/dist/routes/mpegts.d.ts +6 -0
  113. package/dist/routes/mpegts.js +19 -0
  114. package/dist/routes/mpegts.js.map +1 -0
  115. package/dist/routes/play.d.ts +6 -0
  116. package/dist/routes/play.js +18 -0
  117. package/dist/routes/play.js.map +1 -0
  118. package/dist/routes/playlist.d.ts +36 -0
  119. package/dist/routes/playlist.js +134 -0
  120. package/dist/routes/playlist.js.map +1 -0
  121. package/dist/routes/root.d.ts +6 -0
  122. package/dist/routes/root.js +2920 -0
  123. package/dist/routes/root.js.map +1 -0
  124. package/dist/routes/streams.d.ts +6 -0
  125. package/dist/routes/streams.js +88 -0
  126. package/dist/routes/streams.js.map +1 -0
  127. package/dist/routes/theme.d.ts +15 -0
  128. package/dist/routes/theme.js +275 -0
  129. package/dist/routes/theme.js.map +1 -0
  130. package/dist/routes/ui.d.ts +56 -0
  131. package/dist/routes/ui.js +354 -0
  132. package/dist/routes/ui.js.map +1 -0
  133. package/dist/service/commands.d.ts +41 -0
  134. package/dist/service/commands.js +391 -0
  135. package/dist/service/commands.js.map +1 -0
  136. package/dist/service/generators.d.ts +33 -0
  137. package/dist/service/generators.js +432 -0
  138. package/dist/service/generators.js.map +1 -0
  139. package/dist/service/index.d.ts +2 -0
  140. package/dist/service/index.js +7 -0
  141. package/dist/service/index.js.map +1 -0
  142. package/dist/streaming/clients.d.ts +48 -0
  143. package/dist/streaming/clients.js +114 -0
  144. package/dist/streaming/clients.js.map +1 -0
  145. package/dist/streaming/fmp4Segmenter.d.ts +61 -0
  146. package/dist/streaming/fmp4Segmenter.js +461 -0
  147. package/dist/streaming/fmp4Segmenter.js.map +1 -0
  148. package/dist/streaming/hls.d.ts +120 -0
  149. package/dist/streaming/hls.js +722 -0
  150. package/dist/streaming/hls.js.map +1 -0
  151. package/dist/streaming/hlsSegments.d.ts +54 -0
  152. package/dist/streaming/hlsSegments.js +162 -0
  153. package/dist/streaming/hlsSegments.js.map +1 -0
  154. package/dist/streaming/lifecycle.d.ts +33 -0
  155. package/dist/streaming/lifecycle.js +185 -0
  156. package/dist/streaming/lifecycle.js.map +1 -0
  157. package/dist/streaming/monitor.d.ts +74 -0
  158. package/dist/streaming/monitor.js +1310 -0
  159. package/dist/streaming/monitor.js.map +1 -0
  160. package/dist/streaming/mp4Parser.d.ts +74 -0
  161. package/dist/streaming/mp4Parser.js +566 -0
  162. package/dist/streaming/mp4Parser.js.map +1 -0
  163. package/dist/streaming/mpegts.d.ts +14 -0
  164. package/dist/streaming/mpegts.js +248 -0
  165. package/dist/streaming/mpegts.js.map +1 -0
  166. package/dist/streaming/registry.d.ts +119 -0
  167. package/dist/streaming/registry.js +127 -0
  168. package/dist/streaming/registry.js.map +1 -0
  169. package/dist/streaming/setup.d.ts +135 -0
  170. package/dist/streaming/setup.js +670 -0
  171. package/dist/streaming/setup.js.map +1 -0
  172. package/dist/streaming/showInfo.d.ts +30 -0
  173. package/dist/streaming/showInfo.js +362 -0
  174. package/dist/streaming/showInfo.js.map +1 -0
  175. package/dist/streaming/statusEmitter.d.ts +125 -0
  176. package/dist/streaming/statusEmitter.js +139 -0
  177. package/dist/streaming/statusEmitter.js.map +1 -0
  178. package/dist/types/index.d.ts +403 -0
  179. package/dist/types/index.js +6 -0
  180. package/dist/types/index.js.map +1 -0
  181. package/dist/utils/debugFilter.d.ts +38 -0
  182. package/dist/utils/debugFilter.js +157 -0
  183. package/dist/utils/debugFilter.js.map +1 -0
  184. package/dist/utils/delay.d.ts +6 -0
  185. package/dist/utils/delay.js +15 -0
  186. package/dist/utils/delay.js.map +1 -0
  187. package/dist/utils/errors.d.ts +15 -0
  188. package/dist/utils/errors.js +40 -0
  189. package/dist/utils/errors.js.map +1 -0
  190. package/dist/utils/evaluate.d.ts +51 -0
  191. package/dist/utils/evaluate.js +124 -0
  192. package/dist/utils/evaluate.js.map +1 -0
  193. package/dist/utils/ffmpeg.d.ts +65 -0
  194. package/dist/utils/ffmpeg.js +317 -0
  195. package/dist/utils/ffmpeg.js.map +1 -0
  196. package/dist/utils/fileLogger.d.ts +25 -0
  197. package/dist/utils/fileLogger.js +248 -0
  198. package/dist/utils/fileLogger.js.map +1 -0
  199. package/dist/utils/format.d.ts +16 -0
  200. package/dist/utils/format.js +46 -0
  201. package/dist/utils/format.js.map +1 -0
  202. package/dist/utils/html.d.ts +6 -0
  203. package/dist/utils/html.js +24 -0
  204. package/dist/utils/html.js.map +1 -0
  205. package/dist/utils/index.d.ts +15 -0
  206. package/dist/utils/index.js +20 -0
  207. package/dist/utils/index.js.map +1 -0
  208. package/dist/utils/logEmitter.d.ts +17 -0
  209. package/dist/utils/logEmitter.js +30 -0
  210. package/dist/utils/logEmitter.js.map +1 -0
  211. package/dist/utils/logger.d.ts +82 -0
  212. package/dist/utils/logger.js +219 -0
  213. package/dist/utils/logger.js.map +1 -0
  214. package/dist/utils/m3u.d.ts +32 -0
  215. package/dist/utils/m3u.js +148 -0
  216. package/dist/utils/m3u.js.map +1 -0
  217. package/dist/utils/morganStream.d.ts +7 -0
  218. package/dist/utils/morganStream.js +33 -0
  219. package/dist/utils/morganStream.js.map +1 -0
  220. package/dist/utils/platform.d.ts +64 -0
  221. package/dist/utils/platform.js +157 -0
  222. package/dist/utils/platform.js.map +1 -0
  223. package/dist/utils/retry.d.ts +15 -0
  224. package/dist/utils/retry.js +82 -0
  225. package/dist/utils/retry.js.map +1 -0
  226. package/dist/utils/streamContext.d.ts +28 -0
  227. package/dist/utils/streamContext.js +33 -0
  228. package/dist/utils/streamContext.js.map +1 -0
  229. package/dist/utils/version.d.ts +37 -0
  230. package/dist/utils/version.js +228 -0
  231. package/dist/utils/version.js.map +1 -0
  232. package/package.json +92 -0
  233. package/prismcast.png +0 -0
  234. package/prismcast.svg +74 -0
package/dist/app.js ADDED
@@ -0,0 +1,315 @@
1
+ /* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * app.ts: Express application builder for PrismCast.
4
+ */
5
+ import { CONFIG, displayConfiguration, initializeConfiguration, validateConfiguration } from "./config/index.js";
6
+ import { LOG, createMorganStream, formatError, getPackageVersion, isDebugLogging, resolveFFmpegPath, setConsoleLogging, startUpdateChecking, stopUpdateChecking } from "./utils/index.js";
7
+ import { closeBrowser, ensureDataDirectory, getCurrentBrowser, killStaleChrome, minimizeBrowserWindow, prepareExtension, setGracefulShutdown, startBrowserRestartChecking, startStalePageCleanup, stopBrowserRestartChecking, stopStalePageCleanup } from "./browser/index.js";
8
+ import { initializeFileLogger, shutdownFileLogger } from "./utils/fileLogger.js";
9
+ import { startHdhrServer, stopHdhrServer } from "./hdhr/index.js";
10
+ import { startShowInfoPolling, stopShowInfoPolling } from "./streaming/showInfo.js";
11
+ import { cleanupIdleStreams } from "./streaming/hls.js";
12
+ import consoleStamp from "console-stamp";
13
+ import express from "express";
14
+ import { getAllStreams } from "./streaming/registry.js";
15
+ import { initializeUserChannels } from "./config/userChannels.js";
16
+ import morgan from "morgan";
17
+ import { setupRoutes } from "./routes/index.js";
18
+ import { terminateStream } from "./streaming/lifecycle.js";
19
+ import { validateProfiles } from "./config/profiles.js";
20
+ import { verifyCaptureSystem } from "./streaming/setup.js";
21
+ /* The logging mode is set at startup based on the --console CLI flag. When console logging is enabled, timestamps are added via console-stamp and output goes to
22
+ * stdout/stderr. When file logging is used (the default), output goes to ~/.prismcast/prismcast.log.
23
+ */
24
+ // Track whether console logging is enabled, set during startServer().
25
+ let usingConsoleLogging = false;
26
+ /* The HTTP server instance is stored globally so it can be closed during graceful shutdown.
27
+ */
28
+ let server = null;
29
+ // Interval for idle stream cleanup.
30
+ let idleCleanupInterval = null;
31
+ /**
32
+ * Starts the idle cleanup interval. Runs every 10 seconds to check for idle streams and terminate them.
33
+ */
34
+ function startIdleCleanup() {
35
+ if (idleCleanupInterval) {
36
+ return;
37
+ }
38
+ // Check for idle streams every 10 seconds.
39
+ idleCleanupInterval = setInterval(() => {
40
+ cleanupIdleStreams();
41
+ }, 10000);
42
+ }
43
+ /**
44
+ * Stops the idle cleanup interval.
45
+ */
46
+ function stopIdleCleanup() {
47
+ if (idleCleanupInterval) {
48
+ clearInterval(idleCleanupInterval);
49
+ idleCleanupInterval = null;
50
+ }
51
+ }
52
+ /* When the process receives a termination signal, we close all active streams and the browser before exiting. This ensures resources are released cleanly.
53
+ */
54
+ /**
55
+ * Sets up signal handlers for graceful shutdown. When SIGINT or SIGTERM is received, we close all streams, the browser, and the HTTP server before exiting.
56
+ */
57
+ function setupGracefulShutdown() {
58
+ let shutdownInProgress = false;
59
+ async function shutdown() {
60
+ // Prevent multiple shutdown attempts if multiple signals are received.
61
+ if (shutdownInProgress) {
62
+ return;
63
+ }
64
+ shutdownInProgress = true;
65
+ LOG.info("Shutting down.");
66
+ // Set the graceful shutdown flag early so that page close errors are suppressed during stream termination.
67
+ setGracefulShutdown(true);
68
+ // Stop cleanup and polling intervals.
69
+ stopHdhrServer();
70
+ stopBrowserRestartChecking();
71
+ stopStalePageCleanup();
72
+ stopIdleCleanup();
73
+ stopShowInfoPolling();
74
+ stopUpdateChecking();
75
+ // Terminate all streams. terminateStream() handles all cleanup including page closure and registry removal.
76
+ const streams = getAllStreams();
77
+ for (const stream of streams) {
78
+ terminateStream(stream.id, stream.info.storeKey, "server shutdown");
79
+ }
80
+ // Close the browser.
81
+ await closeBrowser();
82
+ // Close the HTTP server.
83
+ try {
84
+ if (server) {
85
+ server.close(() => {
86
+ LOG.info("HTTP server closed successfully.");
87
+ });
88
+ }
89
+ }
90
+ catch (error) {
91
+ LOG.error("Error closing server during shutdown: %s.", formatError(error));
92
+ }
93
+ // Shut down file logger if in use.
94
+ if (!usingConsoleLogging) {
95
+ shutdownFileLogger();
96
+ }
97
+ process.exit(0);
98
+ }
99
+ process.on("SIGINT", () => {
100
+ void shutdown();
101
+ });
102
+ process.on("SIGTERM", () => {
103
+ void shutdown();
104
+ });
105
+ }
106
+ /* The buildApp function creates and configures the Express application with all middleware and routes. This is separated from the server startup to allow for
107
+ * testing and flexibility in deployment.
108
+ */
109
+ /**
110
+ * Creates and configures the Express application with all middleware and routes.
111
+ * @returns The configured Express application.
112
+ */
113
+ async function buildApp() {
114
+ try {
115
+ await prepareExtension();
116
+ }
117
+ catch (error) {
118
+ LOG.error("Cannot build app without extension: %s.", formatError(error));
119
+ throw error;
120
+ }
121
+ const app = express();
122
+ // Trust proxy headers (X-Forwarded-Proto, X-Forwarded-Host) so that req.protocol and req.hostname reflect what the client actually used when accessing through
123
+ // a reverse proxy. This ensures playlist URLs match the client's connection.
124
+ app.set("trust proxy", true);
125
+ // Add body parsing middleware for form submissions (configuration page).
126
+ app.use(express.urlencoded({ extended: true }));
127
+ app.use(express.json());
128
+ // Configure Morgan for HTTP request logging based on httpLogLevel configuration. Morgan output goes through morganStream which handles timestamp formatting
129
+ // consistently for both console and file logging modes.
130
+ if (CONFIG.logging.httpLogLevel !== "none") {
131
+ const morganFormat = ":method :url from :remote-addr responded :status in :response-time ms.";
132
+ const morganStream = createMorganStream();
133
+ // Patterns for browser-initiated asset requests that return 404. These are noise from browsers automatically requesting files that don't exist.
134
+ const browserAssetPatterns = ["/apple-touch-icon", "/favicon", "/robots.txt", "/site.webmanifest"];
135
+ if (CONFIG.logging.httpLogLevel === "errors") {
136
+ // Log requests with 4xx or 5xx status codes, but skip 404s for common browser asset requests.
137
+ app.use(morgan(morganFormat, {
138
+ skip: (req, res) => {
139
+ // Log all non-error responses.
140
+ if (res.statusCode < 400) {
141
+ return true;
142
+ }
143
+ // Skip 404s for browser asset requests (favicon, apple-touch-icon, etc.).
144
+ if (res.statusCode === 404) {
145
+ const url = req.originalUrl || req.url;
146
+ if (browserAssetPatterns.some((pattern) => url.startsWith(pattern))) {
147
+ return true;
148
+ }
149
+ }
150
+ // Skip 503s with Retry-After header. These indicate expected temporary unavailability (e.g., stream starting up) rather than a real error.
151
+ if ((res.statusCode === 503) && res.getHeader("Retry-After")) {
152
+ return true;
153
+ }
154
+ return false;
155
+ },
156
+ stream: morganStream
157
+ }));
158
+ }
159
+ else if (CONFIG.logging.httpLogLevel === "filtered") {
160
+ // Log important requests while skipping high-frequency polling endpoints. We always log errors, slow requests, and critical endpoints.
161
+ const skipPatterns = ["/logs", "/health", "/favicon", "/logo.png", "/logo.svg"];
162
+ app.use(morgan(morganFormat, {
163
+ skip: (req, res) => {
164
+ // Always log errors.
165
+ if (res.statusCode >= 400) {
166
+ return false;
167
+ }
168
+ // Always log slow requests (over 1 second).
169
+ const responseTime = parseFloat(res.getHeader("X-Response-Time") || "0");
170
+ if (responseTime > 1000) {
171
+ return false;
172
+ }
173
+ // Always log streaming and management endpoints.
174
+ const url = req.originalUrl || req.url;
175
+ const importantPatterns = ["/stream", "/streams", "/config", "/playlist", "/debug"];
176
+ if (importantPatterns.some((pattern) => url.startsWith(pattern))) {
177
+ return false;
178
+ }
179
+ // Skip high-frequency endpoints when successful.
180
+ if (skipPatterns.some((pattern) => url.startsWith(pattern))) {
181
+ return true;
182
+ }
183
+ // Skip successful requests to the root landing page.
184
+ if ((url === "/") && (res.statusCode < 400)) {
185
+ return true;
186
+ }
187
+ // Log everything else.
188
+ return false;
189
+ },
190
+ stream: morganStream
191
+ }));
192
+ }
193
+ else {
194
+ // Log all requests.
195
+ app.use(morgan(morganFormat, { stream: morganStream }));
196
+ }
197
+ }
198
+ // Set up all HTTP endpoints.
199
+ setupRoutes(app);
200
+ // Global error handler. Express error handlers require 4 parameters even if unused.
201
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
202
+ app.use((err, _req, res, _next) => {
203
+ LOG.error("Unhandled error in request: %s.", formatError(err));
204
+ if (!res.headersSent) {
205
+ res.status(500).send("Internal server error");
206
+ }
207
+ });
208
+ return app;
209
+ }
210
+ /* The startServer function initializes and starts the HTTP server. It validates configuration, cleans up stale processes, warms up the browser, and starts the
211
+ * Express application.
212
+ */
213
+ /**
214
+ * Initializes and starts the HTTP server. Before accepting connections, we validate configuration, clean up stale Chrome processes, and warm up the browser
215
+ * instance.
216
+ * @param useConsoleLogging - Whether to log to console instead of file. Defaults to false (file logging).
217
+ */
218
+ export async function startServer(useConsoleLogging = false) {
219
+ // Set logging mode early before any log calls.
220
+ usingConsoleLogging = useConsoleLogging;
221
+ setConsoleLogging(useConsoleLogging);
222
+ // Apply console-stamp for timestamps only when using console logging.
223
+ if (useConsoleLogging) {
224
+ consoleStamp(console, { format: ":date(yyyy/mm/dd HH:MM:ss.l)" });
225
+ }
226
+ // Initialize configuration from file and environment variables, then validate.
227
+ try {
228
+ await initializeConfiguration();
229
+ validateConfiguration();
230
+ validateProfiles();
231
+ }
232
+ catch (error) {
233
+ LOG.error(formatError(error));
234
+ process.exit(1);
235
+ }
236
+ displayConfiguration();
237
+ setupGracefulShutdown();
238
+ // Ensure the data directory exists before any operations that depend on it.
239
+ await ensureDataDirectory();
240
+ // Initialize file logger if not using console logging.
241
+ if (!useConsoleLogging) {
242
+ await initializeFileLogger(CONFIG.logging.maxSize);
243
+ }
244
+ // Log the version as the first message captured by the file logger. This makes it easy to identify which release is running when reading logs.
245
+ LOG.info("PrismCast v%s starting.", getPackageVersion());
246
+ // Log the debug filter status after the file logger is ready so the message is captured.
247
+ if (isDebugLogging()) {
248
+ const debugEnv = process.env.PRISMCAST_DEBUG;
249
+ if (debugEnv) {
250
+ LOG.info("Debug logging enabled with filter: %s.", debugEnv);
251
+ }
252
+ else {
253
+ LOG.info("Debug logging enabled for all categories.");
254
+ }
255
+ }
256
+ // Check FFmpeg availability if using FFmpeg capture mode. This must be after file logger initialization so the log message is captured.
257
+ if (CONFIG.streaming.captureMode === "ffmpeg") {
258
+ const ffmpegPath = await resolveFFmpegPath();
259
+ if (!ffmpegPath) {
260
+ LOG.error("FFmpeg is not available. FFmpeg capture mode requires FFmpeg to be installed and in the system PATH.");
261
+ LOG.error("Either install FFmpeg or change the capture mode to 'native' in the configuration.");
262
+ process.exit(1);
263
+ }
264
+ LOG.info("Using FFmpeg at: %s", ffmpegPath);
265
+ }
266
+ // Load user channels from ~/.prismcast/channels.json if it exists.
267
+ await initializeUserChannels();
268
+ killStaleChrome();
269
+ // Warm up browser.
270
+ try {
271
+ await getCurrentBrowser();
272
+ }
273
+ catch (error) {
274
+ LOG.error("Failed to initialize browser during startup: %s.", formatError(error));
275
+ throw error;
276
+ }
277
+ // Verify the capture system works before accepting requests. This detects stale tabCapture state from a previous Chrome process and exits immediately if
278
+ // found, since the puppeteer-stream mutex would be permanently leaked. The probe also ensures the STOP_RECORDING cleanup chain completes before returning.
279
+ try {
280
+ await verifyCaptureSystem();
281
+ }
282
+ catch (error) {
283
+ LOG.error("Capture system verification failed during startup: %s.", formatError(error));
284
+ throw error;
285
+ }
286
+ // Minimize the browser window to reduce GPU usage and desktop clutter. The browser must be visible (not headless) for capture to work, but minimizing it reduces
287
+ // resource consumption. CDP allows us to control window state without affecting capture. We defer minimization until after display detection and capture
288
+ // verification complete, since both require the window in a normal state.
289
+ await minimizeBrowserWindow();
290
+ // Start stale page cleanup.
291
+ startStalePageCleanup();
292
+ // Start browser restart checking.
293
+ startBrowserRestartChecking();
294
+ // Start idle cleanup.
295
+ startIdleCleanup();
296
+ // Start show info polling for Channels DVR integration.
297
+ startShowInfoPolling();
298
+ // Start checking for updates.
299
+ startUpdateChecking(getPackageVersion());
300
+ // Build and start Express application.
301
+ try {
302
+ const app = await buildApp();
303
+ server = app.listen(CONFIG.server.port, CONFIG.server.host, () => {
304
+ LOG.info("PrismCast is now listening on %s:%s.", CONFIG.server.host, CONFIG.server.port);
305
+ });
306
+ }
307
+ catch (error) {
308
+ LOG.error("Failed to build application: %s.", formatError(error));
309
+ throw error;
310
+ }
311
+ // Start HDHomeRun emulation server if enabled. This runs independently of the main server and handles EADDRINUSE gracefully without affecting PrismCast's
312
+ // primary functionality.
313
+ await startHdhrServer();
314
+ }
315
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { CDPSession, Page } from "puppeteer-core";
2
+ /**
3
+ * Executes a CDP (Chrome DevTools Protocol) operation with proper session lifecycle management. This helper handles the common pattern of:
4
+ * 1. Creating a CDP session attached to the page's target
5
+ * 2. Getting the browser window ID for the page
6
+ * 3. Calling the provided operation with the session and window ID
7
+ * 4. Gracefully handling errors when the page is closed during the operation
8
+ *
9
+ * The session is created fresh for each call rather than being reused because CDP sessions become invalid when the page navigates or closes. Creating a new
10
+ * session ensures we always have a valid connection.
11
+ * @param page - The Puppeteer page object to create a CDP session for.
12
+ * @param operation - An async function that receives the CDP session and window ID. The operation can use any CDP commands via session.send().
13
+ * @returns The result of the operation, or undefined if the page was closed or an error occurred.
14
+ */
15
+ export declare function withCDPSession<T>(page: Page, operation: (session: CDPSession, windowId: number) => Promise<T>): Promise<T | undefined>;
16
+ /**
17
+ * Resizes the browser window to match our target viewport dimensions and optionally minimizes it. This function solves the problem of ensuring the video content
18
+ * area exactly matches our configured viewport size.
19
+ *
20
+ * The complication is that browser windows have "chrome" - the title bar, toolbar, borders, and other UI elements that take up space. If we set the window size
21
+ * to 1280x720, the actual content area will be smaller (perhaps 1280x670 after accounting for the toolbar). To get a 1280x720 content area, we need to add the
22
+ * chrome dimensions to our window size.
23
+ *
24
+ * This function:
25
+ * 1. Measures the current chrome dimensions by comparing window.outerWidth/Height to window.innerWidth/Height
26
+ * 2. Sets the window size to viewport + chrome dimensions, giving us the exact viewport size we want
27
+ * 3. Optionally minimizes the window to reduce GPU usage while still allowing capture
28
+ * @param page - The Puppeteer page object.
29
+ * @param shouldMinimize - Whether to minimize the window after resizing. Set to true for stream pages (to reduce GPU usage) and false for debug pages (where
30
+ * visibility is desired).
31
+ */
32
+ export declare function resizeAndMinimizeWindow(page: Page, shouldMinimize: boolean): Promise<void>;
33
+ /**
34
+ * Un-minimizes the browser window, restoring it to normal state. This is used when the user needs to interact with the browser, such as during the authentication
35
+ * login flow where the user must complete TV provider authentication in the visible browser window.
36
+ * @param page - The Puppeteer page object.
37
+ */
38
+ export declare function unminimizeWindow(page: Page): Promise<void>;
@@ -0,0 +1,155 @@
1
+ import { LOG, evaluateWithAbort, formatError } from "../utils/index.js";
2
+ import { CONFIG } from "../config/index.js";
3
+ import { getBrowserChrome } from "./display.js";
4
+ import { getEffectiveViewport } from "../config/presets.js";
5
+ /* The Chrome DevTools Protocol (CDP) provides low-level access to Chrome's internal state and capabilities. While Puppeteer abstracts most common operations, some
6
+ * features require direct CDP access:
7
+ *
8
+ * - Window management: Setting window size, position, and state (minimized, maximized, fullscreen). Puppeteer's viewport API controls the content area, but we
9
+ * need CDP to control the entire window including browser chrome.
10
+ *
11
+ * - Browser-level operations: Operations that affect the browser rather than a specific page, like getting the window ID for a page's target.
12
+ *
13
+ * CDP sessions are created per-page and must be managed carefully:
14
+ * - Sessions can fail if the page or target is closed while we're using it
15
+ * - The "No target with given id" error is common and expected when pages close during operations
16
+ * - We wrap CDP operations in try/catch to handle these transient errors gracefully
17
+ *
18
+ * The withCDPSession helper encapsulates the common pattern of creating a session, getting the window ID, performing an operation, and handling errors.
19
+ */
20
+ /**
21
+ * Executes a CDP (Chrome DevTools Protocol) operation with proper session lifecycle management. This helper handles the common pattern of:
22
+ * 1. Creating a CDP session attached to the page's target
23
+ * 2. Getting the browser window ID for the page
24
+ * 3. Calling the provided operation with the session and window ID
25
+ * 4. Gracefully handling errors when the page is closed during the operation
26
+ *
27
+ * The session is created fresh for each call rather than being reused because CDP sessions become invalid when the page navigates or closes. Creating a new
28
+ * session ensures we always have a valid connection.
29
+ * @param page - The Puppeteer page object to create a CDP session for.
30
+ * @param operation - An async function that receives the CDP session and window ID. The operation can use any CDP commands via session.send().
31
+ * @returns The result of the operation, or undefined if the page was closed or an error occurred.
32
+ */
33
+ export async function withCDPSession(page, operation) {
34
+ // Early exit if the page is already closed. This prevents errors when trying to create a session for a closed page.
35
+ if (page.isClosed()) {
36
+ return undefined;
37
+ }
38
+ try {
39
+ // Create a CDP session attached to the page's target. The session provides access to all CDP domains (Browser, Page, Network, etc.) for this specific
40
+ // target. Each page has its own target in Chrome's DevTools architecture.
41
+ const session = await page.createCDPSession();
42
+ // Get the browser window ID for this page. Chrome organizes pages into windows, and we need the window ID to perform window-level operations like resizing
43
+ // or minimizing. The Browser.getWindowForTarget command returns the window ID for the current target.
44
+ const windowResult = await session.send("Browser.getWindowForTarget");
45
+ const windowId = windowResult.windowId;
46
+ // If we couldn't get a window ID, the target may be in an invalid state. Return undefined to indicate the operation couldn't be performed.
47
+ if (!windowId) {
48
+ return undefined;
49
+ }
50
+ // Execute the caller's operation with the session and window ID.
51
+ return await operation(session, windowId);
52
+ }
53
+ catch (error) {
54
+ const message = formatError(error);
55
+ // "No target with given id" is a common error that occurs when the page closes during our operation. This is expected during stream termination and
56
+ // shouldn't be logged as a warning since it's not actionable. We also check if the page is closed, as errors during page closure are expected.
57
+ if (!message.includes("No target with given id") && !page.isClosed()) {
58
+ LOG.warn("CDP operation failed: %s.", message);
59
+ }
60
+ return undefined;
61
+ }
62
+ }
63
+ /**
64
+ * Resizes the browser window to match our target viewport dimensions and optionally minimizes it. This function solves the problem of ensuring the video content
65
+ * area exactly matches our configured viewport size.
66
+ *
67
+ * The complication is that browser windows have "chrome" - the title bar, toolbar, borders, and other UI elements that take up space. If we set the window size
68
+ * to 1280x720, the actual content area will be smaller (perhaps 1280x670 after accounting for the toolbar). To get a 1280x720 content area, we need to add the
69
+ * chrome dimensions to our window size.
70
+ *
71
+ * This function:
72
+ * 1. Measures the current chrome dimensions by comparing window.outerWidth/Height to window.innerWidth/Height
73
+ * 2. Sets the window size to viewport + chrome dimensions, giving us the exact viewport size we want
74
+ * 3. Optionally minimizes the window to reduce GPU usage while still allowing capture
75
+ * @param page - The Puppeteer page object.
76
+ * @param shouldMinimize - Whether to minimize the window after resizing. Set to true for stream pages (to reduce GPU usage) and false for debug pages (where
77
+ * visibility is desired).
78
+ */
79
+ export async function resizeAndMinimizeWindow(page, shouldMinimize) {
80
+ // Early exit if the page is already closed.
81
+ if (page.isClosed()) {
82
+ return;
83
+ }
84
+ // Get browser chrome dimensions. Prefer cached values from display detection, which were measured when the browser was in a known good state. Fall back to
85
+ // measuring via page.evaluate() if cached values aren't available (e.g., during early initialization or after cache clear).
86
+ let uiSize = getBrowserChrome();
87
+ if (!uiSize) {
88
+ try {
89
+ uiSize = await evaluateWithAbort(page, () => {
90
+ return {
91
+ // Height of chrome = total window height - content height. This includes the title bar, toolbar, and any other vertical UI elements.
92
+ height: window.outerHeight - window.innerHeight,
93
+ // Width of chrome = total window width - content width. This typically includes window borders and any side panels.
94
+ width: window.outerWidth - window.innerWidth
95
+ };
96
+ });
97
+ }
98
+ catch (_error) {
99
+ // If measuring fails (page closed, navigation in progress, etc.), silently return. The resize is not critical and will be attempted again on the next
100
+ // stream if needed.
101
+ return;
102
+ }
103
+ }
104
+ // Use CDP to set the window bounds. We add the chrome dimensions to our target viewport to get the correct total window size. CDP requires separate calls for
105
+ // dimensions and window state - they cannot be combined in a single call.
106
+ await withCDPSession(page, async (session, windowId) => {
107
+ // First, ensure the window is in "normal" state. If the browser launched with a window size larger than the display, Chrome may have automatically maximized
108
+ // the window. Setting bounds on a maximized window is ignored, so we must restore it to normal state first.
109
+ await session.send("Browser.setWindowBounds", {
110
+ bounds: { windowState: "normal" },
111
+ windowId
112
+ });
113
+ // Set the window size to viewport + chrome. After this, the content area will be exactly our target viewport dimensions.
114
+ const viewport = getEffectiveViewport(CONFIG);
115
+ await session.send("Browser.setWindowBounds", {
116
+ bounds: {
117
+ // Total window height = desired viewport height + chrome height (title bar, toolbar, etc.)
118
+ height: viewport.height + uiSize.height,
119
+ // Total window width = desired viewport width + chrome width (borders, etc.)
120
+ width: viewport.width + uiSize.width
121
+ },
122
+ windowId
123
+ });
124
+ // Optionally minimize the window to reduce GPU usage. This must be a separate CDP call because window state cannot be combined with dimensions. Minimizing
125
+ // doesn't stop video capture - the puppeteer-stream extension captures from the compositor rather than the visible display.
126
+ if (shouldMinimize) {
127
+ // Brief delay to allow Chrome's window manager to finish processing the resize before minimizing. Without this delay, the minimize can be ignored when the
128
+ // window is being significantly resized (e.g., during preset degradation from 1080p to 720p).
129
+ await new Promise((resolve) => setTimeout(resolve, 100));
130
+ await session.send("Browser.setWindowBounds", {
131
+ bounds: { windowState: "minimized" },
132
+ windowId
133
+ });
134
+ }
135
+ });
136
+ }
137
+ /**
138
+ * Un-minimizes the browser window, restoring it to normal state. This is used when the user needs to interact with the browser, such as during the authentication
139
+ * login flow where the user must complete TV provider authentication in the visible browser window.
140
+ * @param page - The Puppeteer page object.
141
+ */
142
+ export async function unminimizeWindow(page) {
143
+ // Early exit if the page is already closed.
144
+ if (page.isClosed()) {
145
+ return;
146
+ }
147
+ await withCDPSession(page, async (session, windowId) => {
148
+ // Restore the window to normal (visible) state.
149
+ await session.send("Browser.setWindowBounds", {
150
+ bounds: { windowState: "normal" },
151
+ windowId
152
+ });
153
+ });
154
+ }
155
+ //# sourceMappingURL=cdp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdp.js","sourceRoot":"","sources":["../../src/browser/cdp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ import type { ChannelSelectorResult, ClickTarget, Nullable, ResolvedSiteProfile } from "../types/index.js";
2
+ import type { Page } from "puppeteer-core";
3
+ /**
4
+ * Returns a direct watch URL for the channel specified in the profile, if one can be resolved. Looks up the strategy entry's resolveDirectUrl hook and calls it
5
+ * with the channelSelector and page. Returns null if the strategy has no resolver, the profile has no channelSelector, or the resolver returns null.
6
+ * @param profile - The resolved site profile.
7
+ * @param page - The Puppeteer page object, passed through to the strategy's resolver for response interception setup or API calls.
8
+ * @returns The direct watch URL or null.
9
+ */
10
+ export declare function resolveDirectUrl(profile: ResolvedSiteProfile, page: Page): Promise<Nullable<string>>;
11
+ /**
12
+ * Invalidates the cached direct watch URL for the channel specified in the profile. Looks up the strategy entry's invalidateDirectUrl hook and calls it with
13
+ * the channelSelector. No-op if the strategy has no invalidator or the profile has no channelSelector.
14
+ * @param profile - The resolved site profile.
15
+ */
16
+ export declare function invalidateDirectUrl(profile: ResolvedSiteProfile): void;
17
+ /**
18
+ * Clears all channel selection caches. Called by handleBrowserDisconnect() in browser/index.ts when the browser restarts, since cached state (guide row positions,
19
+ * discovered page URLs, watch URLs) may be stale in a new browser session.
20
+ */
21
+ export declare function clearChannelSelectionCaches(): void;
22
+ /**
23
+ * Clicks at the specified coordinates after a brief settle delay. The delay allows scroll animations and lazy-loaded content to finish before the click fires.
24
+ * Callers are responsible for scrolling the target element into view (typically via scrollIntoView inside a page.evaluate call) before invoking this function.
25
+ * Exported for use by tuning strategy files (thumbnailRow, tileClick, hulu).
26
+ * @param page - The Puppeteer page object.
27
+ * @param target - The x/y coordinates to click.
28
+ * @returns True if the click was executed.
29
+ */
30
+ export declare function scrollAndClick(page: Page, target: ClickTarget): Promise<boolean>;
31
+ export declare function normalizeChannelName(name: string): string;
32
+ /**
33
+ * Logs available channel names from a provider's guide grid when channel selection fails. Produces an actionable log message listing channel names that users can
34
+ * use as `channelSelector` values in user-defined channels. When `presetSuffix` is provided, channels already covered by built-in preset definitions are filtered
35
+ * out so users see only channels that require manual configuration. When omitted (small channel sets like Fox or HBO), all channels are logged unfiltered.
36
+ * @param options - Diagnostic dump configuration.
37
+ * @param options.additionalKnownNames - Extra names to exclude from the filtered list (e.g., CHANNEL_ALTERNATES values for YTTV).
38
+ * @param options.availableChannels - Sorted list of channel names discovered in the guide grid.
39
+ * @param options.channelName - The channelSelector value that failed to match, for the log message.
40
+ * @param options.guideUrl - The URL of the provider's guide page, included in the log message so users know what to set as the channel URL.
41
+ * @param options.presetSuffix - Key suffix to filter preset channels (e.g., "-yttv", "-hulu"). Omit for small unfiltered channel sets.
42
+ * @param options.providerName - Human-readable provider name for the log message (e.g., "YouTube TV", "Hulu").
43
+ */
44
+ export declare function logAvailableChannels(options: {
45
+ additionalKnownNames?: string[];
46
+ availableChannels: string[];
47
+ channelName: string;
48
+ guideUrl: string;
49
+ presetSuffix?: string;
50
+ providerName: string;
51
+ }): void;
52
+ /**
53
+ * Selects a channel from a multi-channel player UI using the strategy specified in the profile. This is the main entry point for channel selection, called by
54
+ * tuneToChannel() after page navigation.
55
+ *
56
+ * The function handles:
57
+ * - Polling for channel slug image readiness before strategy dispatch (when entry.usesImageSlug is true)
58
+ * - Strategy dispatch based on profile.channelSelection.strategy
59
+ * - No-op for single-channel sites (strategy "none" or no channelSelector)
60
+ * - Logging of selection attempts and results
61
+ * @param page - The Puppeteer page object.
62
+ * @param profile - The resolved site profile containing channelSelection config and channelSelector slug.
63
+ * @returns Result object with success status and optional failure reason.
64
+ */
65
+ export declare function selectChannel(page: Page, profile: ResolvedSiteProfile): Promise<ChannelSelectorResult>;