@torrent-tv/proxy 2.98.4 → 2.98.6

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.98.4",
3
+ "version": "2.98.6",
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
@@ -200,7 +200,7 @@ export async function startProxyServer({
200
200
  // The ONE forward reference here, and it is safe by construction: this fires
201
201
  // only once the worker thread is up and a piece has verified, which is long
202
202
  // after the declarations below have run.
203
- onPiecesArrived: ({ sourceKey, fileIndexes }) => {
203
+ onPiecesArrived: ({ sourceKey, fileIndexes, arrived }) => {
204
204
  for (const fileIndex of fileIndexes) {
205
205
  mediaReads.bytesChanged(sourceKey, fileIndex);
206
206
  probeReads.bytesChanged(sourceKey, fileIndex);
@@ -211,7 +211,9 @@ export async function startProxyServer({
211
211
  // is read again now; nothing was recorded for that wait.
212
212
  keyframeTables.readAgainIfUnanswered({ sourceKey, fileIndex, logName: String(fileIndex) });
213
213
  }
214
- outputParts.encodeInputs?.bytesChanged();
214
+ // Which bytes arrived, when the event names them: an input waiting for
215
+ // named bytes is tried again only when some of them have come.
216
+ outputParts.encodeInputs?.bytesChanged(Array.isArray(arrived) ? { sourceKey, ranges: arrived } : null);
215
217
  }
216
218
  });
217
219
  const selectedPort = await getPort({
@@ -259,10 +259,27 @@ export class EncodeInputs {
259
259
  return this.take(output, from, to);
260
260
  }
261
261
 
262
- bytesChanged() {
262
+ /**
263
+ * Bytes of a source changed: every waiting input is tried again, except one
264
+ * that waits for named bytes none of which have come. Tried at every piece
265
+ * of the file, a seek's input was prepared 28 times in 8.5 s on Home
266
+ * Assistant, each time asking the torrent thread which ranges it holds
267
+ * (171 ms on average), and the try that found its bytes could start only
268
+ * after the one in flight (r34, torrent-tv/meta#178).
269
+ *
270
+ * @param {{ sourceKey: string, ranges: Array<{ fileIndex: number, start: number, end: number }> } | null} [arrived] -
271
+ * The bytes that arrived, inclusive, when the event names them.
272
+ */
273
+ bytesChanged(arrived = null) {
263
274
  this.#revision++;
264
275
  for (const request of this.#requests.values()) {
265
- if (!request.pending && request.result?.kind !== "result" && request.result?.kind !== "terminal") this.#prepare(request);
276
+ if (request.pending || request.result?.kind === "result" || request.result?.kind === "terminal") continue;
277
+ if (arrived && request.result?.kind === "needs-bytes" && !awaitedIn(request.result, arrived)) {
278
+ // Nothing it waits for has come: it stands as tried at this revision.
279
+ request.revision = this.#revision;
280
+ continue;
281
+ }
282
+ this.#prepare(request);
266
283
  }
267
284
  }
268
285
 
@@ -486,7 +503,9 @@ export class EncodeInputs {
486
503
  // found pieces missing, and the run was refused for good (torrent-tv/meta#178).
487
504
  if (this.#heldRanges !== null && !await this.#present(chosen.sources, held, request.timing)) {
488
505
  const [first] = chosen.sources;
489
- return { kind: "needs-bytes", sourceKey: first.sourceKey, fileIndex: first.fileIndex, ranges: first.input.ranges };
506
+ return { kind: "needs-bytes", sourceKey: first.sourceKey, fileIndex: first.fileIndex, ranges: first.input.ranges,
507
+ // Every source of the stretch, so the bytes of any of them wake it.
508
+ awaiting: chosen.sources.map(source => ({ sourceKey: source.sourceKey, fileIndex: source.fileIndex, ranges: source.input.ranges })) };
490
509
  }
491
510
  if (this.#requests.get(request.key) !== request) return undefined;
492
511
  this.#wanted.set(request.key, { wanted: chosen.bytes, required: chosen.bytes });
@@ -527,6 +546,21 @@ export class EncodeInputs {
527
546
  }
528
547
  }
529
548
 
549
+ /**
550
+ * Whether some of the bytes an input waits for are among those that arrived.
551
+ *
552
+ * @param {{ sourceKey: string, fileIndex: number, ranges: Array<[number, number]>,
553
+ * awaiting?: Array<{ sourceKey: string, fileIndex: number, ranges: Array<[number, number]> }> }} waiting
554
+ * @param {{ sourceKey: string, ranges: Array<{ fileIndex: number, start: number, end: number }> }} arrived
555
+ * @returns {boolean}
556
+ */
557
+ function awaitedIn(waiting, arrived) {
558
+ const sources = waiting.awaiting ?? [waiting];
559
+ return sources.some(source => source.sourceKey === arrived.sourceKey && Array.isArray(source.ranges) &&
560
+ arrived.ranges.some(range => range.fileIndex === source.fileIndex &&
561
+ source.ranges.some(([start, end]) => range.start <= end && range.end >= start)));
562
+ }
563
+
530
564
  /**
531
565
  * Where a request has spent its time since it was asked, as one clause of its
532
566
  * log line. The resolutions include the reads of the file they made, and those
@@ -341,6 +341,14 @@ export class PlaybackProjection {
341
341
  #ordered = true;
342
342
  /** The segments wanted whose ranges are not worked out yet. @type {Set<Segment>} */
343
343
  #unresolved = new Set();
344
+ /**
345
+ * Outputs whose next run's input is to be raised again before the windows
346
+ * are told: worked out once for everything a map or a pass changed, not at
347
+ * each segment that changed — on a cold start that was once for each of
348
+ * 4500 segments worked out (r34, torrent-tv/meta#178).
349
+ * @type {Set<OutputState>}
350
+ */
351
+ #raiseAgain = new Set();
344
352
  /** The time base of the last map, or 0 for a map stated in absolute time. */
345
353
  #base = 0;
346
354
  #absolute = true;
@@ -408,6 +416,7 @@ export class PlaybackProjection {
408
416
  segment.ready = now;
409
417
  this.#activate(segment);
410
418
  this.#track(segment);
419
+ this.#raiseAgain.add(output);
411
420
  reworked++;
412
421
  }
413
422
  }
@@ -423,8 +432,8 @@ export class PlaybackProjection {
423
432
  }
424
433
  if (leadMoved) for (const segment of output.segments) if (this.#reckon(segment)) reworked++;
425
434
  }
435
+ if (fresh || output.target !== (info.target ?? null)) this.#raiseAgain.add(output);
426
436
  output.target = info.target ?? null;
427
- this.#raise(output);
428
437
  }
429
438
  for (const output of [...this.#outputs.values()]) if (!seen.has(output.outputKey)) this.#dropOutput(output);
430
439
  if (nextBase !== this.#base || absolute !== this.#absolute) this.#told = null;
@@ -463,7 +472,7 @@ export class PlaybackProjection {
463
472
  }
464
473
  this.#activate(segment);
465
474
  this.#track(segment);
466
- this.#raise(segment.output);
475
+ this.#raiseAgain.add(segment.output);
467
476
  }
468
477
 
469
478
  /**
@@ -507,6 +516,8 @@ export class PlaybackProjection {
507
516
  * deferred: boolean, deadlineAt?: number, standing: true }>}
508
517
  */
509
518
  windows() {
519
+ for (const output of this.#raiseAgain) if (this.#outputs.get(output.outputKey) === output) this.#raise(output);
520
+ this.#raiseAgain.clear();
510
521
  if (this.#told && this.#dirty.size === 0) return this.#told;
511
522
  for (const key of this.#dirty) this.#makeWindows(key);
512
523
  this.#dirty.clear();
@@ -560,6 +571,7 @@ export class PlaybackProjection {
560
571
  this.#lead(segment);
561
572
  this.#activate(segment);
562
573
  this.#track(segment);
574
+ this.#raiseAgain.add(segment.output);
563
575
  return true;
564
576
  }
565
577
 
@@ -753,6 +765,7 @@ export class PlaybackProjection {
753
765
  this.#unresolved.delete(segment);
754
766
  }
755
767
  this.#outputs.delete(output.outputKey);
768
+ this.#raiseAgain.delete(output);
756
769
  this.#told = null;
757
770
  }
758
771
  }
@@ -1300,6 +1300,7 @@ export class TorrentPool {
1300
1300
  // depend on facts that are gone by the time it is said.
1301
1301
  if (Array.isArray(zones) && zones.length === 0) {
1302
1302
  withdrawPriorityMap(torrent, fileIndex);
1303
+ reconcileAll();
1303
1304
  // The ends of the file go with it. They are kept for as long as the file
1304
1305
  // is open, and this is what says it is not.
1305
1306
  // And this may have been the last thing anybody wanted of this torrent.
@@ -1346,6 +1347,11 @@ export class TorrentPool {
1346
1347
  // Bands the map no longer has: a viewer moved on, and what they had wanted
1347
1348
  // is not wanted by anybody now.
1348
1349
  withdrawPriorityMap(torrent, fileIndex, ordered.length);
1350
+ // THE SWARM IS TOLD WHEN THE MAP IS APPLIED, not at the next piece verified
1351
+ // or the pool's timer: on Home Assistant a seek's map was applied and the
1352
+ // swarm went on fetching the old first class for 1.16 s, until the next
1353
+ // piece happened to be verified (r34, torrent-tv/meta#178).
1354
+ reconcileAll();
1349
1355
  // WHAT THE SWARM WAS ACTUALLY TOLD, said on change and never on a timer.
1350
1356
  // The map was applied in silence: that it had been BUILT was visible in the
1351
1357
  // encoding's own line, and that the download had received it was visible
@@ -254,7 +254,8 @@ export class TorrentWorkerClient {
254
254
  this.#onPiecesArrived({
255
255
  sourceKey: message.sourceKey,
256
256
  fileIndexes: Array.isArray(message.fileIndexes) ? message.fileIndexes : [],
257
- ...(typeof message.reason === "string" ? { reason: message.reason } : {})
257
+ ...(typeof message.reason === "string" ? { reason: message.reason } : {}),
258
+ ...(Array.isArray(message.arrived) ? { arrived: message.arrived } : {})
258
259
  });
259
260
  break;
260
261
  default:
@@ -881,12 +881,36 @@ function describePieceBuffers() {
881
881
  * @param {object} torrent
882
882
  * @returns {void}
883
883
  */
884
- function announceArrivals(sourceKey, torrent, reason = "verified") {
884
+ function announceArrivals(sourceKey, torrent, reason = "verified", piece = null) {
885
885
  const fileIndexes = [...demandFor(torrent).register.files()];
886
886
  if (fileIndexes.length === 0) {
887
887
  return;
888
888
  }
889
- parentPort.postMessage({ type: Event.PIECES_CHANGED, sourceKey, fileIndexes, reason });
889
+ parentPort.postMessage({ type: Event.PIECES_CHANGED, sourceKey, fileIndexes, reason,
890
+ ...(Number.isSafeInteger(piece) ? { arrived: bytesOfPiece(torrent, piece) } : {}) });
891
+ }
892
+
893
+ /**
894
+ * The bytes of one piece, as each file it covers counts them: what a reader
895
+ * waiting for named bytes compares against, so a piece of another part of the
896
+ * file wakes nobody (torrent-tv/meta#178).
897
+ *
898
+ * @param {object} torrent
899
+ * @param {number} piece
900
+ * @returns {Array<{ fileIndex: number, start: number, end: number }>} Inclusive.
901
+ */
902
+ function bytesOfPiece(torrent, piece) {
903
+ const pieceLength = Number(torrent.pieceLength);
904
+ const length = Number(torrent.length);
905
+ if (!(pieceLength > 0) || !(length > 0)) return [];
906
+ const start = piece * pieceLength;
907
+ const end = Math.min(length, start + pieceLength) - 1;
908
+ const arrived = [];
909
+ (torrent.files ?? []).forEach((file, fileIndex) => {
910
+ const from = Math.max(start, file.offset), to = Math.min(end, file.offset + file.length - 1);
911
+ if (to >= from) arrived.push({ fileIndex, start: from - file.offset, end: to - file.offset });
912
+ });
913
+ return arrived;
890
914
  }
891
915
 
892
916
 
@@ -917,7 +941,7 @@ function ensureArrivalsWired(sourceKey, torrent) {
917
941
  }
918
942
  arrivalsWired.add(torrent);
919
943
  holdingsChanged();
920
- torrent.on("verified", () => announceArrivals(sourceKey, torrent));
944
+ torrent.on("verified", (piece) => announceArrivals(sourceKey, torrent, "verified", piece));
921
945
  torrent.on("piece-withdrawn", () => announceArrivals(sourceKey, torrent, "withdrawn"));
922
946
  torrent.once("close", () => {
923
947
  if (resolvedTorrents.get(sourceKey) !== torrent) return;