@torrent-tv/proxy 2.9.74 → 2.9.75

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 CHANGED
@@ -74,6 +74,7 @@ program
74
74
  .option("--no-transcode-audio", "Disable optional HLS AAC audio transcoding")
75
75
  .option("--no-port-mapping", "Disable automatic UPnP/NAT-PMP port mapping")
76
76
  .option("--max-disk-bytes <bytes>", "Cap total downloaded torrent data (0 = disabled; default min(10GB, half free disk))")
77
+ .option("--memory-bytes <bytes>", "Per-torrent budget for pieces kept in memory before spilling to disk (default 512MB)")
77
78
  .option("--ffmpeg-bin <path>", "Path to ffmpeg binary")
78
79
  .option(
79
80
  "--segment-format <format>",
@@ -108,6 +109,12 @@ const maxDiskBytes =
108
109
  options.maxDiskBytes !== undefined && Number.isFinite(Number(options.maxDiskBytes)) && Number(options.maxDiskBytes) >= 0
109
110
  ? Number(options.maxDiskBytes)
110
111
  : undefined;
112
+ // Per-torrent memory budget for resident pieces. Pieces past it spill to disk
113
+ // rather than being lost, so a small value costs read latency, never data.
114
+ const memoryBytes =
115
+ options.memoryBytes !== undefined && Number.isFinite(Number(options.memoryBytes)) && Number(options.memoryBytes) > 0
116
+ ? Number(options.memoryBytes)
117
+ : undefined;
111
118
  const bundledFfmpegBin = typeof ffmpegStatic === "string" ? ffmpegStatic : "";
112
119
  const ffmpegBin = options.ffmpegBin ? String(options.ffmpegBin) : bundledFfmpegBin || "ffmpeg";
113
120
 
@@ -265,6 +272,7 @@ try {
265
272
  transcodeAudio,
266
273
  ffmpegBin,
267
274
  maxDiskBytes,
275
+ memoryBytes,
268
276
  segmentFormat: options.segmentFormat
269
277
  });
270
278
  app = started.app;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.9.74",
