@torrent-tv/proxy 2.105.13 → 2.106.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.105.13",
3
+ "version": "2.106.0",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -24,7 +24,7 @@ function getPayload(body) {
24
24
  return {};
25
25
  }
26
26
 
27
- export async function handleApiPlaybackPlanPost(req, reply, { playbackPlanner, sourceRegistry, torrentPool, ffmpegBin, localBaseUrl, viewers }) {
27
+ export async function handleApiPlaybackPlanPost(req, reply, { playbackPlanner, sourceRegistry, torrentPool, ffmpegBin, localBaseUrl, viewers, observeWait }) {
28
28
  const payload = getPayload(req.body);
29
29
  const sourceKey = typeof payload.sourceKey === "string" ? payload.sourceKey.trim() : "";
30
30
  const fileIndex = Number(payload.fileIndex);
@@ -38,10 +38,13 @@ export async function handleApiPlaybackPlanPost(req, reply, { playbackPlanner, s
38
38
  if (typeof payload.positionSeconds === "number" && Number.isFinite(payload.positionSeconds) && payload.positionSeconds >= 0) selection.positionSeconds = payload.positionSeconds;
39
39
  if (typeof payload.wantsToPlay === "boolean") selection.wantsToPlay = payload.wantsToPlay;
40
40
  if (consumerId) viewers?.selectsFile(consumerId, sourceKey, fileIndex, Date.now(), selection);
41
+ const startedAt = performance.now();
41
42
 
42
43
  // Interface delegates to PlaybackController (orchestrator + domain). Keeps route thin.
43
44
  const { PlaybackController } = await import("../../../services/server/controllers/PlaybackController.js");
44
45
  const controller = new PlaybackController({ torrentPool, sourceRegistry, ffmpegBin, localBaseUrl, playbackPlanner });
46
+ const waitFields = { process: "P3", sourceKey, fileIndex };
47
+ observeWait?.(sourceKey, "process", { ...waitFields, phase: "begin", at: startedAt });
45
48
  const cancellation = new AbortController();
46
49
  const aborted = () => cancellation.abort();
47
50
  const closed = () => { if (!reply.raw?.writableEnded) aborted(); };
@@ -52,8 +55,14 @@ export async function handleApiPlaybackPlanPost(req, reply, { playbackPlanner, s
52
55
  const params = { sourceKey, fileIndex, userAgent };
53
56
  const plan = payload.waitForReady === true
54
57
  ? await controller.getReadyPlan(params, { signal: cancellation.signal }) : await controller.getPlan(params);
58
+ const endedAt = performance.now();
59
+ observeWait?.(sourceKey, "process", { ...waitFields, phase: "end", at: endedAt, startedAt,
60
+ endedAt, durationMs: endedAt - startedAt, outcome: "plan-ready" });
55
61
  return reply.send(plan);
56
62
  } catch (error) {
63
+ const endedAt = performance.now();
64
+ observeWait?.(sourceKey, "process", { ...waitFields, phase: "end", at: endedAt, startedAt,
65
+ endedAt, durationMs: endedAt - startedAt, outcome: cancellation.signal.aborted ? "cancelled" : "error" });
57
66
  if (cancellation.signal.aborted) return;
58
67
  if (error instanceof Error && error.code === "SOURCE_NOT_FOUND") {
59
68
  return reply.code(404).send({ error: error.message });
package/server.js CHANGED
@@ -1614,7 +1614,8 @@ export async function startProxyServer({
1614
1614
  handleApiClientLogsPost(req, reply, { clientLogs, viewerRequests: outputParts.viewerRequests })
1615
1615
  );
1616
1616
  app.post("/api/playback-plan", async (req, reply) =>
1617
- handleApiPlaybackPlanPost(req, reply, { playbackPlanner, viewers: outputParts.viewers })
1617
+ handleApiPlaybackPlanPost(req, reply, { playbackPlanner, viewers: outputParts.viewers,
1618
+ observeWait: (sourceKey, event, fields) => waitTrace.source(sourceKey, event, fields) })
1618
1619
  );
1619
1620
  app.post("/api/playback-plan/audio-tracks", async (req, reply) =>
1620
1621
  handleApiPlaybackPlanAudioTracksPost(req, reply, { playbackPlanner })
@@ -1234,7 +1234,21 @@ export class EncodeRuns {
1234
1234
  if (mediaRanges === false && tentative) return null;
1235
1235
  if (mediaRanges === false) throw Object.assign(new Error(`Closed piece #${index} has incomplete media.`),
1236
1236
  { code: "ERR_INCOMPLETE_MEDIA" });
1237
- const published = this.#host.segmentFiles.publish(address, name, session.segmentFormat, { mediaRanges, bytes });
1237
+ const publishStartedAt = performance.now();
1238
+ const publishProcess = session.spec?.carries === "audio-only" ? "P11.a" : "P11.v";
1239
+ this.#host.observeWait?.(session.file.sourceKey, "process", { process: publishProcess, phase: "begin",
1240
+ output: address, segment: index, at: publishStartedAt });
1241
+ let published;
1242
+ let publishOutcome = "error";
1243
+ try {
1244
+ published = this.#host.segmentFiles.publish(address, name, session.segmentFormat, { mediaRanges, bytes });
1245
+ publishOutcome = published ? "published" : "not-published";
1246
+ } finally {
1247
+ const publishEndedAt = performance.now();
1248
+ this.#host.observeWait?.(session.file.sourceKey, "process", { process: publishProcess, phase: "end",
1249
+ output: address, segment: index, at: publishEndedAt, startedAt: publishStartedAt, endedAt: publishEndedAt,
1250
+ durationMs: publishEndedAt - publishStartedAt, outcome: publishOutcome });
1251
+ }
1238
1252
  if (!published) throw new Error(`Closed piece #${index} could not be stored.`);
1239
1253
  if (index === safeIndex) this.#host.observeWait?.(session.file.sourceKey, "first-piece-published", {
1240
1254
  output: address, segment: index, startedAt: observedStart, endedAt: performance.now(),
@@ -865,6 +865,11 @@ export class ViewerRequests {
865
865
  }
866
866
 
867
867
  async #playbackReadinessFor(session, consumerId, progress, outputMbps) {
868
+ const waitStartedAt = performance.now();
869
+ const waitFields = { process: "P14", sessionId: session.id, consumerId };
870
+ this.#host.observeWait?.(session.file.sourceKey, "process", { ...waitFields, phase: "begin", at: waitStartedAt });
871
+ let waitOutcome = "error";
872
+ try {
868
873
  const now = Date.now();
869
874
  // WHERE AN ANSWER'S TIME GOES, said with each answer. On Home Assistant a
870
875
  // held viewer's answer took 11.2 s while its forecast took 0.1-0.4 s and
@@ -1116,7 +1121,14 @@ export class ViewerRequests {
1116
1121
  this.#host.noteViewerWaiting?.(consumerId || session.id,
1117
1122
  holding?.kind === "production" ? { address: holding.track, index: holding.segment } : null);
1118
1123
  delete forecast.firstRequiredStages;
1124
+ waitOutcome = "forecast-ready";
1119
1125
  return forecast;
1126
+ } finally {
1127
+ const waitEndedAt = performance.now();
1128
+ this.#host.observeWait?.(session.file.sourceKey, "process", { ...waitFields, phase: "end", at: waitEndedAt,
1129
+ startedAt: waitStartedAt, endedAt: waitEndedAt, durationMs: waitEndedAt - waitStartedAt,
1130
+ outcome: waitOutcome });
1131
+ }
1120
1132
  }
1121
1133
 
1122
1134
  /**
@@ -53,8 +53,9 @@ export class SourceWaitTrace {
53
53
  !Number.isSafeInteger(attempt) || attempt < 0) return;
54
54
  const context = { wait, attempt, operation, startedAt: this.now() };
55
55
  this.requests.set(req, context);
56
- const process = typeof statedProcess === "string" && /^[A-Z][0-9]+(?:\.(?:[0-9]+[a-z]?|[va]))?$/.test(statedProcess)
57
- ? statedProcess : processForRequest(req.url);
56
+ const endpointProcess = processForRequest(req.url);
57
+ const process = endpointProcess ?? (typeof statedProcess === "string" &&
58
+ /^[A-Z][0-9]+(?:\.(?:[0-9]+[a-z]?|[va]))?$/.test(statedProcess) ? statedProcess : null);
58
59
  this.write(context, "http-start", { process, phase: "begin", method: req.method, path: req.url,
59
60
  requestId: req.headers["x-ttv-forwarded-request-id"] ?? req.id });
60
61
  let finished = false;
@@ -92,11 +93,11 @@ export class SourceWaitTrace {
92
93
  }
93
94
 
94
95
  function processForRequest(path) {
96
+ if (/^\/api\/transcode-sessions\/[^/?]+\/progress(?:\?|$)/.test(path)) return "P14";
95
97
  if (path.startsWith("/api/transcode-sessions")) return "P6.v";
96
98
  if (path.startsWith("/api/playback-plan")) return "P3";
97
99
  if (/\/transcode\/.*\.m3u8(?:\?|$)/.test(path)) return "P12";
98
100
  if (/\/transcode\/.*(?:init|\.mp4)(?:\?|$)/.test(path)) return "P12";
99
101
  if (/\/transcode\/.*(?:\.m4s|\.ts)(?:\?|$)/.test(path)) return "P12";
100
- if (path.startsWith("/api/session-progress")) return "P14";
101
102
  return null;
102
103
  }