@torrent-tv/proxy 2.95.9 → 2.95.11
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 +5 -1
- package/services/encode/EncodeInputs.js +13 -3
- package/services/encode/EncodeRuns.js +1 -1
- package/services/encode/segment-formats/presentation-segment.js +22 -7
- package/services/media/KeyframeTables.js +7 -1
- package/services/media/container/AviContainer.js +31 -11
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -494,7 +494,7 @@ export async function startProxyServer({
|
|
|
494
494
|
});
|
|
495
495
|
const indexMemory = new IndexMemory({
|
|
496
496
|
reviseBudget: () => { void outputParts.machineBudget.revise().catch(error => logger.warn(`packet index budget: ${error.message}`)); },
|
|
497
|
-
changed: () => { mediaReads.memoryChanged(); probeReads.memoryChanged(); }
|
|
497
|
+
changed: () => { mediaReads.memoryChanged(); probeReads.memoryChanged(); outputParts.encodeInputs?.memoryChanged(); }
|
|
498
498
|
});
|
|
499
499
|
const containerOver = async ({ sourceKey, fileIndex, requestId, packetInterval, demand }) => {
|
|
500
500
|
const record = sourceRegistry.get(sourceKey);
|
|
@@ -531,6 +531,10 @@ export async function startProxyServer({
|
|
|
531
531
|
onReadResult: async (statement, result, revision) => {
|
|
532
532
|
if (revision.demand !== downloadMaps.epoch(sourceKey, fileIndex)) return;
|
|
533
533
|
if (demand && !downloadMaps.wantsInterval(sourceKey, fileIndex, demand)) return;
|
|
534
|
+
if (statement === "keyframes" && result.kind === "result") {
|
|
535
|
+
keyframeTables.learn(params, { times: result.value?.times ?? null, tolerance: result.value?.tolerance ?? 0,
|
|
536
|
+
format: containerOrchestrator.known(sourceKey, fileIndex)?.formatName ?? "unrecognised" });
|
|
537
|
+
}
|
|
534
538
|
const finished = mediaReads.record(params, statement, result, revision.storage, revision.memory);
|
|
535
539
|
await downloadMaps.metadata({ sourceKey, fileIndex, statement: requestId ? `${statement}:${requestId}` : statement, result,
|
|
536
540
|
...(demand ? { priority: demand.priority, urgent: demand.urgent, deadlineAt: demand.deadlineAt, interval: demand, leadSeconds: demand.leadSeconds ?? 0 } : {}) });
|
|
@@ -5,6 +5,7 @@ import { admitOriginalInput } from "./OriginalInput.js";
|
|
|
5
5
|
export class EncodeInputs {
|
|
6
6
|
#requests = new Map();
|
|
7
7
|
#revision = 0;
|
|
8
|
+
#memoryRevision = 0;
|
|
8
9
|
#held = 0;
|
|
9
10
|
#allowed = 0;
|
|
10
11
|
#wanted = new Map();
|
|
@@ -43,7 +44,13 @@ export class EncodeInputs {
|
|
|
43
44
|
allow(bytes) {
|
|
44
45
|
const changed = this.#allowed !== Math.max(0, bytes);
|
|
45
46
|
this.#allowed = Math.max(0, bytes);
|
|
46
|
-
if (changed) this
|
|
47
|
+
if (changed) this.memoryChanged();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Metadata admission and input admission can change independently. */
|
|
51
|
+
memoryChanged() {
|
|
52
|
+
this.#memoryRevision++;
|
|
53
|
+
this.#retryMemory();
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
#retryMemory() {
|
|
@@ -112,6 +119,7 @@ export class EncodeInputs {
|
|
|
112
119
|
#prepare(request) {
|
|
113
120
|
request.pending = true;
|
|
114
121
|
request.revision = this.#revision;
|
|
122
|
+
request.memoryRevision = this.#memoryRevision;
|
|
115
123
|
request.promise = (async () => {
|
|
116
124
|
const resolved = await this.#resolve(request.output, request.from, request.to);
|
|
117
125
|
if (this.#requests.get(request.key) !== request) return;
|
|
@@ -133,7 +141,7 @@ export class EncodeInputs {
|
|
|
133
141
|
return () => {
|
|
134
142
|
if (!released) {
|
|
135
143
|
released = true; this.#held -= bytes;
|
|
136
|
-
queueMicrotask(() => this
|
|
144
|
+
queueMicrotask(() => this.memoryChanged());
|
|
137
145
|
}
|
|
138
146
|
};
|
|
139
147
|
}
|
|
@@ -149,7 +157,9 @@ export class EncodeInputs {
|
|
|
149
157
|
}).finally(() => {
|
|
150
158
|
request.pending = false;
|
|
151
159
|
if (request.result?.kind !== "needs-memory") this.#wanted.delete(request.key);
|
|
152
|
-
|
|
160
|
+
const changed = request.result?.kind === "needs-memory"
|
|
161
|
+
? request.memoryRevision !== this.#memoryRevision : request.revision !== this.#revision;
|
|
162
|
+
if (this.#requests.get(request.key) === request && changed &&
|
|
153
163
|
request.result?.kind !== "result" && request.result?.kind !== "terminal") this.#prepare(request);
|
|
154
164
|
});
|
|
155
165
|
}
|
|
@@ -986,7 +986,7 @@ export class EncodeRuns {
|
|
|
986
986
|
if (Number.isInteger(index) && (index < safeIndex || index > run.to)) return null;
|
|
987
987
|
let bytes = null;
|
|
988
988
|
if (admittedInput && session.segmentFormat.id === "fmp4" && session.spec.carries !== "audio-only" &&
|
|
989
|
-
session.spec.audio && !this.#host.servesAudioSeparately(session)) {
|
|
989
|
+
(admittedInput.original || session.spec.audio && !this.#host.servesAudioSeparately(session))) {
|
|
990
990
|
try {
|
|
991
991
|
const address = session.outputKey ?? "";
|
|
992
992
|
const grid = session.timeline.published ?? session.timeline.boundaries;
|
|
@@ -9,7 +9,7 @@ export function presentationSegment(current, following, { from, to }) {
|
|
|
9
9
|
const edit = own.edits.get(id);
|
|
10
10
|
if (!edit) throw new Error("A segment track has no declared clock.");
|
|
11
11
|
const samples = [...track.samples];
|
|
12
|
-
if (
|
|
12
|
+
if (next?.tracks.has(id)) {
|
|
13
13
|
const nextEdit = next.edits.get(id);
|
|
14
14
|
if (!nextEdit || nextEdit.timescale !== edit.timescale) throw new Error("Adjacent segment clocks differ.");
|
|
15
15
|
for (const sample of next.tracks.get(id).samples) {
|
|
@@ -18,18 +18,33 @@ export function presentationSegment(current, following, { from, to }) {
|
|
|
18
18
|
}
|
|
19
19
|
const start = BigInt(Math.round(from * Number(edit.timescale)));
|
|
20
20
|
const end = BigInt(Math.round(to * Number(edit.timescale)));
|
|
21
|
-
const
|
|
21
|
+
const wanted = samples.filter(sample => {
|
|
22
22
|
const pts = sample.dts + sample.composition + edit.offset;
|
|
23
|
-
return pts < end && pts + sample.duration > start;
|
|
23
|
+
return pts < end && (track.kind === "vide" ? pts >= start : pts + sample.duration > start);
|
|
24
24
|
});
|
|
25
|
+
if (!wanted.length) throw new Error("A presentation segment lacks required media.");
|
|
26
|
+
// An open-GOP picture before the cut can follow the next CRA in decode
|
|
27
|
+
// order. Retain the reference pictures through its last required DTS;
|
|
28
|
+
// dropping the CRA would make the recovered leading pictures undecodable.
|
|
29
|
+
const lastDecode = wanted.reduce((last, sample) => sample.dts > last ? sample.dts : last, -1n);
|
|
30
|
+
const firstDecode = wanted.reduce((first, sample) => first === null || sample.dts < first ? sample.dts : first, null);
|
|
31
|
+
const selected = track.kind === "vide" ? samples.filter(sample =>
|
|
32
|
+
sample.dts >= firstDecode && sample.dts <= lastDecode && sample.dts + sample.composition + edit.offset >= start) : wanted;
|
|
25
33
|
selected.sort((left, right) => left.dts < right.dts ? -1 : left.dts > right.dts ? 1 : 0);
|
|
26
|
-
if (!selected.length) throw new Error("A presentation segment lacks required media.");
|
|
27
34
|
tracks.push({ id, samples: selected });
|
|
28
35
|
}
|
|
29
36
|
const makeMoof = offset => box("moof", full("mfhd", 0, 0, u32(1)), ...tracks.map(track => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
let decodeTime = track.samples[0].dts;
|
|
38
|
+
const entries = track.samples.map(sample => {
|
|
39
|
+
// Removing leading pictures can leave a gap in decode timestamps.
|
|
40
|
+
// trun advances by durations only; retain each original presentation
|
|
41
|
+
// timestamp relative to the decode clock actually written here.
|
|
42
|
+
const composition = sample.dts + sample.composition - decodeTime;
|
|
43
|
+
decodeTime += sample.duration;
|
|
44
|
+
return Buffer.concat([
|
|
45
|
+
u32(Number(sample.duration)), u32(sample.bytes.length), u32(sample.flags), i32(Number(composition))
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
33
48
|
const payload = Buffer.concat([u32(track.samples.length), i32(offset), ...entries]);
|
|
34
49
|
offset += track.samples.reduce((bytes, sample) => bytes + sample.bytes.length, 0);
|
|
35
50
|
const clock = Buffer.alloc(8);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { KeyframeTable } from "./container/KeyframeTable.js";
|
|
4
4
|
import { isUnavailable } from "./container/unavailable.js";
|
|
5
|
+
import { IndexMemoryUnavailable } from "./container/memory-unavailable.js";
|
|
5
6
|
import { logger as defaultLogger } from "../../utils/logger.js";
|
|
6
7
|
|
|
7
8
|
export class KeyframeTables {
|
|
@@ -123,6 +124,8 @@ export class KeyframeTables {
|
|
|
123
124
|
`keyframe index "${logName}": not downloaded yet after ${Date.now() - startedMs}ms — ` +
|
|
124
125
|
"read again when pieces of the file arrive"
|
|
125
126
|
);
|
|
127
|
+
} else if (error instanceof IndexMemoryUnavailable) {
|
|
128
|
+
this.#logger.info(`keyframe index "${logName}": waiting for ${error.bytes} additional allocation bytes`);
|
|
126
129
|
} else {
|
|
127
130
|
this.#logger.warn(
|
|
128
131
|
`keyframe index "${logName}": the read failed after ${Date.now() - startedMs}ms — ` +
|
|
@@ -183,7 +186,10 @@ export class KeyframeTables {
|
|
|
183
186
|
const table = await this.read(params);
|
|
184
187
|
return { table, arrived: table.answered };
|
|
185
188
|
} catch (error) {
|
|
186
|
-
if (isUnavailable(error)
|
|
189
|
+
if (isUnavailable(error) || error instanceof IndexMemoryUnavailable) {
|
|
190
|
+
const table = this.of(params);
|
|
191
|
+
return { table, arrived: table.answered };
|
|
192
|
+
}
|
|
187
193
|
throw error;
|
|
188
194
|
}
|
|
189
195
|
}
|
|
@@ -11,6 +11,7 @@ import { ContainerTrack } from "../tracks/ContainerTrack.js";
|
|
|
11
11
|
import { PacketIndex } from "./PacketIndex.js";
|
|
12
12
|
import { openDmlPackets } from "./avi-open-dml.js";
|
|
13
13
|
import { isUnavailable } from "./unavailable.js";
|
|
14
|
+
import { IndexMemoryUnavailable } from "./memory-unavailable.js";
|
|
14
15
|
import { Mpeg4PictureTiming } from "./mpeg4-picture-timing.js";
|
|
15
16
|
import { MpegElementaryIndex } from "./mpeg-elementary-index.js";
|
|
16
17
|
import { RetainedReads } from "./RetainedReads.js";
|
|
@@ -25,6 +26,7 @@ export class AviContainer extends Container {
|
|
|
25
26
|
#tracks = null;
|
|
26
27
|
#packets = null;
|
|
27
28
|
#scan = null;
|
|
29
|
+
#indexed = null;
|
|
28
30
|
#declarations = null;
|
|
29
31
|
#workTags = null;
|
|
30
32
|
|
|
@@ -108,7 +110,7 @@ export class AviContainer extends Container {
|
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
|
|
111
|
-
packetIndexBytes() { return (this.#packets ?? this.#scan?.index)?.allocatedBytes() ?? 0; }
|
|
113
|
+
packetIndexBytes() { return (this.#packets ?? this.#scan?.index ?? this.#indexed?.index)?.allocatedBytes() ?? 0; }
|
|
112
114
|
|
|
113
115
|
static detect(head) {
|
|
114
116
|
return isAvi(head);
|
|
@@ -221,6 +223,7 @@ export class AviContainer extends Container {
|
|
|
221
223
|
async readPacketIndex(interval) {
|
|
222
224
|
if (this.#packets) return this.#packets;
|
|
223
225
|
if (this.#scan) return this.#readUnindexed(interval);
|
|
226
|
+
if (this.#indexed) return this.#readIndexed();
|
|
224
227
|
const tracks = await this.readTracks();
|
|
225
228
|
const { streams } = await this.#readHeaders();
|
|
226
229
|
if (streams.some(stream => stream.indexChunks.length)) {
|
|
@@ -278,20 +281,30 @@ export class AviContainer extends Container {
|
|
|
278
281
|
}
|
|
279
282
|
if ((table.end - table.start) % 16 !== 0) throw new Error("AVI packet index entry is truncated.");
|
|
280
283
|
const index = new PacketIndex({ packetMemory: this.packetMemory, deferMemory: true });
|
|
281
|
-
try {
|
|
282
284
|
const clocks = new Map();
|
|
283
285
|
const timings = new Map();
|
|
286
|
+
if (tracks.some(track => ["video", "audio", "subtitle"].includes(track.type) && !(track.timeBase > 0))) {
|
|
287
|
+
throw new Error("AVI stream time base is invalid.");
|
|
288
|
+
}
|
|
284
289
|
for (const track of tracks) {
|
|
285
290
|
if (!["video", "audio", "subtitle"].includes(track.type)) continue;
|
|
286
|
-
if (!(track.timeBase > 0)) throw new Error("AVI stream time base is invalid.");
|
|
287
291
|
index.declareTrack(track.trackNumber, { type: track.type, codecId: track.codecId, codecRanges: streams[track.trackNumber].codecRanges });
|
|
288
292
|
clocks.set(track.trackNumber, track.startTimeSeconds);
|
|
289
293
|
}
|
|
290
|
-
|
|
291
|
-
|
|
294
|
+
this.#indexed = { index, clocks, timings, tracks, movi, table, offsetBase: null, at: table.start };
|
|
295
|
+
return this.#readIndexed();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async #readIndexed() {
|
|
299
|
+
const state = this.#indexed;
|
|
300
|
+
const { index, clocks, timings, tracks, movi, table } = state;
|
|
301
|
+
try {
|
|
302
|
+
index.flushPending();
|
|
303
|
+
for (; state.at < table.end;) {
|
|
304
|
+
const at = state.at;
|
|
292
305
|
const entry = await this.readRange(at, at + 15);
|
|
293
306
|
const chunkId = entry.toString("ascii", 0, 4);
|
|
294
|
-
if (!/^[0-9]{2}(db|dc|wb)$/.test(chunkId)) continue;
|
|
307
|
+
if (!/^[0-9]{2}(db|dc|wb)$/.test(chunkId)) { state.at += 16; continue; }
|
|
295
308
|
const id = Number(chunkId.slice(0, 2));
|
|
296
309
|
const track = tracks.find(track => track.trackNumber === id);
|
|
297
310
|
if (!track || !clocks.has(id)) throw new Error("AVI packet refers to an undeclared stream.");
|
|
@@ -300,18 +313,19 @@ export class AviContainer extends Container {
|
|
|
300
313
|
const length = entry.readUInt32LE(12);
|
|
301
314
|
if (length === 0) {
|
|
302
315
|
if (track.type === "video") { index.extendLastPresentation(id, track.timeBase); clocks.set(id, clocks.get(id) + track.timeBase); }
|
|
316
|
+
state.at += 16;
|
|
303
317
|
continue;
|
|
304
318
|
}
|
|
305
|
-
if (offsetBase === null) {
|
|
319
|
+
if (state.offsetBase === null) {
|
|
306
320
|
for (const base of [movi.start, 0, movi.start + 4]) {
|
|
307
321
|
const address = base + offset;
|
|
308
322
|
if (address < movi.start + 4 || address + 8 + length > movi.end) continue;
|
|
309
323
|
const probe = await this.readRange(address, address + 7);
|
|
310
|
-
if (probe.toString("ascii", 0, 4) === chunkId && probe.readUInt32LE(4) === length) { offsetBase = base; break; }
|
|
324
|
+
if (probe.toString("ascii", 0, 4) === chunkId && probe.readUInt32LE(4) === length) { state.offsetBase = base; break; }
|
|
311
325
|
}
|
|
312
|
-
if (offsetBase === null) throw new Error("AVI packet index has no valid offset base.");
|
|
326
|
+
if (state.offsetBase === null) throw new Error("AVI packet index has no valid offset base.");
|
|
313
327
|
}
|
|
314
|
-
const address = offsetBase + offset;
|
|
328
|
+
const address = state.offsetBase + offset;
|
|
315
329
|
if (address < movi.start + 4 || address + 8 + length > movi.end) throw new Error("AVI indexed packet exceeds its media list.");
|
|
316
330
|
const probe = await this.readRange(address, address + 7);
|
|
317
331
|
if (probe.toString("ascii", 0, 4) !== chunkId || probe.readUInt32LE(4) !== length) throw new Error("AVI packet index disagrees with its chunk header.");
|
|
@@ -321,15 +335,21 @@ export class AviContainer extends Container {
|
|
|
321
335
|
await this.#appendPacket(index, timings, track, { pts, duration, keyframe: track.type !== "video" || chunkId.endsWith("db") || !!(flags & 0x10),
|
|
322
336
|
ranges: [[address + 8, address + 8 + length - 1]] });
|
|
323
337
|
clocks.set(id, pts + duration);
|
|
338
|
+
// The pending facts own this entry even if their allocation must wait.
|
|
339
|
+
state.at += 16;
|
|
324
340
|
index.flushPending();
|
|
325
341
|
}
|
|
326
342
|
for (const timing of timings.values()) timing.elementary?.complete();
|
|
327
343
|
index.flushPending();
|
|
328
344
|
for (const id of clocks.keys()) index.complete(id);
|
|
329
345
|
this.#packets = index;
|
|
346
|
+
this.#indexed = null;
|
|
330
347
|
return index;
|
|
331
348
|
} catch (error) {
|
|
332
|
-
|
|
349
|
+
if (!isUnavailable(error) && !(error instanceof IndexMemoryUnavailable)) {
|
|
350
|
+
index.dispose();
|
|
351
|
+
this.#indexed = null;
|
|
352
|
+
}
|
|
333
353
|
throw error;
|
|
334
354
|
}
|
|
335
355
|
}
|