prismcast 1.0.1 → 1.0.3

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 (216) hide show
  1. package/dist/app.d.ts +6 -0
  2. package/dist/app.js +292 -0
  3. package/dist/app.js.map +1 -0
  4. package/dist/browser/cdp.d.ts +38 -0
  5. package/dist/browser/cdp.js +158 -0
  6. package/dist/browser/cdp.js.map +1 -0
  7. package/dist/browser/channelSelection.d.ts +16 -0
  8. package/dist/browser/channelSelection.js +178 -0
  9. package/dist/browser/channelSelection.js.map +1 -0
  10. package/dist/browser/display.d.ts +33 -0
  11. package/{src/browser/display.ts → dist/browser/display.js} +14 -26
  12. package/dist/browser/display.js.map +1 -0
  13. package/dist/browser/index.d.ts +171 -0
  14. package/dist/browser/index.js +953 -0
  15. package/dist/browser/index.js.map +1 -0
  16. package/dist/browser/video.d.ts +240 -0
  17. package/dist/browser/video.js +781 -0
  18. package/dist/browser/video.js.map +1 -0
  19. package/dist/channels/index.d.ts +19 -0
  20. package/dist/channels/index.js +164 -0
  21. package/dist/channels/index.js.map +1 -0
  22. package/dist/config/index.d.ts +53 -0
  23. package/dist/config/index.js +217 -0
  24. package/dist/config/index.js.map +1 -0
  25. package/dist/config/presets.d.ts +98 -0
  26. package/dist/config/presets.js +244 -0
  27. package/dist/config/presets.js.map +1 -0
  28. package/dist/config/profiles.d.ts +74 -0
  29. package/dist/config/profiles.js +438 -0
  30. package/dist/config/profiles.js.map +1 -0
  31. package/dist/config/userChannels.d.ts +128 -0
  32. package/dist/config/userChannels.js +356 -0
  33. package/dist/config/userChannels.js.map +1 -0
  34. package/dist/config/userConfig.d.ts +215 -0
  35. package/dist/config/userConfig.js +854 -0
  36. package/dist/config/userConfig.js.map +1 -0
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.js +126 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/routes/assets.d.ts +6 -0
  41. package/dist/routes/assets.js +82 -0
  42. package/dist/routes/assets.js.map +1 -0
  43. package/dist/routes/auth.d.ts +6 -0
  44. package/dist/routes/auth.js +69 -0
  45. package/dist/routes/auth.js.map +1 -0
  46. package/dist/routes/components.d.ts +138 -0
  47. package/dist/routes/components.js +210 -0
  48. package/dist/routes/components.js.map +1 -0
  49. package/dist/routes/config.d.ts +66 -0
  50. package/dist/routes/config.js +1149 -0
  51. package/dist/routes/config.js.map +1 -0
  52. package/dist/routes/health.d.ts +6 -0
  53. package/dist/routes/health.js +71 -0
  54. package/dist/routes/health.js.map +1 -0
  55. package/dist/routes/hls.d.ts +6 -0
  56. package/{src/routes/hls.ts → dist/routes/hls.js} +10 -16
  57. package/dist/routes/hls.js.map +1 -0
  58. package/dist/routes/index.d.ts +15 -0
  59. package/{src/routes/index.ts → dist/routes/index.js} +11 -19
  60. package/dist/routes/index.js.map +1 -0
  61. package/dist/routes/logs.d.ts +6 -0
  62. package/dist/routes/logs.js +161 -0
  63. package/dist/routes/logs.js.map +1 -0
  64. package/dist/routes/playlist.d.ts +29 -0
  65. package/dist/routes/playlist.js +80 -0
  66. package/dist/routes/playlist.js.map +1 -0
  67. package/dist/routes/root.d.ts +6 -0
  68. package/dist/routes/root.js +1720 -0
  69. package/dist/routes/root.js.map +1 -0
  70. package/dist/routes/streams.d.ts +6 -0
  71. package/dist/routes/streams.js +96 -0
  72. package/dist/routes/streams.js.map +1 -0
  73. package/dist/routes/theme.d.ts +15 -0
  74. package/dist/routes/theme.js +271 -0
  75. package/dist/routes/theme.js.map +1 -0
  76. package/dist/routes/ui.d.ts +56 -0
  77. package/dist/routes/ui.js +354 -0
  78. package/dist/routes/ui.js.map +1 -0
  79. package/dist/service/commands.d.ts +41 -0
  80. package/dist/service/commands.js +394 -0
  81. package/dist/service/commands.js.map +1 -0
  82. package/dist/service/generators.d.ts +32 -0
  83. package/dist/service/generators.js +438 -0
  84. package/dist/service/generators.js.map +1 -0
  85. package/dist/service/index.d.ts +2 -0
  86. package/{src/service/index.ts → dist/service/index.js} +1 -0
  87. package/dist/service/index.js.map +1 -0
  88. package/dist/streaming/fmp4Segmenter.d.ts +26 -0
  89. package/dist/streaming/fmp4Segmenter.js +191 -0
  90. package/dist/streaming/fmp4Segmenter.js.map +1 -0
  91. package/dist/streaming/hls.d.ts +24 -0
  92. package/dist/streaming/hls.js +416 -0
  93. package/dist/streaming/hls.js.map +1 -0
  94. package/dist/streaming/hlsSegments.d.ts +47 -0
  95. package/{src/streaming/hlsSegments.ts → dist/streaming/hlsSegments.js} +55 -102
  96. package/dist/streaming/hlsSegments.js.map +1 -0
  97. package/dist/streaming/lifecycle.d.ts +33 -0
  98. package/{src/streaming/lifecycle.ts → dist/streaming/lifecycle.js} +74 -119
  99. package/dist/streaming/lifecycle.js.map +1 -0
  100. package/dist/streaming/monitor.d.ts +73 -0
  101. package/dist/streaming/monitor.js +967 -0
  102. package/dist/streaming/monitor.js.map +1 -0
  103. package/dist/streaming/mp4Parser.d.ts +25 -0
  104. package/dist/streaming/mp4Parser.js +96 -0
  105. package/dist/streaming/mp4Parser.js.map +1 -0
  106. package/dist/streaming/registry.d.ts +105 -0
  107. package/dist/streaming/registry.js +110 -0
  108. package/dist/streaming/registry.js.map +1 -0
  109. package/dist/streaming/setup.d.ts +112 -0
  110. package/dist/streaming/setup.js +413 -0
  111. package/dist/streaming/setup.js.map +1 -0
  112. package/dist/streaming/showInfo.d.ts +24 -0
  113. package/dist/streaming/showInfo.js +320 -0
  114. package/dist/streaming/showInfo.js.map +1 -0
  115. package/dist/streaming/statusEmitter.d.ts +118 -0
  116. package/dist/streaming/statusEmitter.js +143 -0
  117. package/dist/streaming/statusEmitter.js.map +1 -0
  118. package/dist/types/index.d.ts +284 -0
  119. package/dist/types/index.js +2 -0
  120. package/dist/types/index.js.map +1 -0
  121. package/dist/utils/delay.d.ts +6 -0
  122. package/{src/utils/delay.ts → dist/utils/delay.js} +5 -7
  123. package/dist/utils/delay.js.map +1 -0
  124. package/dist/utils/errors.d.ts +15 -0
  125. package/{src/utils/errors.ts → dist/utils/errors.js} +13 -23
  126. package/dist/utils/errors.js.map +1 -0
  127. package/dist/utils/evaluate.d.ts +51 -0
  128. package/{src/utils/evaluate.ts → dist/utils/evaluate.js} +57 -105
  129. package/dist/utils/evaluate.js.map +1 -0
  130. package/dist/utils/ffmpeg.d.ts +40 -0
  131. package/dist/utils/ffmpeg.js +190 -0
  132. package/dist/utils/ffmpeg.js.map +1 -0
  133. package/dist/utils/fileLogger.d.ts +24 -0
  134. package/dist/utils/fileLogger.js +271 -0
  135. package/dist/utils/fileLogger.js.map +1 -0
  136. package/dist/utils/format.d.ts +9 -0
  137. package/dist/utils/format.js +26 -0
  138. package/dist/utils/format.js.map +1 -0
  139. package/dist/utils/html.d.ts +6 -0
  140. package/{src/utils/html.ts → dist/utils/html.js} +12 -17
  141. package/dist/utils/html.js.map +1 -0
  142. package/dist/utils/index.d.ts +12 -0
  143. package/{src/utils/index.ts → dist/utils/index.js} +1 -0
  144. package/dist/utils/index.js.map +1 -0
  145. package/dist/utils/logEmitter.d.ts +16 -0
  146. package/{src/utils/logEmitter.ts → dist/utils/logEmitter.js} +8 -30
  147. package/dist/utils/logEmitter.js.map +1 -0
  148. package/dist/utils/logger.d.ts +81 -0
  149. package/dist/utils/logger.js +207 -0
  150. package/dist/utils/logger.js.map +1 -0
  151. package/dist/utils/morganStream.d.ts +7 -0
  152. package/{src/utils/morganStream.ts → dist/utils/morganStream.js} +18 -30
  153. package/dist/utils/morganStream.js.map +1 -0
  154. package/dist/utils/platform.d.ts +63 -0
  155. package/dist/utils/platform.js +161 -0
  156. package/dist/utils/platform.js.map +1 -0
  157. package/dist/utils/retry.d.ts +15 -0
  158. package/dist/utils/retry.js +85 -0
  159. package/dist/utils/retry.js.map +1 -0
  160. package/dist/utils/streamContext.d.ts +28 -0
  161. package/dist/utils/streamContext.js +33 -0
  162. package/dist/utils/streamContext.js.map +1 -0
  163. package/package.json +1 -1
  164. package/.github/ISSUE_TEMPLATE/config.yml +0 -3
  165. package/.github/ISSUE_TEMPLATE/feature-request.yml +0 -29
  166. package/.github/ISSUE_TEMPLATE/support-request.yml +0 -89
  167. package/.github/workflows/ci.yml +0 -84
  168. package/.github/workflows/issue-stale.yml +0 -27
  169. package/.github/workflows/lock-threads.yml +0 -28
  170. package/Changelog.md +0 -9
  171. package/eslint.config.mjs +0 -71
  172. package/prismcast.png +0 -0
  173. package/prismcast.svg +0 -74
  174. package/src/app.ts +0 -409
  175. package/src/browser/cdp.ts +0 -207
  176. package/src/browser/channelSelection.ts +0 -246
  177. package/src/browser/index.ts +0 -1254
  178. package/src/browser/video.ts +0 -1107
  179. package/src/channels/index.ts +0 -220
  180. package/src/config/index.ts +0 -297
  181. package/src/config/presets.ts +0 -390
  182. package/src/config/profiles.ts +0 -574
  183. package/src/config/userChannels.ts +0 -557
  184. package/src/config/userConfig.ts +0 -1310
  185. package/src/index.ts +0 -174
  186. package/src/routes/assets.ts +0 -109
  187. package/src/routes/auth.ts +0 -144
  188. package/src/routes/components.ts +0 -428
  189. package/src/routes/config.ts +0 -1711
  190. package/src/routes/health.ts +0 -99
  191. package/src/routes/logs.ts +0 -244
  192. package/src/routes/playlist.ts +0 -105
  193. package/src/routes/root.ts +0 -1903
  194. package/src/routes/streams.ts +0 -146
  195. package/src/routes/theme.ts +0 -310
  196. package/src/routes/ui.ts +0 -427
  197. package/src/service/commands.ts +0 -576
  198. package/src/service/generators.ts +0 -652
  199. package/src/streaming/fmp4Segmenter.ts +0 -354
  200. package/src/streaming/hls.ts +0 -599
  201. package/src/streaming/monitor.ts +0 -1489
  202. package/src/streaming/mp4Parser.ts +0 -178
  203. package/src/streaming/registry.ts +0 -276
  204. package/src/streaming/setup.ts +0 -695
  205. package/src/streaming/showInfo.ts +0 -546
  206. package/src/streaming/statusEmitter.ts +0 -258
  207. package/src/types/global.d.ts +0 -18
  208. package/src/types/index.ts +0 -724
  209. package/src/utils/ffmpeg.ts +0 -278
  210. package/src/utils/fileLogger.ts +0 -366
  211. package/src/utils/format.ts +0 -32
  212. package/src/utils/logger.ts +0 -268
  213. package/src/utils/platform.ts +0 -230
  214. package/src/utils/retry.ts +0 -118
  215. package/src/utils/streamContext.ts +0 -68
  216. package/tsconfig.json +0 -11
