@torrent-tv/proxy 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +89 -14
- package/package.json +1 -1
- package/routes/api/playback-plan/post.js +19 -0
- package/routes/api/sources/post.js +18 -0
- package/routes/api/transcode-sessions/post.js +18 -0
- package/routes/api/transcode-sessions/progress/get.js +10 -0
- package/routes/api/transcode-sessions/release/post.js +19 -0
- package/routes/health/get.js +9 -0
- package/routes/healthz/get.js +9 -0
- package/routes/stream/get.js +35 -0
- package/routes/transcode/session-file/get.js +29 -0
- package/server.js +29 -0
- package/services/hls-session-manager.js +207 -7
- package/services/playback-planner.js +71 -0
- package/services/registry-api.js +62 -12
- package/services/torrent-pool.js +72 -2
- package/services/tunnel-client.js +220 -0
- package/store/source-registry.js +41 -0
- package/utils/logger.js +30 -0
- package/utils/parse-range.js +13 -0
package/bin/cli.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* @file CLI entry point for the torrent-tv proxy.
|
|
4
|
+
*
|
|
5
|
+
* Parses command-line arguments, starts the local HTTP server, registers
|
|
6
|
+
* this proxy with the registry server, establishes the WebSocket tunnel,
|
|
7
|
+
* and maintains a periodic heartbeat.
|
|
8
|
+
*/
|
|
9
|
+
|
|
3
10
|
import { Command } from "commander";
|
|
4
11
|
import crypto from "node:crypto";
|
|
5
12
|
import { spawnSync } from "node:child_process";
|
|
6
13
|
import ffmpegStatic from "ffmpeg-static";
|
|
7
14
|
import { startProxyServer } from "../server.js";
|
|
8
15
|
import { registerClient, sendHeartbeat } from "../services/registry-api.js";
|
|
16
|
+
import { createTunnelClient } from "../services/tunnel-client.js";
|
|
17
|
+
import { logger } from "../utils/logger.js";
|
|
9
18
|
|
|
10
19
|
const program = new Command();
|
|
11
20
|
|
|
@@ -45,7 +54,7 @@ const options = program.opts();
|
|
|
45
54
|
|
|
46
55
|
const localPort = Number(options.port);
|
|
47
56
|
if (!Number.isInteger(localPort) || localPort <= 0 || localPort > 65535) {
|
|
48
|
-
|
|
57
|
+
logger.error("Invalid --port value.");
|
|
49
58
|
process.exit(1);
|
|
50
59
|
}
|
|
51
60
|
|
|
@@ -61,6 +70,12 @@ const transcodeAudio = options.transcodeAudio !== false;
|
|
|
61
70
|
const bundledFfmpegBin = typeof ffmpegStatic === "string" ? ffmpegStatic : "";
|
|
62
71
|
const ffmpegBin = options.ffmpegBin ? String(options.ffmpegBin) : bundledFfmpegBin || "ffmpeg";
|
|
63
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Verify that the ffmpeg binary is reachable and exits cleanly.
|
|
75
|
+
* Throws with a descriptive message when the check fails.
|
|
76
|
+
*
|
|
77
|
+
* @returns {void}
|
|
78
|
+
*/
|
|
64
79
|
function assertFfmpegAvailability() {
|
|
65
80
|
const probe = spawnSync(ffmpegBin, ["-version"], {
|
|
66
81
|
stdio: "ignore",
|
|
@@ -78,12 +93,30 @@ function assertFfmpegAvailability() {
|
|
|
78
93
|
}
|
|
79
94
|
}
|
|
80
95
|
|
|
96
|
+
/** @type {boolean} */
|
|
81
97
|
let registrationInProgress = false;
|
|
98
|
+
|
|
99
|
+
/** @type {ReturnType<typeof setInterval> | null} */
|
|
82
100
|
let heartbeatTimer = null;
|
|
101
|
+
|
|
102
|
+
/** @type {import("fastify").FastifyInstance | null} */
|
|
83
103
|
let app = null;
|
|
104
|
+
|
|
105
|
+
/** @type {number} */
|
|
84
106
|
let actualPort = localPort;
|
|
107
|
+
|
|
108
|
+
/** @type {boolean} */
|
|
85
109
|
let shutdownInProgress = false;
|
|
86
110
|
|
|
111
|
+
/** @type {ReturnType<typeof import("../services/tunnel-client.js").createTunnelClient> | null} */
|
|
112
|
+
let tunnelClient = null;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Register this proxy with the registry server.
|
|
116
|
+
* Silently skips if a registration is already in flight.
|
|
117
|
+
*
|
|
118
|
+
* @returns {Promise<void>}
|
|
119
|
+
*/
|
|
87
120
|
async function registerClientSafe() {
|
|
88
121
|
if (registrationInProgress) {
|
|
89
122
|
return;
|
|
@@ -97,12 +130,43 @@ async function registerClientSafe() {
|
|
|
97
130
|
baseUrl: explicitBaseUrl || `http://${bindHost}:${actualPort}`,
|
|
98
131
|
token
|
|
99
132
|
});
|
|
100
|
-
|
|
133
|
+
logger.success(`Registered: ${JSON.stringify(result.client)}`);
|
|
101
134
|
} finally {
|
|
102
135
|
registrationInProgress = false;
|
|
103
136
|
}
|
|
104
137
|
}
|
|
105
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Register this proxy with the registry server, retrying indefinitely
|
|
141
|
+
* with exponential backoff until it succeeds. This allows the proxy to
|
|
142
|
+
* survive temporary server outages (restarts, deployments) without crashing.
|
|
143
|
+
*
|
|
144
|
+
* @returns {Promise<void>}
|
|
145
|
+
*/
|
|
146
|
+
async function registerWithRetry() {
|
|
147
|
+
const MAX_DELAY_MS = 60_000;
|
|
148
|
+
let delayMs = 2_000;
|
|
149
|
+
let attempt = 0;
|
|
150
|
+
while (true) {
|
|
151
|
+
attempt++;
|
|
152
|
+
try {
|
|
153
|
+
await registerClientSafe();
|
|
154
|
+
return;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
157
|
+
logger.warn(`Registration attempt ${attempt} failed: ${message}. Retrying in ${delayMs / 1000}s…`);
|
|
158
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
159
|
+
delayMs = Math.min(delayMs * 2, MAX_DELAY_MS);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Gracefully shut down the tunnel, heartbeat timer, and HTTP server.
|
|
166
|
+
*
|
|
167
|
+
* @param {string} signal - Signal name (e.g. "SIGINT").
|
|
168
|
+
* @returns {Promise<void>}
|
|
169
|
+
*/
|
|
106
170
|
async function shutdown(signal) {
|
|
107
171
|
if (shutdownInProgress) {
|
|
108
172
|
return;
|
|
@@ -112,7 +176,11 @@ async function shutdown(signal) {
|
|
|
112
176
|
clearInterval(heartbeatTimer);
|
|
113
177
|
heartbeatTimer = null;
|
|
114
178
|
}
|
|
115
|
-
|
|
179
|
+
if (tunnelClient) {
|
|
180
|
+
tunnelClient.disconnect();
|
|
181
|
+
tunnelClient = null;
|
|
182
|
+
}
|
|
183
|
+
logger.warn(`Received ${signal}, shutting down...`);
|
|
116
184
|
try {
|
|
117
185
|
if (app) {
|
|
118
186
|
await app.close();
|
|
@@ -120,7 +188,7 @@ async function shutdown(signal) {
|
|
|
120
188
|
process.exit(0);
|
|
121
189
|
} catch (error) {
|
|
122
190
|
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
-
|
|
191
|
+
logger.error(`Shutdown failed: ${message}`);
|
|
124
192
|
process.exit(1);
|
|
125
193
|
}
|
|
126
194
|
}
|
|
@@ -139,15 +207,22 @@ try {
|
|
|
139
207
|
actualPort = started.port;
|
|
140
208
|
const directBaseUrl = explicitBaseUrl || `http://${bindHost}:${actualPort}`;
|
|
141
209
|
|
|
142
|
-
|
|
143
|
-
|
|
210
|
+
logger.info(`Local stream endpoint: http://${bindHost}:${actualPort}/stream`);
|
|
211
|
+
logger.info(`Advertised direct URL: ${directBaseUrl}`);
|
|
144
212
|
if (transcodeAudio) {
|
|
145
|
-
|
|
146
|
-
chalk.cyan(`[proxy-client] Optional HLS audio transcode is enabled (ffmpeg: ${ffmpegBin}).`)
|
|
147
|
-
);
|
|
213
|
+
logger.info(`Optional HLS audio transcode is enabled (ffmpeg: ${ffmpegBin}).`);
|
|
148
214
|
}
|
|
149
215
|
|
|
150
|
-
await
|
|
216
|
+
await registerWithRetry();
|
|
217
|
+
|
|
218
|
+
tunnelClient = createTunnelClient({
|
|
219
|
+
serverUrl,
|
|
220
|
+
proxyId: clientId,
|
|
221
|
+
token,
|
|
222
|
+
proxyPort: actualPort,
|
|
223
|
+
onLog: (msg) => logger.info(msg)
|
|
224
|
+
});
|
|
225
|
+
tunnelClient.connect();
|
|
151
226
|
|
|
152
227
|
heartbeatTimer = setInterval(async () => {
|
|
153
228
|
const status = await sendHeartbeat({
|
|
@@ -156,18 +231,18 @@ try {
|
|
|
156
231
|
token
|
|
157
232
|
});
|
|
158
233
|
if (status === 404) {
|
|
159
|
-
|
|
234
|
+
logger.warn("Heartbeat returned 404, re-registering...");
|
|
160
235
|
try {
|
|
161
236
|
await registerClientSafe();
|
|
162
237
|
} catch (error) {
|
|
163
238
|
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
-
|
|
239
|
+
logger.error(`Re-register failed: ${message}`);
|
|
165
240
|
}
|
|
166
241
|
}
|
|
167
242
|
}, 20_000);
|
|
168
243
|
} catch (error) {
|
|
169
244
|
const message = error instanceof Error ? error.message : String(error);
|
|
170
|
-
|
|
245
|
+
logger.error(message);
|
|
171
246
|
process.exit(1);
|
|
172
247
|
}
|
|
173
248
|
|
package/package.json
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determine the best playback mode (direct stream or HLS transcode) for a
|
|
3
|
+
* torrent file and return the corresponding plan.
|
|
4
|
+
*
|
|
5
|
+
* POST /api/playback-plan
|
|
6
|
+
*
|
|
7
|
+
* @param {import("fastify").FastifyRequest} req
|
|
8
|
+
* @param {import("fastify").FastifyReply} reply
|
|
9
|
+
* @param {{ playbackPlanner: ReturnType<import("../../../services/playback-planner.js").createPlaybackPlanner> }} deps
|
|
10
|
+
* @returns {Promise<void>}
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract a plain object from the request body, guarding against
|
|
15
|
+
* non-object payloads (arrays, primitives, null).
|
|
16
|
+
*
|
|
17
|
+
* @param {unknown} body
|
|
18
|
+
* @returns {Record<string, unknown>}
|
|
19
|
+
*/
|
|
1
20
|
function getPayload(body) {
|
|
2
21
|
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
3
22
|
return body;
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register a torrent source with the proxy and receive a stable source key.
|
|
3
|
+
*
|
|
4
|
+
* POST /api/sources
|
|
5
|
+
*
|
|
6
|
+
* @param {import("fastify").FastifyRequest} req
|
|
7
|
+
* @param {import("fastify").FastifyReply} reply
|
|
8
|
+
* @param {{ sourceRegistry: ReturnType<import("../../../store/source-registry.js").createSourceRegistry> }} deps
|
|
9
|
+
* @returns {Promise<void>}
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract a plain object from the request body, guarding against
|
|
14
|
+
* non-object payloads (arrays, primitives, null).
|
|
15
|
+
*
|
|
16
|
+
* @param {unknown} body
|
|
17
|
+
* @returns {Record<string, unknown>}
|
|
18
|
+
*/
|
|
1
19
|
function getPayload(body) {
|
|
2
20
|
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
3
21
|
return body;
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create or return an existing HLS transcode session for a torrent file.
|
|
3
|
+
*
|
|
4
|
+
* POST /api/transcode-sessions
|
|
5
|
+
*
|
|
6
|
+
* @param {import("fastify").FastifyRequest} req
|
|
7
|
+
* @param {import("fastify").FastifyReply} reply
|
|
8
|
+
* @param {{ hlsSessionManager: import("../../../services/hls-session-manager.js").HlsSessionManager }} deps
|
|
9
|
+
* @returns {Promise<void>}
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract a plain object from the request body, guarding against
|
|
14
|
+
* non-object payloads (arrays, primitives, null).
|
|
15
|
+
*
|
|
16
|
+
* @param {unknown} body
|
|
17
|
+
* @returns {Record<string, unknown>}
|
|
18
|
+
*/
|
|
1
19
|
function getPayload(body) {
|
|
2
20
|
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
3
21
|
return body;
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the current encoding progress for an active HLS transcode session.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/transcode-sessions/:sessionId/progress
|
|
5
|
+
*
|
|
6
|
+
* @param {import("fastify").FastifyRequest} req
|
|
7
|
+
* @param {import("fastify").FastifyReply} reply
|
|
8
|
+
* @param {{ hlsSessionManager: import("../../../../services/hls-session-manager.js").HlsSessionManager }} deps
|
|
9
|
+
* @returns {Promise<void>}
|
|
10
|
+
*/
|
|
1
11
|
export async function handleApiTranscodeSessionsProgressGet(req, reply, { hlsSessionManager }) {
|
|
2
12
|
const sessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
|
|
3
13
|
if (!sessionId) {
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release a consumer from a transcode session.
|
|
3
|
+
* When the last consumer is released the session is disposed automatically.
|
|
4
|
+
*
|
|
5
|
+
* POST /api/transcode-sessions/:sessionId/release
|
|
6
|
+
*
|
|
7
|
+
* @param {import("fastify").FastifyRequest} req
|
|
8
|
+
* @param {import("fastify").FastifyReply} reply
|
|
9
|
+
* @param {{ hlsSessionManager: import("../../../../services/hls-session-manager.js").HlsSessionManager }} deps
|
|
10
|
+
* @returns {Promise<void>}
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract a plain object from the request body, guarding against
|
|
15
|
+
* non-object payloads (arrays, primitives, null).
|
|
16
|
+
*
|
|
17
|
+
* @param {unknown} body
|
|
18
|
+
* @returns {Record<string, unknown>}
|
|
19
|
+
*/
|
|
1
20
|
function getPayload(body) {
|
|
2
21
|
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
3
22
|
return body;
|
package/routes/health/get.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic liveness check for load balancers and container orchestrators.
|
|
3
|
+
*
|
|
4
|
+
* GET /health
|
|
5
|
+
*
|
|
6
|
+
* @param {import("fastify").FastifyRequest} _req
|
|
7
|
+
* @param {import("fastify").FastifyReply} reply
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
1
10
|
export async function handleHealthGet(_req, reply) {
|
|
2
11
|
return reply.send({ ok: true });
|
|
3
12
|
}
|
package/routes/healthz/get.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker / Kubernetes readiness probe endpoint.
|
|
3
|
+
*
|
|
4
|
+
* GET /healthz
|
|
5
|
+
*
|
|
6
|
+
* @param {import("fastify").FastifyRequest} _req
|
|
7
|
+
* @param {import("fastify").FastifyReply} reply
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
1
10
|
export async function handleHealthzGet(_req, reply) {
|
|
2
11
|
return reply.send({ ok: true });
|
|
3
12
|
}
|
package/routes/stream/get.js
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Byte-range aware torrent file streaming endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Accepts either a `sourceKey` (registered via POST /api/sources) or a raw
|
|
5
|
+
* `sourceType` + `source` pair. Responds with HTTP 206 for range requests
|
|
6
|
+
* and HTTP 200 for full-file requests.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { parseRange } from "../../utils/parse-range.js";
|
|
2
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Resolve source parameters from the query string.
|
|
13
|
+
* Prefers a registered `sourceKey`; falls back to inline `sourceType`+`source`.
|
|
14
|
+
*
|
|
15
|
+
* @param {import("fastify").FastifyRequest["query"]} query
|
|
16
|
+
* @param {ReturnType<import("../../store/source-registry.js").createSourceRegistry>} sourceRegistry
|
|
17
|
+
* @returns {{ sourceType: string, source: string }}
|
|
18
|
+
*/
|
|
3
19
|
function getSourceParams(query, sourceRegistry) {
|
|
4
20
|
const sourceKey = typeof query.sourceKey === "string" ? query.sourceKey : "";
|
|
5
21
|
const sourceTypeFromQuery = typeof query.sourceType === "string" ? query.sourceType : "";
|
|
@@ -11,6 +27,16 @@ function getSourceParams(query, sourceRegistry) {
|
|
|
11
27
|
return { sourceType, source };
|
|
12
28
|
}
|
|
13
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Stream a torrent file over HTTP with byte-range support.
|
|
32
|
+
*
|
|
33
|
+
* GET /stream
|
|
34
|
+
*
|
|
35
|
+
* @param {import("fastify").FastifyRequest} req
|
|
36
|
+
* @param {import("fastify").FastifyReply} reply
|
|
37
|
+
* @param {{ sourceRegistry: ReturnType<import("../../store/source-registry.js").createSourceRegistry>, torrentPool: import("../../services/torrent-pool.js").TorrentPool }} deps
|
|
38
|
+
* @returns {Promise<void>}
|
|
39
|
+
*/
|
|
14
40
|
export async function handleStreamGet(req, reply, { sourceRegistry, torrentPool }) {
|
|
15
41
|
const fileIndexRaw = typeof req.query.fileIndex === "string" ? req.query.fileIndex : "";
|
|
16
42
|
const fileIndex = Number(fileIndexRaw);
|
|
@@ -58,6 +84,15 @@ export async function handleStreamGet(req, reply, { sourceRegistry, torrentPool
|
|
|
58
84
|
return reply.send(stream);
|
|
59
85
|
}
|
|
60
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Attach event listeners that release the file reference exactly once when
|
|
89
|
+
* the stream or the underlying HTTP connection closes.
|
|
90
|
+
*
|
|
91
|
+
* @param {import("node:stream").Readable} stream
|
|
92
|
+
* @param {import("fastify").FastifyReply} reply
|
|
93
|
+
* @param {() => void} release
|
|
94
|
+
* @returns {void}
|
|
95
|
+
*/
|
|
61
96
|
function bindRelease(stream, reply, release) {
|
|
62
97
|
let released = false;
|
|
63
98
|
const releaseOnce = () => {
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve HLS playlist and segment files from an active transcode session.
|
|
3
|
+
*
|
|
4
|
+
* Polls until the requested file appears (up to 15 s) so that HLS clients
|
|
5
|
+
* do not receive a 404 during the ffmpeg warmup phase.
|
|
6
|
+
*
|
|
7
|
+
* GET /transcode/:sessionId/:fileName
|
|
8
|
+
*
|
|
9
|
+
* @param {import("fastify").FastifyRequest} req
|
|
10
|
+
* @param {import("fastify").FastifyReply} reply
|
|
11
|
+
* @param {{ hlsSessionManager: import("../../../services/hls-session-manager.js").HlsSessionManager }} deps
|
|
12
|
+
* @returns {Promise<void>}
|
|
13
|
+
*/
|
|
1
14
|
export async function handleTranscodeSessionFileGet(req, reply, { hlsSessionManager }) {
|
|
2
15
|
const sessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
|
|
3
16
|
const fileName = typeof req.params.fileName === "string" ? req.params.fileName : "";
|
|
@@ -23,6 +36,16 @@ export async function handleTranscodeSessionFileGet(req, reply, { hlsSessionMana
|
|
|
23
36
|
return reply.send(result.stream);
|
|
24
37
|
}
|
|
25
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Poll `hlsSessionManager.getFileStream()` until the file is available,
|
|
41
|
+
* the session fails, or the timeout elapses.
|
|
42
|
+
*
|
|
43
|
+
* @param {import("../../../services/hls-session-manager.js").HlsSessionManager} hlsSessionManager
|
|
44
|
+
* @param {string} sessionId
|
|
45
|
+
* @param {string} fileName
|
|
46
|
+
* @param {number} timeoutMs
|
|
47
|
+
* @returns {Promise<Awaited<ReturnType<import("../../../services/hls-session-manager.js").HlsSessionManager["getFileStream"]>>>}
|
|
48
|
+
*/
|
|
26
49
|
async function waitForSessionFile(hlsSessionManager, sessionId, fileName, timeoutMs) {
|
|
27
50
|
const startedAt = Date.now();
|
|
28
51
|
while (Date.now() - startedAt < timeoutMs) {
|
|
@@ -35,6 +58,12 @@ async function waitForSessionFile(hlsSessionManager, sessionId, fileName, timeou
|
|
|
35
58
|
return { kind: "warming-up" };
|
|
36
59
|
}
|
|
37
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Resolve after a given number of milliseconds.
|
|
63
|
+
*
|
|
64
|
+
* @param {number} ms
|
|
65
|
+
* @returns {Promise<void>}
|
|
66
|
+
*/
|
|
38
67
|
function delay(ms) {
|
|
39
68
|
return new Promise((resolve) => {
|
|
40
69
|
setTimeout(resolve, ms);
|
package/server.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Proxy HTTP server bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* Creates and configures the Fastify application, registers all routes and
|
|
5
|
+
* plugins, then starts listening on the first available port at or above the
|
|
6
|
+
* requested one.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import Fastify from "fastify";
|
|
2
10
|
import fastifyCors from "@fastify/cors";
|
|
3
11
|
import fastifyHelmet from "@fastify/helmet";
|
|
@@ -23,6 +31,13 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
23
31
|
const __dirname = path.dirname(__filename);
|
|
24
32
|
const publicRoot = path.resolve(__dirname, "./public");
|
|
25
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Build a list of candidate port numbers starting at `startPort`.
|
|
36
|
+
*
|
|
37
|
+
* @param {number} startPort
|
|
38
|
+
* @param {number} [maxAttempts=51]
|
|
39
|
+
* @returns {number[]}
|
|
40
|
+
*/
|
|
26
41
|
function buildPortCandidates(startPort, maxAttempts = 51) {
|
|
27
42
|
const ports = [];
|
|
28
43
|
for (let index = 0; index < maxAttempts; index += 1) {
|
|
@@ -31,6 +46,20 @@ function buildPortCandidates(startPort, maxAttempts = 51) {
|
|
|
31
46
|
return ports;
|
|
32
47
|
}
|
|
33
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} ProxyServerOptions
|
|
51
|
+
* @property {string} host - Bind host (e.g. "127.0.0.1" or "0.0.0.0").
|
|
52
|
+
* @property {number} port - Preferred listen port.
|
|
53
|
+
* @property {boolean} transcodeAudio - Whether HLS audio transcoding is enabled.
|
|
54
|
+
* @property {string} ffmpegBin - Path to the ffmpeg executable.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create, configure, and start the proxy HTTP server.
|
|
59
|
+
*
|
|
60
|
+
* @param {ProxyServerOptions} options
|
|
61
|
+
* @returns {Promise<{ app: import("fastify").FastifyInstance, port: number }>}
|
|
62
|
+
*/
|
|
34
63
|
export async function startProxyServer({ host, port, transcodeAudio, ffmpegBin }) {
|
|
35
64
|
const app = Fastify({
|
|
36
65
|
bodyLimit: 10 * 1024 * 1024
|