@torrent-tv/proxy 1.0.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/.dockerignore ADDED
@@ -0,0 +1,5 @@
1
+ node_modules
2
+ npm-debug.log
3
+ .DS_Store
4
+ .git
5
+ .gitignore
package/Dockerfile ADDED
@@ -0,0 +1,30 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ FROM node:22-alpine
3
+
4
+ ENV NODE_ENV=production
5
+ ENV PORT=9090
6
+ ENV HOST=0.0.0.0
7
+
8
+ WORKDIR /app
9
+
10
+ # ffmpeg is needed for optional HLS audio transcode mode.
11
+ RUN apk add --no-cache ffmpeg
12
+
13
+ # Create an unprivileged runtime user.
14
+ RUN addgroup -S app && adduser -S -G app app
15
+
16
+ # Install only production dependencies first for better layer caching.
17
+ COPY package.json package-lock.json ./
18
+ RUN npm ci --omit=dev && npm cache clean --force
19
+
20
+ # Copy application sources.
21
+ COPY --chown=app:app . .
22
+
23
+ USER app
24
+
25
+ EXPOSE 9090
26
+
27
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
28
+ CMD node -e "fetch(`http://127.0.0.1:${process.env.PORT || 9090}/healthz`).then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
29
+
30
+ CMD ["sh", "-ec", "if [ -z \"${SERVER_URL:-}\" ]; then echo 'SERVER_URL is required' >&2; exit 1; fi; exec node ./bin/cli.js --server-url \"$SERVER_URL\" --host \"$HOST\" --port \"$PORT\" ${PROXY_EXTRA_ARGS:-}"]
package/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ Copyright (C) 2026 Anton Nemtsev
2
+
3
+ This program is free software: you can redistribute it and/or modify
4
+ it under the terms of the GNU General Public License as published by
5
+ the Free Software Foundation, either version 3 of the License, or
6
+ (at your option) any later version.
7
+
8
+ This program is distributed in the hope that it will be useful,
9
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ GNU General Public License for more details.
12
+
13
+ You should have received a copy of the GNU General Public License
14
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
15
+
16
+ SPDX-License-Identifier: GPL-3.0-or-later
package/README.md ADDED
@@ -0,0 +1,230 @@
1
+ # Torrent Proxy Client
2
+
3
+ `@torrent-tv/proxy` is a lightweight Node.js service that turns torrent content into HTTP endpoints that are easy to consume from web players and backend services.
4
+
5
+ It is designed for setups where a central registry/UI needs a simple direct media URL, while the actual torrent fetching happens on a separate edge/client machine.
6
+
7
+ ## Why this exists
8
+
9
+ - Browsers and many media players cannot consume torrents directly.
10
+ - This service exposes torrent files through regular HTTP (`/stream`) with range support.
11
+ - It can optionally create HLS sessions with AAC audio when direct playback is not suitable.
12
+ - It can also create HLS sessions with video transcoding when browser-side decode still fails.
13
+ - It registers itself in an external registry service and sends heartbeats, so other services can discover and use it.
14
+
15
+ ## What it does
16
+
17
+ - Runs a Fastify server with health and media endpoints.
18
+ - Accepts torrent sources (`magnet` or base64 `.torrent`) and returns a stable `sourceKey`.
19
+ - Streams a selected file from a torrent by `fileIndex`.
20
+ - Builds a playback plan (`direct` vs `hls`) based on detected audio codec and returns both audio/video codecs.
21
+ - Starts ffmpeg-based HLS transcoding sessions (`audio-only` or `video+audio`) and serves generated playlist/segments.
22
+ - Tracks multi-client consumers per transcode session and stops ffmpeg when the last consumer releases.
23
+
24
+ ## Requirements
25
+
26
+ - Node.js 18+ (ESM and built-in `fetch` are required).
27
+ - npm.
28
+ - ffmpeg is required only when audio transcoding is enabled.
29
+ - By default, the package uses `ffmpeg-static`.
30
+ - You can override binary path with `--ffmpeg-bin`.
31
+ - You can disable transcoding with `--no-transcode-audio`.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ npm install
37
+ ```
38
+
39
+ ## Run
40
+
41
+ ```bash
42
+ npm start -- --server-url http://localhost:3000
43
+ ```
44
+
45
+ Minimal required argument:
46
+
47
+ - `--server-url <url>`: base URL of your registry server.
48
+
49
+ Useful optional arguments:
50
+
51
+ - `--host <host>`: bind host (default `127.0.0.1`).
52
+ - `--port <port>`: preferred local port (default `9090`; first free port in range is selected).
53
+ - `--public-base-url <url>`: externally reachable base URL advertised to registry.
54
+ - `--id <id>`: stable proxy client id.
55
+ - `--name <name>`: display name for registry.
56
+ - `--token <token>`: token sent to register/heartbeat endpoints.
57
+ - `--ffmpeg-bin <path>`: custom ffmpeg binary path.
58
+ - `--no-transcode-audio`: disable HLS audio transcoding.
59
+ - `--help`: print all options with descriptions and examples, then exit.
60
+
61
+ ## HTTP API
62
+
63
+ Base URL examples below use `http://127.0.0.1:9090`.
64
+
65
+ ### Health
66
+
67
+ ```bash
68
+ curl http://127.0.0.1:9090/health
69
+ curl http://127.0.0.1:9090/healthz
70
+ ```
71
+
72
+ ### 1) Register a source
73
+
74
+ ```bash
75
+ curl -X POST http://127.0.0.1:9090/api/sources \
76
+ -H "Content-Type: application/json" \
77
+ -d '{
78
+ "sourceType": "magnet",
79
+ "source": "magnet:?xt=urn:btih:..."
80
+ }'
81
+ ```
82
+
83
+ Response:
84
+
85
+ ```json
86
+ { "sourceKey": "..." }
87
+ ```
88
+
89
+ Supported `sourceType` values:
90
+
91
+ - `magnet`: magnet URI string.
92
+ - `torrent`: base64-encoded raw `.torrent` file bytes.
93
+
94
+ ### 2) Build playback plan
95
+
96
+ ```bash
97
+ curl -X POST http://127.0.0.1:9090/api/playback-plan \
98
+ -H "Content-Type: application/json" \
99
+ -d '{
100
+ "sourceKey": "<sourceKey>",
101
+ "fileIndex": 0,
102
+ "userAgent": "Mozilla/5.0"
103
+ }'
104
+ ```
105
+
106
+ Typical response:
107
+
108
+ ```json
109
+ {
110
+ "mode": "direct",
111
+ "directUrl": "http://127.0.0.1:9090/stream?sourceKey=...&fileIndex=0",
112
+ "reason": "audio-codec-supported",
113
+ "audioCodec": "aac",
114
+ "videoCodec": "h264"
115
+ }
116
+ ```
117
+
118
+ `mode` can be:
119
+
120
+ - `direct`: play `directUrl` directly.
121
+ - `hls`: create an HLS session, then use playlist URL.
122
+
123
+ ### 3) Direct stream endpoint
124
+
125
+ ```bash
126
+ curl -v "http://127.0.0.1:9090/stream?sourceKey=<sourceKey>&fileIndex=0"
127
+ ```
128
+
129
+ Or without pre-registering source:
130
+
131
+ ```bash
132
+ curl -v "http://127.0.0.1:9090/stream?sourceType=magnet&source=magnet:?xt=...&fileIndex=0"
133
+ ```
134
+
135
+ The endpoint supports HTTP Range requests.
136
+
137
+ ### 4) Create HLS transcode session (optional)
138
+
139
+ ```bash
140
+ curl -X POST http://127.0.0.1:9090/api/transcode-sessions \
141
+ -H "Content-Type: application/json" \
142
+ -d '{
143
+ "sourceKey": "<sourceKey>",
144
+ "fileIndex": 0,
145
+ "transcodeVideo": false,
146
+ "consumerId": "browser-session-uuid",
147
+ "fileName": "Episode01.mkv"
148
+ }'
149
+ ```
150
+
151
+ Set `"transcodeVideo": true` to force video transcoding (for browser decode fallback cases).
152
+
153
+ Response:
154
+
155
+ ```json
156
+ {
157
+ "sessionId": "...",
158
+ "playlistPath": "/transcode/<sessionId>/index.m3u8"
159
+ }
160
+ ```
161
+
162
+ Open playlist as:
163
+
164
+ `http://127.0.0.1:9090/transcode/<sessionId>/index.m3u8`
165
+
166
+ ### 5) Poll transcode progress
167
+
168
+ ```bash
169
+ curl "http://127.0.0.1:9090/api/transcode-sessions/<sessionId>/progress"
170
+ ```
171
+
172
+ Response includes transcode and warmup metrics:
173
+ - `percent`, `processedSeconds`, `totalSeconds`, `remainingSeconds`, `speed`
174
+ - `warmupPercent`, `warmupRemainingSeconds`
175
+
176
+ ### 6) Release transcode consumer
177
+
178
+ ```bash
179
+ curl -X POST http://127.0.0.1:9090/api/transcode-sessions/<sessionId>/release \
180
+ -H "Content-Type: application/json" \
181
+ -d '{
182
+ "consumerId": "browser-session-uuid",
183
+ "reason": "pagehide"
184
+ }'
185
+ ```
186
+
187
+ When the last consumer is released, proxy disposes the session and stops ffmpeg.
188
+
189
+ ## End-to-end flow
190
+
191
+ 1. Start proxy client with `--server-url`.
192
+ 2. Register torrent source via `/api/sources` and get `sourceKey`.
193
+ 3. Request `/api/playback-plan`.
194
+ 4. If plan is `direct`, use `directUrl`.
195
+ 5. If plan is `hls`, create session and play generated playlist.
196
+ 6. Poll `/progress` for UI updates, then release session on client stop/close.
197
+
198
+ ## Transcode Session Lifecycle
199
+
200
+ ```mermaid
201
+ sequenceDiagram
202
+ participant C as Client
203
+ participant P as Proxy
204
+ participant F as FFmpeg
205
+
206
+ C->>P: POST /api/transcode-sessions (consumerId, mode, fileName)
207
+ P->>F: start/reuse transcode session
208
+ C->>P: GET /transcode/:sessionId/index.m3u8
209
+ C->>P: GET /api/transcode-sessions/:sessionId/progress
210
+ C->>P: POST /api/transcode-sessions/:sessionId/release (reason)
211
+ P->>P: remove consumer
212
+ alt no consumers left
213
+ P->>F: stop process and cleanup
214
+ end
215
+ ```
216
+
217
+ ## Notes
218
+
219
+ - HLS session files are stored in OS temp directory and cleaned up automatically.
220
+ - Transcode sessions are cached by `sourceKey:fileIndex:mode`.
221
+ - ffmpeg is bundled via `ffmpeg-static` for out-of-the-box availability.
222
+ - Source registry is in-memory and bounded (old entries are evicted).
223
+
224
+ ## License
225
+
226
+ This project is distributed under GPL-3.0-or-later (see `LICENSE`).
227
+
228
+ Third-party dependencies keep their own licenses. In particular, bundled ffmpeg binaries
229
+ provided by `ffmpeg-static` are GPL-compatible.
230
+
package/bin/cli.js ADDED
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+ import chalk from "chalk";
3
+ import { Command } from "commander";
4
+ import crypto from "node:crypto";
5
+ import { spawnSync } from "node:child_process";
6
+ import ffmpegStatic from "ffmpeg-static";
7
+ import { startProxyServer } from "../server.js";
8
+ import { registerClient, sendHeartbeat } from "../services/registry-api.js";
9
+
10
+ const program = new Command();
11
+
12
+ const HELP_EXAMPLES = `
13
+ Examples:
14
+ torrent-tv-proxy --server-url http://localhost:8080
15
+ torrent-tv-proxy --server-url http://localhost:8080 --host 0.0.0.0 --port 9090
16
+ torrent-tv-proxy --server-url http://localhost:8080 --public-base-url https://proxy.example.com
17
+ torrent-tv-proxy --server-url http://localhost:8080 --ffmpeg-bin /usr/local/bin/ffmpeg
18
+ torrent-tv-proxy --server-url http://localhost:8080 --no-transcode-audio
19
+
20
+ Notes:
21
+ - --help prints this message and exits with code 0.
22
+ - Video transcode is available automatically for per-session fallback when requested by client API.
23
+ `;
24
+
25
+ if (process.argv.includes("help")) {
26
+ process.argv = [process.argv[0], process.argv[1], "--help"];
27
+ }
28
+
29
+ program
30
+ .name("torrent-proxy-client")
31
+ .description("Expose torrent files over HTTP stream endpoints for browser playback.")
32
+ .requiredOption("--server-url <url>", "Registry server base URL")
33
+ .option("--public-base-url <url>", "Direct URL advertised to browser clients")
34
+ .option("--host <host>", "Local bind host", "127.0.0.1")
35
+ .option("--port <port>", "Local HTTP port", "9090")
36
+ .option("--id <id>", "Stable client id")
37
+ .option("--name <name>", "Display name")
38
+ .option("--no-transcode-audio", "Disable optional HLS AAC audio transcoding")
39
+ .option("--ffmpeg-bin <path>", "Path to ffmpeg binary")
40
+ .option("--token <token>", "Registration token", "")
41
+ .addHelpText("after", HELP_EXAMPLES);
42
+
43
+ program.parse(process.argv);
44
+ const options = program.opts();
45
+
46
+ const localPort = Number(options.port);
47
+ if (!Number.isInteger(localPort) || localPort <= 0 || localPort > 65535) {
48
+ console.error(chalk.red("[proxy-client] Invalid --port value."));
49
+ process.exit(1);
50
+ }
51
+
52
+ const serverUrl = String(options.serverUrl).replace(/\/+$/, "");
53
+ const bindHost = String(options.host);
54
+ const explicitBaseUrl = options.publicBaseUrl
55
+ ? String(options.publicBaseUrl).replace(/\/+$/, "")
56
+ : "";
57
+ const clientId = options.id ? String(options.id) : crypto.randomUUID();
58
+ const clientName = options.name ? String(options.name) : `proxy-${clientId.slice(0, 8)}`;
59
+ const token = String(options.token ?? "");
60
+ const transcodeAudio = options.transcodeAudio !== false;
61
+ const bundledFfmpegBin = typeof ffmpegStatic === "string" ? ffmpegStatic : "";
62
+ const ffmpegBin = options.ffmpegBin ? String(options.ffmpegBin) : bundledFfmpegBin || "ffmpeg";
63
+
64
+ function assertFfmpegAvailability() {
65
+ const probe = spawnSync(ffmpegBin, ["-version"], {
66
+ stdio: "ignore",
67
+ windowsHide: true,
68
+ timeout: 5000
69
+ });
70
+ if (probe.error) {
71
+ const message = probe.error instanceof Error ? probe.error.message : String(probe.error);
72
+ throw new Error(`Audio transcode is enabled, but ffmpeg is unavailable (${ffmpegBin}): ${message}`);
73
+ }
74
+ if (typeof probe.status === "number" && probe.status !== 0) {
75
+ throw new Error(
76
+ `Audio transcode is enabled, but ffmpeg check failed (${ffmpegBin}, exit code ${probe.status}).`
77
+ );
78
+ }
79
+ }
80
+
81
+ let registrationInProgress = false;
82
+ let heartbeatTimer = null;
83
+ let app = null;
84
+ let actualPort = localPort;
85
+ let shutdownInProgress = false;
86
+
87
+ async function registerClientSafe() {
88
+ if (registrationInProgress) {
89
+ return;
90
+ }
91
+ registrationInProgress = true;
92
+ try {
93
+ const result = await registerClient({
94
+ serverUrl,
95
+ id: clientId,
96
+ name: clientName,
97
+ baseUrl: explicitBaseUrl || `http://${bindHost}:${actualPort}`,
98
+ token
99
+ });
100
+ console.log(chalk.green(`[proxy-client] Registered: ${JSON.stringify(result.client)}`));
101
+ } finally {
102
+ registrationInProgress = false;
103
+ }
104
+ }
105
+
106
+ async function shutdown(signal) {
107
+ if (shutdownInProgress) {
108
+ return;
109
+ }
110
+ shutdownInProgress = true;
111
+ if (heartbeatTimer) {
112
+ clearInterval(heartbeatTimer);
113
+ heartbeatTimer = null;
114
+ }
115
+ console.log(chalk.yellow(`[proxy-client] Received ${signal}, shutting down...`));
116
+ try {
117
+ if (app) {
118
+ await app.close();
119
+ }
120
+ process.exit(0);
121
+ } catch (error) {
122
+ const message = error instanceof Error ? error.message : String(error);
123
+ console.error(chalk.red(`[proxy-client] Shutdown failed: ${message}`));
124
+ process.exit(1);
125
+ }
126
+ }
127
+
128
+ try {
129
+ if (transcodeAudio) {
130
+ assertFfmpegAvailability();
131
+ }
132
+ const started = await startProxyServer({
133
+ host: bindHost,
134
+ port: localPort,
135
+ transcodeAudio,
136
+ ffmpegBin
137
+ });
138
+ app = started.app;
139
+ actualPort = started.port;
140
+ const directBaseUrl = explicitBaseUrl || `http://${bindHost}:${actualPort}`;
141
+
142
+ console.log(chalk.cyan(`[proxy-client] Local stream endpoint: http://${bindHost}:${actualPort}/stream`));
143
+ console.log(chalk.cyan(`[proxy-client] Advertised direct URL: ${directBaseUrl}`));
144
+ if (transcodeAudio) {
145
+ console.log(
146
+ chalk.cyan(`[proxy-client] Optional HLS audio transcode is enabled (ffmpeg: ${ffmpegBin}).`)
147
+ );
148
+ }
149
+
150
+ await registerClientSafe();
151
+
152
+ heartbeatTimer = setInterval(async () => {
153
+ const status = await sendHeartbeat({
154
+ serverUrl,
155
+ id: clientId,
156
+ token
157
+ });
158
+ if (status === 404) {
159
+ console.log(chalk.yellow("[proxy-client] Heartbeat returned 404, re-registering..."));
160
+ try {
161
+ await registerClientSafe();
162
+ } catch (error) {
163
+ const message = error instanceof Error ? error.message : String(error);
164
+ console.error(chalk.red(`[proxy-client] Re-register failed: ${message}`));
165
+ }
166
+ }
167
+ }, 20_000);
168
+ } catch (error) {
169
+ const message = error instanceof Error ? error.message : String(error);
170
+ console.error(chalk.red(`[proxy-client] ${message}`));
171
+ process.exit(1);
172
+ }
173
+
174
+ process.on("SIGINT", () => {
175
+ void shutdown("SIGINT");
176
+ });
177
+
178
+ process.on("SIGTERM", () => {
179
+ void shutdown("SIGTERM");
180
+ });
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@torrent-tv/proxy",
3
+ "version": "1.0.0",
4
+ "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
+ "license": "GPL-3.0-or-later",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "type": "module",
10
+ "bin": {
11
+ "torrent-tv-proxy": "./bin/cli.js"
12
+ },
13
+ "scripts": {
14
+ "patch": "npm version patch && npm publish && git push --follow-tags",
15
+ "minor": "npm version minor && npm publish && git push --follow-tags",
16
+ "major": "npm version major && npm publish && git push --follow-tags",
17
+ "start": "node ./bin/cli.js",
18
+ "dev": "node --inspect=0 --experimental-network-inspection ./bin/cli.js"
19
+ },
20
+ "dependencies": {
21
+ "@fastify/cors": "^11.2.0",
22
+ "@fastify/helmet": "^13.0.2",
23
+ "@fastify/static": "^9.1.3",
24
+ "chalk": "^5.4.1",
25
+ "commander": "^12.1.0",
26
+ "fastify": "^5.8.5",
27
+ "ffmpeg-static": "^5.3.0",
28
+ "get-port": "^7.1.0",
29
+ "webtorrent": "^2.8.4"
30
+ }
31
+ }
@@ -0,0 +1,31 @@
1
+ function getPayload(body) {
2
+ if (body && typeof body === "object" && !Array.isArray(body)) {
3
+ return body;
4
+ }
5
+ return {};
6
+ }
7
+
8
+ export async function handleApiPlaybackPlanPost(req, reply, { playbackPlanner }) {
9
+ const payload = getPayload(req.body);
10
+ const sourceKey = typeof payload.sourceKey === "string" ? payload.sourceKey.trim() : "";
11
+ const fileIndex = Number(payload.fileIndex);
12
+ const userAgent = typeof payload.userAgent === "string" ? payload.userAgent : "";
13
+
14
+ if (!sourceKey || !Number.isInteger(fileIndex) || fileIndex < 0) {
15
+ return reply.code(400).send({ error: "sourceKey and valid fileIndex are required." });
16
+ }
17
+
18
+ try {
19
+ const plan = await playbackPlanner.getPlan({ sourceKey, fileIndex, userAgent });
20
+ return reply.send(plan);
21
+ } catch (error) {
22
+ if (error instanceof Error && error.code === "SOURCE_NOT_FOUND") {
23
+ return reply.code(404).send({ error: error.message });
24
+ }
25
+ if (error instanceof Error && error.code === "FILE_NOT_FOUND") {
26
+ return reply.code(404).send({ error: error.message });
27
+ }
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ return reply.code(500).send({ error: `Failed to prepare playback plan: ${message}` });
30
+ }
31
+ }
@@ -0,0 +1,18 @@
1
+ function getPayload(body) {
2
+ if (body && typeof body === "object" && !Array.isArray(body)) {
3
+ return body;
4
+ }
5
+ return {};
6
+ }
7
+
8
+ export async function handleApiSourcesPost(req, reply, { sourceRegistry }) {
9
+ const payload = getPayload(req.body);
10
+ const sourceType = typeof payload.sourceType === "string" ? payload.sourceType : "";
11
+ const source = typeof payload.source === "string" ? payload.source : "";
12
+ if (!sourceType || !source) {
13
+ return reply.code(400).send({ error: "sourceType and source are required." });
14
+ }
15
+
16
+ const sourceKey = sourceRegistry.upsert(sourceType, source);
17
+ return reply.send({ sourceKey });
18
+ }
@@ -0,0 +1,39 @@
1
+ function getPayload(body) {
2
+ if (body && typeof body === "object" && !Array.isArray(body)) {
3
+ return body;
4
+ }
5
+ return {};
6
+ }
7
+
8
+ export async function handleApiTranscodeSessionsPost(req, reply, { hlsSessionManager }) {
9
+ const payload = getPayload(req.body);
10
+ const sourceKey = typeof payload.sourceKey === "string" ? payload.sourceKey.trim() : "";
11
+ const fileIndex = Number(payload.fileIndex);
12
+ const transcodeVideo = payload.transcodeVideo === true;
13
+ const consumerId = typeof payload.consumerId === "string" ? payload.consumerId.trim() : "";
14
+ const fileName = typeof payload.fileName === "string" ? payload.fileName.trim() : "";
15
+
16
+ if (!sourceKey || !Number.isInteger(fileIndex) || fileIndex < 0) {
17
+ return reply.code(400).send({ error: "sourceKey and valid fileIndex are required." });
18
+ }
19
+
20
+ try {
21
+ const session = await hlsSessionManager.createOrGetSession({
22
+ sourceKey,
23
+ fileIndex,
24
+ transcodeVideo,
25
+ consumerId,
26
+ fileName
27
+ });
28
+ return reply.send({
29
+ sessionId: session.id,
30
+ playlistPath: `/transcode/${session.id}/index.m3u8`
31
+ });
32
+ } catch (error) {
33
+ if (error instanceof Error && error.code === "TRANSCODE_DISABLED") {
34
+ return reply.code(409).send({ error: error.message });
35
+ }
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ return reply.code(500).send({ error: `Failed to prepare transcode session: ${message}` });
38
+ }
39
+ }
@@ -0,0 +1,13 @@
1
+ export async function handleApiTranscodeSessionsProgressGet(req, reply, { hlsSessionManager }) {
2
+ const sessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
3
+ if (!sessionId) {
4
+ return reply.code(400).send({ error: "sessionId is required." });
5
+ }
6
+
7
+ const progress = hlsSessionManager.getSessionProgress(sessionId);
8
+ if (!progress) {
9
+ return reply.code(404).send({ error: "Transcode session was not found." });
10
+ }
11
+
12
+ return reply.send(progress);
13
+ }
@@ -0,0 +1,22 @@
1
+ function getPayload(body) {
2
+ if (body && typeof body === "object" && !Array.isArray(body)) {
3
+ return body;
4
+ }
5
+ return {};
6
+ }
7
+
8
+ export async function handleApiTranscodeSessionReleasePost(req, reply, { hlsSessionManager }) {
9
+ const sessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
10
+ const payload = getPayload(req.body);
11
+ const consumerId = typeof payload.consumerId === "string" ? payload.consumerId.trim() : "";
12
+ const reason = typeof payload.reason === "string" ? payload.reason.trim() : "";
13
+ if (!sessionId || !consumerId) {
14
+ return reply.code(400).send({ error: "sessionId and consumerId are required." });
15
+ }
16
+
17
+ const released = await hlsSessionManager.releaseSessionConsumer(sessionId, consumerId, reason);
18
+ if (!released) {
19
+ return reply.code(404).send({ error: "Transcode session was not found." });
20
+ }
21
+ return reply.send({ ok: true });
22
+ }
@@ -0,0 +1,3 @@
1
+ export async function handleHealthGet(_req, reply) {
2
+ return reply.send({ ok: true });
3
+ }
@@ -0,0 +1,3 @@
1
+ export async function handleHealthzGet(_req, reply) {
2
+ return reply.send({ ok: true });
3
+ }