@torrent-tv/proxy 2.87.6 → 2.87.8

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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 2.87.8
2
+
3
+ - **Fix**: Keep a viewer registered until every data channel on its WebRTC connection closes, and ignore a closing connection after the viewer has identified themselves on a replacement connection.
4
+
5
+ ## 2.87.7
6
+
7
+ - **Fix**: Refresh audio metadata for sidecar tracks after a delayed container-header read, keeping the existing track numbers and rendition count stable.
8
+
1
9
  ## 2.87.6
2
10
 
3
11
  - **Chore**: Move torrent infohash key derivation into a shared utility so the source registry does not depend on the torrent service layer.
@@ -1,94 +1,101 @@
1
- # Browser–proxy playback contract
2
-
3
- This document records the HTTP contract used by the browser playback code and
4
- implemented by this proxy. The same paths and payloads pass through either the
5
- direct HTTP transport or the WebRTC data channel. It is a description of the
6
- current code, not a generated schema or a version negotiation protocol.
7
-
8
- ## Source and file discovery
9
-
10
- | Request | Required input | Successful answer |
11
- |---|---|---|
12
- | `POST /api/sources` | `{ sourceType, source }` | `{ sourceKey }` identifies the registered source. |
13
- | `GET /api/sources/:sourceKey/files?maxWaitMs=N` | Registered `sourceKey`; optional wait budget | While magnet metadata is unavailable: `{ pending: true }`. When ready: `{ name, infoHash, files, items }`. `files` carries `fileIndex`, `name`, `relativePath`, `length`, and `kind`; `items` maps each video `fileIndex` to audio, subtitle, and image file indices. |
14
- | `GET /api/sources/:sourceKey/stats?fileIndex=N` | Registered source; optional file index | Current peer, transfer, header, and file progress readings. A reading may not contain every measurement; absent or `null` measurements are unknown. |
15
- | `POST /api/sources/:sourceKey/warm` | Optional `{ fileIndex, positionSeconds }`; an empty object warms source metadata and the first video | `{ started, swarm, edges, sidecars }` reports which preparation started. Work continues in the background. |
16
- | `GET /api/link-probe?bytes=N` | Positive byte count | A no-store binary body of zero bytes, capped at 2 MiB. The browser measures completed transfer time; an unavailable route leaves link speed unknown. |
17
-
18
- `pending: true` means the requested metadata or plan is not ready yet. The
19
- browser polls again. It does not mean that the torrent has no video or that the
20
- request failed. A non-pending file-list answer uses the proxy's file indices;
21
- the browser must keep those indices when opening a file.
22
-
23
- ## Playback plan and transcode session
24
-
25
- `POST /api/playback-plan` takes `{ sourceKey, fileIndex, userAgent }`. Its answer
26
- contains `mode`, `directUrl`, codec and container names, duration and source
27
- dimensions, track inventories, paired sidecar files, `offeredHeights`, and
28
- `pending`. The browser uses the codec fields to make the direct-versus-transcode
29
- decision itself; `mode` is advisory.
30
-
31
- `sidecarSubtitles` lists subtitle files the proxy paired with this video. Each
32
- entry carries the torrent `fileIndex` and the proxy's filename-derived language
33
- and release metadata. The browser uses these entries to fetch subtitle files
34
- and to identify a remembered choice. `sidecarImages` is returned beside it but
35
- is not needed for playback. Older proxies and plans made with transcoding
36
- disabled may omit these fields; the browser treats a missing or non-array list
37
- as empty.
38
-
39
- The browser maps a missing `mode` to `direct`, uses its registered `/stream`
40
- URL when `directUrl` is absent, maps missing codec/container names to empty
41
- strings, maps missing duration or dimensions to `0`, and maps missing audio or
42
- subtitle track arrays to `[]`. It maps missing or non-object `offeredHeights` to
43
- `null`; for an object value, missing or invalid `copy` and `transcode` lists
44
- become empty arrays. `null` means no offer was supplied, while empty lists
45
- explicitly offer no heights for their respective playback branch. Only
46
- `pending: true` asks the browser to poll again.
47
-
48
- `POST /api/transcode-sessions` requires `sourceKey`, `fileIndex`, and
49
- `consumerId`. The browser also sends the requested audio/video transcode flags,
50
- the visible picture size, whether audio is requested as separate renditions,
51
- the start position, selected audio track, and segment format when known. The
52
- answer contains `sessionId`, `playlistPath`, `offeredHeights`, declared `tracks`,
53
- and `lookaheadSeconds`. `masterPath` and `variantHeight` are present only when
54
- the session has quality variants.
55
-
56
- The browser interprets `409` with `outcome: "output-unavailable"` as a refusal
57
- for this viewer's link and `outcome: "no-capacity"` as a proxy capacity
58
- refusal. Other non-success statuses remain errors; an error body only adds
59
- display detail and is not required for status handling.
60
-
61
- ## Session control and media files
62
-
63
- | Request | Meaning |
64
- |---|---|
65
- | `POST /api/transcode-sessions/:id/seek` | `{ positionSeconds, consumerId, generation }` reports the viewer's seek. A successful answer is `204`; the proxy handles the seek server side. |
66
- | `GET /api/transcode-sessions/:id/progress?consumer=:id` | Reads current session progress and also keeps the viewer's session alive. The browser consumes available progress fields; an unreadable or failed answer does not imply that playback has ended. |
67
- | `POST /api/transcode-sessions/:id/net-report` | `{ consumerId, bufferedAheadSec, ... }` reports the viewer's buffer and optional state/link measurements. `consumerId` and non-negative `bufferedAheadSec` are required; missing `linkMbps` does not reject the report and leaves the last link reading intact. Missing `playing` becomes `false`; missing `waiting` is derived from the playback and buffer state. Missing `positionSeconds` leaves the position unchanged, missing `qualityMode` leaves the prior mode, `onScreen` defaults to `true`, and `inPictureInPicture` defaults to `false`. Success is `204`. |
68
- | `POST /api/transcode-sessions/:id/fragment-far` | Diagnostic fragment and buffer positions; does not change encoding. Best effort, success `204`. |
69
- | `POST /api/transcode-sessions/:id/release` | `{ consumerId, reason }` releases this viewer's session assignment. |
70
- | `GET /transcode/:id/:fileName` | HLS playlist, init, or media segment. A file that is still being produced is answered with retryable `503`, not `202`. |
71
- | `GET /transcode/:id/v/:height/warm` and `/transcode/:id/a/:track/warm` | Prepare the requested video height or audio track at a position. `204` means ready. Audio warm-up `404` means the proxy does not support this operation for that session. |
72
- | `GET /stream?sourceKey=…&fileIndex=N` | Direct file bytes with HTTP range support. |
73
-
74
- ## Subtitles
75
-
76
- `GET /api/subtitles` requires `sourceKey` and `fileIndex`. Without `trackIndex`
77
- it reads a paired sidecar file. With `trackIndex`, it reads cues from an
78
- embedded text track and may take `consumerId` to subscribe that viewer to
79
- subsequent cue pushes. `since` asks for cues after a previously received
80
- cursor.
81
-
82
- `200` returns WebVTT. Embedded cue answers may include
83
- `X-Subtitle-Cursor`, `X-Subtitle-Covered-Clusters`, and
84
- `X-Subtitle-Indexed-Clusters`; language detection may include
85
- `X-Subtitle-Language` and `X-Subtitle-Language-Name`. While an embedded track
86
- is being extracted, the response is `202` with `{ pending: true }`; the browser
87
- waits and repeats the request. A missing cursor or language header means that
88
- the browser has no value to preserve or display yet. A failed sidecar or
89
- embedded-track request skips that track without failing video playback.
90
-
91
- Subtitle cue pushes are delivered through the established proxy connection,
92
- not by a separate browser polling route. After reconnect, the browser repeats
93
- the embedded-track request with `since` to restore the subscription and receive
94
- missed cues.
1
+ # Browser–proxy playback contract
2
+
3
+ This document records the HTTP contract used by the browser playback code and
4
+ implemented by this proxy. The same paths and payloads pass through either the
5
+ direct HTTP transport or the WebRTC data channel. It is a description of the
6
+ current code, not a generated schema or a version negotiation protocol.
7
+
8
+ ## Source and file discovery
9
+
10
+ | Request | Required input | Successful answer |
11
+ |---|---|---|
12
+ | `POST /api/sources` | `{ sourceType, source }` | `{ sourceKey }` identifies the registered source. |
13
+ | `GET /api/sources/:sourceKey/files?maxWaitMs=N` | Registered `sourceKey`; optional wait budget | While magnet metadata is unavailable: `{ pending: true }`. When ready: `{ name, infoHash, files, items }`. `files` carries `fileIndex`, `name`, `relativePath`, `length`, and `kind`; `items` maps each video `fileIndex` to audio, subtitle, and image file indices. |
14
+ | `GET /api/sources/:sourceKey/stats?fileIndex=N` | Registered source; optional file index | Current peer, transfer, header, and file progress readings. A reading may not contain every measurement; absent or `null` measurements are unknown. |
15
+ | `POST /api/sources/:sourceKey/warm` | Optional `{ fileIndex, positionSeconds }`; an empty object warms source metadata and the first video | `{ started, swarm, edges, sidecars }` reports which preparation started. Work continues in the background. |
16
+ | `GET /api/link-probe?bytes=N` | Positive byte count | A no-store binary body of zero bytes, capped at 2 MiB. The browser measures completed transfer time; an unavailable route leaves link speed unknown. |
17
+
18
+ `pending: true` means the requested metadata or plan is not ready yet. The
19
+ browser polls again. It does not mean that the torrent has no video or that the
20
+ request failed. A non-pending file-list answer uses the proxy's file indices;
21
+ the browser must keep those indices when opening a file.
22
+
23
+ ## Playback plan and transcode session
24
+
25
+ `POST /api/playback-plan` takes `{ sourceKey, fileIndex, userAgent }`. Its answer
26
+ contains `mode`, `directUrl`, codec and container names, duration and source
27
+ dimensions, track inventories, paired sidecar files, `offeredHeights`,
28
+ `audioTracksPending`, and `pending`. The browser uses the codec fields to make
29
+ the direct-versus-transcode decision itself; `mode` is advisory.
30
+
31
+ When `audioTracksPending` is `true`, one or more sidecar headers were still
32
+ unavailable. The browser may call `POST /api/playback-plan/audio-tracks` with
33
+ `{ sourceKey, fileIndex }`; the answer contains the current `audioTracks` and
34
+ `pending`. A resolved header updates metadata on the already-published first
35
+ track for that sidecar. It never changes track indices or the number of HLS
36
+ renditions in the active plan.
37
+
38
+ `sidecarSubtitles` lists subtitle files the proxy paired with this video. Each
39
+ entry carries the torrent `fileIndex` and the proxy's filename-derived language
40
+ and release metadata. The browser uses these entries to fetch subtitle files
41
+ and to identify a remembered choice. `sidecarImages` is returned beside it but
42
+ is not needed for playback. Older proxies and plans made with transcoding
43
+ disabled may omit these fields; the browser treats a missing or non-array list
44
+ as empty.
45
+
46
+ The browser maps a missing `mode` to `direct`, uses its registered `/stream`
47
+ URL when `directUrl` is absent, maps missing codec/container names to empty
48
+ strings, maps missing duration or dimensions to `0`, and maps missing audio or
49
+ subtitle track arrays to `[]`. It maps missing or non-object `offeredHeights` to
50
+ `null`; for an object value, missing or invalid `copy` and `transcode` lists
51
+ become empty arrays. `null` means no offer was supplied, while empty lists
52
+ explicitly offer no heights for their respective playback branch. Only
53
+ `pending: true` asks the browser to poll again.
54
+
55
+ `POST /api/transcode-sessions` requires `sourceKey`, `fileIndex`, and
56
+ `consumerId`. The browser also sends the requested audio/video transcode flags,
57
+ the visible picture size, whether audio is requested as separate renditions,
58
+ the start position, selected audio track, and segment format when known. The
59
+ answer contains `sessionId`, `playlistPath`, `offeredHeights`, declared `tracks`,
60
+ and `lookaheadSeconds`. `masterPath` and `variantHeight` are present only when
61
+ the session has quality variants.
62
+
63
+ The browser interprets `409` with `outcome: "output-unavailable"` as a refusal
64
+ for this viewer's link and `outcome: "no-capacity"` as a proxy capacity
65
+ refusal. Other non-success statuses remain errors; an error body only adds
66
+ display detail and is not required for status handling.
67
+
68
+ ## Session control and media files
69
+
70
+ | Request | Meaning |
71
+ |---|---|
72
+ | `POST /api/transcode-sessions/:id/seek` | `{ positionSeconds, consumerId, generation }` reports the viewer's seek. A successful answer is `204`; the proxy handles the seek server side. |
73
+ | `GET /api/transcode-sessions/:id/progress?consumer=:id` | Reads current session progress and also keeps the viewer's session alive. The browser consumes available progress fields; an unreadable or failed answer does not imply that playback has ended. |
74
+ | `POST /api/transcode-sessions/:id/net-report` | `{ consumerId, bufferedAheadSec, ... }` reports the viewer's buffer and optional state/link measurements. `consumerId` and non-negative `bufferedAheadSec` are required; missing `linkMbps` does not reject the report and leaves the last link reading intact. Missing `playing` becomes `false`; missing `waiting` is derived from the playback and buffer state. Missing `positionSeconds` leaves the position unchanged, missing `qualityMode` leaves the prior mode, `onScreen` defaults to `true`, and `inPictureInPicture` defaults to `false`. Success is `204`. |
75
+ | `POST /api/transcode-sessions/:id/fragment-far` | Diagnostic fragment and buffer positions; does not change encoding. Best effort, success `204`. |
76
+ | `POST /api/transcode-sessions/:id/release` | `{ consumerId, reason }` releases this viewer's session assignment. |
77
+ | `GET /transcode/:id/:fileName` | HLS playlist, init, or media segment. A file that is still being produced is answered with retryable `503`, not `202`. |
78
+ | `GET /transcode/:id/v/:height/warm` and `/transcode/:id/a/:track/warm` | Prepare the requested video height or audio track at a position. `204` means ready. Audio warm-up `404` means the proxy does not support this operation for that session. |
79
+ | `GET /stream?sourceKey=…&fileIndex=N` | Direct file bytes with HTTP range support. |
80
+
81
+ ## Subtitles
82
+
83
+ `GET /api/subtitles` requires `sourceKey` and `fileIndex`. Without `trackIndex`
84
+ it reads a paired sidecar file. With `trackIndex`, it reads cues from an
85
+ embedded text track and may take `consumerId` to subscribe that viewer to
86
+ subsequent cue pushes. `since` asks for cues after a previously received
87
+ cursor.
88
+
89
+ `200` returns WebVTT. Embedded cue answers may include
90
+ `X-Subtitle-Cursor`, `X-Subtitle-Covered-Clusters`, and
91
+ `X-Subtitle-Indexed-Clusters`; language detection may include
92
+ `X-Subtitle-Language` and `X-Subtitle-Language-Name`. While an embedded track
93
+ is being extracted, the response is `202` with `{ pending: true }`; the browser
94
+ waits and repeats the request. A missing cursor or language header means that
95
+ the browser has no value to preserve or display yet. A failed sidecar or
96
+ embedded-track request skips that track without failing video playback.
97
+
98
+ Subtitle cue pushes are delivered through the established proxy connection,
99
+ not by a separate browser polling route. After reconnect, the browser repeats
100
+ the embedded-track request with `since` to restore the subscription and receive
101
+ missed cues.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.87.6",
3
+ "version": "2.87.8",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Refresh metadata for audio tracks whose sidecar headers were unavailable
3
+ * when the playback plan was built.
4
+ *
5
+ * POST /api/playback-plan/audio-tracks
6
+ *
7
+ * @param {import("fastify").FastifyRequest} req
8
+ * @param {import("fastify").FastifyReply} reply
9
+ * @param {{ playbackPlanner: object }} deps
10
+ */
11
+ export async function handleApiPlaybackPlanAudioTracksPost(req, reply, { playbackPlanner }) {
12
+ const payload = req.body && typeof req.body === "object" && !Array.isArray(req.body) ? req.body : {};
13
+ const sourceKey = typeof payload.sourceKey === "string" ? payload.sourceKey.trim() : "";
14
+ const fileIndex = Number(payload.fileIndex);
15
+ if (!sourceKey || !Number.isInteger(fileIndex) || fileIndex < 0) {
16
+ return reply.code(400).send({ error: "sourceKey and valid fileIndex are required." });
17
+ }
18
+ try {
19
+ const { PlaybackController } = await import("../../../../services/server/controllers/PlaybackController.js");
20
+ const controller = new PlaybackController({ playbackPlanner });
21
+ return reply.send(await controller.refreshAudioTracks({ sourceKey, fileIndex }));
22
+ } catch (error) {
23
+ if (error instanceof Error && error.code === "SOURCE_NOT_FOUND") {
24
+ return reply.code(404).send({ error: error.message });
25
+ }
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ return reply.code(500).send({ error: `Failed to refresh audio track metadata: ${message}` });
28
+ }
29
+ }
package/server.js CHANGED
@@ -23,6 +23,7 @@ import { handleApiSourceStatsGet } from "./routes/api/sources/stats/get.js";
23
23
  import { handleApiSourceFilesGet } from "./routes/api/sources/files/get.js";
24
24
  import { handleApiSourceWarmPost } from "./routes/api/sources/warm/post.js";
25
25
  import { handleApiPlaybackPlanPost } from "./routes/api/playback-plan/post.js";
26
+ import { handleApiPlaybackPlanAudioTracksPost } from "./routes/api/playback-plan/audio-tracks/post.js";
26
27
  import { handleApiClientLogsPost } from "./routes/api/client-logs/post.js";
27
28
  import { createClientLogFiles } from "./utils/client-log-file.js";
28
29
  import { handleApiSubtitlesGet } from "./routes/api/subtitles/get.js";
@@ -617,6 +618,9 @@ export async function startProxyServer({
617
618
  app.post("/api/playback-plan", async (req, reply) =>
618
619
  handleApiPlaybackPlanPost(req, reply, { playbackPlanner })
619
620
  );
621
+ app.post("/api/playback-plan/audio-tracks", async (req, reply) =>
622
+ handleApiPlaybackPlanAudioTracksPost(req, reply, { playbackPlanner })
623
+ );
620
624
  app.get("/api/subtitles", async (req, reply) =>
621
625
  handleApiSubtitlesGet(req, reply, {
622
626
  sourceRegistry,