@torrent-tv/proxy 2.0.0 → 2.1.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 +63 -13
- 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 +25 -1
- 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 +253 -17
- 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,18 @@ 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
|
+
* Gracefully shut down the tunnel, heartbeat timer, and HTTP server.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} signal - Signal name (e.g. "SIGINT").
|
|
143
|
+
* @returns {Promise<void>}
|
|
144
|
+
*/
|
|
106
145
|
async function shutdown(signal) {
|
|
107
146
|
if (shutdownInProgress) {
|
|
108
147
|
return;
|
|
@@ -112,7 +151,11 @@ async function shutdown(signal) {
|
|
|
112
151
|
clearInterval(heartbeatTimer);
|
|
113
152
|
heartbeatTimer = null;
|
|
114
153
|
}
|
|
115
|
-
|
|
154
|
+
if (tunnelClient) {
|
|
155
|
+
tunnelClient.disconnect();
|
|
156
|
+
tunnelClient = null;
|
|
157
|
+
}
|
|
158
|
+
logger.warn(`Received ${signal}, shutting down...`);
|
|
116
159
|
try {
|
|
117
160
|
if (app) {
|
|
118
161
|
await app.close();
|
|
@@ -120,7 +163,7 @@ async function shutdown(signal) {
|
|
|
120
163
|
process.exit(0);
|
|
121
164
|
} catch (error) {
|
|
122
165
|
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
-
|
|
166
|
+
logger.error(`Shutdown failed: ${message}`);
|
|
124
167
|
process.exit(1);
|
|
125
168
|
}
|
|
126
169
|
}
|
|
@@ -139,16 +182,23 @@ try {
|
|
|
139
182
|
actualPort = started.port;
|
|
140
183
|
const directBaseUrl = explicitBaseUrl || `http://${bindHost}:${actualPort}`;
|
|
141
184
|
|
|
142
|
-
|
|
143
|
-
|
|
185
|
+
logger.info(`Local stream endpoint: http://${bindHost}:${actualPort}/stream`);
|
|
186
|
+
logger.info(`Advertised direct URL: ${directBaseUrl}`);
|
|
144
187
|
if (transcodeAudio) {
|
|
145
|
-
|
|
146
|
-
chalk.cyan(`[proxy-client] Optional HLS audio transcode is enabled (ffmpeg: ${ffmpegBin}).`)
|
|
147
|
-
);
|
|
188
|
+
logger.info(`Optional HLS audio transcode is enabled (ffmpeg: ${ffmpegBin}).`);
|
|
148
189
|
}
|
|
149
190
|
|
|
150
191
|
await registerClientSafe();
|
|
151
192
|
|
|
193
|
+
tunnelClient = createTunnelClient({
|
|
194
|
+
serverUrl,
|
|
195
|
+
proxyId: clientId,
|
|
196
|
+
token,
|
|
197
|
+
proxyPort: actualPort,
|
|
198
|
+
onLog: (msg) => logger.info(msg)
|
|
199
|
+
});
|
|
200
|
+
tunnelClient.connect();
|
|
201
|
+
|
|
152
202
|
heartbeatTimer = setInterval(async () => {
|
|
153
203
|
const status = await sendHeartbeat({
|
|
154
204
|
serverUrl,
|
|
@@ -156,18 +206,18 @@ try {
|
|
|
156
206
|
token
|
|
157
207
|
});
|
|
158
208
|
if (status === 404) {
|
|
159
|
-
|
|
209
|
+
logger.warn("Heartbeat returned 404, re-registering...");
|
|
160
210
|
try {
|
|
161
211
|
await registerClientSafe();
|
|
162
212
|
} catch (error) {
|
|
163
213
|
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
-
|
|
214
|
+
logger.error(`Re-register failed: ${message}`);
|
|
165
215
|
}
|
|
166
216
|
}
|
|
167
217
|
}, 20_000);
|
|
168
218
|
} catch (error) {
|
|
169
219
|
const message = error instanceof Error ? error.message : String(error);
|
|
170
|
-
|
|
220
|
+
logger.error(message);
|
|
171
221
|
process.exit(1);
|
|
172
222
|
}
|
|
173
223
|
|
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;
|
|
@@ -10,8 +28,11 @@ export async function handleApiTranscodeSessionsPost(req, reply, { hlsSessionMan
|
|
|
10
28
|
const sourceKey = typeof payload.sourceKey === "string" ? payload.sourceKey.trim() : "";
|
|
11
29
|
const fileIndex = Number(payload.fileIndex);
|
|
12
30
|
const transcodeVideo = payload.transcodeVideo === true;
|
|
31
|
+
const transcodeAudio = payload.transcodeAudio === true;
|
|
13
32
|
const consumerId = typeof payload.consumerId === "string" ? payload.consumerId.trim() : "";
|
|
14
33
|
const fileName = typeof payload.fileName === "string" ? payload.fileName.trim() : "";
|
|
34
|
+
const targetWidth = Number(payload.targetWidth);
|
|
35
|
+
const targetHeight = Number(payload.targetHeight);
|
|
15
36
|
|
|
16
37
|
if (!sourceKey || !Number.isInteger(fileIndex) || fileIndex < 0) {
|
|
17
38
|
return reply.code(400).send({ error: "sourceKey and valid fileIndex are required." });
|
|
@@ -22,8 +43,11 @@ export async function handleApiTranscodeSessionsPost(req, reply, { hlsSessionMan
|
|
|
22
43
|
sourceKey,
|
|
23
44
|
fileIndex,
|
|
24
45
|
transcodeVideo,
|
|
46
|
+
transcodeAudio,
|
|
25
47
|
consumerId,
|
|
26
|
-
fileName
|
|
48
|
+
fileName,
|
|
49
|
+
targetWidth: Number.isInteger(targetWidth) && targetWidth > 0 ? targetWidth : 0,
|
|
50
|
+
targetHeight: Number.isInteger(targetHeight) && targetHeight > 0 ? targetHeight : 0
|
|
27
51
|
});
|
|
28
52
|
return reply.send({
|
|
29
53
|
sessionId: session.id,
|
|
@@ -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
|