3
+ "version": "2.9.75",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
package/server.js CHANGED
@@ -1,228 +1,229 @@
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
-
9
- import Fastify from "fastify";
10
- import fastifyCors from "@fastify/cors";
11
- import fastifyHelmet from "@fastify/helmet";
12
- import fastifyStatic from "@fastify/static";
13
- import getPort from "get-port";
14
- import path from "node:path";
15
- import { createRequire } from "node:module";
16
- import { fileURLToPath } from "node:url";
17
- import { handleHealthGet } from "./routes/health/get.js";
18
- import { handleHealthzGet } from "./routes/healthz/get.js";
19
- import { handleApiSourcesPost } from "./routes/api/sources/post.js";
20
- import { handleApiSourceStatsGet } from "./routes/api/sources/stats/get.js";
21
- import { handleApiSourceFilesGet } from "./routes/api/sources/files/get.js";
22
- import { handleApiPlaybackPlanPost } from "./routes/api/playback-plan/post.js";
23
- import { handleApiSubtitlesGet } from "./routes/api/subtitles/get.js";
24
- import { handleApiTranscodeSessionsPost } from "./routes/api/transcode-sessions/post.js";
25
- import { handleApiTranscodeSessionsProgressGet } from "./routes/api/transcode-sessions/progress/get.js";
26
- import { handleApiTranscodeSessionReleasePost } from "./routes/api/transcode-sessions/release/post.js";
27
- import { handleApiTranscodeSessionNetReportPost } from "./routes/api/transcode-sessions/net-report/post.js";
28
- import { handleApiTranscodeSessionSeekPost } from "./routes/api/transcode-sessions/seek/post.js";
29
- import { handleStreamGet } from "./routes/stream/get.js";
30
- import { handleTranscodeSessionFileGet } from "./routes/transcode/session-file/get.js";
31
- import { createSourceRegistry } from "./store/source-registry.js";
32
- import { WorkerTorrentPool } from "./services/torrent-worker/pool-adapter.js";
33
- import { HlsSessionManager } from "./services/hls-session-manager.js";
34
- import { createPlaybackPlanner } from "./services/playback-planner.js";
35
- import { detectVideoEncoder, benchmarkSoftwarePresets, detectTonemapSupport } from "./services/hwaccel.js";
36
- import { logger } from "./utils/logger.js";
37
-
38
- const __filename = fileURLToPath(import.meta.url);
39
- const __dirname = path.dirname(__filename);
40
- const require = createRequire(import.meta.url);
41
- const { version } = require("./package.json");
42
- const publicRoot = path.resolve(__dirname, "./public");
43
-
44
- /**
45
- * Build a list of candidate port numbers starting at `startPort`.
46
- *
47
- * @param {number} startPort
48
- * @param {number} [maxAttempts=51]
49
- * @returns {number[]}
50
- */
51
- function buildPortCandidates(startPort, maxAttempts = 51) {
52
- const ports = [];
53
- for (let index = 0; index < maxAttempts; index += 1) {
54
- ports.push(startPort + index);
55
- }
56
- return ports;
57
- }
58
-
59
- /**
60
- * @typedef {Object} ProxyServerOptions
61
- * @property {string} host - Bind host (e.g. "127.0.0.1" or "0.0.0.0").
62
- * @property {number} port - Preferred listen port.
63
- * @property {boolean} transcodeAudio - Whether HLS audio transcoding is enabled.
64
- * @property {string} ffmpegBin - Path to the ffmpeg executable.
65
- * @property {number} [maxDiskBytes] - Global disk cap for torrent data (undefined = pool default).
66
- * @property {string} [segmentFormat] - HLS output container: "fmp4" (default) or "mpegts".
67
- */
68
-
69
- /**
70
- * Create, configure, and start the proxy HTTP server.
71
- *
72
- * @param {ProxyServerOptions} options
73
- * @returns {Promise<{ app: import("fastify").FastifyInstance, port: number }>}
74
- */
75
- export async function startProxyServer({ host, port, transcodeAudio, ffmpegBin, maxDiskBytes, segmentFormat }) {
76
- const app = Fastify({
77
- // No practical body-size limit — the proxy server is localhost-only and
78
- // receives torrent source payloads that may be arbitrarily large.
79
- bodyLimit: 256 * 1024 * 1024 // 256 MB
80
- });
81
-
82
- await app.register(fastifyHelmet, {
83
- // Proxy serves media to a different origin (registry UI), so CORP must allow cross-origin usage.
84
- crossOriginResourcePolicy: {
85
- policy: "cross-origin"
86
- }
87
- });
88
- await app.register(fastifyCors, {
89
- origin: true,
90
- methods: ["GET", "POST", "OPTIONS"],
91
- allowedHeaders: ["Content-Type", "Range"]
92
- });
93
-
94
- // Allow browser requests from an HTTPS page to this private-network proxy
95
- // without triggering Chromium's Private Network Access permission prompt.
96
- app.addHook("onRequest", async (_req, reply) => {
97
- reply.header("Access-Control-Allow-Private-Network", "true");
98
- });
99
-
100
- const sourceRegistry = createSourceRegistry(200);
101
- // The torrent runs on its own thread. Profiling a live seek (2026-08-02)
102
- // found the main thread ~85% occupied by WebTorrent buffer concatenation
103
- // ~15%, wire updates ~9%, garbage collection ~5% while three of four cores
104
- // idled. Serving a segment shared that thread, so reading an already-finished
105
- // 10 MB file took 12-23 s against 125 ms to hand it to the channel. The
106
- // adapter keeps TorrentPool's interface, so nothing downstream changed.
107
- const torrentPool = new WorkerTorrentPool({ maxDiskBytes });
108
- const selectedPort = await getPort({
109
- port: buildPortCandidates(port)
110
- });
111
- // Auto-detect the best available H.264 encoder (hardware-accelerated or
112
- // software) once at startup, with a real test-encode and graceful fallback.
113
- // Only needed when transcoding can occur.
114
- const videoEncoder = transcodeAudio
115
- ? await detectVideoEncoder({ ffmpegBin, logger })
116
- : null;
117
- // For software libx264, benchmark preset throughput once at startup so the
118
- // session manager can pick the highest-quality preset that still encodes each
119
- // stream faster than realtime. Hardware encoders use their own fixed preset.
120
- const softwarePresetBenchmark = videoEncoder?.kind === "software"
121
- ? await benchmarkSoftwarePresets({ ffmpegBin, logger })
122
- : null;
123
- // Whether this ffmpeg build can tone-map HDR→SDR (zscale + tonemap filters).
124
- // Detected once; the session manager applies the tonemap chain only for HDR
125
- // sources on the software path when available.
126
- const tonemapSupported = transcodeAudio
127
- ? await detectTonemapSupport({ ffmpegBin, logger })
128
- : false;
129
- const hlsSessionManager = new HlsSessionManager({
130
- enabled: transcodeAudio,
131
- ffmpegBin,
132
- localBindHost: host,
133
- localPort: selectedPort,
134
- videoEncoder,
135
- softwarePresetBenchmark,
136
- tonemapSupported,
137
- segmentFormatId: segmentFormat,
138
- // Live download stats accessor for the realtime budget: lets it tell a
139
- // CPU-bound transcode from a download-starved input before downscaling.
140
- getSourceStats: async (sourceKey, fileIndex) => {
141
- const record = sourceRegistry.get(sourceKey);
142
- if (!record) {
143
- return null;
144
- }
145
- try {
146
- const torrent = await torrentPool.getTorrent(record.sourceType, record.source);
147
- // Awaited for the same reason as the stats route: this now crosses a
148
- // thread boundary and returns a promise.
149
- return await torrentPool.getFileStats(torrent, Number.isInteger(fileIndex) ? fileIndex : null);
150
- } catch {
151
- return null;
152
- }
153
- },
154
- // Reuse the media info the planner already probed for this file (same
155
- // ffmpeg scan) so createSession skips its own probe. Late-bound: invoked
156
- // only at session-create time, after playbackPlanner is initialised.
157
- getCachedMediaInfo: (params) => playbackPlanner.getCachedMediaInfo(params)
158
- });
159
- const playbackPlanner = createPlaybackPlanner({
160
- ffmpegBin,
161
- transcodeAudioEnabled: transcodeAudio,
162
- localBaseUrl: hlsSessionManager.localBaseUrl,
163
- sourceRegistry,
164
- torrentPool
165
- });
166
-
167
- app.get("/health", async (req, reply) => handleHealthGet(req, reply, { version }));
168
- app.get("/healthz", async (req, reply) => handleHealthzGet(req, reply, { version }));
169
- app.post("/api/sources", async (req, reply) =>
170
- handleApiSourcesPost(req, reply, { sourceRegistry })
171
- );
172
- app.get("/api/sources/:sourceKey/stats", async (req, reply) =>
173
- handleApiSourceStatsGet(req, reply, { sourceRegistry, torrentPool })
174
- );
175
- app.get("/api/sources/:sourceKey/files", async (req, reply) =>
176
- handleApiSourceFilesGet(req, reply, { sourceRegistry, torrentPool })
177
- );
178
- app.post("/api/playback-plan", async (req, reply) =>
179
- handleApiPlaybackPlanPost(req, reply, { playbackPlanner })
180
- );
181
- app.get("/api/subtitles", async (req, reply) =>
182
- handleApiSubtitlesGet(req, reply, {
183
- sourceRegistry,
184
- torrentPool,
185
- ffmpegBin,
186
- localBaseUrl: hlsSessionManager.localBaseUrl
187
- })
188
- );
189
- app.get("/stream", async (req, reply) =>
190
- handleStreamGet(req, reply, { sourceRegistry, torrentPool })
191
- );
192
- app.post("/api/transcode-sessions", async (req, reply) =>
193
- handleApiTranscodeSessionsPost(req, reply, { hlsSessionManager })
194
- );
195
- app.post("/api/transcode-sessions/:sessionId/release", async (req, reply) =>
196
- handleApiTranscodeSessionReleasePost(req, reply, { hlsSessionManager })
197
- );
198
- app.get("/api/transcode-sessions/:sessionId/progress", async (req, reply) =>
199
- handleApiTranscodeSessionsProgressGet(req, reply, { hlsSessionManager })
200
- );
201
- app.post("/api/transcode-sessions/:sessionId/net-report", async (req, reply) =>
202
- handleApiTranscodeSessionNetReportPost(req, reply, { hlsSessionManager })
203
- );
204
- app.post("/api/transcode-sessions/:sessionId/seek", async (req, reply) =>
205
- handleApiTranscodeSessionSeekPost(req, reply, { hlsSessionManager })
206
- );
207
- app.get("/transcode/:sessionId/:fileName", async (req, reply) =>
208
- handleTranscodeSessionFileGet(req, reply, { hlsSessionManager })
209
- );
210
- await app.register(fastifyStatic, {
211
- root: publicRoot,
212
- prefix: "/",
213
- serveDotFiles: true
214
- });
215
-
216
- app.addHook("onClose", async () => {
217
- // Order matters: stop the ffmpeg readers (HLS sessions) before destroying
218
- // the torrents whose files they read from, then remove the torrent data.
219
- await hlsSessionManager.disposeAll();
220
- await torrentPool.destroyAll();
221
- });
222
-
223
- await app.listen({ host, port: selectedPort });
224
- return {
225
- app,
226
- port: selectedPort
227
- };
228
- }
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
+
9
+ import Fastify from "fastify";
10
+ import fastifyCors from "@fastify/cors";
11
+ import fastifyHelmet from "@fastify/helmet";
12
+ import fastifyStatic from "@fastify/static";
13
+ import getPort from "get-port";
14
+ import path from "node:path";
15
+ import { createRequire } from "node:module";
16
+ import { fileURLToPath } from "node:url";
17
+ import { handleHealthGet } from "./routes/health/get.js";
18
+ import { handleHealthzGet } from "./routes/healthz/get.js";
19
+ import { handleApiSourcesPost } from "./routes/api/sources/post.js";
20
+ import { handleApiSourceStatsGet } from "./routes/api/sources/stats/get.js";
21
+ import { handleApiSourceFilesGet } from "./routes/api/sources/files/get.js";
22
+ import { handleApiPlaybackPlanPost } from "./routes/api/playback-plan/post.js";
23
+ import { handleApiSubtitlesGet } from "./routes/api/subtitles/get.js";
24
+ import { handleApiTranscodeSessionsPost } from "./routes/api/transcode-sessions/post.js";
25
+ import { handleApiTranscodeSessionsProgressGet } from "./routes/api/transcode-sessions/progress/get.js";
26
+ import { handleApiTranscodeSessionReleasePost } from "./routes/api/transcode-sessions/release/post.js";
27
+ import { handleApiTranscodeSessionNetReportPost } from "./routes/api/transcode-sessions/net-report/post.js";
28
+ import { handleApiTranscodeSessionSeekPost } from "./routes/api/transcode-sessions/seek/post.js";
29
+ import { handleStreamGet } from "./routes/stream/get.js";
30
+ import { handleTranscodeSessionFileGet } from "./routes/transcode/session-file/get.js";
31
+ import { createSourceRegistry } from "./store/source-registry.js";
32
+ import { WorkerTorrentPool } from "./services/torrent-worker/pool-adapter.js";
33
+ import { HlsSessionManager } from "./services/hls-session-manager.js";
34
+ import { createPlaybackPlanner } from "./services/playback-planner.js";
35
+ import { detectVideoEncoder, benchmarkSoftwarePresets, detectTonemapSupport } from "./services/hwaccel.js";
36
+ import { logger } from "./utils/logger.js";
37
+
38
+ const __filename = fileURLToPath(import.meta.url);
39
+ const __dirname = path.dirname(__filename);
40
+ const require = createRequire(import.meta.url);
41
+ const { version } = require("./package.json");
42
+ const publicRoot = path.resolve(__dirname, "./public");
43
+
44
+ /**
45
+ * Build a list of candidate port numbers starting at `startPort`.
46
+ *
47
+ * @param {number} startPort
48
+ * @param {number} [maxAttempts=51]
49
+ * @returns {number[]}
50
+ */
51
+ function buildPortCandidates(startPort, maxAttempts = 51) {
52
+ const ports = [];
53
+ for (let index = 0; index < maxAttempts; index += 1) {
54
+ ports.push(startPort + index);
55
+ }
56
+ return ports;
57
+ }
58
+
59
+ /**
60
+ * @typedef {Object} ProxyServerOptions
61
+ * @property {string} host - Bind host (e.g. "127.0.0.1" or "0.0.0.0").
62
+ * @property {number} port - Preferred listen port.
63
+ * @property {boolean} transcodeAudio - Whether HLS audio transcoding is enabled.
64
+ * @property {string} ffmpegBin - Path to the ffmpeg executable.
65
+ * @property {number} [maxDiskBytes] - Global disk cap for torrent data (undefined = pool default).
66
+ * @property {number} [memoryBytes] - Per-torrent budget for pieces held in memory (undefined = store default).
67
+ * @property {string} [segmentFormat] - HLS output container: "fmp4" (default) or "mpegts".
68
+ */
69
+
70
+ /**
71
+ * Create, configure, and start the proxy HTTP server.
72
+ *
73
+ * @param {ProxyServerOptions} options
74
+ * @returns {Promise<{ app: import("fastify").FastifyInstance, port: number }>}
75
+ */
76
+ export async function startProxyServer({ host, port, transcodeAudio, ffmpegBin, maxDiskBytes, memoryBytes, segmentFormat }) {
77
+ const app = Fastify({
78
+ // No practical body-size limit the proxy server is localhost-only and
79
+ // receives torrent source payloads that may be arbitrarily large.
80
+ bodyLimit: 256 * 1024 * 1024 // 256 MB
81
+ });
82
+
83
+ await app.register(fastifyHelmet, {
84
+ // Proxy serves media to a different origin (registry UI), so CORP must allow cross-origin usage.
85
+ crossOriginResourcePolicy: {
86
+ policy: "cross-origin"
87
+ }
88
+ });
89
+ await app.register(fastifyCors, {
90
+ origin: true,
91
+ methods: ["GET", "POST", "OPTIONS"],
92
+ allowedHeaders: ["Content-Type", "Range"]
93
+ });
94
+
95
+ // Allow browser requests from an HTTPS page to this private-network proxy
96
+ // without triggering Chromium's Private Network Access permission prompt.
97
+ app.addHook("onRequest", async (_req, reply) => {
98
+ reply.header("Access-Control-Allow-Private-Network", "true");
99
+ });
100
+
101
+ const sourceRegistry = createSourceRegistry(200);
102
+ // The torrent runs on its own thread. Profiling a live seek (2026-08-02)
103
+ // found the main thread ~85% occupied by WebTorrent buffer concatenation
104
+ // ~15%, wire updates ~9%, garbage collection ~5% while three of four cores
105
+ // idled. Serving a segment shared that thread, so reading an already-finished
106
+ // 10 MB file took 12-23 s against 125 ms to hand it to the channel. The
107
+ // adapter keeps TorrentPool's interface, so nothing downstream changed.
108
+ const torrentPool = new WorkerTorrentPool({ maxDiskBytes, memoryBytes });
109
+ const selectedPort = await getPort({
110
+ port: buildPortCandidates(port)
111
+ });
112
+ // Auto-detect the best available H.264 encoder (hardware-accelerated or
113
+ // software) once at startup, with a real test-encode and graceful fallback.
114
+ // Only needed when transcoding can occur.
115
+ const videoEncoder = transcodeAudio
116
+ ? await detectVideoEncoder({ ffmpegBin, logger })
117
+ : null;
118
+ // For software libx264, benchmark preset throughput once at startup so the
119
+ // session manager can pick the highest-quality preset that still encodes each
120
+ // stream faster than realtime. Hardware encoders use their own fixed preset.
121
+ const softwarePresetBenchmark = videoEncoder?.kind === "software"
122
+ ? await benchmarkSoftwarePresets({ ffmpegBin, logger })
123
+ : null;
124
+ // Whether this ffmpeg build can tone-map HDR→SDR (zscale + tonemap filters).
125
+ // Detected once; the session manager applies the tonemap chain only for HDR
126
+ // sources on the software path when available.
127
+ const tonemapSupported = transcodeAudio
128
+ ? await detectTonemapSupport({ ffmpegBin, logger })
129
+ : false;
130
+ const hlsSessionManager = new HlsSessionManager({
131
+ enabled: transcodeAudio,
132
+ ffmpegBin,
133
+ localBindHost: host,
134
+ localPort: selectedPort,
135
+ videoEncoder,
136
+ softwarePresetBenchmark,
137
+ tonemapSupported,
138
+ segmentFormatId: segmentFormat,
139
+ // Live download stats accessor for the realtime budget: lets it tell a
140
+ // CPU-bound transcode from a download-starved input before downscaling.
141
+ getSourceStats: async (sourceKey, fileIndex) => {
142
+ const record = sourceRegistry.get(sourceKey);
143
+ if (!record) {
144
+ return null;
145
+ }
146
+ try {
147
+ const torrent = await torrentPool.getTorrent(record.sourceType, record.source);
148
+ // Awaited for the same reason as the stats route: this now crosses a
149
+ // thread boundary and returns a promise.
150
+ return await torrentPool.getFileStats(torrent, Number.isInteger(fileIndex) ? fileIndex : null);
151
+ } catch {
152
+ return null;
153
+ }
154
+ },
155
+ // Reuse the media info the planner already probed for this file (same
156
+ // ffmpeg scan) so createSession skips its own probe. Late-bound: invoked
157
+ // only at session-create time, after playbackPlanner is initialised.
158
+ getCachedMediaInfo: (params) => playbackPlanner.getCachedMediaInfo(params)
159
+ });
160
+ const playbackPlanner = createPlaybackPlanner({
161
+ ffmpegBin,
162
+ transcodeAudioEnabled: transcodeAudio,
163
+ localBaseUrl: hlsSessionManager.localBaseUrl,
164
+ sourceRegistry,
165
+ torrentPool
166
+ });
167
+
168
+ app.get("/health", async (req, reply) => handleHealthGet(req, reply, { version }));
169
+ app.get("/healthz", async (req, reply) => handleHealthzGet(req, reply, { version }));
170
+ app.post("/api/sources", async (req, reply) =>
171
+ handleApiSourcesPost(req, reply, { sourceRegistry })
172
+ );
173
+ app.get("/api/sources/:sourceKey/stats", async (req, reply) =>
174
+ handleApiSourceStatsGet(req, reply, { sourceRegistry, torrentPool })
175
+ );
176
+ app.get("/api/sources/:sourceKey/files", async (req, reply) =>
177
+ handleApiSourceFilesGet(req, reply, { sourceRegistry, torrentPool })
178
+ );
179
+ app.post("/api/playback-plan", async (req, reply) =>
180
+ handleApiPlaybackPlanPost(req, reply, { playbackPlanner })
181
+ );
182
+ app.get("/api/subtitles", async (req, reply) =>
183
+ handleApiSubtitlesGet(req, reply, {
184
+ sourceRegistry,
185
+ torrentPool,
186
+ ffmpegBin,
187
+ localBaseUrl: hlsSessionManager.localBaseUrl
188
+ })
189
+ );
190
+ app.get("/stream", async (req, reply) =>
191
+ handleStreamGet(req, reply, { sourceRegistry, torrentPool })
192
+ );
193
+ app.post("/api/transcode-sessions", async (req, reply) =>
194
+ handleApiTranscodeSessionsPost(req, reply, { hlsSessionManager })
195
+ );
196
+ app.post("/api/transcode-sessions/:sessionId/release", async (req, reply) =>
197
+ handleApiTranscodeSessionReleasePost(req, reply, { hlsSessionManager })
198
+ );
199
+ app.get("/api/transcode-sessions/:sessionId/progress", async (req, reply) =>
200
+ handleApiTranscodeSessionsProgressGet(req, reply, { hlsSessionManager })
201
+ );
202
+ app.post("/api/transcode-sessions/:sessionId/net-report", async (req, reply) =>
203
+ handleApiTranscodeSessionNetReportPost(req, reply, { hlsSessionManager })
204
+ );
205
+ app.post("/api/transcode-sessions/:sessionId/seek", async (req, reply) =>
206
+ handleApiTranscodeSessionSeekPost(req, reply, { hlsSessionManager })
207
+ );
208
+ app.get("/transcode/:sessionId/:fileName", async (req, reply) =>
209
+ handleTranscodeSessionFileGet(req, reply, { hlsSessionManager })
210
+ );
211
+ await app.register(fastifyStatic, {
212
+ root: publicRoot,
213
+ prefix: "/",
214
+ serveDotFiles: true
215
+ });
216
+
217
+ app.addHook("onClose", async () => {
218
+ // Order matters: stop the ffmpeg readers (HLS sessions) before destroying
219
+ // the torrents whose files they read from, then remove the torrent data.
220
+ await hlsSessionManager.disposeAll();
221
+ await torrentPool.destroyAll();
222
+ });
223
+
224
+ await app.listen({ host, port: selectedPort });
225
+ return {
226
+ app,
227
+ port: selectedPort
228
+ };
229
+ }