@torrent-tv/proxy 2.98.2 → 2.98.4

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.
@@ -49,6 +49,10 @@ export class DownloadMaps {
49
49
  // Assistant 2026-10-07, torrent-tv/meta#95). A zone no new zone overlaps —
50
50
  // a seek away — is withdrawn at once.
51
51
  const retained = file.zones.flatMap(zone => {
52
+ // A window its resolver keeps current itself — the projection of the
53
+ // file's segments, brought to this map by the pass below — stands as it
54
+ // is until the pass states it (`PlaybackProjection`, torrent-tv/meta#178).
55
+ if (zone.standing === true) return [zone];
52
56
  const interval = zone.downloadInterval ?? zone;
53
57
  const current = sourceZones.exactly(interval);
54
58
  if (!current) {
@@ -343,7 +347,10 @@ export class DownloadMaps {
343
347
  const kept = file.packed;
344
348
  if (kept && kept.zones === file.zones && kept.nativeZones === file.nativeZones) return kept.result;
345
349
  const input = [...file.zones, ...file.nativeZones];
346
- const result = this.#pack(input);
350
+ // Windows already packed by their resolver are not packed again unless
351
+ // something else is to be packed with them.
352
+ const result = file.nativeZones.length === 0 && file.zones.length > 0 && file.zones.every(zone => zone.standing === true)
353
+ ? file.zones : this.#pack(input);
347
354
  const relative = input.some(zone => !Number.isFinite(zone.deadlineAt) && Number.isFinite(zone.withinSeconds));
348
355
  file.packed = relative ? null : { zones: file.zones, nativeZones: file.nativeZones, result };
349
356
  return result;
@@ -435,7 +442,11 @@ function zoneIndexOf(file) {
435
442
  function withRetained(resolved, retained) {
436
443
  const keyOf = zone => typeof zone.outputKey === "string" && Number.isSafeInteger(zone.index) ? `${zone.outputKey}:${zone.index}` : null;
437
444
  const resolvedKeys = new Set(resolved.map(keyOf).filter(key => key !== null));
445
+ // Windows a resolver keeps current are all of what it states: the ones it
446
+ // stated before are what it states now.
447
+ const standing = resolved.some(zone => zone.standing === true);
438
448
  return [...resolved, ...retained.filter(zone => {
449
+ if (standing && zone.standing === true) return false;
439
450
  const key = keyOf(zone);
440
451
  return key === null || !resolvedKeys.has(key);
441
452
  })];
@@ -388,6 +388,9 @@ export class Viewer {
388
388
  * picture is in picture-in-picture. Absent means on screen.
389
389
  * @param {boolean} [report.inPictureInPicture] - Whether the picture has been
390
390
  * pulled out of the page, which is watching it with the tab hidden.
391
+ * @param {number} [report.generation] - Which of this viewer's viewings the
392
+ * page made the report in (`Assignments`). Absent from a page one release
393
+ * behind; then the report is taken as current.
391
394
  * @param {number} [now]
392
395
  */
393
396
  report(
@@ -406,11 +409,18 @@ export class Viewer {
406
409
  onScreen,
407
410
  inPictureInPicture,
408
411
  qualityMode,
409
- visiblePicture
412
+ visiblePicture,
413
+ generation
410
414
  },
411
415
  now = Date.now()
412
416
  ) {
413
417
  this.noteVisiblePicture(visiblePicture);
418
+ // A REPORT MADE BEFORE THE VIEWER'S LAST SEEK says where they were, not
419
+ // where they are: sent just before the seek and answered after it, it
420
+ // would place them back in the viewing they left, and the deadlines worked
421
+ // out from their position would follow it (torrent-tv/meta#178 review).
422
+ // What it measured of the link is still a measurement of the link.
423
+ const stale = this.isOfAnEarlierViewing(generation);
414
424
  // Whether the size on their screen was picked by hand or is the automatic
415
425
  // choice. Kept only when stated: a page one release behind says nothing,
416
426
  // and is taken as picking, which is what every page was before.
@@ -428,6 +438,7 @@ export class Viewer {
428
438
  const linkMeasuredAt = hasLinkMeasurement ? now : this.netReport?.linkMeasuredAt ?? null;
429
439
  const sampleMbps = Number(linkSampleMbps);
430
440
  const sampleAt = Number(linkSampleAt);
441
+ const previous = this.netReport;
431
442
  const hasPlaybackSample = Number.isFinite(sampleMbps) && sampleMbps > 0 &&
432
443
  Number.isFinite(sampleAt) && sampleAt > 0;
433
444
  const newerPlaybackSample = hasPlaybackSample && sampleAt > (this.netReport?.linkSampleAt ?? 0);
@@ -458,6 +469,13 @@ export class Viewer {
458
469
  };
459
470
  // Where the picture is now. A report states a place the viewer has played
460
471
  // to, not a place they moved to: their buffer's history stands.
472
+ if (stale) {
473
+ this.netReport = { ...this.netReport, positionSeconds: previous?.positionSeconds ?? null,
474
+ bufferedAheadSec: previous?.bufferedAheadSec ?? 0, bufferedRanges: previous?.bufferedRanges ?? null,
475
+ timestampOffsets: previous?.timestampOffsets ?? {}, at: previous?.at ?? now };
476
+ this.seen(now);
477
+ return;
478
+ }
461
479
  if (Number.isFinite(positionSeconds) && positionSeconds >= 0) {
462
480
  this.#place(/** @type {number} */ (positionSeconds), now);
463
481
  }
@@ -491,6 +509,18 @@ export class Viewer {
491
509
  this.seen(now);
492
510
  }
493
511
 
512
+ /**
513
+ * Whether something the page stated in viewing `generation` was stated
514
+ * before this viewer's last seek. A page that names no viewing is taken as
515
+ * current, as every page was before it could name one.
516
+ *
517
+ * @param {number | undefined} generation
518
+ * @returns {boolean}
519
+ */
520
+ isOfAnEarlierViewing(generation) {
521
+ return Number.isInteger(generation) && generation < this.assignments.generation;
522
+ }
523
+
494
524
  /**
495
525
  * Whether this viewer is here.
496
526
  *
@@ -1,5 +1,5 @@
1
- /** Translate map demand to complete output intervals and selected source tracks. */
2
- export function segmentDemands(output, fileIndex, zones) {
1
+ /** The tracks of this file an output reads, and how: copied or encoded again. */
2
+ export function trackChoicesOf(output, fileIndex) {
3
3
  const tracks = [];
4
4
  for (const type of ["video", "audio"]) {
5
5
  const spec = output.spec?.[type];
@@ -7,59 +7,5 @@ export function segmentDemands(output, fileIndex, zones) {
7
7
  tracks.push({ type, index: type === "video" ? 0 : spec.trackIndex,
8
8
  mode: type === "video" ? spec.encode ? "transcode" : "copy" : spec.transcode ? "transcode" : "copy" });
9
9
  }
10
- if (!tracks.length) return [];
11
- const grid = output.timeline?.published ?? output.timeline?.boundaries;
12
- if (!Array.isArray(grid)) return [];
13
- const demands = [];
14
- // THE ZONES ARE WALKED BESIDE THE SEGMENTS, not searched for each of them.
15
- // A map's zones come ascending and apart (`PriorityMap.runs`) and the grid is
16
- // ascending, so the zones a segment overlaps begin at or after those of the
17
- // segment before it. Every zone filtered for every segment was thousands of
18
- // segments by tens of zones on every pass over a film (r26,
19
- // torrent-tv/meta#178). Zones in any other order are filtered as before.
20
- const ordered = ascendingAndApart(zones);
21
- let first = 0;
22
- for (let index = 0; index + 1 < grid.length; index++) {
23
- const from = grid[index], to = grid[index + 1];
24
- if (!Number.isFinite(from) || !Number.isFinite(to) || !(to > from)) throw new Error("An output segment needs a valid interval.");
25
- if (ordered) while (first < zones.length && zones[first].to <= from) first += 1;
26
- // The zones a segment overlaps are folded as they are met, into numbers:
27
- // a list of them, and the lists made of it for each field, were five
28
- // arrays for every segment of a film on every pass (r30,
29
- // torrent-tv/meta#178).
30
- let owner = null, ownerDeadline = Infinity, priority = 0, urgent = false, behind = true, deferred = true;
31
- for (let at = ordered ? first : 0; at < zones.length; at += 1) {
32
- const zone = zones[at];
33
- if (ordered && !(zone.from < to)) break;
34
- if (!(zone.priority > 0) || !(zone.from < to && zone.to > from)) continue;
35
- // A zone states the deadline of its near edge. A later segment within it
36
- // is reached later, at one second of film per second of playback.
37
- const deadline = Number.isFinite(zone.deadlineAt) ? zone.deadlineAt + Math.max(0, from - zone.from) * 1000 : Infinity;
38
- if (owner === null || deadline < ownerDeadline) { owner = zone; ownerDeadline = deadline; }
39
- if (zone.priority > priority) priority = zone.priority;
40
- if (zone.urgent) urgent = true;
41
- if (!zone.behind) behind = false;
42
- if (!zone.deferred) deferred = false;
43
- }
44
- if (owner === null) continue;
45
- demands.push({ index, from, to, tracks, owner, priority, urgent, behind, deferred, deadlineAt: ownerDeadline });
46
- }
47
- return demands.sort((left, right) => left.deadlineAt - right.deadlineAt || right.priority - left.priority || left.index - right.index);
48
- }
49
-
50
- /** Whether zones come ascending and without overlaps, as a map's runs do. */
51
- function ascendingAndApart(zones) {
52
- for (let index = 1; index < zones.length; index += 1) {
53
- if (!(zones[index].from >= zones[index - 1].to)) return false;
54
- }
55
- return true;
56
- }
57
-
58
- /** Source bytes must arrive before measured processing and delivery consume time. */
59
- export function sourceDeadline(demand, { encodeSpeed, outputBytes, linkMbps }) {
60
- const span = demand.to - demand.from;
61
- const processing = Number.isFinite(encodeSpeed) && encodeSpeed > 0 ? span / encodeSpeed : 0;
62
- const delivery = Number.isFinite(outputBytes) && outputBytes > 0 && Number.isFinite(linkMbps) && linkMbps > 0
63
- ? outputBytes * 8 / (linkMbps * 1_000_000) : 0;
64
- return Number.isFinite(demand.deadlineAt) ? demand.deadlineAt - (processing + delivery) * 1000 : demand.deadlineAt;
10
+ return tracks;
65
11
  }
@@ -1,70 +0,0 @@
1
- /**
2
- * @file What one pass that turns a file's playback map into bytes is worked out
3
- * from, so a pass whose inputs have not changed hands back what the last one
4
- * resolved (torrent-tv/meta#178).
5
- *
6
- * The viewer's map moves a second at a time while segments are seconds long,
7
- * so most passes give the zones the pass before gave: on Home Assistant 30–110
8
- * of 50–120 download maps a minute came out unchanged, and working them out
9
- * took 72 s of the main thread in fifteen minutes (r22). The deadlines are left
10
- * out of what is compared and their order is kept in it: a segment's deadline
11
- * is when the viewer reaches it, which moves only when they stop or seek, and
12
- * what is sent to the torrent is compared by the deadlines' order alone
13
- * (`DownloadMaps` `shapeOf`).
14
- */
15
-
16
- /**
17
- * @param {object} params
18
- * @param {number} params.pieceLength - The precision the torrent fetches at.
19
- * @param {number} params.shift - Where the file's own clock starts.
20
- * @param {Map<string, number | null>} params.targets - Each output's next input, by output.
21
- * @param {Array<{ outputKey: string, index: number, priority: number, urgent?: boolean,
22
- * behind?: boolean, deferred?: boolean, from: number, to: number,
23
- * sourceInterval?: { from: number, to: number } }>} params.demands - In the
24
- * order of their deadlines.
25
- * @returns {{ outputs: string[], inputs: Float64Array }}
26
- */
27
- export function passInputs({ pieceLength, shift, targets, demands }) {
28
- const outputs = [...targets.keys()];
29
- const position = new Map(outputs.map((outputKey, at) => [outputKey, at]));
30
- // Numbers in a list made to its length: grown a value at a time, eight for
31
- // each of a film's 3500 segments, it was most of what comparing a pass cost
32
- // (r30, torrent-tv/meta#178).
33
- const inputs = new Float64Array(2 + targets.size + demands.length * 8);
34
- let at = 0;
35
- inputs[at++] = pieceLength;
36
- inputs[at++] = shift;
37
- for (const target of targets.values()) inputs[at++] = target ?? -1;
38
- for (const demand of demands) {
39
- // The segment's own interval too: a cut grid read again can give one
40
- // number other bytes.
41
- inputs[at++] = position.get(demand.outputKey) ?? -1;
42
- inputs[at++] = demand.index;
43
- inputs[at++] = demand.priority;
44
- inputs[at++] = (demand.urgent ? 1 : 0) + (demand.behind ? 2 : 0) + (demand.deferred ? 4 : 0);
45
- inputs[at++] = demand.from;
46
- inputs[at++] = demand.to;
47
- inputs[at++] = demand.sourceInterval?.from ?? -1;
48
- inputs[at++] = demand.sourceInterval?.to ?? -1;
49
- }
50
- return { outputs, inputs };
51
- }
52
-
53
- /**
54
- * Whether a pass is worked out from what an earlier one was.
55
- *
56
- * @param {{ tracks: unknown, outputs: string[], inputs: ArrayLike<number> } | undefined} last
57
- * @param {{ tracks: unknown, outputs: string[], inputs: ArrayLike<number> }} next
58
- * @returns {boolean}
59
- */
60
- export function samePass(last, next) {
61
- return Boolean(last) && last.tracks === next.tracks &&
62
- sameValues(last.outputs, next.outputs) && sameValues(last.inputs, next.inputs);
63
- }
64
-
65
- /** Whether two lists hold the same values in the same order. */
66
- function sameValues(left, right) {
67
- if (left.length !== right.length) return false;
68
- for (let index = 0; index < left.length; index++) if (!Object.is(left[index], right[index])) return false;
69
- return true;
70
- }
@@ -1,174 +0,0 @@
1
- /**
2
- * @file The next run's whole input, asked of the swarm as one group.
3
- *
4
- * While anything urgent is missing, the swarm is told only the first class
5
- * (`torrent/download/SwarmSelection.js`), and at a cold start the map's first
6
- * class is one segment's input. A peer holding none of its pieces then has
7
- * nothing to send, and a run is admitted only as far as its input has arrived
8
- * (`encode/EncodeInputs.js`), so it opens again a few seconds of film later.
9
- *
10
- * WebTorrent 2.8.5 keeps no order between several non-zero selections — it
11
- * rotates them (`shufflePriority`) — so the run's input is not stated as a
12
- * second group behind the first: the segments it covers take the first
13
- * segment's place in the map, and the swarm is told them as one group. How
14
- * far that reaches is what `EncodeInputs` would copy for the run, from its
15
- * copy rate, its open and the memory allowed; it is not worked out again here.
16
- * A read stopped on its bytes is stated at a level of its own above any of
17
- * this, and a seek, which moves the map's first segment, moves this with it
18
- * (torrent-tv/meta#178).
19
- */
20
-
21
- /**
22
- * The bytes a run's input holds, each counted once: the declarations and
23
- * indexes every segment of an original file names are held once by the run
24
- * that reads them. Kept merged and in order, so a segment's ranges are
25
- * counted against it by search rather than by sorting everything held again —
26
- * a soundtrack's segments add a few bytes each, and its stretch is walked far.
27
- */
28
- class HeldBytes {
29
- /** Inclusive [start, end], merged, ascending. @type {Array<[number, number]>} */
30
- #ranges = [];
31
- total = 0;
32
-
33
- /** @param {Array<[number, number]>} ranges */
34
- constructor(ranges = []) {
35
- this.add(ranges);
36
- }
37
-
38
- /** The first held range that ends at or after `at`. */
39
- #from(at) {
40
- let low = 0;
41
- let high = this.#ranges.length;
42
- while (low < high) {
43
- const middle = (low + high) >> 1;
44
- if (this.#ranges[middle][1] < at) low = middle + 1;
45
- else high = middle;
46
- }
47
- return low;
48
- }
49
-
50
- /**
51
- * What these ranges would add, without adding them.
52
- *
53
- * @param {Array<[number, number]>} ranges
54
- * @returns {number}
55
- */
56
- adds(ranges) {
57
- let added = 0;
58
- for (const [start, end] of merged(ranges)) {
59
- let at = start;
60
- for (let index = this.#from(start); at <= end && index < this.#ranges.length && this.#ranges[index][0] <= end; index++) {
61
- const [heldStart, heldEnd] = this.#ranges[index];
62
- if (heldStart > at) added += heldStart - at;
63
- at = Math.max(at, heldEnd + 1);
64
- }
65
- if (at <= end) added += end - at + 1;
66
- }
67
- return added;
68
- }
69
-
70
- /** @param {Array<[number, number]>} ranges */
71
- add(ranges) {
72
- for (const [start, end] of merged(ranges)) {
73
- this.total += this.adds([[start, end]]);
74
- // Joined with every held range it touches or overlaps.
75
- const first = this.#from(start - 1);
76
- let last = first;
77
- let joinedStart = start;
78
- let joinedEnd = end;
79
- while (last < this.#ranges.length && this.#ranges[last][0] <= end + 1) {
80
- joinedStart = Math.min(joinedStart, this.#ranges[last][0]);
81
- joinedEnd = Math.max(joinedEnd, this.#ranges[last][1]);
82
- last++;
83
- }
84
- this.#ranges.splice(first, last - first, [joinedStart, joinedEnd]);
85
- }
86
- }
87
- }
88
-
89
- /** Inclusive ranges merged where they overlap or touch, ascending. @param {Array<[number, number]>} ranges */
90
- function merged(ranges) {
91
- const sorted = [...ranges].sort((left, right) => left[0] - right[0]);
92
- const out = [];
93
- for (const [start, end] of sorted) {
94
- const previous = out.at(-1);
95
- if (previous && start <= previous[1] + 1) previous[1] = Math.max(previous[1], end);
96
- else out.push([start, end]);
97
- }
98
- return out;
99
- }
100
-
101
- /**
102
- * The map's byte zones with each output's next run input raised to its first
103
- * segment's place.
104
- *
105
- * @template {{ outputKey: string, index: number, priority: number, urgent?: boolean, behind?: boolean, deferred?: boolean,
106
- * ranges?: Array<[number, number]>, byteStart?: number, byteEnd?: number }} Zone
107
- * @param {Zone[]} zones - One or more per segment, as the map's pass made
108
- * them: a segment's zone names all its ranges (`ranges`), any other one
109
- * range (`byteStart`-`byteEnd`).
110
- * @param {(outputKey: string) => number | null} targetBytesOf - What the
111
- * output's next run would be given to copy; nothing is raised without it.
112
- * @returns {Zone[]} In their order; raised zones are copies.
113
- */
114
- export function expandToTargetInputs(zones, targetBytesOf) {
115
- // ONE WALK FOR EACH OUTPUT'S FIRST SEGMENT, and the segments after it only
116
- // as far as the run's input reaches. The map names every segment of a film,
117
- // and each pass used to group all of them and work out each one's highest
118
- // priority before looking at the few the run copies: 26 s of the main thread
119
- // in fifteen minutes of a two-hour AVI on Home Assistant (r24,
120
- // torrent-tv/meta#178).
121
- /** @type {Map<string, { index: number, priority: number, zone: Zone }>} */
122
- const first = new Map();
123
- for (const zone of zones) {
124
- const best = first.get(zone.outputKey);
125
- if (!best || zone.priority > best.priority || (zone.priority === best.priority && zone.index < best.index)) {
126
- first.set(zone.outputKey, { index: zone.index, priority: zone.priority, zone });
127
- }
128
- }
129
- /** @type {Map<string, { first: { index: number, priority: number, zone: Zone }, target: number, after: Map<number, Zone[]> }>} */
130
- const runs = new Map();
131
- for (const [outputKey, best] of first) {
132
- const target = targetBytesOf(outputKey);
133
- if (target > 0) runs.set(outputKey, { first: best, target, after: new Map() });
134
- }
135
- if (runs.size === 0) return zones;
136
- /** @type {Map<string, Array<[number, number]>>} */
137
- const held = new Map();
138
- for (const zone of zones) {
139
- const run = runs.get(zone.outputKey);
140
- if (!run) continue;
141
- if (zone.index === run.first.index) {
142
- let ranges = held.get(zone.outputKey);
143
- if (!ranges) held.set(zone.outputKey, ranges = []);
144
- ranges.push(...rangesOf(zone));
145
- } else if (zone.index > run.first.index) {
146
- const list = run.after.get(zone.index);
147
- if (list) list.push(zone);
148
- else run.after.set(zone.index, [zone]);
149
- }
150
- }
151
- /** @type {Map<Zone, Zone>} */
152
- const raised = new Map();
153
- for (const [outputKey, run] of runs) {
154
- const holding = new HeldBytes(held.get(outputKey) ?? []);
155
- // The run's input is a stretch of consecutive segments: one already made,
156
- // or not yet worked out, ends it.
157
- for (let index = run.first.index + 1; run.after.has(index); index++) {
158
- const list = run.after.get(index);
159
- const ranges = list.flatMap(rangesOf);
160
- if (holding.total + holding.adds(ranges) > run.target) break;
161
- holding.add(ranges);
162
- for (const zone of list) {
163
- raised.set(zone, { ...zone, priority: run.first.priority, urgent: run.first.zone.urgent,
164
- behind: run.first.zone.behind, deferred: run.first.zone.deferred });
165
- }
166
- }
167
- }
168
- return raised.size === 0 ? zones : zones.map(zone => raised.get(zone) ?? zone);
169
- }
170
-
171
- /** The ranges a zone names, inclusive. */
172
- function rangesOf(zone) {
173
- return Array.isArray(zone.ranges) ? zone.ranges : [[zone.byteStart, zone.byteEnd]];
174
- }