@torrent-tv/proxy 2.98.1 → 2.98.2
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 +1 -1
- package/server.js +77 -52
- package/services/viewer/playback-pass.js +20 -8
- package/services/viewer/segment-demands.js +20 -21
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -437,44 +437,55 @@ export async function startProxyServer({
|
|
|
437
437
|
/** Source byte ranges of one segment interval, by file and interval; forgotten with the source. */
|
|
438
438
|
const segmentSourceRanges = new Map();
|
|
439
439
|
/**
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
440
|
+
* WHAT ONE SEGMENT OF AN OUTPUT IS, kept from pass to pass: where it lies in
|
|
441
|
+
* the source, what it reads — its tracks and the key its bytes are kept under
|
|
442
|
+
* — and, once known, those bytes joined at the precision the torrent fetches,
|
|
443
|
+
* by output and segment. Worked out again only when the output's timeline,
|
|
444
|
+
* the tracks, the file's start time or the output's choices change; forgotten
|
|
445
|
+
* with the source. None of it depends on where a viewer stands, and a pass is
|
|
446
|
+
* made every time one moves: looked up, made and joined again for each of a
|
|
447
|
+
* film's 3500 segments on every pass, they were 6 s of the 40 s the passes
|
|
448
|
+
* cost the main thread in fifteen minutes on Home Assistant (r30,
|
|
449
|
+
* torrent-tv/meta#178).
|
|
444
450
|
*/
|
|
445
451
|
const segmentReadings = new Map();
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
const kept =
|
|
463
|
-
if (kept && kept.trackList === trackList && kept.shift === shift && kept.choicesKey === choicesKey
|
|
464
|
-
|
|
465
|
-
const selected =
|
|
452
|
+
/** One output's segments, by index. */
|
|
453
|
+
const segmentRecordsOf = (map, output) => {
|
|
454
|
+
const key = `${map.sourceKey}:${map.fileIndex}:${output.outputKey}`;
|
|
455
|
+
let records = segmentReadings.get(key);
|
|
456
|
+
if (!records) segmentReadings.set(key, records = new Map());
|
|
457
|
+
return records;
|
|
458
|
+
};
|
|
459
|
+
/** The choices of one output's tracks as a key, worked out once for the list of them a pass shares. */
|
|
460
|
+
const choicesKeys = new WeakMap();
|
|
461
|
+
const choicesKeyOf = (choices) => {
|
|
462
|
+
let key = choicesKeys.get(choices);
|
|
463
|
+
if (key === undefined) choicesKeys.set(choices, key = choices.map(choice => `${choice.type}/${choice.index}/${choice.mode}`).join(","));
|
|
464
|
+
return key;
|
|
465
|
+
};
|
|
466
|
+
const segmentRecordOf = (records, demand, { timeline, trackList, shift, map }) => {
|
|
467
|
+
const choicesKey = choicesKeyOf(demand.tracks);
|
|
468
|
+
const kept = records.get(demand.index);
|
|
469
|
+
if (kept && kept.timeline === timeline && kept.trackList === trackList && kept.shift === shift && kept.choicesKey === choicesKey) return kept;
|
|
470
|
+
const sourceInterval = timeline.sourceInterval(demand.index, demand.index, shift);
|
|
471
|
+
const selected = demand.tracks.map(choice => ({ choice, track: choice.type === "video"
|
|
466
472
|
? ContainerTrack.firstUsable(trackList, "video")
|
|
467
473
|
: trackList.filter(track => track.type === choice.type)[choice.index] }));
|
|
468
474
|
let reading = null;
|
|
469
475
|
if (!selected.some(({ track }) => !track)) {
|
|
470
476
|
const wanted = selected.map(({ track }) => track);
|
|
471
477
|
const modes = new Map(selected.map(({ choice, track }) => [track, choice.mode]));
|
|
472
|
-
const interval = { ...
|
|
478
|
+
const interval = { ...sourceInterval, trackIds: wanted.map(track => track.trackNumber),
|
|
473
479
|
modes: Object.fromEntries(wanted.map(track => [track.trackNumber, modes.get(track)])) };
|
|
474
480
|
reading = { wanted, modes, interval, rangesKey: `${map.sourceKey}:${map.fileIndex}:${JSON.stringify(interval)}` };
|
|
475
481
|
}
|
|
476
|
-
|
|
477
|
-
|
|
482
|
+
// `joined` is the segment's ranges as the torrent is told them, made from
|
|
483
|
+
// the list `ranges` at `pieceLength`; `registered` is the list the input
|
|
484
|
+
// table was last given.
|
|
485
|
+
const record = { timeline, trackList, shift, choicesKey, sourceInterval, reading,
|
|
486
|
+
ranges: null, pieceLength: Number.NaN, joined: null, registered: null };
|
|
487
|
+
records.set(demand.index, record);
|
|
488
|
+
return record;
|
|
478
489
|
};
|
|
479
490
|
/**
|
|
480
491
|
* The last complete pass over each file, by file: what it was worked out
|
|
@@ -508,15 +519,6 @@ export async function startProxyServer({
|
|
|
508
519
|
`${passCosts.built} zone list(s) built for the map in ${Math.round(passCosts.buildMs)}ms`);
|
|
509
520
|
Object.assign(passCosts, { since: now, reused: 0, worked: 0, incomplete: 0, segments: 0, ranges: 0, zones: 0, built: 0, buildMs: 0, ms: 0, maxMs: 0 });
|
|
510
521
|
};
|
|
511
|
-
/** A segment's ranges joined at the precision the torrent fetches, once per kept list and piece length. */
|
|
512
|
-
const joinedRanges = new WeakMap();
|
|
513
|
-
const joinedRangesOf = (ranges, pieceLength) => {
|
|
514
|
-
const kept = joinedRanges.get(ranges);
|
|
515
|
-
if (kept && kept.pieceLength === pieceLength) return kept.joined;
|
|
516
|
-
const joined = joinedWithinPieces(ranges, pieceLength);
|
|
517
|
-
if (ranges !== null && typeof ranges === "object") joinedRanges.set(ranges, { pieceLength, joined });
|
|
518
|
-
return joined;
|
|
519
|
-
};
|
|
520
522
|
const downloadMaps = new DownloadMaps({
|
|
521
523
|
// The ranges a picture reads at open are named again by every segment;
|
|
522
524
|
// only the zone the swarm would rank first is sent (torrent-tv/meta#166).
|
|
@@ -574,14 +576,22 @@ export async function startProxyServer({
|
|
|
574
576
|
const links = consumers.map(viewer => viewer.linkReading()?.linkMbps)
|
|
575
577
|
.filter(value => Number.isFinite(value) && value > 0);
|
|
576
578
|
const encodeSpeed = outputParts.encodeCost.latestSpeedReadingOf(output)?.speed;
|
|
579
|
+
const linkMbps = links.length ? Math.min(...links) : null;
|
|
580
|
+
const records = segmentRecordsOf(map, output);
|
|
581
|
+
const facts = { timeline: output.timeline, trackList: tracks.value, shift, map };
|
|
582
|
+
// Each demand is this pass's own object (`segmentDemands`), so it is
|
|
583
|
+
// completed in place rather than copied.
|
|
577
584
|
for (const demand of segmentDemands(output, map.fileIndex, map.zones)) {
|
|
578
585
|
if (inventory.has(demand.index)) continue;
|
|
579
586
|
const deadlineAt = sourceDeadline(demand, { encodeSpeed,
|
|
580
|
-
outputBytes: seconds > 0 ? bytes / seconds * (demand.to - demand.from) : null,
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
587
|
+
outputBytes: seconds > 0 ? bytes / seconds * (demand.to - demand.from) : null, linkMbps });
|
|
588
|
+
const record = segmentRecordOf(records, demand, facts);
|
|
589
|
+
demand.leadSeconds = Number.isFinite(demand.deadlineAt) ? Math.max(0, (demand.deadlineAt - deadlineAt) / 1000) : 0;
|
|
590
|
+
demand.deadlineAt = deadlineAt;
|
|
591
|
+
demand.outputKey = output.outputKey;
|
|
592
|
+
demand.sourceInterval = record.sourceInterval;
|
|
593
|
+
demand.record = record;
|
|
594
|
+
demands.push(demand);
|
|
585
595
|
}
|
|
586
596
|
}
|
|
587
597
|
demands.sort((left, right) => left.deadlineAt - right.deadlineAt || right.priority - left.priority || left.index - right.index);
|
|
@@ -618,24 +628,37 @@ export async function startProxyServer({
|
|
|
618
628
|
// two-hour AVI (torrent-tv/meta#178).
|
|
619
629
|
const waiting = new Set();
|
|
620
630
|
const turn = takeTurns();
|
|
631
|
+
/** One download interval per map zone, shared by the segments it owns: read, never changed. */
|
|
632
|
+
const downloadIntervals = new Map();
|
|
633
|
+
const downloadIntervalOf = (owner) => {
|
|
634
|
+
let interval = downloadIntervals.get(owner);
|
|
635
|
+
if (!interval) downloadIntervals.set(owner, interval = { from: owner.from, to: owner.to });
|
|
636
|
+
return interval;
|
|
637
|
+
};
|
|
621
638
|
for (const zone of demands) {
|
|
622
639
|
if (!map.isCurrent()) return [];
|
|
623
|
-
// WHAT A SEGMENT READS IS WORKED OUT ONCE, not on every publication
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
// new map changes. Rebuilt for every segment of the film on every map,
|
|
627
|
-
// this pass held the main thread up to 1.5 s at a time on Home
|
|
640
|
+
// WHAT A SEGMENT READS IS WORKED OUT ONCE, not on every publication
|
|
641
|
+
// (`segmentRecordOf`). Rebuilt for every segment of the film on every
|
|
642
|
+
// map, this pass held the main thread up to 1.5 s at a time on Home
|
|
628
643
|
// Assistant (torrent-tv/meta#178).
|
|
629
|
-
const
|
|
644
|
+
const { record } = zone;
|
|
645
|
+
const reading = record.reading;
|
|
630
646
|
if (!reading) continue;
|
|
631
647
|
const { wanted, modes, interval, rangesKey } = reading;
|
|
632
|
-
const { tracks: _choices, owner: _owner, sourceInterval: _sourceInterval, ...demand } = zone;
|
|
633
648
|
const convert = ranges => {
|
|
634
|
-
|
|
649
|
+
if (record.ranges !== ranges || record.pieceLength !== pieceLength) {
|
|
650
|
+
record.ranges = ranges;
|
|
651
|
+
record.pieceLength = pieceLength;
|
|
652
|
+
record.joined = joinedWithinPieces(ranges, pieceLength);
|
|
653
|
+
}
|
|
654
|
+
const joined = record.joined;
|
|
635
655
|
rangeCount += joined.length;
|
|
636
656
|
// What the segment reads is kept for the forecast in a table of its
|
|
637
657
|
// own, whatever form the torrent is told it in.
|
|
638
|
-
|
|
658
|
+
if (record.registered !== joined) {
|
|
659
|
+
segmentInputs.set(map.sourceKey, map.fileIndex, zone.outputKey, zone.index, joined);
|
|
660
|
+
record.registered = joined;
|
|
661
|
+
}
|
|
639
662
|
// ONE ZONE FOR THE SEGMENT, its demand stated once for all the
|
|
640
663
|
// ranges it reads. The demand — level, priority, deadline, the map
|
|
641
664
|
// zone it belongs to — is this pass's; the ranges are the list kept
|
|
@@ -643,14 +666,16 @@ export async function startProxyServer({
|
|
|
643
666
|
// range copied the demand into every one: ten thousand objects a
|
|
644
667
|
// pass over a two-hour AVI, made again 21-61 times a minute on Home
|
|
645
668
|
// Assistant (r26, torrent-tv/meta#178).
|
|
646
|
-
converted.push({
|
|
669
|
+
converted.push({ index: zone.index, from: zone.from, to: zone.to, priority: zone.priority, urgent: zone.urgent,
|
|
670
|
+
behind: zone.behind, deferred: zone.deferred, deadlineAt: zone.deadlineAt, leadSeconds: zone.leadSeconds,
|
|
671
|
+
outputKey: zone.outputKey, downloadInterval: downloadIntervalOf(zone.owner), ranges: joined });
|
|
647
672
|
};
|
|
648
673
|
// The bytes one segment needs are a fact of the file and the output, so
|
|
649
674
|
// they are worked out once. A whole film is thousands of segments and
|
|
650
675
|
// the map changes every few seconds; worked out afresh on every change,
|
|
651
676
|
// a pass over a two-hour AVI never finished and the next segment's bytes
|
|
652
677
|
// were never asked for (Home Assistant 2026-10-08, torrent-tv/meta#151).
|
|
653
|
-
const known = segmentSourceRanges.get(rangesKey);
|
|
678
|
+
const known = record.ranges ?? segmentSourceRanges.get(rangesKey);
|
|
654
679
|
if (known) { convert(known); continue; }
|
|
655
680
|
if (waiting.has(zone.outputKey)) { complete = false; continue; }
|
|
656
681
|
// What is resolved so far is stated before a lower zone is worked out
|
|
@@ -22,18 +22,30 @@
|
|
|
22
22
|
* behind?: boolean, deferred?: boolean, from: number, to: number,
|
|
23
23
|
* sourceInterval?: { from: number, to: number } }>} params.demands - In the
|
|
24
24
|
* order of their deadlines.
|
|
25
|
-
* @returns {{ outputs: string[], inputs:
|
|
25
|
+
* @returns {{ outputs: string[], inputs: Float64Array }}
|
|
26
26
|
*/
|
|
27
27
|
export function passInputs({ pieceLength, shift, targets, demands }) {
|
|
28
28
|
const outputs = [...targets.keys()];
|
|
29
|
-
const
|
|
30
|
-
|
|
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;
|
|
31
38
|
for (const demand of demands) {
|
|
32
39
|
// The segment's own interval too: a cut grid read again can give one
|
|
33
40
|
// number other bytes.
|
|
34
|
-
inputs.
|
|
35
|
-
|
|
36
|
-
|
|
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;
|
|
37
49
|
}
|
|
38
50
|
return { outputs, inputs };
|
|
39
51
|
}
|
|
@@ -41,8 +53,8 @@ export function passInputs({ pieceLength, shift, targets, demands }) {
|
|
|
41
53
|
/**
|
|
42
54
|
* Whether a pass is worked out from what an earlier one was.
|
|
43
55
|
*
|
|
44
|
-
* @param {{ tracks: unknown, outputs: string[], inputs: number
|
|
45
|
-
* @param {{ tracks: unknown, outputs: string[], inputs: number
|
|
56
|
+
* @param {{ tracks: unknown, outputs: string[], inputs: ArrayLike<number> } | undefined} last
|
|
57
|
+
* @param {{ tracks: unknown, outputs: string[], inputs: ArrayLike<number> }} next
|
|
46
58
|
* @returns {boolean}
|
|
47
59
|
*/
|
|
48
60
|
export function samePass(last, next) {
|
|
@@ -22,28 +22,27 @@ export function segmentDemands(output, fileIndex, zones) {
|
|
|
22
22
|
for (let index = 0; index + 1 < grid.length; index++) {
|
|
23
23
|
const from = grid[index], to = grid[index + 1];
|
|
24
24
|
if (!Number.isFinite(from) || !Number.isFinite(to) || !(to > from)) throw new Error("An output segment needs a valid interval.");
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
|
34
43
|
}
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
// is reached later, at one second of film per second of playback.
|
|
38
|
-
const deadlineOf = zone => Number.isFinite(zone.deadlineAt)
|
|
39
|
-
? zone.deadlineAt + Math.max(0, from - zone.from) * 1000 : Infinity;
|
|
40
|
-
const owner = overlapping.reduce((first, zone) => deadlineOf(zone) < deadlineOf(first) ? zone : first);
|
|
41
|
-
demands.push({ index, from, to, tracks, owner,
|
|
42
|
-
priority: Math.max(...overlapping.map(zone => zone.priority)),
|
|
43
|
-
urgent: overlapping.some(zone => zone.urgent),
|
|
44
|
-
behind: overlapping.every(zone => zone.behind),
|
|
45
|
-
deferred: overlapping.every(zone => zone.deferred),
|
|
46
|
-
deadlineAt: Math.min(...overlapping.map(deadlineOf)) });
|
|
44
|
+
if (owner === null) continue;
|
|
45
|
+
demands.push({ index, from, to, tracks, owner, priority, urgent, behind, deferred, deadlineAt: ownerDeadline });
|
|
47
46
|
}
|
|
48
47
|
return demands.sort((left, right) => left.deadlineAt - right.deadlineAt || right.priority - left.priority || left.index - right.index);
|
|
49
48
|
}
|