@torrent-tv/proxy 2.107.28 → 2.107.29
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 +3 -2
- package/services/encode/EncodeInputs.js +7 -3
- package/services/media/ContainerOrchestrator.js +5 -4
- package/services/media/container/AsfContainer.js +21 -20
- package/services/media/container/Container.js +1 -1
- package/services/media/container/unavailable.js +24 -4
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1023,7 +1023,7 @@ export async function startProxyServer({
|
|
|
1023
1023
|
},
|
|
1024
1024
|
// `timing`, when given, is told how long each read of a file waited behind
|
|
1025
1025
|
// the reads asked before it and how long it took (torrent-tv/meta#166).
|
|
1026
|
-
resolveEncodeInput: async (output, fromIndex, toIndex, timing = null, isCurrent = null) => {
|
|
1026
|
+
resolveEncodeInput: async (output, fromIndex, toIndex, timing = null, isCurrent = null, signal = null) => {
|
|
1027
1027
|
const grid = output.timeline?.published ?? output.timeline?.boundaries;
|
|
1028
1028
|
const from = grid?.[fromIndex], to = grid?.[toIndex + 1];
|
|
1029
1029
|
if (!Number.isFinite(from) || !(to > from)) return { kind: "terminal", reason: "output-interval-not-declared" };
|
|
@@ -1043,6 +1043,8 @@ export async function startProxyServer({
|
|
|
1043
1043
|
for (const [fileIndex, choices] of selected) {
|
|
1044
1044
|
const params = await containerOver({ sourceKey: output.file.sourceKey, fileIndex });
|
|
1045
1045
|
if (!params) return { kind: "needs-source" };
|
|
1046
|
+
if (isCurrent) params.isCurrent = isCurrent;
|
|
1047
|
+
if (signal) params.signal = signal;
|
|
1046
1048
|
if (timing) {
|
|
1047
1049
|
params.walkAccount = timing.walk ??= { lockMs: 0, reads: 0, bytes: 0 };
|
|
1048
1050
|
params.onTimed = (statement, waitedMs, readMs) => {
|
|
@@ -1073,7 +1075,6 @@ export async function startProxyServer({
|
|
|
1073
1075
|
if (media.kind !== "result") return media;
|
|
1074
1076
|
const timeShiftSeconds = Number(media.value?.startTimeSeconds) || 0;
|
|
1075
1077
|
const sourceInterval = output.timeline.sourceInterval(fromIndex, toIndex, timeShiftSeconds);
|
|
1076
|
-
if (isCurrent) params.isCurrent = isCurrent;
|
|
1077
1078
|
params.packetInterval = { ...sourceInterval,
|
|
1078
1079
|
trackIds: wanted.map(track => track.trackNumber),
|
|
1079
1080
|
modes: Object.fromEntries(wanted.map(track => [track.trackNumber, modes.get(track)])) };
|
|
@@ -115,10 +115,10 @@ export class EncodeInputs {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/** The container's statement of an interval, its time added to the request's account. */
|
|
118
|
-
async #resolveTimed(timing, output, from, to, isCurrent = null) {
|
|
118
|
+
async #resolveTimed(timing, output, from, to, isCurrent = null, signal = null) {
|
|
119
119
|
const startedAt = this.#now();
|
|
120
120
|
try {
|
|
121
|
-
return await this.#resolve(output, from, to, timing, isCurrent);
|
|
121
|
+
return await this.#resolve(output, from, to, timing, isCurrent, signal);
|
|
122
122
|
} finally {
|
|
123
123
|
timing.resolutions += 1;
|
|
124
124
|
timing.resolveMs += this.#now() - startedAt;
|
|
@@ -208,6 +208,7 @@ export class EncodeInputs {
|
|
|
208
208
|
: "withdrawn: no wanted window covers it");
|
|
209
209
|
this.#requests.delete(key);
|
|
210
210
|
this.#wanted.delete(key);
|
|
211
|
+
request.controller?.abort();
|
|
211
212
|
request.result?.release?.();
|
|
212
213
|
}
|
|
213
214
|
this.#retryMemory();
|
|
@@ -233,6 +234,7 @@ export class EncodeInputs {
|
|
|
233
234
|
this.#note(request, "withdrawn: the plan places no run there");
|
|
234
235
|
this.#requests.delete(key);
|
|
235
236
|
this.#wanted.delete(key);
|
|
237
|
+
request.controller?.abort();
|
|
236
238
|
// A copy already answered may still be copying its stretch; released,
|
|
237
239
|
// the stretch is dropped when its read returns.
|
|
238
240
|
request.result?.release?.();
|
|
@@ -335,6 +337,7 @@ export class EncodeInputs {
|
|
|
335
337
|
if (request.output !== output) continue;
|
|
336
338
|
this.#requests.delete(key);
|
|
337
339
|
this.#wanted.delete(key);
|
|
340
|
+
request.controller?.abort();
|
|
338
341
|
request.result?.release?.();
|
|
339
342
|
}
|
|
340
343
|
this.#shared.forget(output);
|
|
@@ -404,6 +407,7 @@ export class EncodeInputs {
|
|
|
404
407
|
}
|
|
405
408
|
|
|
406
409
|
#prepare(request) {
|
|
410
|
+
request.controller ??= new AbortController();
|
|
407
411
|
request.pending = true;
|
|
408
412
|
request.revision = this.#revision;
|
|
409
413
|
request.memoryRevision = this.#memoryRevision;
|
|
@@ -421,7 +425,7 @@ export class EncodeInputs {
|
|
|
421
425
|
// packet walk, left to run, held the file's reads and the piece the
|
|
422
426
|
// viewer had seeked to waited 16.9 s behind it (r59, torrent-tv/meta#178).
|
|
423
427
|
const resolved = await this.#resolveTimed(request.timing, request.output, request.from, request.from,
|
|
424
|
-
() => this.#requests.get(request.key) === request);
|
|
428
|
+
() => this.#requests.get(request.key) === request && !request.controller.signal.aborted, request.controller.signal);
|
|
425
429
|
if (resolved.kind === "result") this.#original.set(request.output, resolved.sources?.every(source => source.input.original === true) === true);
|
|
426
430
|
if (this.#requests.get(request.key) !== request) return;
|
|
427
431
|
let result;
|
|
@@ -146,7 +146,7 @@ export class ContainerOrchestrator {
|
|
|
146
146
|
const key = `${params.sourceKey}:${params.fileIndex}`;
|
|
147
147
|
let lifetime = this.#lifetimes.get(key);
|
|
148
148
|
if (!lifetime) this.#lifetimes.set(key, lifetime = {});
|
|
149
|
-
const isCurrent = () => this.#lifetimes.get(key) === lifetime && params.isCurrent?.() !== false;
|
|
149
|
+
const isCurrent = () => this.#lifetimes.get(key) === lifetime && params.isCurrent?.() !== false && params.signal?.aborted !== true;
|
|
150
150
|
// Reads of one file run one after another, so an answer can wait for every
|
|
151
151
|
// read asked before it. Told to whoever asked, apart from the read itself,
|
|
152
152
|
// because the two call for different remedies (torrent-tv/meta#166).
|
|
@@ -237,9 +237,10 @@ export class ContainerOrchestrator {
|
|
|
237
237
|
value = await container.readSourceRanges(params.packetInterval);
|
|
238
238
|
if (value.kind !== "result") return { ...value, requestId };
|
|
239
239
|
} else if (statement === "packets") {
|
|
240
|
-
value = await container.readPacketIndex(params.packetInterval, { isCurrent: params.isCurrent, account: params.walkAccount });
|
|
240
|
+
value = await container.readPacketIndex(params.packetInterval, { isCurrent: params.isCurrent, signal: params.signal, account: params.walkAccount });
|
|
241
241
|
if (value === null) return { kind: "terminal", reason: "request-obsolete", requestId };
|
|
242
|
-
|
|
242
|
+
const readRange = (start, end) => container.readRange(start, end, { signal: params.signal });
|
|
243
|
+
await value?.prepareAudioDependencies?.(params.packetInterval, readRange);
|
|
243
244
|
} else if (statement === "chapters") {
|
|
244
245
|
value = await container.readChapters({ read: params.readHeld ?? params.readRange,
|
|
245
246
|
isCurrent: params.isCurrent,
|
|
@@ -252,7 +253,7 @@ export class ContainerOrchestrator {
|
|
|
252
253
|
}
|
|
253
254
|
return { kind: "result", value, requestId };
|
|
254
255
|
} catch (error) {
|
|
255
|
-
if (
|
|
256
|
+
if (params.isCurrent?.() === false) return { kind: "terminal", reason: "request-obsolete", requestId };
|
|
256
257
|
if (error instanceof ChapterUnsupported) return { kind: "terminal", reason: "chapter-not-supported", chapterReason: error.message, requestId };
|
|
257
258
|
if (error instanceof IndexMemoryUnavailable) {
|
|
258
259
|
const result = { kind: "needs-memory", bytes: error.bytes, requestId };
|
|
@@ -42,11 +42,11 @@ export class AsfContainer extends Container {
|
|
|
42
42
|
}
|
|
43
43
|
static detect(head) { return head.length >= 16 && head.subarray(0, 16).toString("hex") === HEADER; }
|
|
44
44
|
|
|
45
|
-
async #readHeader() {
|
|
45
|
+
async #readHeader(readRange = this.readRange) {
|
|
46
46
|
if (this.#info) return;
|
|
47
47
|
this.#declarations ??= new RetainedReads(this.packetMemory);
|
|
48
48
|
if (!this.#header) {
|
|
49
|
-
const bytes = await
|
|
49
|
+
const bytes = await readRange(0, 29);
|
|
50
50
|
if (!AsfContainer.detect(bytes) || bytes[28] !== 1 || bytes[29] !== 2) throw new Error("ASF header is invalid.");
|
|
51
51
|
const size = Number(bytes.readBigUInt64LE(16));
|
|
52
52
|
const count = bytes.readUInt32LE(24);
|
|
@@ -56,7 +56,7 @@ export class AsfContainer extends Container {
|
|
|
56
56
|
this.#header = { size, count };
|
|
57
57
|
}
|
|
58
58
|
while (this.#objects.length < this.#header.count) {
|
|
59
|
-
const head = await
|
|
59
|
+
const head = await readRange(this.#offset, this.#offset + 23);
|
|
60
60
|
const size = Number(head.readBigUInt64LE(16));
|
|
61
61
|
if (!Number.isSafeInteger(size) || size < 24 || this.#offset + size > this.#header.size) {
|
|
62
62
|
throw new Error("ASF child object exceeds its header.");
|
|
@@ -64,7 +64,7 @@ export class AsfContainer extends Container {
|
|
|
64
64
|
const guid = head.subarray(0, 16).toString("hex");
|
|
65
65
|
if (ENCRYPTION.has(guid)) throw new Error("ASF media is encrypted.");
|
|
66
66
|
const bytes = [FILE, STREAM, EXTENSION, EXTENDED_STREAM, LANGUAGES].includes(guid)
|
|
67
|
-
? await this.#declarations.read(this.#offset + 24, this.#offset + size - 1,
|
|
67
|
+
? await this.#declarations.read(this.#offset + 24, this.#offset + size - 1, readRange) : null;
|
|
68
68
|
this.#objects.push({ guid, bytes });
|
|
69
69
|
this.#offset += size;
|
|
70
70
|
}
|
|
@@ -212,18 +212,19 @@ export class AsfContainer extends Container {
|
|
|
212
212
|
const index = await this.readPacketIndex();
|
|
213
213
|
return { times: index.keyframesOf(track.trackNumber), tolerance: 0 };
|
|
214
214
|
}
|
|
215
|
-
async readPacketIndex(interval, { isCurrent } = {}) {
|
|
215
|
+
async readPacketIndex(interval, { isCurrent, signal } = {}) {
|
|
216
|
+
const readRange = signal ? (start, end) => this.readRange(start, end, { signal }) : this.readRange;
|
|
216
217
|
if (isCurrent?.() === false) return null;
|
|
217
|
-
await this.#readHeader();
|
|
218
|
+
await this.#readHeader(readRange);
|
|
218
219
|
if (isCurrent?.() === false) return null;
|
|
219
220
|
if (!this.#packets && Number.isFinite(interval?.from) && Number.isFinite(interval?.to) && interval.to > interval.from) {
|
|
220
|
-
const window = await this.#readWindow(interval, isCurrent);
|
|
221
|
+
const window = await this.#readWindow(interval, isCurrent, readRange);
|
|
221
222
|
if (isCurrent?.() === false) return null;
|
|
222
223
|
if (window) return window;
|
|
223
224
|
}
|
|
224
225
|
if (isCurrent?.() === false) return null;
|
|
225
226
|
if (!this.#packets) {
|
|
226
|
-
const index = await readAsfPackets({ readRange
|
|
227
|
+
const index = await readAsfPackets({ readRange, fileSize: this.fileSize, tracks: this.#tracks,
|
|
227
228
|
info: { ...this.#info, durationSeconds: this.#duration }, state: this.#packetState, packetMemory: this.packetMemory, isCurrent });
|
|
228
229
|
if (index === null) return null;
|
|
229
230
|
const timeline = this.#tracks.find(track => track.type === "video") ?? this.#tracks.find(track => track.type === "audio");
|
|
@@ -251,8 +252,8 @@ export class AsfContainer extends Container {
|
|
|
251
252
|
* interval, is read again from twice as far back, and from the first packet
|
|
252
253
|
* at the latest; which it reaches is checked on what it read, not assumed.
|
|
253
254
|
*/
|
|
254
|
-
async #readWindow(interval, isCurrent) {
|
|
255
|
-
const data = await this.#dataLayout();
|
|
255
|
+
async #readWindow(interval, isCurrent, readRange = this.readRange) {
|
|
256
|
+
const data = await this.#dataLayout(readRange);
|
|
256
257
|
if (isCurrent?.() === false) return null;
|
|
257
258
|
if (!data) return null;
|
|
258
259
|
const requested = this.#tracks.filter(track => ["video", "audio"].includes(track.type) &&
|
|
@@ -262,9 +263,9 @@ export class AsfContainer extends Container {
|
|
|
262
263
|
for (let lead = this.#info.prerollSeconds + WINDOW_LEAD_SECONDS; ; lead *= 2) {
|
|
263
264
|
if (isCurrent?.() === false) return null;
|
|
264
265
|
const sentFrom = interval.from - lead;
|
|
265
|
-
const first = await this.#packetSentBefore(sentFrom, data, interval.to + WINDOW_LEAD_SECONDS, isCurrent);
|
|
266
|
+
const first = await this.#packetSentBefore(sentFrom, data, interval.to + WINDOW_LEAD_SECONDS, isCurrent, readRange);
|
|
266
267
|
if (isCurrent?.() === false) return null;
|
|
267
|
-
const window = await readAsfWindow({ readRange
|
|
268
|
+
const window = await readAsfWindow({ readRange, data, tracks: this.#tracks, requested, info, first, interval, sentFrom, isCurrent });
|
|
268
269
|
if (isCurrent?.() === false) return null;
|
|
269
270
|
if (window.startCovered || first === 0) return window.index;
|
|
270
271
|
}
|
|
@@ -275,9 +276,9 @@ export class AsfContainer extends Container {
|
|
|
275
276
|
* the format requires it (the File Properties state one length as both the
|
|
276
277
|
* least and the most), and a packet is then found by its number.
|
|
277
278
|
*/
|
|
278
|
-
async #dataLayout() {
|
|
279
|
+
async #dataLayout(readRange = this.readRange) {
|
|
279
280
|
if (this.#data === null) {
|
|
280
|
-
const data = await readAsfData({ readRange
|
|
281
|
+
const data = await readAsfData({ readRange, fileSize: this.fileSize, info: this.#info });
|
|
281
282
|
const length = this.#info.packetLength;
|
|
282
283
|
const fixed = length > 0 && this.#info.minimumPacketLength === length;
|
|
283
284
|
const whole = fixed ? Math.floor((data.end - data.start) / length) : 0;
|
|
@@ -300,16 +301,16 @@ export class AsfContainer extends Container {
|
|
|
300
301
|
* bytes the window from it to `until` is estimated to take, so the swarm is
|
|
301
302
|
* asked for the packet and the window it begins at once.
|
|
302
303
|
*/
|
|
303
|
-
async #packetSentBefore(seconds, data, until, isCurrent) {
|
|
304
|
+
async #packetSentBefore(seconds, data, until, isCurrent, readRange = this.readRange) {
|
|
304
305
|
if (isCurrent?.() === false) return null;
|
|
305
|
-
let low = 0, high = data.count, lowSent = await this.#sentAt(0, data, 1, isCurrent), highSent = this.#duration;
|
|
306
|
+
let low = 0, high = data.count, lowSent = await this.#sentAt(0, data, 1, isCurrent, readRange), highSent = this.#duration;
|
|
306
307
|
if (isCurrent?.() === false) return null;
|
|
307
308
|
if (lowSent > seconds) return 0;
|
|
308
309
|
// Past the last packet nothing is sent before the film ends; the last
|
|
309
310
|
// packet itself is read only when the search needs it.
|
|
310
311
|
if (!(highSent > seconds)) {
|
|
311
312
|
high = data.count - 1;
|
|
312
|
-
highSent = await this.#sentAt(high, data, 1, isCurrent);
|
|
313
|
+
highSent = await this.#sentAt(high, data, 1, isCurrent, readRange);
|
|
313
314
|
if (highSent <= seconds) return high;
|
|
314
315
|
}
|
|
315
316
|
for (const [number, at] of this.#sent) {
|
|
@@ -324,7 +325,7 @@ export class AsfContainer extends Container {
|
|
|
324
325
|
const guess = guesses >= 3 || !(perSecond > 0) ? Math.floor((low + high) / 2) : low + Math.floor((aim - lowSent) * perSecond);
|
|
325
326
|
const probe = Math.min(high - 1, Math.max(low + 1, guess));
|
|
326
327
|
const window = perSecond > 0 ? Math.ceil((until - aim) * perSecond) : 1;
|
|
327
|
-
const at = await this.#sentAt(probe, data, Math.max(1, window), isCurrent);
|
|
328
|
+
const at = await this.#sentAt(probe, data, Math.max(1, window), isCurrent, readRange);
|
|
328
329
|
if (isCurrent?.() === false) return null;
|
|
329
330
|
if (at <= seconds) { low = probe; lowSent = at; }
|
|
330
331
|
else { high = probe; highSent = at; }
|
|
@@ -336,12 +337,12 @@ export class AsfContainer extends Container {
|
|
|
336
337
|
* When packet `number` is sent. A packet that is not held is said as
|
|
337
338
|
* missing with the `packets` packets from it.
|
|
338
339
|
*/
|
|
339
|
-
async #sentAt(number, data, packets = 1, isCurrent) {
|
|
340
|
+
async #sentAt(number, data, packets = 1, isCurrent, readRange = this.readRange) {
|
|
340
341
|
if (isCurrent?.() === false) return null;
|
|
341
342
|
if (!this.#sent.has(number)) {
|
|
342
343
|
const at = data.start + number * data.length;
|
|
343
344
|
try {
|
|
344
|
-
this.#sent.set(number, await readAsfPacketTime({ readRange
|
|
345
|
+
this.#sent.set(number, await readAsfPacketTime({ readRange, at, end: data.end, info: this.#info }));
|
|
345
346
|
} catch (error) {
|
|
346
347
|
if (!isUnavailable(error) || packets <= 1) throw error;
|
|
347
348
|
throw new BytesUnavailable(at, Math.min(data.start + data.count * data.length, at + packets * data.length) - 1, 0);
|
|
@@ -54,7 +54,7 @@ export class Container {
|
|
|
54
54
|
* the medium — a torrent's piece length. Absent, an element is read in one go.
|
|
55
55
|
*
|
|
56
56
|
* @param {object} params
|
|
57
|
-
* @param {(start:number,end:number)=>Promise<Buffer|null>} [params.readRange]
|
|
57
|
+
* @param {(start:number,end:number,options?:{signal?:AbortSignal})=>Promise<Buffer|null>} [params.readRange]
|
|
58
58
|
* @param {number} [params.fileSize]
|
|
59
59
|
* @param {string} [params.label]
|
|
60
60
|
* @param {number} [params.portionBytes]
|
|
@@ -56,17 +56,37 @@ export function isUnavailable(error) {
|
|
|
56
56
|
* the clamped range is a read that did not finish: a stream that ended early,
|
|
57
57
|
* a piece that is not downloaded, a store that could not serve it.
|
|
58
58
|
*
|
|
59
|
-
* @param {(start: number, end: number) => Promise<Buffer | null>} read
|
|
59
|
+
* @param {(start: number, end: number, options?: { signal?: AbortSignal }) => Promise<Buffer | null>} read
|
|
60
60
|
* @param {number} fileSize
|
|
61
|
-
* @returns {(start: number, end: number) => Promise<Buffer>}
|
|
61
|
+
* @returns {(start: number, end: number, options?: { signal?: AbortSignal }) => Promise<Buffer>}
|
|
62
62
|
*/
|
|
63
63
|
export function strictReader(read, fileSize) {
|
|
64
|
-
return async (start, end) => {
|
|
64
|
+
return async (start, end, { signal } = {}) => {
|
|
65
65
|
const last = Math.min(end, fileSize - 1);
|
|
66
66
|
if (!(start >= 0) || last < start) {
|
|
67
67
|
return Buffer.alloc(0);
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
let bytes;
|
|
70
|
+
if (!signal) {
|
|
71
|
+
bytes = await read(start, last);
|
|
72
|
+
} else {
|
|
73
|
+
const abort = () => {
|
|
74
|
+
const error = new Error("Read cancelled.");
|
|
75
|
+
error.name = "AbortError";
|
|
76
|
+
return error;
|
|
77
|
+
};
|
|
78
|
+
if (signal.aborted) throw abort();
|
|
79
|
+
let onAbort;
|
|
80
|
+
const cancelled = new Promise((_, reject) => {
|
|
81
|
+
onAbort = () => reject(abort());
|
|
82
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
bytes = await Promise.race([read(start, last, { signal }), cancelled]);
|
|
86
|
+
} finally {
|
|
87
|
+
signal.removeEventListener("abort", onAbort);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
70
90
|
const wanted = last - start + 1;
|
|
71
91
|
const received = bytes ? bytes.length : 0;
|
|
72
92
|
if (received < wanted) {
|