@torrent-tv/proxy 2.95.3 → 2.95.5

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.
@@ -208,6 +208,8 @@ export function predictPlaybackReadiness(input = {}) {
208
208
  for (let trackIndex = 0; trackIndex < trackState.length; trackIndex += 1) {
209
209
  const state = trackState[trackIndex];
210
210
  const { track, segments } = state;
211
+ const processingCoverage = numericCoverage((Array.isArray(track.processingRanges) ? track.processingRanges : [])
212
+ .filter(range => Number.isFinite(range?.start) && Number.isFinite(range?.end) && range.end > range.start));
211
213
  const measuredBitsPerMediaSecond = state.bitsPerMediaSecond;
212
214
  for (const segment of segments) {
213
215
  // Media the page already holds is not transferred again.
@@ -230,9 +232,7 @@ export function predictPlaybackReadiness(input = {}) {
230
232
  // Attribute unfinished work to the actual run interval. A global
231
233
  // position from another run must not erase a missing earlier segment.
232
234
  const workStart = segment.startSeconds;
233
- const remainingProcessing = uncoveredIntervals({ start: workStart, end: segment.endSeconds },
234
- (Array.isArray(track.processingRanges) ? track.processingRanges : []).filter((range) =>
235
- Number.isFinite(range?.start) && Number.isFinite(range?.end) && range.end > range.start));
235
+ const remainingProcessing = uncoveredIntervals({ start: workStart, end: segment.endSeconds }, processingCoverage);
236
236
  const encodeWork = produced ? 0 : remainingProcessing.reduce((sum, part) => sum + part.end - part.start, 0);
237
237
 
238
238
  const sourceByteIntervals = new Map();
@@ -309,29 +309,29 @@ export function predictPlaybackReadiness(input = {}) {
309
309
  }
310
310
  }
311
311
  const trackFinish = trackState.map(({ track }) => finiteNonNegative(track.startupRemainingSeconds));
312
+ const sourceByteCoverage = new Map([...sourceByteService].map(([id, ranges]) => [id, numericCoverage(ranges)]));
313
+ const sourceArrivals = new Map([...sourceByteService].map(([id, ranges]) => [id, indexedArrivals(ranges)]));
312
314
  let transferFinish = 0;
313
315
  const completions = [];
314
316
  const productions = [];
315
317
  for (const job of jobs) {
316
318
  const { track, trackIndex, segment, sizeBits, encodeWork, sourceByteIntervals } = job;
317
- const sourceArrivals = [];
319
+ let sourceArrivalAt = 0;
318
320
  for (const [sourceId, ranges] of sourceByteIntervals) {
319
- const served = sourceByteService.get(sourceId) ?? [];
320
321
  for (const interval of ranges) {
321
- if (uncoveredIntervals(interval, served).length) return result(false, null, buffered, reserve, null, "download-schedule-unavailable", preparedSegments);
322
- for (const part of served.filter(part => part.end > interval.start && part.start < interval.end)) {
323
- sourceArrivals.push({ at: part.finish });
324
- }
322
+ const missing = uncoveredIntervals(interval, sourceByteCoverage.get(sourceId) ?? []);
323
+ if (missing.length) return { ...result(false, null, buffered, reserve, null, "download-schedule-unavailable", preparedSegments),
324
+ unavailableSource: { sourceId, segmentIndex: segment.index, range: interval, missing } };
325
+ sourceArrivalAt = Math.max(sourceArrivalAt, latestArrival(interval, sourceArrivals.get(sourceId)));
325
326
  }
326
- sourceByteService.set(sourceId, served);
327
327
  }
328
328
  // Production accepts a complete held segment input. It cannot overlap its
329
329
  // own source download; reused input retains its original arrival time.
330
330
  const processing = trackState[trackIndex].curve;
331
- const admittedAt = Math.max(trackFinish[trackIndex], ...sourceArrivals.map(({ at }) => at));
331
+ const admittedAt = Math.max(trackFinish[trackIndex], sourceArrivalAt);
332
332
  const producedAt = job.produced || encodeWork === 0 ? 0 : processing.finish(encodeWork, admittedAt);
333
333
  trackFinish[trackIndex] = Math.max(trackFinish[trackIndex], producedAt);
334
- productions.push({ at: producedAt, trackIndex, segment, requiresSource: sourceArrivals.some(({ at }) => at > 0) });
334
+ productions.push({ at: producedAt, trackIndex, segment, requiresSource: sourceArrivalAt > 0 });
335
335
  const deliveryAt = link.finish(sizeBits, Math.max(transferFinish, producedAt));
336
336
  transferFinish = deliveryAt;
337
337
  completions.push({ at: deliveryAt + finiteNonNegative(track.appendSeconds), trackIndex, segment });
@@ -483,15 +483,57 @@ function heldRanges(ranges, segments, unit) {
483
483
  return held;
484
484
  }
485
485
 
486
+ /** Coverage is joined once per resource, independently of arrival times. */
487
+ function numericCoverage(ranges) {
488
+ const joined = [];
489
+ for (const range of [...ranges].sort((left, right) => left.start - right.start || left.end - right.end)) {
490
+ const previous = joined.at(-1);
491
+ if (previous && range.start <= previous.end) previous.end = Math.max(previous.end, range.end);
492
+ else joined.push({ start: range.start, end: range.end });
493
+ }
494
+ return joined;
495
+ }
496
+
497
+ /** Already held bytes have no arrival delay. Index future arrivals once. */
498
+ function indexedArrivals(ranges) {
499
+ let maximumEnd = 0;
500
+ return ranges.filter(range => range.finish > 0)
501
+ .sort((left, right) => left.start - right.start || left.end - right.end)
502
+ .map(range => ({ ...range, maximumEnd: maximumEnd = Math.max(maximumEnd, range.end) }));
503
+ }
504
+
505
+ function latestArrival(interval, ranges = []) {
506
+ let low = 0, high = ranges.length;
507
+ while (low < high) {
508
+ const middle = (low + high) >>> 1;
509
+ if (ranges[middle].maximumEnd <= interval.start) low = middle + 1;
510
+ else high = middle;
511
+ }
512
+ let finish = 0;
513
+ for (let index = low; index < ranges.length && ranges[index].start < interval.end; index++) {
514
+ if (ranges[index].end > interval.start) finish = Math.max(finish, ranges[index].finish);
515
+ }
516
+ return finish;
517
+ }
518
+
519
+ /** Exact gaps in sorted disjoint numeric coverage, without per-range splitting. */
486
520
  function uncoveredIntervals(interval, served) {
487
- let remaining = [{ ...interval }];
488
- for (const held of served) {
489
- remaining = remaining.flatMap((part) => held.end <= part.start || held.start >= part.end ? [part] : [
490
- { start: part.start, end: Math.min(part.end, held.start) },
491
- { start: Math.max(part.start, held.end), end: part.end }
492
- ].filter(({ start, end }) => end > start));
521
+ let low = 0, high = served.length;
522
+ while (low < high) {
523
+ const middle = (low + high) >>> 1;
524
+ if (served[middle].end <= interval.start) low = middle + 1;
525
+ else high = middle;
526
+ }
527
+ const missing = [];
528
+ let cursor = interval.start;
529
+ for (let index = low; index < served.length && cursor < interval.end; index++) {
530
+ const held = served[index];
531
+ if (held.start >= interval.end) break;
532
+ if (held.start > cursor) missing.push({ start: cursor, end: held.start });
533
+ cursor = Math.max(cursor, held.end);
493
534
  }
494
- return remaining;
535
+ if (cursor < interval.end) missing.push({ start: cursor, end: interval.end });
536
+ return missing;
495
537
  }
496
538
 
497
539
  /**
@@ -16,13 +16,17 @@ export function segmentDemands(output, fileIndex, zones) {
16
16
  if (!Number.isFinite(from) || !Number.isFinite(to) || !(to > from)) throw new Error("An output segment needs a valid interval.");
17
17
  const overlapping = zones.filter(zone => zone.priority > 0 && zone.from < to && zone.to > from);
18
18
  if (!overlapping.length) continue;
19
- const owner = overlapping.reduce((first, zone) => (zone.deadlineAt ?? Infinity) < (first.deadlineAt ?? Infinity) ? zone : first);
19
+ // A zone states the deadline of its near edge. A later segment within it
20
+ // is reached later, at one second of film per second of playback.
21
+ const deadlineOf = zone => Number.isFinite(zone.deadlineAt)
22
+ ? zone.deadlineAt + Math.max(0, from - zone.from) * 1000 : Infinity;
23
+ const owner = overlapping.reduce((first, zone) => deadlineOf(zone) < deadlineOf(first) ? zone : first);
20
24
  demands.push({ index, from, to, tracks, owner,
21
25
  priority: Math.max(...overlapping.map(zone => zone.priority)),
22
26
  urgent: overlapping.some(zone => zone.urgent),
23
27
  behind: overlapping.every(zone => zone.behind),
24
28
  deferred: overlapping.every(zone => zone.deferred),
25
- deadlineAt: Math.min(...overlapping.map(zone => Number.isFinite(zone.deadlineAt) ? zone.deadlineAt : Infinity)) });
29
+ deadlineAt: Math.min(...overlapping.map(deadlineOf)) });
26
30
  }
27
31
  return demands.sort((left, right) => left.deadlineAt - right.deadlineAt || right.priority - left.priority || left.index - right.index);
28
32
  }
@@ -1,195 +0,0 @@
1
- import { piecesOf } from "../demand/pieces.js";
2
- import { pieceStoreOf } from "../piece-store-of.js";
3
-
4
- /** The block length and one-second request allowance used by WebTorrent. */
5
- const BLOCK_BYTES = 16 * 1024;
6
- const compiledDemand = new WeakMap();
7
- const requestArrivals = new WeakMap();
8
-
9
- /** Keep the same reassignment evidence in an isolated forecast request. */
10
- export function copyRequestArrival(source, target) {
11
- if (requestArrivals.has(source)) requestArrivals.set(target, requestArrivals.get(source));
12
- return target;
13
- }
14
-
15
- function requestArrival(wire, request, now) {
16
- if (!requestArrivals.has(request)) {
17
- const speed = Number(wire.downloadSpeed?.());
18
- let bytes = 0;
19
- for (const queued of wire.requests ?? []) {
20
- bytes += queued.length;
21
- if (queued === request) break;
22
- }
23
- requestArrivals.set(request, speed > 0 ? now + bytes / speed * 1000 : Infinity);
24
- }
25
- return requestArrivals.get(request);
26
- }
27
-
28
- export function peerRequestCapacity(torrent, wire, speed = Math.max(0, Number(wire.downloadSpeed?.()) || 0)) {
29
- if (wire.type === "webSeed") {
30
- return Math.min(1 + Math.ceil(speed / torrent.pieceLength), torrent.maxWebConns);
31
- }
32
- let capacity = 2 + Math.ceil(speed / BLOCK_BYTES);
33
- const maximum = wire.peerExtendedHandshake?.reqq;
34
- if (Number.isFinite(maximum) && maximum > 0) capacity = Math.min(capacity, Math.floor(maximum));
35
- return capacity;
36
- }
37
-
38
- export function requestCompletionAt(wire, bytes, now, speed = Number(wire.downloadSpeed?.())) {
39
- if (!(speed > 0)) return Number.POSITIVE_INFINITY;
40
- const queued = (wire.requests ?? []).reduce((total, request) => total + request.length, 0);
41
- return now + (queued + bytes) / speed * 1000;
42
- }
43
-
44
- export function peerCanServe(wire, piece) {
45
- return !wire.destroyed && (!wire.peerChoking || (wire.hasFast && wire.peerAllowedFastSet?.includes(piece))) &&
46
- wire.peerPieces?.get(piece);
47
- }
48
-
49
- function peerCanRequest(wire) {
50
- return !wire.destroyed &&
51
- (!wire.peerChoking || (wire.hasFast && wire.peerAllowedFastSet?.length > 0));
52
- }
53
-
54
- /** Identical map ordering for present requests and conditional future requests. */
55
- export function compareDownloadCandidates(a, b) {
56
- return a.deadlineAt - b.deadlineAt || b.priority - a.priority || a.order - b.order || a.piece - b.piece;
57
- }
58
-
59
- /** One ordering across torrents; peers retain protocol-owned block verification. */
60
- export class DeadlineScheduler {
61
- #entries;
62
- #running = false;
63
- #store;
64
-
65
- constructor({ entries, findStore = pieceStoreOf }) {
66
- this.#entries = entries;
67
- this.#store = findStore;
68
- }
69
-
70
- reconcile(now = Date.now(), { torrent = null, wire = null } = {}) {
71
- if (this.#running) return { requested: 0, reassigned: 0 };
72
- // WebTorrent updates each peer in turn. A full or choked peer cannot issue
73
- // a request, so its heartbeat must not scan every file's byte availability.
74
- if (wire && (!peerCanRequest(wire) ||
75
- (wire.requests?.length ?? 0) >= peerRequestCapacity(torrent, wire))) {
76
- return { requested: 0, reassigned: 0 };
77
- }
78
- this.#running = true;
79
- try {
80
- const entries = [...this.#entries()].filter(entry => !torrent || entry.torrent === torrent);
81
- return dispatchDownloadCandidates(downloadCandidates(entries, this.#store), now, { wire });
82
- } finally {
83
- this.#running = false;
84
- }
85
- }
86
- }
87
-
88
-
89
- /** The published map defines the same candidates for every scheduling pass. */
90
- export function downloadCandidates(entries, findStore = pieceStoreOf) {
91
- const candidates = [];
92
- for (const { torrent, register } of entries) {
93
- if (torrent.destroyed || torrent.ready === false || typeof torrent._request !== "function") continue;
94
- const store = findStore(torrent);
95
- let compiled = compiledDemand.get(register);
96
- if (!Number.isSafeInteger(register.revision) || compiled?.revision !== register.revision ||
97
- compiled.torrent !== torrent || compiled.files !== torrent.files || compiled.pieceLength !== torrent.pieceLength) {
98
- const byPiece = new Map();
99
- for (const window of register.windows()) {
100
- const file = torrent.files?.[window.fileIndex];
101
- if (!file) continue;
102
- const range = piecesOf({ fileOffset: file.offset, byteStart: window.byteStart, byteEnd: Math.min(window.byteEnd, file.length - 1), pieceLength: torrent.pieceLength });
103
- if (!range) continue;
104
- for (let piece = range.from; piece <= range.to; piece++) {
105
- const prior = byPiece.get(piece);
106
- if (!prior || window.deadlineAt < prior.deadlineAt ||
107
- (window.deadlineAt === prior.deadlineAt && (window.priority > prior.priority ||
108
- (window.priority === prior.priority && window.order < prior.order)))) {
109
- byPiece.set(piece, { torrent, piece, deadlineAt: window.deadlineAt, priority: window.priority, order: window.order, requestId: window.requestId });
110
- }
111
- }
112
- }
113
- compiled = { torrent, revision: register.revision, files: torrent.files,
114
- pieceLength: torrent.pieceLength, pieces: [...byPiece.values()] };
115
- compiledDemand.set(register, compiled);
116
- }
117
- for (const candidate of compiled.pieces) {
118
- const present = store?.locationOf ? store.locationOf(candidate.piece) !== "missing" : torrent.bitfield?.get(candidate.piece);
119
- if (!present) candidates.push(candidate);
120
- }
121
- }
122
- candidates.sort(compareDownloadCandidates);
123
- return candidates;
124
- }
125
-
126
- /** Run against protocol peers or an isolated future-state copy. */
127
- export function dispatchDownloadCandidates(candidates, now, { wire: changedWire = null } = {}) {
128
- let requested = 0, reassigned = 0;
129
- // Rates and advertised limits cannot change during this synchronous pass.
130
- // Read each peer once, and only inspect torrents once regardless of map size.
131
- const readings = new Map();
132
- const readingOf = (torrent, wire) => {
133
- let reading = readings.get(wire);
134
- if (!reading) {
135
- const speed = Math.max(0, Number(wire.downloadSpeed?.()) || 0);
136
- readings.set(wire, reading = { speed, capacity: peerRequestCapacity(torrent, wire, speed) });
137
- }
138
- return reading;
139
- };
140
- const available = (torrent, wire) => peerCanRequest(wire) &&
141
- (wire.requests?.length ?? 0) < readingOf(torrent, wire).capacity;
142
- const completionAt = (torrent, wire, bytes) =>
143
- requestCompletionAt(wire, bytes, now, readingOf(torrent, wire).speed);
144
- const writable = new Map();
145
- for (const torrent of new Set(candidates.map(candidate => candidate.torrent))) {
146
- const peers = (changedWire ? [changedWire] : torrent.wires ?? [])
147
- .filter(wire => available(torrent, wire));
148
- if (peers.length) writable.set(torrent, peers);
149
- }
150
- for (const candidate of candidates) {
151
- if (!writable.size) break;
152
- const { torrent, piece, deadlineAt, requestId } = candidate;
153
- if (!writable.has(torrent)) continue;
154
- const peers = writable.get(torrent);
155
- const wires = peers.filter(wire => peerCanServe(wire, piece) && available(torrent, wire));
156
- while (wires.length) {
157
- wires.sort((a, b) => completionAt(torrent, a, BLOCK_BYTES) - completionAt(torrent, b, BLOCK_BYTES) ||
158
- (a.requests?.length ?? 0) - (b.requests?.length ?? 0));
159
- const wire = wires[0];
160
- const before = wire.requests?.length ?? 0;
161
- let accepted = torrent._request(wire, piece, false);
162
- if (!accepted && Number.isFinite(deadlineAt)) {
163
- const fasterAt = completionAt(torrent, wire, BLOCK_BYTES);
164
- for (const slower of torrent.wires ?? []) {
165
- if (slower === wire || typeof slower.cancel !== "function") continue;
166
- const lateAt = completionAt(torrent, slower, 0);
167
- if (!Number.isFinite(lateAt) || !(lateAt > deadlineAt && fasterAt < lateAt)) continue;
168
- const request = slower.requests?.find(one => one.piece === piece);
169
- if (!request) continue;
170
- // A later queue estimate must not move the same block between peers
171
- // indefinitely. Reassignment must improve its previous arrival;
172
- // protocol completion or timeout ends this request's evidence.
173
- if (!(fasterAt < requestArrival(slower, request, now))) continue;
174
- slower.cancel(piece, request.offset, request.length);
175
- reassigned++;
176
- torrent.emit?.("download-request-cancelled", { ...request, reason: "deadline-reassigned", requestId });
177
- accepted = torrent._request(wire, piece, false);
178
- if (accepted) {
179
- const replacement = wire.requests?.find(one => one.piece === piece && one.offset === request.offset);
180
- if (replacement) requestArrivals.set(replacement, fasterAt);
181
- }
182
- if (accepted) break;
183
- }
184
- }
185
- if (accepted) {
186
- requested++;
187
- torrent.emit?.("download-requested", { piece, deadlineAt, requestId, peer: wire.remoteAddress, queued: wire.requests?.length ?? 0 });
188
- }
189
- if (!accepted || (wire.requests?.length ?? 0) <= before ||
190
- (wire.requests?.length ?? 0) >= readingOf(torrent, wire).capacity) wires.shift();
191
- }
192
- if (!peers.some(wire => available(torrent, wire))) writable.delete(torrent);
193
- }
194
- return { requested, reassigned };
195
- }