@torrent-tv/proxy 2.98.3 → 2.98.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.
- package/package.json +1 -1
- package/routes/api/transcode-sessions/net-report/post.js +10 -7
- package/server.js +124 -120
- package/services/encode/EncodeInputs.js +1 -1
- package/services/storage/segment-store/SegmentStore.js +21 -1
- package/services/torrent/PlaybackProjection.js +771 -0
- package/services/viewer/DownloadMaps.js +12 -1
- package/services/viewer/Viewer.js +31 -1
- package/services/viewer/segment-demands.js +3 -57
- package/services/viewer/playback-pass.js +0 -70
- package/services/viewer/target-input-expansion.js +0 -174
package/package.json
CHANGED
|
@@ -83,6 +83,10 @@ export async function handleApiTranscodeSessionNetReportPost(req, reply,
|
|
|
83
83
|
// by an output of the same quality or better that is already made.
|
|
84
84
|
const qualityMode = body.qualityMode === "auto" || body.qualityMode === "manual" ? body.qualityMode : undefined;
|
|
85
85
|
const positionSeconds = Number(body.positionSeconds);
|
|
86
|
+
// Which of this viewer's viewings the page made the report in; a page one
|
|
87
|
+
// release behind does not say (`Viewer.isOfAnEarlierViewing`).
|
|
88
|
+
const stated = Number(body.generation);
|
|
89
|
+
const generation = Number.isInteger(stated) ? stated : undefined;
|
|
86
90
|
// The picture as the viewer sees it, in physical pixels: the upper bound of
|
|
87
91
|
// the height of a re-encoded output made for them (roadmap item 98). Sent the
|
|
88
92
|
// moment it changes; a page that does not say leaves the size as it was.
|
|
@@ -94,12 +98,10 @@ export async function handleApiTranscodeSessionNetReportPost(req, reply,
|
|
|
94
98
|
// to the rung on screen. A page that does not say leaves the rung as it was.
|
|
95
99
|
const playingHeight = Number(body.playingHeight);
|
|
96
100
|
if (Number.isInteger(playingHeight) && playingHeight > 0) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
consumerId
|
|
100
|
-
|
|
101
|
-
Number.isFinite(positionSeconds) && positionSeconds >= 0 ? positionSeconds : undefined
|
|
102
|
-
);
|
|
101
|
+
// Where they stood when they said it, unless they have seeked since.
|
|
102
|
+
const placed = Number.isFinite(positionSeconds) && positionSeconds >= 0 &&
|
|
103
|
+
!viewers.get?.(consumerId)?.isOfAnEarlierViewing(generation);
|
|
104
|
+
renditions.viewerPlays(sessionId, consumerId, playingHeight, placed ? positionSeconds : undefined);
|
|
103
105
|
}
|
|
104
106
|
// Where they stood and whether they were playing or waiting, before this
|
|
105
107
|
// report: what the encoders are planned from (`movedForThePlan`).
|
|
@@ -126,7 +128,8 @@ export async function handleApiTranscodeSessionNetReportPost(req, reply,
|
|
|
126
128
|
qualityMode,
|
|
127
129
|
visiblePicture,
|
|
128
130
|
positionSeconds:
|
|
129
|
-
Number.isFinite(positionSeconds) && positionSeconds >= 0 ? positionSeconds : undefined
|
|
131
|
+
Number.isFinite(positionSeconds) && positionSeconds >= 0 ? positionSeconds : undefined,
|
|
132
|
+
generation
|
|
130
133
|
}
|
|
131
134
|
});
|
|
132
135
|
if (!recorded) {
|
package/server.js
CHANGED
|
@@ -16,8 +16,8 @@ import { handleMediaGet } from "./routes/media/get.js";
|
|
|
16
16
|
import { ProbeRequests } from "./services/media/ProbeRequests.js";
|
|
17
17
|
import { IndexMemory } from "./services/storage/IndexMemory.js";
|
|
18
18
|
import { probePackets } from "./services/media/probe-packets.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { trackChoicesOf } from "./services/viewer/segment-demands.js";
|
|
20
|
+
import { PlaybackProjection } from "./services/torrent/PlaybackProjection.js";
|
|
21
21
|
import { SegmentInputTable } from "./services/viewer/segment-input-table.js";
|
|
22
22
|
import { SourcePreparation } from "./services/viewer/SourcePreparation.js";
|
|
23
23
|
import { handleHealthzGet } from "./routes/healthz/get.js";
|
|
@@ -62,7 +62,6 @@ import { SubtitleOrchestrator } from "./services/media/SubtitleOrchestrator.js";
|
|
|
62
62
|
import { containerOrchestrator, CONTAINER_HEAD_BYTES, describeWorkTags } from "./services/media/ContainerOrchestrator.js";
|
|
63
63
|
import { readPlaybackDeclarations } from "./services/media/read-playback-declarations.js";
|
|
64
64
|
import { DownloadMaps } from "./services/viewer/DownloadMaps.js";
|
|
65
|
-
import { passInputs, samePass } from "./services/viewer/playback-pass.js";
|
|
66
65
|
import { nativeSourceMap, nativeOriginalSourceMap } from "./services/viewer/NativeSourceMap.js";
|
|
67
66
|
import { MediaReadRequests } from "./services/media/MediaReadRequests.js";
|
|
68
67
|
import { SegmentInputs } from "./services/media/SegmentInputs.js";
|
|
@@ -188,7 +187,8 @@ export async function startProxyServer({
|
|
|
188
187
|
downloadMaps.retire(sourceKey);
|
|
189
188
|
for (const key of segmentSourceRanges.keys()) if (key.startsWith(`${sourceKey}:`)) segmentSourceRanges.delete(key);
|
|
190
189
|
for (const key of segmentReadings.keys()) if (key.startsWith(`${sourceKey}:`)) segmentReadings.delete(key);
|
|
191
|
-
for (const key of
|
|
190
|
+
for (const key of playbackProjections.keys()) if (key.startsWith(`${sourceKey}:`)) playbackProjections.delete(key);
|
|
191
|
+
for (const key of madeRates.keys()) if (key.startsWith(`${sourceKey}:`)) madeRates.delete(key);
|
|
192
192
|
segmentInputs.forget(sourceKey);
|
|
193
193
|
outputParts.encodeInputs?.bytesChanged();
|
|
194
194
|
},
|
|
@@ -487,37 +487,67 @@ export async function startProxyServer({
|
|
|
487
487
|
records.set(demand.index, record);
|
|
488
488
|
return record;
|
|
489
489
|
};
|
|
490
|
-
/**
|
|
491
|
-
|
|
492
|
-
* from and what it resolved (`playback-pass.js`); forgotten with the source.
|
|
493
|
-
*/
|
|
494
|
-
const resolvedPasses = new Map();
|
|
490
|
+
/** What the torrent is told of each played file, kept from map to map (`PlaybackProjection`); forgotten with the source. */
|
|
491
|
+
const playbackProjections = new Map();
|
|
495
492
|
/** Which source bytes each segment of an output reads, for the forecast (`segment-input-table.js`). */
|
|
496
493
|
const segmentInputs = new SegmentInputTable();
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if (reused) passCosts.reused += 1;
|
|
508
|
-
else {
|
|
509
|
-
passCosts.worked += 1;
|
|
510
|
-
if (!complete) passCosts.incomplete += 1;
|
|
511
|
-
passCosts.segments += segments; passCosts.ranges += ranges; passCosts.zones += zones;
|
|
512
|
-
passCosts.ms += ms; passCosts.maxMs = Math.max(passCosts.maxMs, ms);
|
|
494
|
+
/** An output's cut grid, checked once for each timeline. */
|
|
495
|
+
const checkedGrids = new WeakMap();
|
|
496
|
+
const gridOf = (output) => {
|
|
497
|
+
const timeline = output.timeline;
|
|
498
|
+
if (!timeline || typeof timeline !== "object") return null;
|
|
499
|
+
if (checkedGrids.has(timeline)) return checkedGrids.get(timeline);
|
|
500
|
+
const grid = timeline.published ?? timeline.boundaries;
|
|
501
|
+
if (!Array.isArray(grid)) {
|
|
502
|
+
checkedGrids.set(timeline, null);
|
|
503
|
+
return null;
|
|
513
504
|
}
|
|
505
|
+
for (let index = 0; index + 1 < grid.length; index++) {
|
|
506
|
+
if (!Number.isFinite(grid[index]) || !Number.isFinite(grid[index + 1]) || !(grid[index + 1] > grid[index])) {
|
|
507
|
+
throw new Error("An output segment needs a valid interval.");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
checkedGrids.set(timeline, grid);
|
|
511
|
+
return grid;
|
|
512
|
+
};
|
|
513
|
+
/**
|
|
514
|
+
* How many bytes a second of an output's film takes, from what it has made,
|
|
515
|
+
* read again only when what it has made changes (`SegmentStore.revisionOf`):
|
|
516
|
+
* summed over every made segment on every pass, it was a walk of the whole
|
|
517
|
+
* film a second (torrent-tv/meta#178).
|
|
518
|
+
*/
|
|
519
|
+
const madeRates = new Map();
|
|
520
|
+
const bytesPerSecondOf = (output, revision) => {
|
|
521
|
+
const kept = madeRates.get(output.outputKey);
|
|
522
|
+
if (kept && kept.revision === revision && kept.timeline === output.timeline) return kept.bytesPerSecond;
|
|
523
|
+
let bytes = 0, seconds = 0;
|
|
524
|
+
for (const [index, size] of outputParts.segmentStore.sizesOf(output.outputKey)) {
|
|
525
|
+
const span = output.timeline.publishedStartOf(index + 1) - output.timeline.publishedStartOf(index);
|
|
526
|
+
if (span > 0) { bytes += size; seconds += span; }
|
|
527
|
+
}
|
|
528
|
+
const bytesPerSecond = seconds > 0 ? bytes / seconds : null;
|
|
529
|
+
madeRates.set(output.outputKey, { revision, timeline: output.timeline, bytesPerSecond });
|
|
530
|
+
return bytesPerSecond;
|
|
531
|
+
};
|
|
532
|
+
// WHAT THE PASSES COST, said once a minute: how many maps were taken, how
|
|
533
|
+
// many segments a map made the projection work out again and how many of
|
|
534
|
+
// those maps it worked out whole, how many segments had their ranges worked
|
|
535
|
+
// out, the windows told, and the time from a pass's start to its result —
|
|
536
|
+
// which includes waits for reads, so it bounds the main thread's share
|
|
537
|
+
// rather than measuring it (torrent-tv/meta#178).
|
|
538
|
+
const passCosts = { since: Date.now(), passes: 0, whole: 0, reworked: 0, resolved: 0, windows: 0, ms: 0, maxMs: 0 };
|
|
539
|
+
const notePass = ({ whole = false, reworked = 0, resolved = 0, windows = 0, ms = 0 }) => {
|
|
540
|
+
passCosts.passes += 1;
|
|
541
|
+
if (whole) passCosts.whole += 1;
|
|
542
|
+
passCosts.reworked += reworked; passCosts.resolved += resolved; passCosts.windows += windows;
|
|
543
|
+
passCosts.ms += ms; passCosts.maxMs = Math.max(passCosts.maxMs, ms);
|
|
514
544
|
const now = Date.now();
|
|
515
545
|
if (now - passCosts.since < 60_000) return;
|
|
516
|
-
logger.info(`download map passes in ${((now - passCosts.since) / 1000).toFixed(1)}s: ${passCosts.
|
|
517
|
-
`${passCosts.
|
|
518
|
-
|
|
519
|
-
`${passCosts.
|
|
520
|
-
Object.assign(passCosts, { since: now,
|
|
546
|
+
logger.info(`download map passes in ${((now - passCosts.since) / 1000).toFixed(1)}s: ${passCosts.passes} map(s), ` +
|
|
547
|
+
`${passCosts.whole} worked out whole, ${passCosts.reworked} segment(s) worked out again, ` +
|
|
548
|
+
`${passCosts.resolved} segment(s) had their ranges worked out, ${passCosts.windows} window(s) told; ` +
|
|
549
|
+
`${Math.round(passCosts.ms)}ms from start to result in all, longest ${Math.round(passCosts.maxMs)}ms`);
|
|
550
|
+
Object.assign(passCosts, { since: now, passes: 0, whole: 0, reworked: 0, resolved: 0, windows: 0, ms: 0, maxMs: 0 });
|
|
521
551
|
};
|
|
522
552
|
const downloadMaps = new DownloadMaps({
|
|
523
553
|
// The ranges a picture reads at open are named again by every segment;
|
|
@@ -530,11 +560,16 @@ export async function startProxyServer({
|
|
|
530
560
|
publish: (map) => torrentPool.setPriorityMap({ ...map, zones: withoutRepeatedZones(map.zones) }),
|
|
531
561
|
// And what is sent is packed: a stretch of one level and priority is one
|
|
532
562
|
// window, not one zone per range of each of its segments
|
|
533
|
-
// (`coalescedZones`, torrent-tv/meta#178).
|
|
563
|
+
// (`coalescedZones`, torrent-tv/meta#178). The playback zones come packed
|
|
564
|
+
// already from the projection; this packs them with the native ones.
|
|
534
565
|
pack: (zones) => coalescedZones(zones),
|
|
535
566
|
log: (line) => logger.info(line),
|
|
536
567
|
resolvePlayback: async (map) => {
|
|
537
|
-
|
|
568
|
+
const fileKey = `${map.sourceKey}:${map.fileIndex}`;
|
|
569
|
+
if (map.zones.length === 0) {
|
|
570
|
+
playbackProjections.get(fileKey)?.clear();
|
|
571
|
+
return [];
|
|
572
|
+
}
|
|
538
573
|
const params = await containerOver(map);
|
|
539
574
|
if (!params || !map.isCurrent()) return [];
|
|
540
575
|
const tracks = await containerOrchestrator.inspect(params, "tracks");
|
|
@@ -548,67 +583,47 @@ export async function startProxyServer({
|
|
|
548
583
|
// ranges closer than a piece hold the same pieces as one.
|
|
549
584
|
const pieceLength = Number(torrentPool.knownTorrent(map.sourceKey)?.pieceLength);
|
|
550
585
|
const passStartedAt = performance.now();
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
586
|
+
// THE PROJECTION IS KEPT while what it was made from stands: the file's
|
|
587
|
+
// tracks, where its clock starts and how the torrent fetches. Any of them
|
|
588
|
+
// changed, every segment's ranges are other ranges.
|
|
589
|
+
let projection = playbackProjections.get(fileKey);
|
|
590
|
+
if (!projection || projection.trackList !== tracks.value || projection.shift !== shift ||
|
|
591
|
+
!Object.is(projection.pieceLength, pieceLength)) {
|
|
592
|
+
projection?.clear();
|
|
593
|
+
projection = new PlaybackProjection();
|
|
594
|
+
Object.assign(projection, { trackList: tracks.value, shift, pieceLength });
|
|
595
|
+
playbackProjections.set(fileKey, projection);
|
|
596
|
+
}
|
|
597
|
+
// Every output of this file somebody is watching, with what decides how
|
|
598
|
+
// long its segments take to make and deliver, what it has made, and what
|
|
599
|
+
// its next run would copy.
|
|
600
|
+
const played = [];
|
|
601
|
+
/** By output: what its segments read and how they are looked up. */
|
|
602
|
+
const readers = new Map();
|
|
564
603
|
for (const output of outputParts.outputs.values()) {
|
|
565
604
|
if (output.file.sourceKey !== map.sourceKey) continue;
|
|
566
605
|
const consumers = [...outputParts.viewers.forOutput(output).values()].filter(viewer =>
|
|
567
606
|
viewer.isPresent() && !outputParts.outputs.supersededBy(output, viewer.activeVariantId ?? null));
|
|
568
607
|
if (consumers.length === 0) continue;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (span > 0) { bytes += size; seconds += span; }
|
|
575
|
-
}
|
|
608
|
+
const choices = trackChoicesOf(output, map.fileIndex);
|
|
609
|
+
if (choices.length === 0) continue;
|
|
610
|
+
const grid = gridOf(output);
|
|
611
|
+
if (!grid) continue;
|
|
612
|
+
const revision = outputParts.segmentStore.revisionOf(output.outputKey);
|
|
576
613
|
const links = consumers.map(viewer => viewer.linkReading()?.linkMbps)
|
|
577
614
|
.filter(value => Number.isFinite(value) && value > 0);
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if (inventory.has(demand.index)) continue;
|
|
586
|
-
const deadlineAt = sourceDeadline(demand, { encodeSpeed,
|
|
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);
|
|
595
|
-
}
|
|
615
|
+
played.push({ outputKey: output.outputKey, grid,
|
|
616
|
+
encodeSpeed: outputParts.encodeCost.latestSpeedReadingOf(output)?.speed,
|
|
617
|
+
bytesPerSecond: bytesPerSecondOf(output, revision), linkMbps: links.length ? Math.min(...links) : null,
|
|
618
|
+
readyRevision: revision, ready: () => outputParts.segmentStore.sizesOf(output.outputKey),
|
|
619
|
+
target: outputParts.encodeInputs?.targetInputBytes?.(output) ?? null });
|
|
620
|
+
readers.set(output.outputKey, { records: segmentRecordsOf(map, output), choices,
|
|
621
|
+
facts: { timeline: output.timeline, trackList: tracks.value, shift, map } });
|
|
596
622
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
// asked for, in the order of its deadline, with its level and priority.
|
|
602
|
-
const fileKey = `${map.sourceKey}:${map.fileIndex}`;
|
|
603
|
-
const pass = { tracks: tracks.value, ...passInputs({ pieceLength, shift, targets, demands }) };
|
|
604
|
-
const last = resolvedPasses.get(fileKey);
|
|
605
|
-
if (samePass(last, pass)) {
|
|
606
|
-
notePass({ reused: true });
|
|
607
|
-
return last.result;
|
|
608
|
-
}
|
|
609
|
-
// Kept only from a pass that worked every segment out; one that met a
|
|
610
|
-
// reading not there yet is worked out again until it is.
|
|
611
|
-
let complete = true;
|
|
623
|
+
// WHAT THE MAP CHANGED, and only that, is worked out again here.
|
|
624
|
+
projection.update({ zones: map.zones, outputs: played });
|
|
625
|
+
const reworked = projection.lastUpdate;
|
|
626
|
+
let resolved = 0;
|
|
612
627
|
// A container that names the bytes of an interval from an index it holds
|
|
613
628
|
// answers every segment by arithmetic: once that index is read
|
|
614
629
|
// ("source-navigation"), the ranges are computed here directly instead of
|
|
@@ -628,22 +643,21 @@ export async function startProxyServer({
|
|
|
628
643
|
// two-hour AVI (torrent-tv/meta#178).
|
|
629
644
|
const waiting = new Set();
|
|
630
645
|
const turn = takeTurns();
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const
|
|
634
|
-
let interval = downloadIntervals.get(owner);
|
|
635
|
-
if (!interval) downloadIntervals.set(owner, interval = { from: owner.from, to: owner.to });
|
|
636
|
-
return interval;
|
|
637
|
-
};
|
|
638
|
-
for (const zone of demands) {
|
|
646
|
+
// ONLY THE SEGMENTS WHOSE RANGES ARE NOT KNOWN are worked out, in the
|
|
647
|
+
// order the swarm is to fetch them.
|
|
648
|
+
for (const zone of projection.unresolved()) {
|
|
639
649
|
if (!map.isCurrent()) return [];
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
|
|
650
|
+
const reader = readers.get(zone.outputKey);
|
|
651
|
+
if (!reader) continue;
|
|
652
|
+
// WHAT A SEGMENT READS IS WORKED OUT ONCE (`segmentRecordOf`). Rebuilt
|
|
653
|
+
// for every segment of the film on every map, this pass held the main
|
|
654
|
+
// thread up to 1.5 s at a time on Home Assistant (torrent-tv/meta#178).
|
|
655
|
+
const record = segmentRecordOf(reader.records, { index: zone.index, tracks: reader.choices }, reader.facts);
|
|
645
656
|
const reading = record.reading;
|
|
646
|
-
if (!reading)
|
|
657
|
+
if (!reading) {
|
|
658
|
+
projection.setUnreadable(zone.outputKey, zone.index);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
647
661
|
const { wanted, modes, interval, rangesKey } = reading;
|
|
648
662
|
const convert = ranges => {
|
|
649
663
|
if (record.ranges !== ranges || record.pieceLength !== pieceLength) {
|
|
@@ -652,23 +666,14 @@ export async function startProxyServer({
|
|
|
652
666
|
record.joined = joinedWithinPieces(ranges, pieceLength);
|
|
653
667
|
}
|
|
654
668
|
const joined = record.joined;
|
|
655
|
-
rangeCount += joined.length;
|
|
656
669
|
// What the segment reads is kept for the forecast in a table of its
|
|
657
670
|
// own, whatever form the torrent is told it in.
|
|
658
671
|
if (record.registered !== joined) {
|
|
659
672
|
segmentInputs.set(map.sourceKey, map.fileIndex, zone.outputKey, zone.index, joined);
|
|
660
673
|
record.registered = joined;
|
|
661
674
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
// zone it belongs to — is this pass's; the ranges are the list kept
|
|
665
|
-
// for the segment, unchanged from pass to pass. A zone for each
|
|
666
|
-
// range copied the demand into every one: ten thousand objects a
|
|
667
|
-
// pass over a two-hour AVI, made again 21-61 times a minute on Home
|
|
668
|
-
// Assistant (r26, torrent-tv/meta#178).
|
|
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 });
|
|
675
|
+
projection.setRanges(zone.outputKey, zone.index, joined);
|
|
676
|
+
resolved += 1;
|
|
672
677
|
};
|
|
673
678
|
// The bytes one segment needs are a fact of the file and the output, so
|
|
674
679
|
// they are worked out once. A whole film is thousands of segments and
|
|
@@ -677,10 +682,10 @@ export async function startProxyServer({
|
|
|
677
682
|
// were never asked for (Home Assistant 2026-10-08, torrent-tv/meta#151).
|
|
678
683
|
const known = record.ranges ?? segmentSourceRanges.get(rangesKey);
|
|
679
684
|
if (known) { convert(known); continue; }
|
|
680
|
-
if (waiting.has(zone.outputKey))
|
|
685
|
+
if (waiting.has(zone.outputKey)) continue;
|
|
681
686
|
// What is resolved so far is stated before a lower zone is worked out
|
|
682
687
|
// (DownloadMaps, torrent-tv/meta#178).
|
|
683
|
-
map.workingOut?.(zone.priority,
|
|
688
|
+
map.workingOut?.(zone.priority, () => projection.windows());
|
|
684
689
|
if (direct) {
|
|
685
690
|
// Asked per zone too: a soundtrack of an AVI is read from its own
|
|
686
691
|
// packets, which the same index states (torrent-tv/meta#166).
|
|
@@ -710,10 +715,9 @@ export async function startProxyServer({
|
|
|
710
715
|
requestId: `download:${map.sourceKey}:${map.fileIndex}:${zone.outputKey}:${zone.index}`,
|
|
711
716
|
demand: { ...zone.owner, leadSeconds: zone.leadSeconds } });
|
|
712
717
|
if (!map.isCurrent()) return [];
|
|
713
|
-
if (!intervalParams)
|
|
718
|
+
if (!intervalParams) continue;
|
|
714
719
|
const navigation = await containerOrchestrator.inspect(intervalParams, "source-navigation");
|
|
715
720
|
if (navigation.kind !== "result") {
|
|
716
|
-
complete = false;
|
|
717
721
|
if (navigation.kind === "needs-ranges" || navigation.kind === "needs-memory") waiting.add(zone.outputKey);
|
|
718
722
|
continue;
|
|
719
723
|
}
|
|
@@ -721,16 +725,15 @@ export async function startProxyServer({
|
|
|
721
725
|
const packets = await containerOrchestrator.inspect(intervalParams, originalRanges ? "source-ranges" : "packets");
|
|
722
726
|
if (!map.isCurrent()) return [];
|
|
723
727
|
if (packets.kind === "needs-memory") waiting.add(zone.outputKey);
|
|
724
|
-
if (packets.kind !== "result")
|
|
728
|
+
if (packets.kind !== "result") continue;
|
|
725
729
|
const input = originalRanges ? packets.value : new SegmentInputs({ index: packets.value, tracks: wanted }).forInterval({ ...interval, mode: track => modes.get(track) });
|
|
726
|
-
if (input.kind !== "result")
|
|
730
|
+
if (input.kind !== "result") continue;
|
|
727
731
|
segmentSourceRanges.set(rangesKey, input.ranges);
|
|
728
732
|
convert(input.ranges);
|
|
729
733
|
}
|
|
730
|
-
const result =
|
|
731
|
-
notePass({
|
|
732
|
-
|
|
733
|
-
else resolvedPasses.delete(fileKey);
|
|
734
|
+
const result = projection.windows();
|
|
735
|
+
notePass({ whole: reworked.whole, reworked: reworked.segments, resolved, windows: result.length,
|
|
736
|
+
ms: performance.now() - passStartedAt });
|
|
734
737
|
return result;
|
|
735
738
|
}
|
|
736
739
|
});
|
|
@@ -1001,6 +1004,7 @@ export async function startProxyServer({
|
|
|
1001
1004
|
mediaReads.retain(sourceKey, fileIndex, params => params.requestId?.startsWith("prepare:"));
|
|
1002
1005
|
if (!outputParts.viewers.forSource(sourceKey).length) probeReads.forget(sourceKey, fileIndex);
|
|
1003
1006
|
await downloadMaps.forget(sourceKey, fileIndex, { keepPreparation: true });
|
|
1007
|
+
playbackProjections.get(`${sourceKey}:${fileIndex}`)?.clear();
|
|
1004
1008
|
return;
|
|
1005
1009
|
}
|
|
1006
1010
|
await downloadMaps.playback({ sourceKey, fileIndex, durationSeconds, zones });
|
|
@@ -307,7 +307,7 @@ export class EncodeInputs {
|
|
|
307
307
|
* What the next run of this output would be given to copy, whatever has
|
|
308
308
|
* arrived: the stretch `stretchFor` sizes, within the memory encoder inputs
|
|
309
309
|
* may hold. The download map asks the swarm for that much as one group
|
|
310
|
-
* (`
|
|
310
|
+
* (`torrent/PlaybackProjection.js`); a stretch shortened to what is here
|
|
311
311
|
* would ask only for what is here. Null while it is not known.
|
|
312
312
|
*
|
|
313
313
|
* @param {object} output
|
|
@@ -139,6 +139,9 @@ export class SegmentStore {
|
|
|
139
139
|
*/
|
|
140
140
|
#inits = new Map();
|
|
141
141
|
|
|
142
|
+
/** The last number handed out by `revisionOf`'s readings. */
|
|
143
|
+
#revisions = 0;
|
|
144
|
+
|
|
142
145
|
/** Told of every segment published, whatever it is. @type {Set<(key: string, index: number) => void>} */
|
|
143
146
|
#publishedListeners = new Set();
|
|
144
147
|
|
|
@@ -332,7 +335,8 @@ export class SegmentStore {
|
|
|
332
335
|
this.#held.delete(key);
|
|
333
336
|
return empty;
|
|
334
337
|
}
|
|
335
|
-
const contents = { readAt: mtime, byNumber, sizes, bytes, unproven: highest, largest, followed: watched
|
|
338
|
+
const contents = { readAt: mtime, byNumber, sizes, bytes, unproven: highest, largest, followed: watched,
|
|
339
|
+
revision: ++this.#revisions };
|
|
336
340
|
this.#held.set(key, contents);
|
|
337
341
|
return contents;
|
|
338
342
|
}
|
|
@@ -361,6 +365,7 @@ export class SegmentStore {
|
|
|
361
365
|
return;
|
|
362
366
|
}
|
|
363
367
|
contents.bytes += size - (contents.sizes.get(index) ?? 0);
|
|
368
|
+
contents.revision = ++this.#revisions;
|
|
364
369
|
contents.byNumber.set(index, full);
|
|
365
370
|
contents.sizes.set(index, size);
|
|
366
371
|
if (index > contents.unproven) contents.unproven = index;
|
|
@@ -381,6 +386,7 @@ export class SegmentStore {
|
|
|
381
386
|
return;
|
|
382
387
|
}
|
|
383
388
|
contents.bytes -= contents.sizes.get(index) ?? 0;
|
|
389
|
+
contents.revision = ++this.#revisions;
|
|
384
390
|
contents.byNumber.delete(index);
|
|
385
391
|
contents.sizes.delete(index);
|
|
386
392
|
if (contents.unproven === index) contents.unproven = Math.max(-1, ...contents.byNumber.keys());
|
|
@@ -440,6 +446,20 @@ export class SegmentStore {
|
|
|
440
446
|
return new Map(this.refresh(key).sizes ?? []);
|
|
441
447
|
}
|
|
442
448
|
|
|
449
|
+
/**
|
|
450
|
+
* A number that moves whenever what this output holds changes, and only
|
|
451
|
+
* then: whoever keeps a reading of `sizesOf` reads it again when this moves
|
|
452
|
+
* rather than on every question. Unique across outputs and across the
|
|
453
|
+
* lifetimes of one output's directory, so a directory made again does not
|
|
454
|
+
* repeat an earlier number (torrent-tv/meta#178).
|
|
455
|
+
*
|
|
456
|
+
* @param {string} key
|
|
457
|
+
* @returns {number}
|
|
458
|
+
*/
|
|
459
|
+
revisionOf(key) {
|
|
460
|
+
return this.refresh(key).revision ?? 0;
|
|
461
|
+
}
|
|
462
|
+
|
|
443
463
|
/**
|
|
444
464
|
* The biggest piece this output has made, and which number it is.
|
|
445
465
|
*
|