package/dist/app.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Initializes and starts the HTTP server. Before accepting connections, we validate configuration, clean up stale Chrome processes, and warm up the browser
3
+ * instance.
4
+ * @param useConsoleLogging - Whether to log to console instead of file. Defaults to false (file logging).
5
+ */
6
+ export declare function startServer(useConsoleLogging?: boolean): Promise<void>;
package/dist/app.js ADDED
@@ -0,0 +1,292 @@
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, isFFmpegAvailable, setConsoleLogging } from "./utils/index.js";
7
+ import { closeBrowser, ensureDataDirectory, getCurrentBrowser, killStaleChromium, prepareExtension, startStalePageCleanup, stopStalePageCleanup } from "./browser/index.js";
8
+ import { initializeFileLogger, shutdownFileLogger } from "./utils/fileLogger.js";
9
+ import { startShowInfoPolling, stopShowInfoPolling } from "./streaming/showInfo.js";
10
+ import { cleanupIdleStreams } from "./streaming/hls.js";
11
+ import consoleStamp from "console-stamp";
12
+ import express from "express";
13
+ import { getAllStreams } from "./streaming/registry.js";
14
+ import { initializeUserChannels } from "./config/userChannels.js";
15
+ import morgan from "morgan";
16
+ import { setupRoutes } from "./routes/index.js";
17
+ import { terminateStream } from "./streaming/lifecycle.js";
18
+ import { validateProfiles } from "./config/profiles.js";
19
+ /*
20
+ * LOGGING MODE
21
+ *
22
+ * 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
23
+ * stdout/stderr. When file logging is used (the default), output goes to ~/.prismcast/prismcast.log.
24
+ */
25
+ // Track whether console logging is enabled, set during startServer().
26
+ let usingConsoleLogging = false;
27
+ /*
28
+ * APPLICATION STATE
29
+ *
30
+ * The HTTP server instance is stored globally so it can be closed during graceful shutdown.
31
+ */
32
+ let server = null;
33
+ // Interval for idle stream cleanup.
34
+ let idleCleanupInterval = null;
35
+ /**
36
+ * Starts the idle cleanup interval. Runs every 10 seconds to check for idle streams and terminate them.
37
+ */
38
+ function startIdleCleanup() {
39
+ if (idleCleanupInterval) {
40
+ return;
41
+ }
42
+ // Check for idle streams every 10 seconds.
43
+ idleCleanupInterval = setInterval(() => {
44
+ cleanupIdleStreams();
45
+ }, 10000);
46
+ }
47
+ /**
48
+ * Stops the idle cleanup interval.
49
+ */
50
+ function stopIdleCleanup() {
51
+ if (idleCleanupInterval) {
52
+ clearInterval(idleCleanupInterval);
53
+ idleCleanupInterval = null;
54
+ }
55
+ }
56
+ /*
57
+ * GRACEFUL SHUTDOWN
58
+ *
59
+ * When the process receives a termination signal, we close all active streams and the browser before exiting. This ensures resources are released cleanly.
60
+ */
61
+ /**
62
+ * 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.
63
+ */
64
+ function setupGracefulShutdown() {
65
+ let shutdownInProgress = false;
66
+ async function shutdown(signal) {
67
+ // Prevent multiple shutdown attempts if multiple signals are received.
68
+ if (shutdownInProgress) {
69
+ return;
70
+ }
71
+ shutdownInProgress = true;
72
+ LOG.info("Received %s, starting graceful shutdown.", signal);
73
+ // Stop cleanup and polling intervals.
74
+ stopStalePageCleanup();
75
+ stopIdleCleanup();
76
+ stopShowInfoPolling();
77
+ // Terminate all streams. terminateStream() handles all cleanup including page closure and registry removal.
78
+ const streams = getAllStreams();
79
+ for (const stream of streams) {
80
+ terminateStream(stream.id, stream.info.storeKey, "server shutdown");
81
+ }
82
+ // Close the browser.
83
+ await closeBrowser();
84
+ // Close the HTTP server.
85
+ try {
86
+ if (server) {
87
+ server.close(() => {
88
+ LOG.info("HTTP server closed successfully.");
89
+ });
90
+ }
91
+ }
92
+ catch (error) {
93
+ LOG.error("Error closing server during shutdown: %s.", formatError(error));
94
+ }
95
+ // Shut down file logger if in use.
96
+ if (!usingConsoleLogging) {
97
+ shutdownFileLogger();
98
+ }
99
+ process.exit(0);
100
+ }
101
+ process.on("SIGINT", () => {
102
+ void shutdown("SIGINT");
103
+ });
104
+ process.on("SIGTERM", () => {
105
+ void shutdown("SIGTERM");
106
+ });
107
+ }
108
+ /*
109
+ * APPLICATION BUILDER
110
+ *
111
+ * The buildApp function creates and configures the Express application with all middleware and routes. This is separated from the server startup to allow for
112
+ * testing and flexibility in deployment.
113
+ */
114
+ /**
115
+ * Creates and configures the Express application with all middleware and routes.
116
+ * @returns The configured Express application.
117
+ */
118
+ async function buildApp() {
119
+ try {
120
+ await prepareExtension();
121
+ }
122
+ catch (error) {
123
+ LOG.error("Cannot build app without extension: %s.", formatError(error));
124
+ throw error;
125
+ }
126
+ const app = express();
127
+ // 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
128
+ // a reverse proxy. This ensures playlist URLs match the client's connection.
129
+ app.set("trust proxy", true);
130
+ // Add body parsing middleware for form submissions (configuration page).
131
+ app.use(express.urlencoded({ extended: true }));
132
+ app.use(express.json());
133
+ // Configure Morgan for HTTP request logging based on httpLogLevel configuration. Morgan output goes through morganStream which handles timestamp formatting
134
+ // consistently for both console and file logging modes.
135
+ if (CONFIG.logging.httpLogLevel !== "none") {
136
+ const morganFormat = ":method :url from :remote-addr responded :status in :response-time ms.";
137
+ const morganStream = createMorganStream();
138
+ // Patterns for browser-initiated asset requests that return 404. These are noise from browsers automatically requesting files that don't exist.
139
+ const browserAssetPatterns = ["/apple-touch-icon", "/favicon", "/robots.txt", "/site.webmanifest"];
140
+ if (CONFIG.logging.httpLogLevel === "errors") {
141
+ // Log requests with 4xx or 5xx status codes, but skip 404s for common browser asset requests.
142
+ app.use(morgan(morganFormat, {
143
+ skip: (req, res) => {
144
+ // Log all non-error responses.
145
+ if (res.statusCode < 400) {
146
+ return true;
147
+ }
148
+ // Skip 404s for browser asset requests (favicon, apple-touch-icon, etc.).
149
+ if (res.statusCode === 404) {
150
+ const url = req.originalUrl || req.url;
151
+ if (browserAssetPatterns.some((pattern) => url.startsWith(pattern))) {
152
+ return true;
153
+ }
154
+ }
155
+ // Skip 503s with Retry-After header. These indicate expected temporary unavailability (e.g., stream starting up) rather than a real error.
156
+ if ((res.statusCode === 503) && res.getHeader("Retry-After")) {
157
+ return true;
158
+ }
159
+ return false;
160
+ },
161
+ stream: morganStream
162
+ }));
163
+ }
164
+ else if (CONFIG.logging.httpLogLevel === "filtered") {
165
+ // Log important requests while skipping high-frequency polling endpoints. We always log errors, slow requests, and critical endpoints.
166
+ const skipPatterns = ["/logs", "/health", "/favicon", "/logo.png", "/logo.svg"];
167
+ app.use(morgan(morganFormat, {
168
+ skip: (req, res) => {
169
+ // Always log errors.
170
+ if (res.statusCode >= 400) {
171
+ return false;
172
+ }
173
+ // Always log slow requests (over 1 second).
174
+ const responseTime = parseFloat(res.getHeader("X-Response-Time") || "0");
175
+ if (responseTime > 1000) {
176
+ return false;
177
+ }
178
+ // Always log streaming and management endpoints.
179
+ const url = req.originalUrl || req.url;
180
+ const importantPatterns = ["/stream", "/streams", "/config", "/playlist", "/debug"];
181
+ if (importantPatterns.some((pattern) => url.startsWith(pattern))) {
182
+ return false;
183
+ }
184
+ // Skip high-frequency endpoints when successful.
185
+ if (skipPatterns.some((pattern) => url.startsWith(pattern))) {
186
+ return true;
187
+ }
188
+ // Skip successful requests to the root landing page.
189
+ if ((url === "/") && (res.statusCode < 400)) {
190
+ return true;
191
+ }
192
+ // Log everything else.
193
+ return false;
194
+ },
195
+ stream: morganStream
196
+ }));
197
+ }
198
+ else {
199
+ // Log all requests.
200
+ app.use(morgan(morganFormat, { stream: morganStream }));
201
+ }
202
+ }
203
+ // Set up all HTTP endpoints.
204
+ setupRoutes(app);
205
+ // Global error handler. Express error handlers require 4 parameters even if unused.
206
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
207
+ app.use((err, _req, res, _next) => {
208
+ LOG.error("Unhandled error in request: %s.", formatError(err));
209
+ if (!res.headersSent) {
210
+ res.status(500).send("Internal server error");
211
+ }
212
+ });
213
+ return app;
214
+ }
215
+ /*
216
+ * SERVER STARTUP
217
+ *
218
+ * The startServer function initializes and starts the HTTP server. It validates configuration, cleans up stale processes, warms up the browser, and starts the
219
+ * Express application.
220
+ */
221
+ /**
222
+ * Initializes and starts the HTTP server. Before accepting connections, we validate configuration, clean up stale Chrome processes, and warm up the browser
223
+ * instance.
224
+ * @param useConsoleLogging - Whether to log to console instead of file. Defaults to false (file logging).
225
+ */
226
+ export async function startServer(useConsoleLogging = false) {
227
+ // Set logging mode early before any log calls.
228
+ usingConsoleLogging = useConsoleLogging;
229
+ setConsoleLogging(useConsoleLogging);
230
+ // Apply console-stamp for timestamps only when using console logging.
231
+ if (useConsoleLogging) {
232
+ consoleStamp(console, { format: ":date(yyyy/mm/dd HH:MM:ss.l)" });
233
+ }
234
+ // Initialize configuration from file and environment variables, then validate.
235
+ try {
236
+ await initializeConfiguration();
237
+ validateConfiguration();
238
+ validateProfiles();
239
+ }
240
+ catch (error) {
241
+ LOG.error(formatError(error));
242
+ process.exit(1);
243
+ }
244
+ displayConfiguration();
245
+ setupGracefulShutdown();
246
+ // Check FFmpeg availability if using FFmpeg capture mode.
247
+ if (CONFIG.streaming.captureMode === "ffmpeg") {
248
+ const ffmpegAvailable = await isFFmpegAvailable();
249
+ if (!ffmpegAvailable) {
250
+ LOG.error("FFmpeg is not available. FFmpeg capture mode requires FFmpeg to be installed and in the system PATH.");
251
+ LOG.error("Either install FFmpeg or change the capture mode to 'native' in the configuration.");
252
+ process.exit(1);
253
+ }
254
+ LOG.info("FFmpeg is available for FFmpeg capture mode.");
255
+ }
256
+ // Ensure the data directory exists before any operations that depend on it.
257
+ await ensureDataDirectory();
258
+ // Initialize file logger if not using console logging.
259
+ if (!useConsoleLogging) {
260
+ await initializeFileLogger(CONFIG.logging.maxSize);
261
+ }
262
+ // Load user channels from ~/.prismcast/channels.json if it exists.
263
+ await initializeUserChannels();
264
+ killStaleChromium();
265
+ // Warm up browser.
266
+ try {
267
+ await getCurrentBrowser();
268
+ LOG.info("Chrome ready.");
269
+ }
270
+ catch (error) {
271
+ LOG.error("Failed to initialize browser during startup: %s.", formatError(error));
272
+ throw error;
273
+ }
274
+ // Start stale page cleanup.
275
+ startStalePageCleanup();
276
+ // Start idle cleanup.
277
+ startIdleCleanup();
278
+ // Start show info polling for Channels DVR integration.
279
+ startShowInfoPolling();
280
+ // Build and start Express application.
281
+ try {
282
+ const app = await buildApp();
283
+ server = app.listen(CONFIG.server.port, CONFIG.server.host, () => {
284
+ LOG.info("PrismCast is now listening on %s:%s.", CONFIG.server.host, CONFIG.server.port);
285
+ });
286
+ }
287
+ catch (error) {
288
+ LOG.error("Failed to build application: %s.", formatError(error));
289
+ throw error;
290
+ }
291
+ }
292
+ //# 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,158 @@
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
+ /*
6
+ * CDP SESSION HELPERS
7
+ *
8
+ * The Chrome DevTools Protocol (CDP) provides low-level access to Chrome's internal state and capabilities. While Puppeteer abstracts most common operations, some
9
+ * features require direct CDP access:
10
+ *
11
+ * - Window management: Setting window size, position, and state (minimized, maximized, fullscreen). Puppeteer's viewport API controls the content area, but we
12
+ * need CDP to control the entire window including browser chrome.
13
+ *
14
+ * - Browser-level operations: Operations that affect the browser rather than a specific page, like getting the window ID for a page's target.
15
+ *
16
+ * CDP sessions are created per-page and must be managed carefully:
17
+ * - Sessions can fail if the page or target is closed while we're using it
18
+ * - The "No target with given id" error is common and expected when pages close during operations
19
+ * - We wrap CDP operations in try/catch to handle these transient errors gracefully
20
+ *
21
+ * The withCDPSession helper encapsulates the common pattern of creating a session, getting the window ID, performing an operation, and handling errors.
22
+ */
23
+ /**
24
+ * Executes a CDP (Chrome DevTools Protocol) operation with proper session lifecycle management. This helper handles the common pattern of:
25
+ * 1. Creating a CDP session attached to the page's target
26
+ * 2. Getting the browser window ID for the page
27
+ * 3. Calling the provided operation with the session and window ID
28
+ * 4. Gracefully handling errors when the page is closed during the operation
29
+ *
30
+ * 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
31
+ * session ensures we always have a valid connection.
32
+ * @param page - The Puppeteer page object to create a CDP session for.
33
+ * @param operation - An async function that receives the CDP session and window ID. The operation can use any CDP commands via session.send().
34
+ * @returns The result of the operation, or undefined if the page was closed or an error occurred.
35
+ */
36
+ export async function withCDPSession(page, operation) {
37
+ // Early exit if the page is already closed. This prevents errors when trying to create a session for a closed page.
38
+ if (page.isClosed()) {
39
+ return undefined;
40
+ }
41
+ try {
42
+ // 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
43
+ // target. Each page has its own target in Chrome's DevTools architecture.
44
+ const session = await page.createCDPSession();
45
+ // 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
46
+ // or minimizing. The Browser.getWindowForTarget command returns the window ID for the current target.
47
+ const windowResult = await session.send("Browser.getWindowForTarget");
48
+ const windowId = windowResult.windowId;
49
+ // 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.
50
+ if (!windowId) {
51
+ return undefined;
52
+ }
53
+ // Execute the caller's operation with the session and window ID.
54
+ return await operation(session, windowId);
55
+ }
56
+ catch (error) {
57
+ const message = formatError(error);
58
+ // "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
59
+ // 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.
60
+ if ((message.indexOf("No target with given id") === -1) && !page.isClosed()) {
61
+ LOG.warn("CDP operation failed: %s.", message);
62
+ }
63
+ return undefined;
64
+ }
65
+ }
66
+ /**
67
+ * Resizes the browser window to match our target viewport dimensions and optionally minimizes it. This function solves the problem of ensuring the video content
68
+ * area exactly matches our configured viewport size.
69
+ *
70
+ * 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
71
+ * 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
72
+ * chrome dimensions to our window size.
73
+ *
74
+ * This function:
75
+ * 1. Measures the current chrome dimensions by comparing window.outerWidth/Height to window.innerWidth/Height
76
+ * 2. Sets the window size to viewport + chrome dimensions, giving us the exact viewport size we want
77
+ * 3. Optionally minimizes the window to reduce GPU usage while still allowing capture
78
+ * @param page - The Puppeteer page object.
79
+ * @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
80
+ * visibility is desired).
81
+ */
82
+ export async function resizeAndMinimizeWindow(page, shouldMinimize) {
83
+ // Early exit if the page is already closed.
84
+ if (page.isClosed()) {
85
+ return;
86
+ }
87
+ // 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
88
+ // measuring via page.evaluate() if cached values aren't available (e.g., during early initialization or after cache clear).
89
+ let uiSize = getBrowserChrome();
90
+ if (!uiSize) {
91
+ try {
92
+ uiSize = await evaluateWithAbort(page, () => {
93
+ return {
94
+ // Height of chrome = total window height - content height. This includes the title bar, toolbar, and any other vertical UI elements.
95
+ height: window.outerHeight - window.innerHeight,
96
+ // Width of chrome = total window width - content width. This typically includes window borders and any side panels.
97
+ width: window.outerWidth - window.innerWidth
98
+ };
99
+ });
100
+ }
101
+ catch (_error) {
102
+ // If measuring fails (page closed, navigation in progress, etc.), silently return. The resize is not critical and will be attempted again on the next
103
+ // stream if needed.
104
+ return;
105
+ }
106
+ }
107
+ // 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
108
+ // dimensions and window state - they cannot be combined in a single call.
109
+ await withCDPSession(page, async (session, windowId) => {
110
+ // 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
111
+ // the window. Setting bounds on a maximized window is ignored, so we must restore it to normal state first.
112
+ await session.send("Browser.setWindowBounds", {
113
+ bounds: { windowState: "normal" },
114
+ windowId
115
+ });
116
+ // Set the window size to viewport + chrome. After this, the content area will be exactly our target viewport dimensions.
117
+ const viewport = getEffectiveViewport(CONFIG);
118
+ await session.send("Browser.setWindowBounds", {
119
+ bounds: {
120
+ // Total window height = desired viewport height + chrome height (title bar, toolbar, etc.)
121
+ height: viewport.height + uiSize.height,
122
+ // Total window width = desired viewport width + chrome width (borders, etc.)
123
+ width: viewport.width + uiSize.width
124
+ },
125
+ windowId
126
+ });
127
+ // Optionally minimize the window to reduce GPU usage. This must be a separate CDP call because window state cannot be combined with dimensions. Minimizing
128
+ // doesn't stop video capture - the puppeteer-stream extension captures from the compositor rather than the visible display.
129
+ if (shouldMinimize) {
130
+ // 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
131
+ // window is being significantly resized (e.g., during preset degradation from 1080p to 720p).
132
+ await new Promise((resolve) => setTimeout(resolve, 100));
133
+ await session.send("Browser.setWindowBounds", {
134
+ bounds: { windowState: "minimized" },
135
+ windowId
136
+ });
137
+ }
138
+ });
139
+ }
140
+ /**
141
+ * 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
142
+ * login flow where the user must complete TV provider authentication in the visible browser window.
143
+ * @param page - The Puppeteer page object.
144
+ */
145
+ export async function unminimizeWindow(page) {
146
+ // Early exit if the page is already closed.
147
+ if (page.isClosed()) {
148
+ return;
149
+ }
150
+ await withCDPSession(page, async (session, windowId) => {
151
+ // Restore the window to normal (visible) state.
152
+ await session.send("Browser.setWindowBounds", {
153
+ bounds: { windowState: "normal" },
154
+ windowId
155
+ });
156
+ });
157
+ }
158
+ //# 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,16 @@
1
+ import type { ChannelSelectorResult, ResolvedSiteProfile } from "../types/index.js";
2
+ import type { Page } from "puppeteer-core";
3
+ /**
4
+ * 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
5
+ * tuneToChannel() after page navigation.
6
+ *
7
+ * The function handles:
8
+ * - Strategy dispatch based on profile.channelSelection.strategy
9
+ * - No-op for single-channel sites (strategy "none" or no channelSelector)
10
+ * - Logging of selection attempts and results
11
+ * - Timing delays before and after selection
12
+ * @param page - The Puppeteer page object.
13
+ * @param profile - The resolved site profile containing channelSelection config and channelSelector slug.
14
+ * @returns Result object with success status and optional failure reason.
15
+ */
16
+ export declare function selectChannel(page: Page, profile: ResolvedSiteProfile): Promise<ChannelSelectorResult>;