@torrent-tv/proxy 2.83.8 → 2.85.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/CHANGELOG.md +23 -0
- package/CLAUDE.md +5 -3
- package/docs/encode-architecture.md +26 -1
- package/docs/logs.md +97 -51
- package/package.json +1 -1
- package/routes/api/link-probe/get.js +51 -0
- package/routes/api/transcode-sessions/net-report/post.js +84 -64
- package/routes/api/transcode-sessions/post.js +139 -137
- package/routes/transcode/session-file/get.js +268 -240
- package/server.js +3 -0
- package/services/data-channel-handler.js +12 -0
- package/services/delivery-probe.js +842 -828
- package/services/hls-session-manager.js +9759 -10006
- package/services/output/OutputSpec.js +202 -195
- package/services/priority/PriorityMap.js +390 -362
- package/services/priority/PriorityOrchestrator.js +320 -318
- package/services/viewer/Viewer.js +179 -83
- package/services/viewer/positions.js +120 -48
- package/services/viewer/report-intake.js +63 -0
- package/test/held-request-width.test.js +8 -5
- package/test/link-probe.test.js +56 -0
- package/test/orchestrator-wired.test.js +14 -8
- package/test/output-spec.test.js +157 -157
- package/test/plan-is-steady.test.js +225 -0
- package/test/priority-map-emptied.test.js +205 -207
- package/test/priority-map-per-output.test.js +209 -211
- package/test/quality-variants.test.js +1167 -1222
- package/test/seek-target-not-superseded.test.js +14 -12
- package/test/stale-request-after-seek.test.js +2 -3
- package/test/viewer-states.test.js +151 -0
- package/test/viewer.test.js +136 -108
- package/test/viewer-position-on-open.test.js +0 -92
|
@@ -1,137 +1,139 @@
|
|
|
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, sourceRegistry: object, torrentPool: object }} deps
|
|
9
|
-
* @returns {Promise<void>}
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { logger } from "../../../utils/logger.js";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Extract a plain object from the request body, guarding against
|
|
16
|
-
* non-object payloads (arrays, primitives, null).
|
|
17
|
-
*
|
|
18
|
-
* @param {unknown} body
|
|
19
|
-
* @returns {Record<string, unknown>}
|
|
20
|
-
*/
|
|
21
|
-
function getPayload(body) {
|
|
22
|
-
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
23
|
-
return body;
|
|
24
|
-
}
|
|
25
|
-
return {};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
export async function handleApiTranscodeSessionsPost(req, reply, { hlsSessionManager, sourceRegistry, torrentPool }) {
|
|
30
|
-
const payload = getPayload(req.body);
|
|
31
|
-
const sourceKey = typeof payload.sourceKey === "string" ? payload.sourceKey.trim() : "";
|
|
32
|
-
const fileIndex = Number(payload.fileIndex);
|
|
33
|
-
const transcodeVideo = payload.transcodeVideo === true;
|
|
34
|
-
const transcodeAudio = payload.transcodeAudio === true;
|
|
35
|
-
const consumerId = typeof payload.consumerId === "string" ? payload.consumerId.trim() : "";
|
|
36
|
-
const fileName = typeof payload.fileName === "string" ? payload.fileName.trim() : "";
|
|
37
|
-
const targetWidth = Number(payload.targetWidth);
|
|
38
|
-
const targetHeight = Number(payload.targetHeight);
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
// two
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
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, sourceRegistry: object, torrentPool: object }} deps
|
|
9
|
+
* @returns {Promise<void>}
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { logger } from "../../../utils/logger.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract a plain object from the request body, guarding against
|
|
16
|
+
* non-object payloads (arrays, primitives, null).
|
|
17
|
+
*
|
|
18
|
+
* @param {unknown} body
|
|
19
|
+
* @returns {Record<string, unknown>}
|
|
20
|
+
*/
|
|
21
|
+
function getPayload(body) {
|
|
22
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
23
|
+
return body;
|
|
24
|
+
}
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
export async function handleApiTranscodeSessionsPost(req, reply, { hlsSessionManager, sourceRegistry, torrentPool }) {
|
|
30
|
+
const payload = getPayload(req.body);
|
|
31
|
+
const sourceKey = typeof payload.sourceKey === "string" ? payload.sourceKey.trim() : "";
|
|
32
|
+
const fileIndex = Number(payload.fileIndex);
|
|
33
|
+
const transcodeVideo = payload.transcodeVideo === true;
|
|
34
|
+
const transcodeAudio = payload.transcodeAudio === true;
|
|
35
|
+
const consumerId = typeof payload.consumerId === "string" ? payload.consumerId.trim() : "";
|
|
36
|
+
const fileName = typeof payload.fileName === "string" ? payload.fileName.trim() : "";
|
|
37
|
+
const targetWidth = Number(payload.targetWidth);
|
|
38
|
+
const targetHeight = Number(payload.targetHeight);
|
|
39
|
+
// The target box is produced exactly (capped to source), with the realtime
|
|
40
|
+
// budget's auto-downscale and runtime downswitch off for this session.
|
|
41
|
+
//
|
|
42
|
+
// It says nothing about WHO asked — a rung sets it whether the viewer picked
|
|
43
|
+
// that rung or the player moved itself onto one.
|
|
44
|
+
const exactSize = payload.exactSize === true;
|
|
45
|
+
// Whether this browser will take its audio from a separate rendition group in
|
|
46
|
+
// the master playlist rather than muxed into the picture. It has to say so:
|
|
47
|
+
// publishing renditions AND muxing the same audio would play it twice, while
|
|
48
|
+
// a browser that does not know about them would get no sound at all.
|
|
49
|
+
const audioRenditions = payload.audioRenditions === true;
|
|
50
|
+
const startPositionSeconds = Number(payload.startPositionSeconds);
|
|
51
|
+
const audioTrackIndex = Number(payload.audioTrackIndex);
|
|
52
|
+
// Which container to produce. The browser knows what its media stack will
|
|
53
|
+
// accept for the tracks it asked to be copied; an absent or unknown value
|
|
54
|
+
// leaves the proxy's own `--segment-format` in charge.
|
|
55
|
+
const segmentFormatId =
|
|
56
|
+
typeof payload.segmentFormat === "string" ? payload.segmentFormat.trim() : "";
|
|
57
|
+
|
|
58
|
+
if (!sourceKey || !Number.isInteger(fileIndex) || fileIndex < 0) {
|
|
59
|
+
return reply.code(400).send({ error: "sourceKey and valid fileIndex are required." });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const session = await hlsSessionManager.createOrGetSession({
|
|
64
|
+
sourceKey,
|
|
65
|
+
fileIndex,
|
|
66
|
+
transcodeVideo,
|
|
67
|
+
transcodeAudio,
|
|
68
|
+
consumerId,
|
|
69
|
+
fileName,
|
|
70
|
+
targetWidth: Number.isInteger(targetWidth) && targetWidth > 0 ? targetWidth : 0,
|
|
71
|
+
targetHeight: Number.isInteger(targetHeight) && targetHeight > 0 ? targetHeight : 0,
|
|
72
|
+
exactSize,
|
|
73
|
+
audioRenditions,
|
|
74
|
+
startPositionSeconds:
|
|
75
|
+
Number.isFinite(startPositionSeconds) && startPositionSeconds > 0
|
|
76
|
+
? startPositionSeconds
|
|
77
|
+
: 0,
|
|
78
|
+
audioTrackIndex:
|
|
79
|
+
Number.isInteger(audioTrackIndex) && audioTrackIndex > 0 ? audioTrackIndex : 0,
|
|
80
|
+
segmentFormatId
|
|
81
|
+
});
|
|
82
|
+
// The index of quality variants, when this session has more than one to
|
|
83
|
+
// offer. Its presence is what tells the browser it can change quality
|
|
84
|
+
// without a new session: the player switches variants itself, appending the
|
|
85
|
+
// new one after what is already buffered. Absent for a copied video, whose
|
|
86
|
+
// segments are cut at the source's own keyframes and so cannot be spliced
|
|
87
|
+
// with a re-encoded rung.
|
|
88
|
+
const hasVariants = hlsSessionManager.buildMasterPlaylist(session.id) !== null;
|
|
89
|
+
return reply.send({
|
|
90
|
+
sessionId: session.id,
|
|
91
|
+
playlistPath: `/transcode/${session.id}/index.m3u8`,
|
|
92
|
+
...(hasVariants
|
|
93
|
+
? {
|
|
94
|
+
masterPath: `/transcode/${session.id}/master.m3u8`,
|
|
95
|
+
// Which of the master's variants this session IS. The browser pins
|
|
96
|
+
// the player to it, so loading the master costs nothing: an encoder
|
|
97
|
+
// is already producing that height, and any other rung would be a
|
|
98
|
+
// second cold start before the first frame.
|
|
99
|
+
variantHeight: hlsSessionManager.liveOutputs.variantHeightOf(session)
|
|
100
|
+
}
|
|
101
|
+
: {}),
|
|
102
|
+
// The heights this host will actually serve this file at, largest first
|
|
103
|
+
// — the ladder minus every rung it cannot produce faster than it is
|
|
104
|
+
// watched. The browser needs it whether or not a master exists: without
|
|
105
|
+
// it, it fell back to a ladder of its own invention and offered rungs the
|
|
106
|
+
// proxy had just refused, and picking one re-opened the session at a
|
|
107
|
+
// height measured at a third of realtime.
|
|
108
|
+
offeredHeights: hlsSessionManager.offeredHeights(session),
|
|
109
|
+
// What this session's output will carry, stated rather than left to be
|
|
110
|
+
// discovered. The browser checks what it actually got against this: a
|
|
111
|
+
// track that never arrives is otherwise noticed only by its absence,
|
|
112
|
+
// minutes later, as a black picture with working sound.
|
|
113
|
+
tracks: hlsSessionManager.declaredTracks(session),
|
|
114
|
+
// How far ahead of the viewer this proxy lets the encoder run, in seconds
|
|
115
|
+
// of playback. The browser sizes its own forward buffer from it, so the
|
|
116
|
+
// two sides agree by construction instead of each carrying a constant of
|
|
117
|
+
// its own — which is how the browser came to hold thirty seconds while
|
|
118
|
+
// two minutes stood produced on disk (roadmap item 4).
|
|
119
|
+
lookaheadSeconds: hlsSessionManager.lookaheadSeconds
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error instanceof Error && error.code === "TRANSCODE_DISABLED") {
|
|
123
|
+
return reply.code(409).send({ error: error.message });
|
|
124
|
+
}
|
|
125
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
126
|
+
// Say why on the proxy's own log, not only in the answer. This route
|
|
127
|
+
// answered 500 for every viewer of proxy 2.9.101-2.9.102 (an undeclared
|
|
128
|
+
// constant) and the addon log carried nothing but the data-channel layer's
|
|
129
|
+
// bare "→ 500": the cause had to be recovered by replaying the request
|
|
130
|
+
// against the live proxy. The stack is worth the two lines it costs — a
|
|
131
|
+
// programming error here is invisible to the viewer, who only sees that
|
|
132
|
+
// nothing plays.
|
|
133
|
+
logger.error(
|
|
134
|
+
`transcode-sessions: ${sourceKey}:${fileIndex} failed to prepare: ${message}\n` +
|
|
135
|
+
`${error instanceof Error ? (error.stack ?? "") : ""}`
|
|
136
|
+
);
|
|
137
|
+
return reply.code(500).send({ error: `Failed to prepare transcode session: ${message}` });
|
|
138
|
+
}
|
|
139
|
+
}
|