@torrent-tv/proxy 2.96.9 → 2.96.11

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.96.9",
3
+ "version": "2.96.11",
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
@@ -54,6 +54,8 @@ import { wireOutputs } from "./services/server/wire-outputs.js";
54
54
  import { createPlaybackPlanner } from "./services/media/playback-planner.js";
55
55
  import { KeyframeTables } from "./services/media/KeyframeTables.js";
56
56
  import { contentsOf } from "./services/torrent/Contents.js";
57
+ import { joinedWithinPieces } from "./services/torrent/demand/pieces.js";
58
+ import { withoutRepeatedZones } from "./services/torrent/map-zones.js";
57
59
  import { SubtitleOrchestrator } from "./services/media/SubtitleOrchestrator.js";
58
60
  import { containerOrchestrator, CONTAINER_HEAD_BYTES, describeWorkTags } from "./services/media/ContainerOrchestrator.js";
59
61
  import { readPlaybackDeclarations } from "./services/media/read-playback-declarations.js";
@@ -434,6 +436,9 @@ export async function startProxyServer({
434
436
  const media = await containerOrchestrator.inspect(params, "media-info");
435
437
  if (media.kind !== "result" || !map.isCurrent()) return [];
436
438
  const shift = Number(media.value?.startTimeSeconds) || 0;
439
+ // A zone's ranges are stated to the torrent at the precision it fetches:
440
+ // ranges closer than a piece hold the same pieces as one.
441
+ const pieceLength = Number(torrentPool.knownTorrent(map.sourceKey)?.pieceLength);
437
442
  const converted = [];
438
443
  const demands = [];
439
444
  for (const output of outputParts.outputs.values()) {
@@ -483,7 +488,7 @@ export async function startProxyServer({
483
488
  modes: Object.fromEntries(wanted.map(track => [track.trackNumber, modes.get(track)])) };
484
489
  const { tracks: _choices, owner: _owner, sourceInterval: _sourceInterval, ...demand } = zone;
485
490
  const convert = ranges => {
486
- for (const [byteStart, byteEnd] of ranges) converted.push({ ...demand,
491
+ for (const [byteStart, byteEnd] of joinedWithinPieces(ranges, pieceLength)) converted.push({ ...demand,
487
492
  downloadInterval: { from: zone.owner.from, to: zone.owner.to }, byteStart, byteEnd });
488
493
  };
489
494
  // The bytes one segment needs are a fact of the file and the output, so
@@ -529,7 +534,9 @@ export async function startProxyServer({
529
534
  segmentSourceRanges.set(rangesKey, input.ranges);
530
535
  convert(input.ranges);
531
536
  }
532
- return converted;
537
+ // The ranges a picture reads at open are named again by every segment;
538
+ // only the zone the swarm would rank first is sent (torrent-tv/meta#166).
539
+ return withoutRepeatedZones(converted);
533
540
  }
534
541
  });
535
542
  const mediaReads = new MediaReadRequests({
@@ -106,3 +106,29 @@ export function nearestFirst({ byteStart, byteEnd, parts }) {
106
106
  }
107
107
  return ranges;
108
108
  }
109
+
110
+ /**
111
+ * Byte ranges joined wherever the gap between two of them is shorter than a
112
+ * piece: the pieces they hold are the same, and there are fewer of them.
113
+ *
114
+ * A gap shorter than a piece cannot hold a whole piece, so every piece it
115
+ * touches is touched by a range beside it. The soundtrack of an AVI read from
116
+ * its own packets is one range per packet, interleaved with the picture's
117
+ * bytes: field 2026-10-10 (torrent-tv/meta#166), a map of 350 191 zones took
118
+ * 20.8 s to reach the torrent thread and grew its heap to 818 MB, for pieces of
119
+ * 4 MiB that the whole soundtrack touches anyway.
120
+ *
121
+ * @param {Array<[number, number]>} ranges - Inclusive byte ranges in ascending order.
122
+ * @param {number} pieceLength
123
+ * @returns {Array<[number, number]>} The same ranges when the piece length is not usable.
124
+ */
125
+ export function joinedWithinPieces(ranges, pieceLength) {
126
+ if (!Number.isFinite(pieceLength) || pieceLength <= 0) return ranges;
127
+ const joined = [];
128
+ for (const [start, end] of ranges) {
129
+ const previous = joined.at(-1);
130
+ if (previous && start >= previous[0] && start - previous[1] - 1 < pieceLength) previous[1] = Math.max(previous[1], end);
131
+ else joined.push([start, end]);
132
+ }
133
+ return joined;
134
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @file What one zone of the priority map tells the torrent: the level of
3
+ * urgency it is stated at, and the band the swarm ranks it by.
4
+ *
5
+ * Pure arithmetic over the zone's own fields, so the main thread can ask the
6
+ * same question before a map is sent and the torrent thread when it is applied.
7
+ */
8
+ import { isAtAWatchingViewer, isBehindEverybody, isNobodyComingNow } from "../viewer/PriorityMap.js";
9
+ import { Urgency } from "./demand/Urgency.js";
10
+ import { compareBands } from "./download/bands.js";
11
+
12
+ /**
13
+ * Which level of urgency one zone is stated at, from the map's own number
14
+ * and nothing else. The map's numbers are a scale as long as the film needs
15
+ * — ten bands on a fifty-minute film — while the register has five levels,
16
+ * and the fit is by meaning:
17
+ *
18
+ * - the top of the scale is where a viewer is standing, so it is the
19
+ * cushion being built: {@link Urgency.NEAR}. Never BLOCKED — that level
20
+ * means a reader is stopped on those bytes right now, which only a read
21
+ * can say;
22
+ * - the bottom is what nobody is approaching: behind a viewer moving
23
+ * forward, and the whole film of a viewer who has stopped the picture.
24
+ * Wanted only if somebody seeks back, which is {@link Urgency.BEHIND};
25
+ * - one above the bottom is the far tail — wanted for certain if the
26
+ * viewer watches on, and not before: {@link Urgency.TAIL};
27
+ * - everything between is the lead being built: {@link Urgency.AHEAD}.
28
+ *
29
+ * Read from the scale's own ends rather than from the highest and lowest
30
+ * number in THIS file's map. The two speculative levels are withheld across
31
+ * every torrent at once while anything urgent is missing anywhere, so a
32
+ * paused viewer's film has to compare as wanted-last against another film
33
+ * somebody is watching — and relative to itself alone it would compare as
34
+ * the most urgent thing there is.
35
+ *
36
+ * @param {{ priority?: number, behind?: boolean, deferred?: boolean, urgent?: boolean }} zone
37
+ * @returns {number}
38
+ */
39
+ export function levelOfMapZone(zone) {
40
+ // Read through the map's own words rather than by comparing its numbers.
41
+ // The scale is that layer's, and the numbers inside a band mean nothing
42
+ // but their order.
43
+ const priority = zone.priority ?? 0;
44
+ if (zone.behind === true || (zone.behind === undefined && isBehindEverybody(priority))) {
45
+ return Urgency.BEHIND;
46
+ }
47
+ if (zone.deferred === true || (zone.deferred === undefined && isNobodyComingNow(priority))) {
48
+ // In front of somebody who has stopped the picture, and of nobody who is
49
+ // watching. Wanted, and wanted after everyone who is on their way.
50
+ return Urgency.TAIL;
51
+ }
52
+ return (zone.urgent === true || (zone.urgent === undefined && isAtAWatchingViewer(priority))) ? Urgency.NEAR : Urgency.AHEAD;
53
+ }
54
+
55
+ /** The priority a zone is stated at, within the register's scale. */
56
+ export function priorityOfMapZone(zone) {
57
+ return Number.isFinite(zone.priority) ? Math.max(1, Math.min(100, zone.priority)) : 1;
58
+ }
59
+
60
+ /** When a zone's bytes are needed, as an instant; `now` reads `withinSeconds`. */
61
+ export function deadlineOfMapZone(zone, now = Date.now()) {
62
+ return Number.isFinite(zone.deadlineAt) ? zone.deadlineAt :
63
+ Number.isFinite(zone.withinSeconds) ? now + Math.max(0, zone.withinSeconds) * 1000 : Number.POSITIVE_INFINITY;
64
+ }
65
+
66
+ /**
67
+ * The zones with every repeat of the same bytes left out but the one the swarm
68
+ * would rank first.
69
+ *
70
+ * Each piece is fetched in the band of the best zone that holds it
71
+ * ({@link compareBands}), so a zone naming exactly the bytes of a better one
72
+ * changes nothing the swarm is told. The ranges an AVI's picture reads at open
73
+ * — its declarations and its indexes — are named again in the zone of every
74
+ * segment: field 2026-10-10 (torrent-tv/meta#166), maps of 9700 zones, for a
75
+ * film of 2035 segments, cost half a second each to deliver.
76
+ *
77
+ * @template {{ byteStart: number, byteEnd: number }} Zone
78
+ * @param {Zone[]} zones
79
+ * @param {number} [now]
80
+ * @returns {Zone[]} In their order, without the repeats.
81
+ */
82
+ export function withoutRepeatedZones(zones, now = Date.now()) {
83
+ const bandOf = zone => ({ urgency: levelOfMapZone(zone), priority: priorityOfMapZone(zone), deadlineAt: deadlineOfMapZone(zone, now) });
84
+ const best = new Map();
85
+ zones.forEach((zone, index) => {
86
+ const key = `${zone.byteStart}:${zone.byteEnd}`;
87
+ const kept = best.get(key);
88
+ if (!kept || compareBands(bandOf(zone), kept.band) < 0) best.set(key, { index, band: bandOf(zone) });
89
+ });
90
+ return zones.filter((zone, index) => best.get(`${zone.byteStart}:${zone.byteEnd}`).index === index);
91
+ }
@@ -20,7 +20,7 @@ import { demandProgress } from "./demand-progress.js";
20
20
  import { Urgency, urgencyName } from "./demand/index.js";
21
21
  import { demandFor, firstClassStatement, forgetTorrent, reconcileAll, hasUnmetDemand } from "./download/registry.js";
22
22
  import { withdrawClaim } from "./download/withdraw-claim.js";
23
- import { isAtAWatchingViewer, isBehindEverybody, isNobodyComingNow } from "../viewer/PriorityMap.js";
23
+ import { deadlineOfMapZone, levelOfMapZone, priorityOfMapZone } from "./map-zones.js";
24
24
  import { deriveSourceKey } from "../../utils/torrent-source-key.js";
25
25
  import { noteTorrentDestroyed } from "./destroyed-torrents.js";
26
26
 
@@ -1290,48 +1290,9 @@ export class TorrentPool {
1290
1290
  .filter((zone) => Number.isSafeInteger(zone?.byteStart) && Number.isSafeInteger(zone?.byteEnd) &&
1291
1291
  zone.byteStart >= 0 && zone.byteEnd >= zone.byteStart && zone.byteStart < length)
1292
1292
  .sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
1293
- /**
1294
- * Which level of urgency one zone is stated at, from the map's own number
1295
- * and nothing else. The map's numbers are a scale as long as the film needs
1296
- * — ten bands on a fifty-minute film — while the register has five levels,
1297
- * and the fit is by meaning:
1298
- *
1299
- * - the top of the scale is where a viewer is standing, so it is the
1300
- * cushion being built: {@link Urgency.NEAR}. Never BLOCKED — that level
1301
- * means a reader is stopped on those bytes right now, which only a read
1302
- * can say;
1303
- * - the bottom is what nobody is approaching: behind a viewer moving
1304
- * forward, and the whole film of a viewer who has stopped the picture.
1305
- * Wanted only if somebody seeks back, which is {@link Urgency.BEHIND};
1306
- * - one above the bottom is the far tail — wanted for certain if the
1307
- * viewer watches on, and not before: {@link Urgency.TAIL};
1308
- * - everything between is the lead being built: {@link Urgency.AHEAD}.
1309
- *
1310
- * Read from the scale's own ends rather than from the highest and lowest
1311
- * number in THIS file's map. The two speculative levels are withheld across
1312
- * every torrent at once while anything urgent is missing anywhere, so a
1313
- * paused viewer's film has to compare as wanted-last against another film
1314
- * somebody is watching — and relative to itself alone it would compare as
1315
- * the most urgent thing there is.
1316
- *
1317
- * @param {{ priority?: number }} zone
1318
- * @returns {number}
1319
- */
1320
- const levelOf = (zone) => {
1321
- // Read through the map's own words rather than by comparing its numbers.
1322
- // The scale is that layer's, and the numbers inside a band mean nothing
1323
- // but their order.
1324
- const priority = zone.priority ?? 0;
1325
- if (zone.behind === true || (zone.behind === undefined && isBehindEverybody(priority))) {
1326
- return Urgency.BEHIND;
1327
- }
1328
- if (zone.deferred === true || (zone.deferred === undefined && isNobodyComingNow(priority))) {
1329
- // In front of somebody who has stopped the picture, and of nobody who is
1330
- // watching. Wanted, and wanted after everyone who is on their way.
1331
- return Urgency.TAIL;
1332
- }
1333
- return (zone.urgent === true || (zone.urgent === undefined && isAtAWatchingViewer(priority))) ? Urgency.NEAR : Urgency.AHEAD;
1334
- };
1293
+ // Which level each zone is stated at: `map-zones.js`, where the main thread
1294
+ // reads the same rule before it sends a map.
1295
+ const levelOf = levelOfMapZone;
1335
1296
  ordered.forEach((zone, index) => {
1336
1297
  // Only ranges declared by the container index or metadata read are wanted.
1337
1298
  const byteStart = zone.byteStart;
@@ -1345,10 +1306,9 @@ export class TorrentPool {
1345
1306
  byteStart,
1346
1307
  byteEnd,
1347
1308
  urgency: levelOf(zone),
1348
- priority: Number.isFinite(zone.priority) ? Math.max(1, Math.min(100, zone.priority)) : 1,
1309
+ priority: priorityOfMapZone(zone),
1349
1310
  order: Number.isSafeInteger(zone.order) && zone.order >= 0 ? zone.order : 0,
1350
- deadlineAt: Number.isFinite(zone.deadlineAt) ? zone.deadlineAt :
1351
- Number.isFinite(zone.withinSeconds) ? Date.now() + Math.max(0, zone.withinSeconds) * 1000 : Number.POSITIVE_INFINITY,
1311
+ deadlineAt: deadlineOfMapZone(zone),
1352
1312
  requestId: zone.requestId ?? `${torrent.infoHash}:${fileIndex}:map`
1353
1313
  });
1354
1314
  });