@torrent-tv/proxy 2.105.1 → 2.105.3
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
|
@@ -18,6 +18,7 @@ import { readAviIndex } from "./avi-index.js";
|
|
|
18
18
|
import { isUnavailable } from "./unavailable.js";
|
|
19
19
|
import { Mpeg4PictureTiming } from "./mpeg4-picture-timing.js";
|
|
20
20
|
import { MpegElementaryIndex } from "./mpeg-elementary-index.js";
|
|
21
|
+
import { ac3Frame } from "./ac3-frame.js";
|
|
21
22
|
import { RetainedReads } from "./RetainedReads.js";
|
|
22
23
|
import { OutsideReadableEdges, edgeReader, emptyWorkTags, text, textList, yearOf } from "./work-tags.js";
|
|
23
24
|
|
|
@@ -208,8 +209,14 @@ export class AviContainer extends Container {
|
|
|
208
209
|
* ones, and the swarm was asked for the picture's next piece only after it
|
|
209
210
|
* (torrent-tv/meta#178).
|
|
210
211
|
*/
|
|
211
|
-
packetTableWouldWait() {
|
|
212
|
+
packetTableWouldWait(interval = null) {
|
|
212
213
|
if (!this.#index || this.#packets || this.#scan) return false;
|
|
214
|
+
if (interval?.trackIds?.length) {
|
|
215
|
+
const tracks = this.#tracks ?? [];
|
|
216
|
+
const selected = tracks.filter(track => interval.trackIds.includes(track.trackNumber));
|
|
217
|
+
if (selected.length === interval.trackIds.length && selected.length === 1 && selected[0].type === "audio" &&
|
|
218
|
+
selected[0].codecId === "ac3") return false;
|
|
219
|
+
}
|
|
213
220
|
return this.#soundPacketsRefused === null || this.packetMemory?.fits?.(this.#soundPacketsRefused) !== false;
|
|
214
221
|
}
|
|
215
222
|
|
|
@@ -414,7 +421,16 @@ export class AviContainer extends Container {
|
|
|
414
421
|
}
|
|
415
422
|
|
|
416
423
|
async readPacketIndex(interval) {
|
|
417
|
-
if (this.#packets)
|
|
424
|
+
if (this.#packets) {
|
|
425
|
+
if (this.#index && interval?.trackIds?.length) {
|
|
426
|
+
const tracks = await this.readTracks();
|
|
427
|
+
const selected = tracks.filter(track => interval.trackIds.includes(track.trackNumber));
|
|
428
|
+
if (selected.length === 1 && selected[0].type === "audio" && selected[0].codecId === "ac3") {
|
|
429
|
+
return this.#indexedAc3PacketsOf(this.#index, selected[0], interval);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return this.#packets;
|
|
433
|
+
}
|
|
418
434
|
if (this.#scan) return this.#readUnindexed(interval);
|
|
419
435
|
// A picture of a file with an index is read by FFmpeg from the original
|
|
420
436
|
// bytes (torrent-tv/meta#151). Its SOUND is not: the index states every
|
|
@@ -424,6 +440,12 @@ export class AviContainer extends Container {
|
|
|
424
440
|
// copied 2.3 GB for 0.19 GB of its own (torrent-tv/meta#166).
|
|
425
441
|
const indexed = await this.#readIndex();
|
|
426
442
|
if (indexed) {
|
|
443
|
+
const tracks = await this.readTracks();
|
|
444
|
+
const selected = interval?.trackIds?.length
|
|
445
|
+
? tracks.filter(track => interval.trackIds.includes(track.trackNumber)) : [];
|
|
446
|
+
if (selected.length === 1 && selected[0].type === "audio" && selected[0].codecId === "ac3") {
|
|
447
|
+
return this.#indexedAc3PacketsOf(indexed, selected[0], interval);
|
|
448
|
+
}
|
|
427
449
|
// A refusal of memory for the table is kept for as long as the memory's
|
|
428
450
|
// owner says the same reservation still would not fit. Not kept, the
|
|
429
451
|
// next reader walked the index, counted the packets and was refused
|
|
@@ -463,6 +485,68 @@ export class AviContainer extends Container {
|
|
|
463
485
|
return this.#readUnindexed(interval);
|
|
464
486
|
}
|
|
465
487
|
|
|
488
|
+
/** Parse only the indexed AC-3 blocks needed for one audio interval. */
|
|
489
|
+
async #indexedAc3PacketsOf(index, track, interval) {
|
|
490
|
+
const stream = index.packets[track.trackNumber];
|
|
491
|
+
if (!stream?.count) throw new Error("Indexed AVI AC-3 track has no packets.");
|
|
492
|
+
const read = new Map();
|
|
493
|
+
const bytesAt = async at => {
|
|
494
|
+
if (read.has(at)) return read.get(at);
|
|
495
|
+
const length = stream.lengths[at];
|
|
496
|
+
const bytes = length ? await this.readRange(stream.starts[at], stream.starts[at] + length - 1) : Buffer.alloc(0);
|
|
497
|
+
read.set(at, bytes);
|
|
498
|
+
return bytes;
|
|
499
|
+
};
|
|
500
|
+
const chainAt = async start => {
|
|
501
|
+
let bytes = Buffer.alloc(0), cursor = 0, at = start, frames = 0;
|
|
502
|
+
while (frames < 3 && at < stream.count && bytes.length < 32 * 1024) {
|
|
503
|
+
bytes = Buffer.concat([bytes, await bytesAt(at++)]);
|
|
504
|
+
while (bytes.length - cursor >= 7 && frames < 3) {
|
|
505
|
+
if (bytes[cursor] !== 0x0b || bytes[cursor + 1] !== 0x77) return false;
|
|
506
|
+
let frame;
|
|
507
|
+
try { frame = ac3Frame(bytes.subarray(cursor)); }
|
|
508
|
+
catch { return false; }
|
|
509
|
+
if (bytes.length - cursor < frame.size) break;
|
|
510
|
+
cursor += frame.size;
|
|
511
|
+
frames++;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return frames === 3;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const anchor = stream.atOrBefore(interval.from);
|
|
518
|
+
let first = -1, scannedBytes = 0;
|
|
519
|
+
for (let at = anchor; at >= 0 && scannedBytes < 128 * 1024; at--) {
|
|
520
|
+
const bytes = await bytesAt(at);
|
|
521
|
+
scannedBytes += bytes.length;
|
|
522
|
+
if (bytes.length >= 7 && bytes[0] === 0x0b && bytes[1] === 0x77 && await chainAt(at)) {
|
|
523
|
+
first = at;
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (first < 0) throw new Error("Indexed AVI AC-3 has no complete frame chain near the requested interval.");
|
|
528
|
+
|
|
529
|
+
const elementary = new MpegElementaryIndex([track], { packetMemory: this.packetMemory });
|
|
530
|
+
try {
|
|
531
|
+
for (let at = first; at < stream.count; at++) {
|
|
532
|
+
const bytes = await bytesAt(at);
|
|
533
|
+
if (bytes.length) elementary.push(track.trackNumber, bytes, stream.starts[at], at === first ? { pts: stream.times[first] } : {});
|
|
534
|
+
const input = elementary.index.inputFor({ trackId: track.trackNumber, from: interval.from, to: interval.to,
|
|
535
|
+
mode: interval.modes?.[track.trackNumber] ?? "transcode" });
|
|
536
|
+
if (input.kind === "result") {
|
|
537
|
+
elementary.index.disposeAfterUse = true;
|
|
538
|
+
return elementary.index;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
elementary.complete();
|
|
542
|
+
elementary.index.disposeAfterUse = true;
|
|
543
|
+
return elementary.index;
|
|
544
|
+
} catch (error) {
|
|
545
|
+
elementary.index.dispose();
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
466
550
|
/**
|
|
467
551
|
* The sound packets an AVI index states, on FFmpeg's clock: the time the
|
|
468
552
|
* index gives each packet is the one FFmpeg's demuxer gives it, and a packet
|
|
@@ -33,11 +33,15 @@ export async function directSegmentInput(container, { interval, tracks, mode },
|
|
|
33
33
|
const walked = await container.packetWalkRanges?.(interval);
|
|
34
34
|
if (walked) return walked;
|
|
35
35
|
// The table is begun, or joined, and not waited for.
|
|
36
|
-
if (container.packetTableWouldWait?.() === true) {
|
|
36
|
+
if (container.packetTableWouldWait?.(interval) === true) {
|
|
37
37
|
whenBuilt(container.readPacketIndex(interval).then(() => undefined, () => undefined));
|
|
38
38
|
return { kind: "pending" };
|
|
39
39
|
}
|
|
40
40
|
const index = await container.readPacketIndex(interval);
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
try {
|
|
42
|
+
await index.prepareAudioDependencies?.(interval, container.readRange);
|
|
43
|
+
return new SegmentInputs({ index, tracks }).forInterval({ ...interval, mode });
|
|
44
|
+
} finally {
|
|
45
|
+
if (index.disposeAfterUse === true) index.dispose();
|
|
46
|
+
}
|
|
43
47
|
}
|
|
@@ -575,6 +575,7 @@ export class ViewerRequests {
|
|
|
575
575
|
// still answered by the output it was made against — see the two deadlines
|
|
576
576
|
// in `Assignments.js`.
|
|
577
577
|
viewer.assignments.statedGeneration(generation);
|
|
578
|
+
this.#beginSeekWait(named, consumerId, generation);
|
|
578
579
|
this.#host.outputs.touch(named);
|
|
579
580
|
// EVERY REQUEST THIS VIEWER HAS OUTSTANDING WAS MADE FOR WHERE THEY WERE,
|
|
580
581
|
// and hls.js keeps one fragment load outstanding per track: a request held
|
|
@@ -601,6 +602,40 @@ export class ViewerRequests {
|
|
|
601
602
|
return true;
|
|
602
603
|
}
|
|
603
604
|
|
|
605
|
+
#beginSeekWait(output, consumerId, generation) {
|
|
606
|
+
const file = output?.file;
|
|
607
|
+
if (!Number.isSafeInteger(generation) || !file || typeof file.sourceKey !== "string" ||
|
|
608
|
+
!Number.isSafeInteger(file.fileIndex) || file.fileIndex < 0) return;
|
|
609
|
+
const waitId = `${file.sourceKey}:${file.fileIndex}:${generation}`;
|
|
610
|
+
const key = `${consumerId}:${waitId}`;
|
|
611
|
+
const fileName = file.relativePath ?? file.name ?? "";
|
|
612
|
+
const extension = typeof fileName === "string" ? fileName.split(".").at(-1)?.toLowerCase() ?? "" : "";
|
|
613
|
+
const viewer = this.#host.viewers.get(consumerId);
|
|
614
|
+
const link = linkMeasurementOf(viewer);
|
|
615
|
+
const features = {
|
|
616
|
+
extension,
|
|
617
|
+
fileLength: Number.isFinite(file.length) && file.length > 0 ? file.length : null,
|
|
618
|
+
downloadSpeed: null,
|
|
619
|
+
linkMbps: link.mbps,
|
|
620
|
+
linkSampleAgeMs: link.ageMs,
|
|
621
|
+
preparedOutputBytes: this.#preparedOutputBytes(file.sourceKey, file.fileIndex),
|
|
622
|
+
fileAvailable: null,
|
|
623
|
+
headerRemaining: null,
|
|
624
|
+
peers: null,
|
|
625
|
+
videoOperation: null,
|
|
626
|
+
audioOperation: null,
|
|
627
|
+
videoSpeedX: null,
|
|
628
|
+
audioSpeedX: null
|
|
629
|
+
};
|
|
630
|
+
// A seek is a new waiting action even when a caller repeats the same
|
|
631
|
+
// generation. Start its clock at the explicit seek request, before the
|
|
632
|
+
// next readiness poll can return the first current-work estimate.
|
|
633
|
+
this.#earlyWaits.delete(key);
|
|
634
|
+
const context = this.#setEarlyWait({ consumerId, generation, sourceKey: file.sourceKey,
|
|
635
|
+
fileIndex: file.fileIndex, waitId, features, revision: 0, current: null, waitElapsedMs: 0 });
|
|
636
|
+
this.#recordEarlyWaitSnapshot(context, features);
|
|
637
|
+
}
|
|
638
|
+
|
|
604
639
|
/**
|
|
605
640
|
* Whether this viewer still takes a request made in the generation it states.
|
|
606
641
|
*
|