@torrent-tv/proxy 2.105.4 → 2.105.6
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 +114 -58
- package/services/media/container/AviContainer.js +17 -6
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -754,84 +754,140 @@ export async function startProxyServer({
|
|
|
754
754
|
if (!map.isCurrent()) return [];
|
|
755
755
|
await turn();
|
|
756
756
|
if (!map.isCurrent()) return [];
|
|
757
|
-
const
|
|
758
|
-
|
|
757
|
+
const resolutionStartedAt = performance.now();
|
|
758
|
+
const resolution = { outcome: "unresolved", method: null, trackCount: null, rangeCount: null, rangeBytes: null };
|
|
759
|
+
const resolutionId = `${map.fileIndex}:${zone.outputKey}:${zone.index}:${passStartedAt.toFixed(3)}`;
|
|
760
|
+
waitTrace.source(map.sourceKey, "process", { process: "P4.1", phase: "begin", resolutionId,
|
|
761
|
+
sourceId: `${map.sourceKey}:${map.fileIndex}`, fileIndex: map.fileIndex, outputKey: zone.outputKey,
|
|
762
|
+
segment: zone.index, priority: zone.priority, deadlineAt: zone.deadlineAt });
|
|
763
|
+
try {
|
|
764
|
+
const reader = readers.get(zone.outputKey);
|
|
765
|
+
if (!reader) {
|
|
766
|
+
resolution.outcome = "reader-missing";
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
759
769
|
// WHAT A SEGMENT READS IS WORKED OUT ONCE (`segmentRecordOf`). Rebuilt
|
|
760
770
|
// for every segment of the film on every map, this pass held the main
|
|
761
771
|
// thread up to 1.5 s at a time on Home Assistant (torrent-tv/meta#178).
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
const { wanted, modes, interval, rangesKey } = reading;
|
|
769
|
-
const convert = ranges => {
|
|
770
|
-
if (record.ranges !== ranges || record.pieceLength !== pieceLength) {
|
|
771
|
-
record.ranges = ranges;
|
|
772
|
-
record.pieceLength = pieceLength;
|
|
773
|
-
record.joined = joinedWithinPieces(ranges, pieceLength);
|
|
774
|
-
}
|
|
775
|
-
const joined = record.joined;
|
|
776
|
-
// What the segment reads is kept for the forecast in a table of its
|
|
777
|
-
// own, whatever form the torrent is told it in.
|
|
778
|
-
if (record.registered !== joined) {
|
|
779
|
-
segmentInputs.set(map.sourceKey, map.fileIndex, zone.outputKey, zone.index, joined);
|
|
780
|
-
record.registered = joined;
|
|
772
|
+
const record = segmentRecordOf(reader.records, { index: zone.index, tracks: reader.choices }, reader.facts);
|
|
773
|
+
const reading = record.reading;
|
|
774
|
+
if (!reading) {
|
|
775
|
+
resolution.outcome = "segment-reading-unavailable";
|
|
776
|
+
projection.setUnreadable(zone.outputKey, zone.index);
|
|
777
|
+
continue;
|
|
781
778
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
779
|
+
const { wanted, modes, interval, rangesKey } = reading;
|
|
780
|
+
resolution.trackCount = wanted.length;
|
|
781
|
+
resolution.trackIds = wanted.map(track => track.trackNumber);
|
|
782
|
+
resolution.sourceInterval = { from: interval.from, to: interval.to };
|
|
783
|
+
const convert = (ranges, method) => {
|
|
784
|
+
if (record.ranges !== ranges || record.pieceLength !== pieceLength) {
|
|
785
|
+
record.ranges = ranges;
|
|
786
|
+
record.pieceLength = pieceLength;
|
|
787
|
+
record.joined = joinedWithinPieces(ranges, pieceLength);
|
|
788
|
+
}
|
|
789
|
+
const joined = record.joined;
|
|
790
|
+
resolution.method = method;
|
|
791
|
+
resolution.rangeCount = joined.length;
|
|
792
|
+
resolution.rangeBytes = joined.reduce((total, [start, end]) => total + end - start + 1, 0);
|
|
793
|
+
// What the segment reads is kept for the forecast in a table of its
|
|
794
|
+
// own, whatever form the torrent is told it in.
|
|
795
|
+
if (record.registered !== joined) {
|
|
796
|
+
segmentInputs.set(map.sourceKey, map.fileIndex, zone.outputKey, zone.index, joined);
|
|
797
|
+
record.registered = joined;
|
|
798
|
+
}
|
|
799
|
+
projection.setRanges(zone.outputKey, zone.index, joined);
|
|
800
|
+
resolution.outcome = "resolved";
|
|
801
|
+
resolved += 1;
|
|
802
|
+
};
|
|
785
803
|
// The bytes one segment needs are a fact of the file and the output, so
|
|
786
804
|
// they are worked out once. A whole film is thousands of segments and
|
|
787
805
|
// the map changes every few seconds; worked out afresh on every change,
|
|
788
806
|
// a pass over a two-hour AVI never finished and the next segment's bytes
|
|
789
807
|
// were never asked for (Home Assistant 2026-10-08, torrent-tv/meta#151).
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
808
|
+
const known = record.ranges ?? segmentSourceRanges.get(rangesKey);
|
|
809
|
+
if (known) { convert(known, record.ranges ? "segment-record-cache" : "source-range-cache"); continue; }
|
|
810
|
+
if (waiting.has(zone.outputKey)) {
|
|
811
|
+
resolution.outcome = "waiting-for-output-input";
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
793
814
|
// What is resolved so far is stated before a lower zone is worked out
|
|
794
815
|
// (DownloadMaps, torrent-tv/meta#178).
|
|
795
|
-
|
|
796
|
-
|
|
816
|
+
map.workingOut?.(zone.priority, () => projection.windows());
|
|
817
|
+
if (direct) {
|
|
797
818
|
// Asked per zone too: a soundtrack of an AVI is read from its own
|
|
798
819
|
// packets, which the same index states (torrent-tv/meta#166).
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
820
|
+
let value = null;
|
|
821
|
+
try {
|
|
822
|
+
value = await directSegmentInput(direct, { interval, tracks: wanted, mode: track => modes.get(track) },
|
|
823
|
+
built => refreshWhenBuilt(direct, built, map));
|
|
824
|
+
} catch { value = null; }
|
|
804
825
|
// A table still being built holds this output's segments, and only
|
|
805
826
|
// this output's: the map is built again when it is.
|
|
806
|
-
|
|
807
|
-
|
|
827
|
+
if (value?.kind === "pending") {
|
|
828
|
+
resolution.method = "direct-source-index";
|
|
829
|
+
resolution.outcome = "source-index-pending";
|
|
830
|
+
waiting.add(zone.outputKey);
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (value?.kind === "result") segmentSourceRanges.set(rangesKey, value.ranges);
|
|
834
|
+
if (value?.kind === "result") {
|
|
835
|
+
convert(value.ranges, "direct-source-index");
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
resolution.method = "container-index-fallback";
|
|
840
|
+
const intervalParams = await containerOver({ ...map, packetInterval: interval,
|
|
841
|
+
requestId: `download:${map.sourceKey}:${map.fileIndex}:${zone.outputKey}:${zone.index}`,
|
|
842
|
+
demand: { ...zone.owner, leadSeconds: zone.leadSeconds } });
|
|
843
|
+
if (!map.isCurrent()) {
|
|
844
|
+
resolution.outcome = "map-replaced";
|
|
845
|
+
return [];
|
|
846
|
+
}
|
|
847
|
+
if (!intervalParams) {
|
|
848
|
+
resolution.outcome = "source-file-unavailable";
|
|
808
849
|
continue;
|
|
809
850
|
}
|
|
810
|
-
|
|
811
|
-
if (
|
|
812
|
-
|
|
851
|
+
const navigation = await containerOrchestrator.inspect(intervalParams, "source-navigation");
|
|
852
|
+
if (!map.isCurrent()) {
|
|
853
|
+
resolution.outcome = "map-replaced";
|
|
854
|
+
return [];
|
|
855
|
+
}
|
|
856
|
+
if (navigation.kind !== "result") {
|
|
857
|
+
resolution.outcome = `source-navigation-${navigation.kind}`;
|
|
858
|
+
if (navigation.kind === "needs-ranges" || navigation.kind === "needs-memory") waiting.add(zone.outputKey);
|
|
813
859
|
continue;
|
|
814
860
|
}
|
|
861
|
+
const originalRanges = navigation.value;
|
|
862
|
+
const packets = await containerOrchestrator.inspect(intervalParams, originalRanges ? "source-ranges" : "packets");
|
|
863
|
+
if (!map.isCurrent()) {
|
|
864
|
+
resolution.outcome = "map-replaced";
|
|
865
|
+
return [];
|
|
866
|
+
}
|
|
867
|
+
if (packets.kind === "needs-memory") waiting.add(zone.outputKey);
|
|
868
|
+
if (packets.kind !== "result") {
|
|
869
|
+
resolution.outcome = `source-packets-${packets.kind}`;
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
const input = originalRanges ? packets.value : new SegmentInputs({ index: packets.value, tracks: wanted }).forInterval({ ...interval, mode: track => modes.get(track) });
|
|
873
|
+
if (input.kind !== "result") {
|
|
874
|
+
resolution.outcome = `segment-input-${input.kind}`;
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
segmentSourceRanges.set(rangesKey, input.ranges);
|
|
878
|
+
convert(input.ranges, originalRanges ? "container-source-ranges" : "packet-index");
|
|
879
|
+
} catch (error) {
|
|
880
|
+
resolution.outcome = "error";
|
|
881
|
+
resolution.error = error?.message ?? String(error);
|
|
882
|
+
throw error;
|
|
883
|
+
} finally {
|
|
884
|
+
const resolutionEndedAt = performance.now();
|
|
885
|
+
waitTrace.source(map.sourceKey, "process", { process: "P4.1", phase: "end", resolutionId,
|
|
886
|
+
sourceId: `${map.sourceKey}:${map.fileIndex}`, fileIndex: map.fileIndex, outputKey: zone.outputKey,
|
|
887
|
+
segment: zone.index, priority: zone.priority,
|
|
888
|
+
startedAt: resolutionStartedAt, endedAt: resolutionEndedAt, durationMs: resolutionEndedAt - resolutionStartedAt,
|
|
889
|
+
...resolution });
|
|
815
890
|
}
|
|
816
|
-
const intervalParams = await containerOver({ ...map, packetInterval: interval,
|
|
817
|
-
requestId: `download:${map.sourceKey}:${map.fileIndex}:${zone.outputKey}:${zone.index}`,
|
|
818
|
-
demand: { ...zone.owner, leadSeconds: zone.leadSeconds } });
|
|
819
|
-
if (!map.isCurrent()) return [];
|
|
820
|
-
if (!intervalParams) continue;
|
|
821
|
-
const navigation = await containerOrchestrator.inspect(intervalParams, "source-navigation");
|
|
822
|
-
if (navigation.kind !== "result") {
|
|
823
|
-
if (navigation.kind === "needs-ranges" || navigation.kind === "needs-memory") waiting.add(zone.outputKey);
|
|
824
|
-
continue;
|
|
825
|
-
}
|
|
826
|
-
const originalRanges = navigation.value;
|
|
827
|
-
const packets = await containerOrchestrator.inspect(intervalParams, originalRanges ? "source-ranges" : "packets");
|
|
828
|
-
if (!map.isCurrent()) return [];
|
|
829
|
-
if (packets.kind === "needs-memory") waiting.add(zone.outputKey);
|
|
830
|
-
if (packets.kind !== "result") continue;
|
|
831
|
-
const input = originalRanges ? packets.value : new SegmentInputs({ index: packets.value, tracks: wanted }).forInterval({ ...interval, mode: track => modes.get(track) });
|
|
832
|
-
if (input.kind !== "result") continue;
|
|
833
|
-
segmentSourceRanges.set(rangesKey, input.ranges);
|
|
834
|
-
convert(input.ranges);
|
|
835
891
|
}
|
|
836
892
|
const result = projection.windows();
|
|
837
893
|
notePass({ whole: reworked.whole, reworked: reworked.segments, resolved, windows: result.length,
|
|
@@ -497,10 +497,11 @@ export class AviContainer extends Container {
|
|
|
497
497
|
read.set(at, bytes);
|
|
498
498
|
return bytes;
|
|
499
499
|
};
|
|
500
|
-
const chainAt = async start => {
|
|
500
|
+
const chainAt = async (start, firstOffset) => {
|
|
501
501
|
let bytes = Buffer.alloc(0), cursor = 0, at = start, frames = 0;
|
|
502
502
|
while (frames < 3 && at < stream.count && bytes.length < 32 * 1024) {
|
|
503
503
|
bytes = Buffer.concat([bytes, await bytesAt(at++)]);
|
|
504
|
+
if (at === start + 1) cursor = firstOffset;
|
|
504
505
|
while (bytes.length - cursor >= 7 && frames < 3) {
|
|
505
506
|
if (bytes[cursor] !== 0x0b || bytes[cursor + 1] !== 0x77) return false;
|
|
506
507
|
let frame;
|
|
@@ -515,14 +516,22 @@ export class AviContainer extends Container {
|
|
|
515
516
|
};
|
|
516
517
|
|
|
517
518
|
const anchor = stream.atOrBefore(interval.from);
|
|
518
|
-
let first = -1, scannedBytes = 0;
|
|
519
|
+
let first = -1, firstOffset = -1, scannedBytes = 0;
|
|
519
520
|
for (let at = anchor; at >= 0 && scannedBytes < 128 * 1024; at--) {
|
|
520
521
|
const bytes = await bytesAt(at);
|
|
521
522
|
scannedBytes += bytes.length;
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
523
|
+
for (let offset = 0; offset < bytes.length; offset++) {
|
|
524
|
+
if (bytes[offset] !== 0x0b) continue;
|
|
525
|
+
if (offset + 1 < bytes.length) {
|
|
526
|
+
if (bytes[offset + 1] !== 0x77) continue;
|
|
527
|
+
} else if (at + 1 >= stream.count || (await bytesAt(at + 1))[0] !== 0x77) continue;
|
|
528
|
+
if (await chainAt(at, offset)) {
|
|
529
|
+
first = at;
|
|
530
|
+
firstOffset = offset;
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
525
533
|
}
|
|
534
|
+
if (first >= 0) break;
|
|
526
535
|
}
|
|
527
536
|
if (first < 0) throw new Error("Indexed AVI AC-3 has no complete frame chain near the requested interval.");
|
|
528
537
|
|
|
@@ -530,7 +539,9 @@ export class AviContainer extends Container {
|
|
|
530
539
|
try {
|
|
531
540
|
for (let at = first; at < stream.count; at++) {
|
|
532
541
|
const bytes = await bytesAt(at);
|
|
533
|
-
|
|
542
|
+
const offset = at === first ? firstOffset : 0;
|
|
543
|
+
if (bytes.length > offset) elementary.push(track.trackNumber, bytes.subarray(offset), stream.starts[at] + offset,
|
|
544
|
+
at === first ? { pts: stream.times[first] } : {});
|
|
534
545
|
const input = elementary.index.inputFor({ trackId: track.trackNumber, from: interval.from, to: interval.to,
|
|
535
546
|
mode: interval.modes?.[track.trackNumber] ?? "transcode" });
|
|
536
547
|
if (input.kind === "result") {
|