@torrent-tv/proxy 2.98.6 → 2.98.8
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
|
@@ -273,7 +273,15 @@ export class EncodeInputs {
|
|
|
273
273
|
bytesChanged(arrived = null) {
|
|
274
274
|
this.#revision++;
|
|
275
275
|
for (const request of this.#requests.values()) {
|
|
276
|
-
if (request.pending
|
|
276
|
+
if (request.pending) {
|
|
277
|
+
// The try in flight may have read before these bytes came. What it
|
|
278
|
+
// waits for is known only when it answers, so they are kept for that
|
|
279
|
+
// answer; bytes not named make it try again whatever it answers.
|
|
280
|
+
if (!arrived) request.unnamedArrival = true;
|
|
281
|
+
else (request.arrivedMeanwhile ??= []).push(arrived);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (request.result?.kind === "result" || request.result?.kind === "terminal") continue;
|
|
277
285
|
if (arrived && request.result?.kind === "needs-bytes" && !awaitedIn(request.result, arrived)) {
|
|
278
286
|
// Nothing it waits for has come: it stands as tried at this revision.
|
|
279
287
|
request.revision = this.#revision;
|
|
@@ -341,6 +349,8 @@ export class EncodeInputs {
|
|
|
341
349
|
request.pending = true;
|
|
342
350
|
request.revision = this.#revision;
|
|
343
351
|
request.memoryRevision = this.#memoryRevision;
|
|
352
|
+
request.arrivedMeanwhile = null;
|
|
353
|
+
request.unnamedArrival = false;
|
|
344
354
|
// Where the time from the request to its input goes, across every attempt:
|
|
345
355
|
// field 2026-10-10, a picture's request took 38 s before its copy began and
|
|
346
356
|
// nothing said on what (torrent-tv/meta#166).
|
|
@@ -382,12 +392,29 @@ export class EncodeInputs {
|
|
|
382
392
|
request.pending = false;
|
|
383
393
|
if (request.result?.kind !== "needs-memory") this.#wanted.delete(request.key);
|
|
384
394
|
const changed = request.result?.kind === "needs-memory"
|
|
385
|
-
? request.memoryRevision !== this.#memoryRevision :
|
|
395
|
+
? request.memoryRevision !== this.#memoryRevision : this.#arrivedFor(request);
|
|
386
396
|
if (this.#requests.get(request.key) === request && changed &&
|
|
387
397
|
request.result?.kind !== "result" && request.result?.kind !== "terminal") this.#prepare(request);
|
|
388
398
|
});
|
|
389
399
|
}
|
|
390
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Whether bytes came during a try that the try may have missed. A try that
|
|
403
|
+
* waits for named bytes is repeated only when some of them came, or when an
|
|
404
|
+
* event named nothing; the others stand it as tried at this revision. In r36
|
|
405
|
+
* a held-range check took 344 ms, and a piece of the file arriving during it
|
|
406
|
+
* started another (torrent-tv/meta#178).
|
|
407
|
+
*/
|
|
408
|
+
#arrivedFor(request) {
|
|
409
|
+
if (request.revision === this.#revision) return false;
|
|
410
|
+
const arrivals = request.arrivedMeanwhile ?? [];
|
|
411
|
+
request.arrivedMeanwhile = null;
|
|
412
|
+
if (request.unnamedArrival || request.result?.kind !== "needs-bytes" ||
|
|
413
|
+
arrivals.some(arrived => awaitedIn(request.result, arrived))) return true;
|
|
414
|
+
request.revision = this.#revision;
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
|
|
391
418
|
/** The reservation a packet input makes: all of it, or nothing. */
|
|
392
419
|
#reserveFor(request) {
|
|
393
420
|
return async bytes => {
|
|
@@ -60,7 +60,13 @@ export class ViewerRequests {
|
|
|
60
60
|
#host;
|
|
61
61
|
/** @type {WeakMap<object, Map<string, object>>} */
|
|
62
62
|
#rateStateBySession = new WeakMap();
|
|
63
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* The pieces of each output as the start forecast read them last (see
|
|
65
|
+
* `#segmentsOf`), and what the forecast keeps of their time geometry while
|
|
66
|
+
* they stand (`playback-readiness.js` `timescaleOfPieces`).
|
|
67
|
+
*
|
|
68
|
+
* @type {WeakMap<object, { pieces: object[], geometry: object }>}
|
|
69
|
+
*/
|
|
64
70
|
#forecastPieces = new WeakMap();
|
|
65
71
|
|
|
66
72
|
/**
|
|
@@ -658,6 +664,7 @@ export class ViewerRequests {
|
|
|
658
664
|
this.#host.initialOutputBitsPerMediaSecondOf?.(output) ?? 0,
|
|
659
665
|
readings: trackRates,
|
|
660
666
|
segments,
|
|
667
|
+
geometry: this.#forecastPieces.get(output)?.geometry,
|
|
661
668
|
readySegmentIndices: [...inventory.keys()],
|
|
662
669
|
segmentSizesBytes: inventory
|
|
663
670
|
});
|
|
@@ -808,11 +815,12 @@ export class ViewerRequests {
|
|
|
808
815
|
#segmentsOf(output, sessionInit, inventory, positionSeconds) {
|
|
809
816
|
const timeline = output.timeline;
|
|
810
817
|
const count = timeline?.segmentCount ?? 0;
|
|
811
|
-
let
|
|
812
|
-
if (!
|
|
813
|
-
|
|
814
|
-
this.#forecastPieces.set(output,
|
|
818
|
+
let forecast = this.#forecastPieces.get(output);
|
|
819
|
+
if (!forecast) {
|
|
820
|
+
forecast = { pieces: [], geometry: {} };
|
|
821
|
+
this.#forecastPieces.set(output, forecast);
|
|
815
822
|
}
|
|
823
|
+
const kept = forecast.pieces;
|
|
816
824
|
const segments = [];
|
|
817
825
|
for (let index = 0; index < count; index++) {
|
|
818
826
|
const endSeconds = timeline.publishedStartOf(index + 1);
|
|
@@ -139,21 +139,16 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
139
139
|
range.end > range.start).map((range) => ({ start: fromSeconds(range.start), end: fromSeconds(range.end) }))
|
|
140
140
|
: null);
|
|
141
141
|
// One timescale in which every time of this forecast is a whole number of
|
|
142
|
-
// ticks, so that adding and comparing never seek a common denominator.
|
|
142
|
+
// ticks, so that adding and comparing never seek a common denominator. A
|
|
143
|
+
// track's pieces give it the same timescale while they are the same pieces.
|
|
143
144
|
const unit = commonTimescale([
|
|
144
145
|
fromSeconds(finiteNonNegative(input.positionSeconds)), fromSeconds(duration),
|
|
145
146
|
fromSeconds(finiteNonNegative(input.bufferedAheadSeconds)), REPORTED_TIME_ERROR, ...clocks,
|
|
146
147
|
...reported.flatMap((ranges) => (ranges ?? []).flatMap(({ start, end }) => [start, end])),
|
|
147
|
-
...tracks.
|
|
148
|
-
...(segment.mediaRanges?.timescale ? [mediaTime(0n, segment.mediaRanges.timescale)] : []),
|
|
149
|
-
...exactBoundsOf(segment)
|
|
150
|
-
]))
|
|
148
|
+
...tracks.map((track) => mediaTime(0n, timescaleOfPieces(track)))
|
|
151
149
|
]);
|
|
152
150
|
const zero = mediaTime(0n, unit);
|
|
153
|
-
const segmentsOf = tracks.map((track, trackIndex) =>
|
|
154
|
-
(Array.isArray(track.segments) ? track.segments : []).map((segment) => ({
|
|
155
|
-
...segment, exact: exactTimesOf(segment), ranges: playerRanges(segment.mediaRanges, rescale(clocks[trackIndex], unit), unit)
|
|
156
|
-
})), unit));
|
|
151
|
+
const segmentsOf = tracks.map((track, trackIndex) => placedPieces(track, rescale(clocks[trackIndex], unit), unit));
|
|
157
152
|
const origin = latest(zero, ...segmentsOf.map((segments) =>
|
|
158
153
|
segments.find(({ index }) => index === 0)?.ranges?.[0]?.start ?? zero));
|
|
159
154
|
const at = latest(origin, rescale(fromSeconds(finiteNonNegative(input.positionSeconds)), unit));
|
|
@@ -645,10 +640,69 @@ function timesOf(segment) {
|
|
|
645
640
|
return segment.exact ?? exactTimesOf(segment);
|
|
646
641
|
}
|
|
647
642
|
|
|
648
|
-
/**
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
643
|
+
/**
|
|
644
|
+
* WHAT A TRACK'S PIECES GIVE THE FORECAST'S TIME, KEPT BETWEEN POLLS while
|
|
645
|
+
* they are the same pieces. The forecast runs on every poll of a waiting
|
|
646
|
+
* viewer, and gathering the edges of every remaining piece for the timescale,
|
|
647
|
+
* then placing each on the page's clock, was about 3 s of the proxy's main
|
|
648
|
+
* thread in r35's first 15 minutes on Home Assistant (torrent-tv/meta#178).
|
|
649
|
+
* `track.geometry` is kept by whoever keeps the pieces; without it nothing is
|
|
650
|
+
* kept, and the answer is the same either way: a placement is reused only for
|
|
651
|
+
* the same pieces, the same timescale and the same clock offset, so no time is
|
|
652
|
+
* rounded for the reuse.
|
|
653
|
+
*
|
|
654
|
+
* @param {{ segments?: object[], geometry?: object }} track
|
|
655
|
+
* @returns {bigint} The least timescale holding every edge and every served tick of its pieces.
|
|
656
|
+
*/
|
|
657
|
+
function timescaleOfPieces(track) {
|
|
658
|
+
const segments = Array.isArray(track.segments) ? track.segments : [];
|
|
659
|
+
const geometry = track.geometry;
|
|
660
|
+
if (geometry && samePieces(geometry.pieces, segments)) return geometry.timescale;
|
|
661
|
+
const distinct = new Set();
|
|
662
|
+
for (const segment of segments) {
|
|
663
|
+
if (segment.mediaRanges?.timescale) distinct.add(segment.mediaRanges.timescale);
|
|
664
|
+
const { start, end } = exactTimesOf(segment);
|
|
665
|
+
if (start) distinct.add(start.timescale);
|
|
666
|
+
if (end) distinct.add(end.timescale);
|
|
667
|
+
}
|
|
668
|
+
const timescale = commonTimescale([...distinct].map((value) => mediaTime(0n, value)));
|
|
669
|
+
if (geometry) {
|
|
670
|
+
geometry.pieces = segments.map((segment) => ({ segment, index: segment.index, startSeconds: segment.startSeconds,
|
|
671
|
+
endSeconds: segment.endSeconds, mediaRanges: segment.mediaRanges }));
|
|
672
|
+
geometry.timescale = timescale;
|
|
673
|
+
geometry.placed = null;
|
|
674
|
+
}
|
|
675
|
+
return timescale;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* A track's pieces on the page's clock in `unit`: read pieces at their served
|
|
680
|
+
* ranges, the others placed between them. Reused while the pieces are those
|
|
681
|
+
* {@link timescaleOfPieces} last took and `unit` and `clock` are the same.
|
|
682
|
+
*
|
|
683
|
+
* @param {{ segments?: object[], geometry?: object }} track
|
|
684
|
+
* @param {import("./media-time.js").MediaTime} clock - In `unit`.
|
|
685
|
+
* @param {bigint} unit
|
|
686
|
+
*/
|
|
687
|
+
function placedPieces(track, clock, unit) {
|
|
688
|
+
const geometry = track.geometry;
|
|
689
|
+
if (geometry?.placed && geometry.placedUnit === unit && geometry.placedClock === clock.ticks) return geometry.placed;
|
|
690
|
+
const placed = predictedMediaSegments((Array.isArray(track.segments) ? track.segments : []).map((segment) => ({
|
|
691
|
+
...segment, exact: exactTimesOf(segment), ranges: playerRanges(segment.mediaRanges, clock, unit)
|
|
692
|
+
})), unit);
|
|
693
|
+
if (geometry) Object.assign(geometry, { placed, placedUnit: unit, placedClock: clock.ticks });
|
|
694
|
+
return placed;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** Whether `segments` are the pieces `kept` recorded, field by field the forecast reads. */
|
|
698
|
+
function samePieces(kept, segments) {
|
|
699
|
+
if (!Array.isArray(kept) || kept.length !== segments.length) return false;
|
|
700
|
+
for (let index = 0; index < segments.length; index++) {
|
|
701
|
+
const segment = segments[index], was = kept[index];
|
|
702
|
+
if (segment !== was.segment || segment.index !== was.index || segment.startSeconds !== was.startSeconds ||
|
|
703
|
+
segment.endSeconds !== was.endSeconds || segment.mediaRanges !== was.mediaRanges) return false;
|
|
704
|
+
}
|
|
705
|
+
return true;
|
|
652
706
|
}
|
|
653
707
|
|
|
654
708
|
/**
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export class SegmentInputTable {
|
|
13
|
-
/** `sourceKey` → output → segment →
|
|
13
|
+
/** `sourceKey` → output → segment → its files' ranges, and the list `inputsFor` answers while they stand. @type {Map<string, Map<string, Map<number, { files: Map<number, { joined: Array<[number, number]>, ranges: Array<{ start: number, end: number }> }>, inputs: Array<{ sourceId: string, ranges: Array<{ start: number, end: number }> }> | null }>>>} */
|
|
14
14
|
#bySource = new Map();
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -25,15 +25,19 @@ export class SegmentInputTable {
|
|
|
25
25
|
if (!outputs) this.#bySource.set(sourceKey, outputs = new Map());
|
|
26
26
|
let segments = outputs.get(outputKey);
|
|
27
27
|
if (!segments) outputs.set(outputKey, segments = new Map());
|
|
28
|
-
let
|
|
29
|
-
if (!
|
|
28
|
+
let segment = segments.get(index);
|
|
29
|
+
if (!segment) segments.set(index, segment = { files: new Map(), inputs: null });
|
|
30
30
|
// The same reading keeps its list: whoever compares inputs compares them by it.
|
|
31
|
-
if (files.get(fileIndex)?.joined === joined) return;
|
|
32
|
-
files.set(fileIndex, { joined, ranges: joined.map(([start, end]) => ({ start, end: end + 1 })) });
|
|
31
|
+
if (segment.files.get(fileIndex)?.joined === joined) return;
|
|
32
|
+
segment.files.set(fileIndex, { joined, ranges: joined.map(([start, end]) => ({ start, end: end + 1 })) });
|
|
33
|
+
segment.inputs = null;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
* The bytes one segment reads, by file, or null where none are known.
|
|
37
|
+
* The bytes one segment reads, by file, or null where none are known. The
|
|
38
|
+
* same list while they stand: the start forecast asks for every segment of
|
|
39
|
+
* the film on each poll, and keeps what it derived from a segment whose
|
|
40
|
+
* inputs are the same list.
|
|
37
41
|
*
|
|
38
42
|
* @param {string} sourceKey
|
|
39
43
|
* @param {string} outputKey
|
|
@@ -41,9 +45,10 @@ export class SegmentInputTable {
|
|
|
41
45
|
* @returns {Array<{ sourceId: string, ranges: Array<{ start: number, end: number }> }> | null}
|
|
42
46
|
*/
|
|
43
47
|
inputsFor(sourceKey, outputKey, index) {
|
|
44
|
-
const
|
|
45
|
-
if (!files
|
|
46
|
-
|
|
48
|
+
const segment = this.#bySource.get(sourceKey)?.get(outputKey)?.get(index);
|
|
49
|
+
if (!segment?.files.size) return null;
|
|
50
|
+
segment.inputs ??= [...segment.files].map(([fileIndex, { ranges }]) => ({ sourceId: `${sourceKey}:${fileIndex}`, ranges }));
|
|
51
|
+
return segment.inputs;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
/** @param {string} sourceKey */
|