@torrent-tv/proxy 2.99.19 → 2.99.21
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 +4 -1
- package/services/encode/EncodeInputs.js +7 -3
- package/services/media/ContainerOrchestrator.js +2 -1
- package/services/media/container/MatroskaContainer.js +11 -4
- package/services/media/container/PacketRecords.js +2 -0
- package/services/media/container/matroska-packets.js +6 -2
- package/services/storage/IndexMemory.js +19 -2
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -15,6 +15,7 @@ import { handleHealthGet } from "./routes/health/get.js";
|
|
|
15
15
|
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
|
+
import { LARGEST_GROWTH_BYTES } from "./services/media/container/PacketRecords.js";
|
|
18
19
|
import { probePackets } from "./services/media/probe-packets.js";
|
|
19
20
|
import { trackChoicesOf } from "./services/viewer/segment-demands.js";
|
|
20
21
|
import { PlaybackProjection } from "./services/torrent/PlaybackProjection.js";
|
|
@@ -770,6 +771,7 @@ export async function startProxyServer({
|
|
|
770
771
|
failed: (request, error) => logger.warn(`media request=${request.key} probe failed: ${error?.message ?? error}`)
|
|
771
772
|
});
|
|
772
773
|
const indexMemory = new IndexMemory({
|
|
774
|
+
growthBytes: LARGEST_GROWTH_BYTES,
|
|
773
775
|
reviseBudget: () => { void outputParts.machineBudget.revise("memory", "media metadata").catch(error => logger.warn(`packet index budget: ${error.message}`)); },
|
|
774
776
|
changed: () => { mediaReads.memoryChanged(); probeReads.memoryChanged(); outputParts.encodeInputs?.memoryChanged(); }
|
|
775
777
|
});
|
|
@@ -866,7 +868,7 @@ export async function startProxyServer({
|
|
|
866
868
|
},
|
|
867
869
|
// `timing`, when given, is told how long each read of a file waited behind
|
|
868
870
|
// the reads asked before it and how long it took (torrent-tv/meta#166).
|
|
869
|
-
resolveEncodeInput: async (output, fromIndex, toIndex, timing = null) => {
|
|
871
|
+
resolveEncodeInput: async (output, fromIndex, toIndex, timing = null, isCurrent = null) => {
|
|
870
872
|
const grid = output.timeline?.published ?? output.timeline?.boundaries;
|
|
871
873
|
const from = grid?.[fromIndex], to = grid?.[toIndex + 1];
|
|
872
874
|
if (!Number.isFinite(from) || !(to > from)) return { kind: "terminal", reason: "output-interval-not-declared" };
|
|
@@ -912,6 +914,7 @@ export async function startProxyServer({
|
|
|
912
914
|
if (media.kind !== "result") return media;
|
|
913
915
|
const timeShiftSeconds = Number(media.value?.startTimeSeconds) || 0;
|
|
914
916
|
const sourceInterval = output.timeline.sourceInterval(fromIndex, toIndex, timeShiftSeconds);
|
|
917
|
+
if (isCurrent) params.isCurrent = isCurrent;
|
|
915
918
|
params.packetInterval = { ...sourceInterval,
|
|
916
919
|
trackIds: wanted.map(track => track.trackNumber),
|
|
917
920
|
modes: Object.fromEntries(wanted.map(track => [track.trackNumber, modes.get(track)])) };
|
|
@@ -92,10 +92,10 @@ export class EncodeInputs {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/** The container's statement of an interval, its time added to the request's account. */
|
|
95
|
-
async #resolveTimed(timing, output, from, to) {
|
|
95
|
+
async #resolveTimed(timing, output, from, to, isCurrent = null) {
|
|
96
96
|
const startedAt = this.#now();
|
|
97
97
|
try {
|
|
98
|
-
return await this.#resolve(output, from, to, timing);
|
|
98
|
+
return await this.#resolve(output, from, to, timing, isCurrent);
|
|
99
99
|
} finally {
|
|
100
100
|
timing.resolutions += 1;
|
|
101
101
|
timing.resolveMs += this.#now() - startedAt;
|
|
@@ -384,7 +384,11 @@ export class EncodeInputs {
|
|
|
384
384
|
reads: 0, queuedMs: 0, readMs: 0, heldMs: 0, budgetMs: 0 };
|
|
385
385
|
request.timing.attempts += 1;
|
|
386
386
|
request.promise = (async () => {
|
|
387
|
-
|
|
387
|
+
// A request the plan withdraws stops its reads: a soundtrack piece's
|
|
388
|
+
// packet walk, left to run, held the file's reads and the piece the
|
|
389
|
+
// viewer had seeked to waited 16.9 s behind it (r59, torrent-tv/meta#178).
|
|
390
|
+
const resolved = await this.#resolveTimed(request.timing, request.output, request.from, request.from,
|
|
391
|
+
() => this.#requests.get(request.key) === request);
|
|
388
392
|
if (resolved.kind === "result") this.#original.set(request.output, resolved.sources?.every(source => source.input.original === true) === true);
|
|
389
393
|
if (this.#requests.get(request.key) !== request) return;
|
|
390
394
|
let result;
|
|
@@ -215,7 +215,8 @@ export class ContainerOrchestrator {
|
|
|
215
215
|
value = await container.readSourceRanges(params.packetInterval);
|
|
216
216
|
if (value.kind !== "result") return { ...value, requestId };
|
|
217
217
|
} else if (statement === "packets") {
|
|
218
|
-
value = await container.readPacketIndex(params.packetInterval);
|
|
218
|
+
value = await container.readPacketIndex(params.packetInterval, { isCurrent: params.isCurrent });
|
|
219
|
+
if (value === null) return { kind: "terminal", reason: "request-obsolete", requestId };
|
|
219
220
|
await value?.prepareAudioDependencies?.(params.packetInterval, container.readRange);
|
|
220
221
|
} else {
|
|
221
222
|
throw new TypeError(`Unknown media statement: ${statement}`);
|
|
@@ -782,7 +782,12 @@ export class MatroskaContainer extends Container {
|
|
|
782
782
|
streamSearchSeconds: FFMPEG_STREAM_SEARCH_SECONDS };
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
-
|
|
785
|
+
/**
|
|
786
|
+
* @param {object} [interval]
|
|
787
|
+
* @param {{ isCurrent?: () => boolean }} [options] - A walk whose request is
|
|
788
|
+
* no longer current stops between clusters and answers null.
|
|
789
|
+
*/
|
|
790
|
+
async readPacketIndex(interval, { isCurrent } = {}) {
|
|
786
791
|
if (this.#packets) return this.#packets;
|
|
787
792
|
const layout = await this.#segmentLayout();
|
|
788
793
|
if (!layout) throw new Error("Matroska Segment is absent.");
|
|
@@ -801,7 +806,8 @@ export class MatroskaContainer extends Container {
|
|
|
801
806
|
const complete = new Set();
|
|
802
807
|
let whole = true;
|
|
803
808
|
for (const track of requested) {
|
|
804
|
-
const chain = await this.#readTrackPackets(layout, tracks, track, start, interval);
|
|
809
|
+
const chain = await this.#readTrackPackets(layout, tracks, track, start, interval, isCurrent);
|
|
810
|
+
if (!chain) return null;
|
|
805
811
|
streams.set(track.trackNumber, chain.records);
|
|
806
812
|
if (chain.reachesEnd) complete.add(track.trackNumber);
|
|
807
813
|
if (!chain.reachesEnd || chain.startAt !== layout.firstClusterAt) whole = false;
|
|
@@ -825,7 +831,7 @@ export class MatroskaContainer extends Container {
|
|
|
825
831
|
*
|
|
826
832
|
* @returns {Promise<{ records: PacketRecordsChain, startAt: number, reachesEnd: boolean }>}
|
|
827
833
|
*/
|
|
828
|
-
async #readTrackPackets(layout, tracks, track, start, interval) {
|
|
834
|
+
async #readTrackPackets(layout, tracks, track, start, interval, isCurrent) {
|
|
829
835
|
const id = track.trackNumber;
|
|
830
836
|
const previous = this.#packetWalks.get(id) ?? Promise.resolve();
|
|
831
837
|
let finished;
|
|
@@ -882,7 +888,8 @@ export class MatroskaContainer extends Container {
|
|
|
882
888
|
const following = readings.find(reading => reading.startAt > current.startAt);
|
|
883
889
|
const outcome = await walkMatroskaPackets({ readRange: this.readRange, fileSize: this.fileSize,
|
|
884
890
|
portionBytes: this.portionBytes, layout, tracks, trackIds: [id], interval, packetMemory: this.packetMemory,
|
|
885
|
-
state: current, until: following?.startAt ?? null, isCovered: covered, readsThrough });
|
|
891
|
+
state: current, until: following?.startAt ?? null, isCovered: covered, readsThrough, isCurrent });
|
|
892
|
+
if (outcome === "obsolete") return null;
|
|
886
893
|
if (outcome !== "until") break;
|
|
887
894
|
current.next = following;
|
|
888
895
|
}
|
|
@@ -9,6 +9,8 @@ const BLOCK_BYTES = 65536;
|
|
|
9
9
|
// Matroska soundtrack read in stretches: 144 tables of one 25-minute film held
|
|
10
10
|
// 18.9 MB for 3.7 MB of records (r47 reproduced, torrent-tv/meta#178).
|
|
11
11
|
const FIRST_BLOCK_BYTES = 4096;
|
|
12
|
+
/** The most one record can make a table take at once: a block of records and one of addresses. */
|
|
13
|
+
export const LARGEST_GROWTH_BYTES = 2 * BLOCK_BYTES;
|
|
12
14
|
const ADDRESS_BYTES = 8;
|
|
13
15
|
const HEADER_BYTES = 56;
|
|
14
16
|
|
|
@@ -44,9 +44,12 @@ function recordedTracks(tracks, trackIds) {
|
|
|
44
44
|
* `start` on, where the caller knows it: those are read a portion at a time
|
|
45
45
|
* across clusters too.
|
|
46
46
|
*
|
|
47
|
-
*
|
|
47
|
+
* A walk whose `isCurrent` turns false stops at the next cluster, where its
|
|
48
|
+
* state is whole, and answers "obsolete".
|
|
49
|
+
*
|
|
50
|
+
* @returns {Promise<"covered" | "until" | "end" | "obsolete">}
|
|
48
51
|
*/
|
|
49
|
-
export async function walkMatroskaPackets({ readRange, fileSize, portionBytes, layout, tracks, trackIds, interval, packetMemory, state, until = null, isCovered = null, readsThrough = null }) {
|
|
52
|
+
export async function walkMatroskaPackets({ readRange, fileSize, portionBytes, layout, tracks, trackIds, interval, packetMemory, state, until = null, isCovered = null, readsThrough = null, isCurrent = null }) {
|
|
50
53
|
// INSIDE A CLUSTER EVERY BLOCK HEADER IS READ, so its bytes are read a
|
|
51
54
|
// portion at a time up to the cluster's end, and never past it. Read a
|
|
52
55
|
// header at a time, each a read of a few bytes from the torrent thread, the
|
|
@@ -71,6 +74,7 @@ export async function walkMatroskaPackets({ readRange, fileSize, portionBytes, l
|
|
|
71
74
|
if (covered()) return "covered";
|
|
72
75
|
let at = state.at;
|
|
73
76
|
while (at < limit) {
|
|
77
|
+
if (!state.cluster && isCurrent?.() === false) return "obsolete";
|
|
74
78
|
const element = state.cluster?.element ?? await validHeader(reader, at, layout.segmentEnd);
|
|
75
79
|
if (element.id !== CLUSTER) {
|
|
76
80
|
if (element.end === null) throw new Error("An unknown-sized Matroska element is not a Cluster.");
|
|
@@ -7,18 +7,35 @@ export class IndexMemory {
|
|
|
7
7
|
#revise;
|
|
8
8
|
#changed;
|
|
9
9
|
#revisionQueued = false;
|
|
10
|
+
#growthBytes;
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
/**
|
|
13
|
+
* @param {object} params
|
|
14
|
+
* @param {() => void} params.reviseBudget
|
|
15
|
+
* @param {() => void} params.changed
|
|
16
|
+
* @param {number} [params.growthBytes] - What one growth of a table can
|
|
17
|
+
* take at most, kept free beyond what is held while anything is held. A
|
|
18
|
+
* table grows a block at a time, each twice the last; allowed exactly what
|
|
19
|
+
* it held, each growth was refused first and its whole read tried again:
|
|
20
|
+
* a soundtrack piece after a seek was refused three times and ready after
|
|
21
|
+
* 53.7 s (r58, torrent-tv/meta#178).
|
|
22
|
+
*/
|
|
23
|
+
constructor({ reviseBudget, changed, growthBytes = 0 }) {
|
|
12
24
|
this.#revise = reviseBudget;
|
|
13
25
|
this.#changed = changed;
|
|
26
|
+
this.#growthBytes = growthBytes;
|
|
14
27
|
}
|
|
15
28
|
|
|
16
29
|
held() { return this.#held; }
|
|
17
30
|
packetBytes() { return this.#packetBytes; }
|
|
18
31
|
wanted() {
|
|
32
|
+
const short = [...this.#files.values()].reduce((sum, file) => sum + Math.max(0, file.needed - file.held), 0);
|
|
33
|
+
return this.#held + Math.max(short, this.#held > 0 ? this.#growthBytes : 0);
|
|
34
|
+
}
|
|
35
|
+
/** What is held and asked for, without the margin kept for growth. */
|
|
36
|
+
required() {
|
|
19
37
|
return this.#held + [...this.#files.values()].reduce((sum, file) => sum + Math.max(0, file.needed - file.held), 0);
|
|
20
38
|
}
|
|
21
|
-
required() { return this.wanted(); }
|
|
22
39
|
|
|
23
40
|
allow(bytes) {
|
|
24
41
|
const allowed = Math.max(0, bytes);
|