@torrent-tv/proxy 2.101.35 → 2.101.37
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/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { logger } from "../../../../utils/logger.js";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Return download statistics for a registered torrent source.
|
|
@@ -65,7 +66,13 @@ export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torr
|
|
|
65
66
|
// Awaited: with the torrent on its own thread this is a round trip, not a
|
|
66
67
|
// local lookup. Without the await the reply was the pending promise itself,
|
|
67
68
|
// which serialises to `{}` — the empty stats seen in the field 2026-08-02.
|
|
68
|
-
const
|
|
69
|
+
const statsStartedAt = performance.now();
|
|
70
|
+
let stats;
|
|
71
|
+
try {
|
|
72
|
+
stats = await torrentPool.getFileStats(torrent, fileIndex);
|
|
73
|
+
} finally {
|
|
74
|
+
logger.info(`[stats] ${sourceKey.slice(0, 8)} getFileStatsMs=${(performance.now() - statsStartedAt).toFixed(1)}`);
|
|
75
|
+
}
|
|
69
76
|
if (stats?.pending || !stats) {
|
|
70
77
|
const waitEstimate = initialEstimate();
|
|
71
78
|
return reply.code(202).send({ pending: true, ...(waitEstimate ? { waitEstimate } : {}) });
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { logger } from "../../../utils/logger.js";
|
|
13
|
+
import { performance } from "node:perf_hooks";
|
|
13
14
|
import { OUTPUT_NO_CAPACITY, OUTPUT_UNAVAILABLE } from "../../../services/encode/output/index.js";
|
|
14
15
|
import { replyNoCapacity, replyOutputUnavailable } from "../../../services/server/http-responses.js";
|
|
15
16
|
import { waitForPlan } from "../../../services/media/await-plan.js";
|
|
@@ -81,38 +82,51 @@ export async function handleApiTranscodeSessionsPost(req, reply, { viewerRequest
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
const cancellation = new AbortController();
|
|
85
|
+
const timing = { startedAt: performance.now(), openMs: 0, openCalls: 0,
|
|
86
|
+
planWaitInclusiveMs: null, masterMs: null, offerMs: null, tracksMs: null };
|
|
87
|
+
const forwardedRequestId = typeof req.headers?.["x-ttv-forwarded-request-id"] === "string"
|
|
88
|
+
? req.headers["x-ttv-forwarded-request-id"] : "unknown";
|
|
84
89
|
const cancelled = () => cancellation.abort();
|
|
85
90
|
reply.raw?.once?.("close", cancelled);
|
|
86
91
|
if (reply.raw?.destroyed) cancelled();
|
|
87
92
|
try {
|
|
88
|
-
const open = () =>
|
|
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
|
-
|
|
93
|
+
const open = async () => {
|
|
94
|
+
const openStartedAt = performance.now();
|
|
95
|
+
timing.openCalls += 1;
|
|
96
|
+
try {
|
|
97
|
+
return await viewerRequests.createOrGetSession({
|
|
98
|
+
signal: cancellation.signal,
|
|
99
|
+
sourceKey,
|
|
100
|
+
fileIndex,
|
|
101
|
+
transcodeVideo,
|
|
102
|
+
transcodeAudio,
|
|
103
|
+
consumerId,
|
|
104
|
+
fileName,
|
|
105
|
+
targetWidth: Number.isInteger(targetWidth) && targetWidth > 0 ? targetWidth : 0,
|
|
106
|
+
targetHeight: Number.isInteger(targetHeight) && targetHeight > 0 ? targetHeight : 0,
|
|
107
|
+
exactSize,
|
|
108
|
+
visiblePicture,
|
|
109
|
+
audioRenditions,
|
|
110
|
+
startPositionSeconds:
|
|
111
|
+
Number.isFinite(startPositionSeconds) && startPositionSeconds > 0
|
|
112
|
+
? startPositionSeconds
|
|
113
|
+
: 0,
|
|
114
|
+
viewerLinkMbps: Number.isFinite(viewerLinkMbps) && viewerLinkMbps > 0 ? viewerLinkMbps : null,
|
|
115
|
+
linkSampleMbps: Number.isFinite(linkSampleMbps) && linkSampleMbps > 0 ? linkSampleMbps : null,
|
|
116
|
+
linkSampleAt: Number.isFinite(linkSampleAt) && linkSampleAt > 0 ? linkSampleAt : null,
|
|
117
|
+
linkSampleAgeMs: Number.isFinite(linkSampleAgeMs) && linkSampleAgeMs >= 0 ? linkSampleAgeMs : null,
|
|
118
|
+
bufferedAheadSec: Number.isFinite(bufferedAheadSec) && bufferedAheadSec >= 0 ? bufferedAheadSec : 0,
|
|
119
|
+
bufferLimitSeconds:
|
|
120
|
+
Number.isFinite(bufferLimitSeconds) && bufferLimitSeconds > 0 ? bufferLimitSeconds : null,
|
|
121
|
+
audioTrackIndex:
|
|
122
|
+
Number.isInteger(audioTrackIndex) && audioTrackIndex > 0 ? audioTrackIndex : 0,
|
|
123
|
+
segmentFormatId
|
|
124
|
+
});
|
|
125
|
+
} finally {
|
|
126
|
+
timing.openMs += performance.now() - openStartedAt;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const planWaitStartedAt = performance.now();
|
|
116
130
|
const session = subscribeSource ? (await waitForPlan({ signal: cancellation.signal,
|
|
117
131
|
subscribe: listener => subscribeSource(sourceKey, fileIndex, listener),
|
|
118
132
|
read: async () => {
|
|
@@ -123,13 +137,22 @@ export async function handleApiTranscodeSessionsPost(req, reply, { viewerRequest
|
|
|
123
137
|
}
|
|
124
138
|
}
|
|
125
139
|
})).session : await open();
|
|
140
|
+
timing.planWaitInclusiveMs = performance.now() - planWaitStartedAt;
|
|
126
141
|
// The index of quality variants, when this session has more than one to
|
|
127
142
|
// offer. Its presence is what tells the browser it can change quality
|
|
128
143
|
// without a new session: the player switches variants itself, appending the
|
|
129
144
|
// new one after what is already buffered. Absent for a copied video, whose
|
|
130
145
|
// segments are cut at the source's own keyframes and so cannot be spliced
|
|
131
146
|
// with a re-encoded rung.
|
|
147
|
+
let stageStartedAt = performance.now();
|
|
132
148
|
const hasVariants = renditions.buildMasterPlaylist(session.id) !== null;
|
|
149
|
+
timing.masterMs = performance.now() - stageStartedAt;
|
|
150
|
+
stageStartedAt = performance.now();
|
|
151
|
+
const offeredHeights = quality.offeredHeights(session);
|
|
152
|
+
timing.offerMs = performance.now() - stageStartedAt;
|
|
153
|
+
stageStartedAt = performance.now();
|
|
154
|
+
const tracks = renditions.declaredTracks(session);
|
|
155
|
+
timing.tracksMs = performance.now() - stageStartedAt;
|
|
133
156
|
return reply.send({
|
|
134
157
|
sessionId: session.id,
|
|
135
158
|
playlistPath: `/transcode/${session.id}/index.m3u8`,
|
|
@@ -149,12 +172,12 @@ export async function handleApiTranscodeSessionsPost(req, reply, { viewerRequest
|
|
|
149
172
|
// it, it fell back to a ladder of its own invention and offered rungs the
|
|
150
173
|
// proxy had just refused, and picking one re-opened the session at a
|
|
151
174
|
// height measured at a third of realtime.
|
|
152
|
-
offeredHeights
|
|
175
|
+
offeredHeights,
|
|
153
176
|
// What this session's output will carry, stated rather than left to be
|
|
154
177
|
// discovered. The browser checks what it actually got against this: a
|
|
155
178
|
// track that never arrives is otherwise noticed only by its absence,
|
|
156
179
|
// minutes later, as a black picture with working sound.
|
|
157
|
-
tracks
|
|
180
|
+
tracks,
|
|
158
181
|
// The soundtrack this viewer is sent and whether it is re-encoded — the
|
|
159
182
|
// proxy's decision, which can differ from what the page asked for: a
|
|
160
183
|
// track no figure states a rate for is re-encoded even for a browser that
|
|
@@ -196,5 +219,14 @@ export async function handleApiTranscodeSessionsPost(req, reply, { viewerRequest
|
|
|
196
219
|
return reply.code(500).send({ error: `Failed to prepare transcode session: ${message}` });
|
|
197
220
|
} finally {
|
|
198
221
|
reply.raw?.removeListener?.("close", cancelled);
|
|
222
|
+
const elapsed = (key) => Number.isFinite(timing[key]) ? timing[key].toFixed(1) : "n/a";
|
|
223
|
+
logger.info(
|
|
224
|
+
`transcode-session timing outcome=${cancellation.signal.aborted ? "cancelled" : "handler-return"} ` +
|
|
225
|
+
`requestId=${forwardedRequestId} ` +
|
|
226
|
+
`consumer=${consumerId || "unknown"} file=${fileIndex} openCalls=${timing.openCalls} ` +
|
|
227
|
+
`openMs=${timing.openMs.toFixed(1)} planWaitInclusiveMs=${elapsed("planWaitInclusiveMs")} ` +
|
|
228
|
+
`masterMs=${elapsed("masterMs")} offerMs=${elapsed("offerMs")} tracksMs=${elapsed("tracksMs")} ` +
|
|
229
|
+
`handlerMs=${(performance.now() - timing.startedAt).toFixed(1)}`
|
|
230
|
+
);
|
|
199
231
|
}
|
|
200
232
|
}
|
package/server.js
CHANGED
|
@@ -11,6 +11,7 @@ import fastifyCors from "@fastify/cors";
|
|
|
11
11
|
import fastifyHelmet from "@fastify/helmet";
|
|
12
12
|
import getPort from "get-port";
|
|
13
13
|
import { createRequire } from "node:module";
|
|
14
|
+
import { performance } from "node:perf_hooks";
|
|
14
15
|
import { handleHealthGet } from "./routes/health/get.js";
|
|
15
16
|
import { handleMediaGet } from "./routes/media/get.js";
|
|
16
17
|
import { ProbeRequests } from "./services/media/ProbeRequests.js";
|
|
@@ -170,6 +171,22 @@ export async function startProxyServer({
|
|
|
170
171
|
app.addHook("onRequest", async (_req, reply) => {
|
|
171
172
|
reply.header("Access-Control-Allow-Private-Network", "true");
|
|
172
173
|
});
|
|
174
|
+
app.addHook("onRequest", async (req, reply) => {
|
|
175
|
+
const requestId = req.headers["x-ttv-forwarded-request-id"];
|
|
176
|
+
if (typeof requestId !== "string") return;
|
|
177
|
+
const startedAt = performance.now();
|
|
178
|
+
reply.raw.once("finish", () => {
|
|
179
|
+
logger.info(`[http-response] requestId=${requestId} method=${req.method} url=${req.url} ` +
|
|
180
|
+
`responseMs=${(performance.now() - startedAt).toFixed(1)} status=${reply.statusCode} ` +
|
|
181
|
+
`contentLength=${reply.raw.getHeader("content-length") ?? "unknown"}`);
|
|
182
|
+
});
|
|
183
|
+
reply.raw.once("close", () => {
|
|
184
|
+
if (!reply.raw.writableFinished) {
|
|
185
|
+
logger.warn(`[http-response] requestId=${requestId} method=${req.method} url=${req.url} ` +
|
|
186
|
+
`closedMs=${(performance.now() - startedAt).toFixed(1)} status=${reply.statusCode}`);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
173
190
|
|
|
174
191
|
const sourceRegistry = createSourceRegistry(200);
|
|
175
192
|
// The torrent runs on its own thread. Profiling a live seek (2026-08-02)
|
|
@@ -276,13 +276,16 @@ export class LocalObservations {
|
|
|
276
276
|
*/
|
|
277
277
|
noteFirstFrame({ seconds, features, featureSnapshots = null, stages = null }) {
|
|
278
278
|
if (!Number.isFinite(seconds) || seconds < 0 || !features || typeof features !== "object") return;
|
|
279
|
+
const measuredStages = stagesOf(stages);
|
|
280
|
+
if (!measuredStages) return;
|
|
279
281
|
const key = this.#firstFrameKey();
|
|
280
282
|
const snapshots = [features, ...(Array.isArray(featureSnapshots) ? featureSnapshots : [])]
|
|
281
283
|
.filter(value => value && typeof value === "object").slice(0, 2).map(firstFrameFeatures);
|
|
282
|
-
const
|
|
284
|
+
const usableReadings = (this.#entries.firstFrame[key] ?? []).filter(reading => stagesOf(reading.stages));
|
|
285
|
+
const readings = [...usableReadings, {
|
|
283
286
|
...snapshots[0],
|
|
284
287
|
featureSnapshots: snapshots,
|
|
285
|
-
stages:
|
|
288
|
+
stages: measuredStages,
|
|
286
289
|
at: this.#now()
|
|
287
290
|
}].slice(-READINGS_KEPT);
|
|
288
291
|
this.#entries.firstFrame[key] = readings;
|
|
@@ -1171,7 +1171,11 @@ export function createDataChannelHandler({
|
|
|
1171
1171
|
log(`[dc] ${method} ${path}${queryInfo}${bodyInfo}`);
|
|
1172
1172
|
|
|
1173
1173
|
const targetUrl = `http://127.0.0.1:${proxyPort}${path}${query ? `?${query}` : ""}`;
|
|
1174
|
-
const requestHeaders = {
|
|
1174
|
+
const requestHeaders = {
|
|
1175
|
+
...(forwardedHeaders ?? {}),
|
|
1176
|
+
host: `127.0.0.1:${proxyPort}`,
|
|
1177
|
+
"x-ttv-forwarded-request-id": requestId
|
|
1178
|
+
};
|
|
1175
1179
|
|
|
1176
1180
|
let response;
|
|
1177
1181
|
// [net-debug] TEMPORARY: time spent in the local fetch (waiting for the
|