@torrent-tv/proxy 2.107.32 → 2.107.33

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.107.32",
3
+ "version": "2.107.33",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
package/server.js CHANGED
@@ -917,7 +917,8 @@ export async function startProxyServer({
917
917
  reviseBudget: () => { void outputParts.machineBudget.revise("memory", "media metadata").catch(error => logger.warn(`packet index budget: ${error.message}`)); },
918
918
  changed: () => { mediaReads.memoryChanged(); probeReads.memoryChanged(); outputParts.encodeInputs?.memoryChanged(); }
919
919
  });
920
- const containerOver = async ({ sourceKey, fileIndex, requestId, packetInterval, demand, readPriority }) => {
920
+ const encodeMetadataRequests = new WeakMap();
921
+ const containerOver = async ({ sourceKey, fileIndex, requestId, packetInterval, demand, readPriority, signal }) => {
921
922
  const record = sourceRegistry.get(sourceKey);
922
923
  if (!record) {
923
924
  return null;
@@ -927,10 +928,34 @@ export async function startProxyServer({
927
928
  if (!file || !(file.length > 0)) {
928
929
  return null;
929
930
  }
931
+ let metadataTracker = null;
932
+ let trackersForSignal = null;
933
+ const trackerKey = `${sourceKey}:${fileIndex}`;
934
+ if (readPriority?.requestId && signal) {
935
+ trackersForSignal = encodeMetadataRequests.get(signal);
936
+ if (!trackersForSignal) encodeMetadataRequests.set(signal, trackersForSignal = new Map());
937
+ metadataTracker = trackersForSignal.get(trackerKey);
938
+ if (!metadataTracker) {
939
+ metadataTracker = { statements: new Set(), abort: null };
940
+ metadataTracker.abort = () => {
941
+ for (const statement of metadataTracker.statements) {
942
+ void downloadMaps.withdrawMetadata(sourceKey, fileIndex, statement).catch(error =>
943
+ logger.warn(`encode input demand withdrawal failed: ${error.message}`));
944
+ }
945
+ metadataTracker.statements.clear();
946
+ trackersForSignal.delete(trackerKey);
947
+ if (trackersForSignal.size === 0) encodeMetadataRequests.delete(signal);
948
+ };
949
+ trackersForSignal.set(trackerKey, metadataTracker);
950
+ if (signal.aborted) metadataTracker.abort();
951
+ else signal.addEventListener("abort", metadataTracker.abort, { once: true });
952
+ }
953
+ }
930
954
  const params = {
931
955
  sourceKey,
932
956
  fileIndex,
933
957
  requestId,
958
+ signal,
934
959
  packetInterval,
935
960
  readPriority,
936
961
  packetMemory: indexMemory.forFile(sourceKey, fileIndex),
@@ -969,9 +994,17 @@ export async function startProxyServer({
969
994
  }
970
995
  const finished = mediaReads.record(params, statement, result, revision.storage, revision.memory);
971
996
  if (statement === "chapters" && result.passive) await downloadMaps.withdrawMetadata(sourceKey, fileIndex, statement);
972
- else await downloadMaps.metadata({ sourceKey, fileIndex, statement: requestId ? `${statement}:${requestId}` : statement, result,
973
- ...(statement === "chapters" ? { priority: 1, urgent: false, deadlineAt: null } : {}),
974
- ...(demand ? { priority: demand.priority, urgent: demand.urgent, deadlineAt: demand.deadlineAt, interval: demand, leadSeconds: demand.leadSeconds ?? 0 } : {}) });
997
+ else {
998
+ const metadataStatement = requestId ? `${statement}:${requestId}` : statement;
999
+ if (metadataTracker && result.kind === "needs-ranges") metadataTracker.statements.add(metadataStatement);
1000
+ if (metadataTracker && result.kind !== "needs-ranges") metadataTracker.statements.delete(metadataStatement);
1001
+ await downloadMaps.metadata({ sourceKey, fileIndex, statement: metadataStatement, result,
1002
+ ...(statement === "chapters" ? { priority: 1, urgent: false, deadlineAt: null } : {}),
1003
+ ...(demand ? { priority: demand.priority, urgent: demand.urgent, deadlineAt: demand.deadlineAt, interval: demand, leadSeconds: demand.leadSeconds ?? 0 } : {}),
1004
+ ...(readPriority?.requestId ? { priority: readPriority.rank, urgent: readPriority.urgent === true,
1005
+ deadlineAt: Number.isFinite(readPriority.deadlineAt) ? readPriority.deadlineAt : Number.POSITIVE_INFINITY,
1006
+ scope: "preparation" } : {}) });
1007
+ }
975
1008
  if (finished && result.kind === "result") {
976
1009
  void downloadMaps.refresh(sourceKey, fileIndex).catch(error =>
977
1010
  logger.warn(`download map source=${sourceKey} file=${fileIndex} metadata refresh failed: ${error?.message ?? error}`));
@@ -1042,7 +1075,8 @@ export async function startProxyServer({
1042
1075
  // reassembled packets otherwise.
1043
1076
  const files = [];
1044
1077
  for (const [fileIndex, choices] of selected) {
1045
- const params = await containerOver({ sourceKey: output.file.sourceKey, fileIndex, readPriority });
1078
+ const params = await containerOver({ sourceKey: output.file.sourceKey, fileIndex,
1079
+ requestId: readPriority?.requestId, readPriority, signal });
1046
1080
  if (!params) return { kind: "needs-source" };
1047
1081
  if (isCurrent) params.isCurrent = isCurrent;
1048
1082
  if (signal) params.signal = signal;
@@ -1145,6 +1179,7 @@ export async function startProxyServer({
1145
1179
  .finally(() => releaseInput?.());
1146
1180
  }
1147
1181
  },
1182
+ repriceEncodeDemand: demand => downloadMaps.repriceRequest(demand.requestId, demand),
1148
1183
  readEncodeRanges: async (source, ranges, maxBytes) => {
1149
1184
  const torrent = torrentPool.knownTorrent(source.sourceKey);
1150
1185
  return torrent ? torrentPool.readHeldRangesOf(torrent, source.fileIndex, ranges, maxBytes) : null;
@@ -56,6 +56,7 @@ export class EncodeInputs {
56
56
  #capacity;
57
57
  #urgent;
58
58
  #rankAt;
59
+ #demandChanged;
59
60
  #log;
60
61
  #now;
61
62
  /**
@@ -68,7 +69,7 @@ export class EncodeInputs {
68
69
  #observeWait;
69
70
 
70
71
  constructor({ resolve, readRanges, openWholeFile = null, heldRanges = null, reviseBudget, changed, failed, capacity = () => null, urgent = () => true,
71
- rankAt = () => null, openOf = () => null, log = () => {}, now = Date.now, stretchCopied = () => {},
72
+ rankAt = () => null, demandChanged = () => {}, openOf = () => null, log = () => {}, now = Date.now, stretchCopied = () => {},
72
73
  observeWait = null }) {
73
74
  this.#stretchCopied = stretchCopied;
74
75
  this.#observeWait = observeWait;
@@ -83,6 +84,7 @@ export class EncodeInputs {
83
84
  this.#capacity = capacity;
84
85
  this.#urgent = urgent;
85
86
  this.#rankAt = rankAt;
87
+ this.#demandChanged = demandChanged;
86
88
  this.#log = log;
87
89
  this.#now = now;
88
90
  }
@@ -204,7 +206,12 @@ export class EncodeInputs {
204
206
  if (request.output !== output) continue;
205
207
  const covered = windows.some(window => request.from <= window.to && request.to >= window.from);
206
208
  const unused = starts !== null && request.result?.kind === "result" && !starts.has(request.from);
207
- if (covered && !unused) continue;
209
+ if (covered && !unused) {
210
+ const current = this.#readPriority(request, request.from, request.from);
211
+ Object.assign(request.readPriority ??= {}, current);
212
+ this.#demandChanged(output, request.readPriority);
213
+ continue;
214
+ }
208
215
  this.#note(request, covered ? "withdrawn: no live run ends before it and no start is placed there"
209
216
  : "withdrawn: no wanted window covers it");
210
217
  this.#requests.delete(key);
@@ -503,13 +510,15 @@ export class EncodeInputs {
503
510
  * @param {object} request
504
511
  * @param {number} from
505
512
  * @param {number} to
506
- * @returns {{ rank: number, serves: number, from: number, to: number }}
513
+ * @returns {{ rank: number, topRank: number, serves: number, from: number, to: number, urgent?: boolean, deadlineAt?: number, requestId: string }}
507
514
  */
508
515
  #readPriority(request, from, to) {
509
516
  const serves = request.serves ?? request.from;
510
517
  const rank = this.#rankAt(request.output, serves);
511
518
  return { rank: Number.isFinite(rank?.rank) ? rank.rank : 0, topRank: Number.isFinite(rank?.topRank) ? rank.topRank : 0,
512
- serves, from, to };
519
+ ...(typeof rank?.urgent === "boolean" ? { urgent: rank.urgent } : {}),
520
+ ...(Number.isFinite(rank?.deadlineAt) ? { deadlineAt: rank.deadlineAt } : {}),
521
+ serves, from, to, requestId: `encode:${request.key}` };
513
522
  }
514
523
 
515
524
  /**
@@ -503,6 +503,11 @@ export class EncodeOrchestrator {
503
503
  return this.#demand.rankOf(address, index);
504
504
  }
505
505
 
506
+ /** The viewer-derived priority, urgency and due time for one output segment. */
507
+ demandAt(address, index) {
508
+ return this.#demand.demandAt(address, index);
509
+ }
510
+
506
511
  /**
507
512
  * How many encoder processes are running on this machine right now, over
508
513
  * every output.
@@ -92,4 +92,27 @@ export class SegmentDemand {
92
92
  }
93
93
  return { rank, topRank };
94
94
  }
95
+
96
+ /**
97
+ * The viewer-derived demand for one output segment, including its due time.
98
+ *
99
+ * @param {string} address
100
+ * @param {number} index
101
+ * @param {number} now
102
+ * @returns {{ rank: number, topRank: number, urgent: boolean, deadlineAt: number }}
103
+ */
104
+ demandAt(address, index, now = Date.now()) {
105
+ const zones = this.mapOn(address);
106
+ let rank = 0, topRank = 0, urgent = false, deadlineAt = Number.POSITIVE_INFINITY;
107
+ for (const zone of zones) {
108
+ topRank = Math.max(topRank, Number.isFinite(zone.priority) ? zone.priority : 0);
109
+ if (index < zone.from || index > zone.to || !(zone.priority > 0)) continue;
110
+ rank = Math.max(rank, zone.priority);
111
+ urgent ||= zone.urgent === true;
112
+ const due = Number.isFinite(zone.deadlineAt) ? zone.deadlineAt
113
+ : Number.isFinite(zone.withinSeconds) ? now + Math.max(0, zone.withinSeconds) * 1000 : Number.POSITIVE_INFINITY;
114
+ deadlineAt = Math.min(deadlineAt, due);
115
+ }
116
+ return { rank, topRank, urgent, deadlineAt };
117
+ }
95
118
  }
@@ -228,6 +228,7 @@ export function wireOutputs({
228
228
  diagnosticsRoot = "",
229
229
  memoryClaimant = null,
230
230
  resolveEncodeInput = null,
231
+ repriceEncodeDemand = null,
231
232
  readSourceMedia = null,
232
233
  readMetadataActivity = null,
233
234
  sourceInputsFor = null,
@@ -1191,7 +1192,12 @@ export function wireOutputs({
1191
1192
  urgent: (output, index) => parts.encodeOrchestrator.wantedSegmentsOn(output.outputKey).some(window =>
1192
1193
  index >= window.from && index <= window.to && window.urgent === true),
1193
1194
  // The piece's rank in the output's own map, said with an input's state.
1194
- rankAt: (output, index) => parts.encodeOrchestrator?.rankAt?.(output.outputKey ?? "", index) ?? null,
1195
+ rankAt: (output, index) => parts.encodeOrchestrator?.demandAt?.(output.outputKey ?? "", index) ?? null,
1196
+ demandChanged: (_output, demand) => {
1197
+ void Promise.resolve(repriceEncodeDemand?.({ priority: demand.rank, urgent: demand.urgent === true,
1198
+ deadlineAt: Number.isFinite(demand.deadlineAt) ? demand.deadlineAt : Number.POSITIVE_INFINITY,
1199
+ requestId: demand.requestId })).catch(error => logger.warn(`encode input demand repricing failed: ${error.message}`));
1200
+ },
1195
1201
  log: line => logger.info(line),
1196
1202
  observeWait,
1197
1203
  changed: (output, result) => {
@@ -238,6 +238,22 @@ export class DownloadMaps {
238
238
  return this.#emit(file);
239
239
  }
240
240
 
241
+ /** Reprice source metadata owned by one live encode input request. */
242
+ repriceRequest(requestId, demand) {
243
+ const changed = [];
244
+ for (const file of this.#files.values()) {
245
+ let fileChanged = false;
246
+ for (const metadata of file.metadata.values()) {
247
+ if (metadata.requestId !== requestId) continue;
248
+ if (Object.keys(demand).every(key => Object.is(metadata[key], demand[key]))) continue;
249
+ Object.assign(metadata, demand);
250
+ fileChanged = true;
251
+ }
252
+ if (fileChanged) changed.push(this.#emit(file));
253
+ }
254
+ return Promise.all(changed);
255
+ }
256
+
241
257
  forget(sourceKey, fileIndex, { keepPreparation = false } = {}) {
242
258
  const file = this.#file(sourceKey, fileIndex);
243
259
  this.#setZones(file, []);