@torrent-tv/proxy 2.96.8 → 2.96.10
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
package/server.js
CHANGED
|
@@ -54,6 +54,7 @@ import { wireOutputs } from "./services/server/wire-outputs.js";
|
|
|
54
54
|
import { createPlaybackPlanner } from "./services/media/playback-planner.js";
|
|
55
55
|
import { KeyframeTables } from "./services/media/KeyframeTables.js";
|
|
56
56
|
import { contentsOf } from "./services/torrent/Contents.js";
|
|
57
|
+
import { joinedWithinPieces } from "./services/torrent/demand/pieces.js";
|
|
57
58
|
import { SubtitleOrchestrator } from "./services/media/SubtitleOrchestrator.js";
|
|
58
59
|
import { containerOrchestrator, CONTAINER_HEAD_BYTES, describeWorkTags } from "./services/media/ContainerOrchestrator.js";
|
|
59
60
|
import { readPlaybackDeclarations } from "./services/media/read-playback-declarations.js";
|
|
@@ -434,6 +435,9 @@ export async function startProxyServer({
|
|
|
434
435
|
const media = await containerOrchestrator.inspect(params, "media-info");
|
|
435
436
|
if (media.kind !== "result" || !map.isCurrent()) return [];
|
|
436
437
|
const shift = Number(media.value?.startTimeSeconds) || 0;
|
|
438
|
+
// A zone's ranges are stated to the torrent at the precision it fetches:
|
|
439
|
+
// ranges closer than a piece hold the same pieces as one.
|
|
440
|
+
const pieceLength = Number(torrentPool.knownTorrent(map.sourceKey)?.pieceLength);
|
|
437
441
|
const converted = [];
|
|
438
442
|
const demands = [];
|
|
439
443
|
for (const output of outputParts.outputs.values()) {
|
|
@@ -483,7 +487,7 @@ export async function startProxyServer({
|
|
|
483
487
|
modes: Object.fromEntries(wanted.map(track => [track.trackNumber, modes.get(track)])) };
|
|
484
488
|
const { tracks: _choices, owner: _owner, sourceInterval: _sourceInterval, ...demand } = zone;
|
|
485
489
|
const convert = ranges => {
|
|
486
|
-
for (const [byteStart, byteEnd] of ranges) converted.push({ ...demand,
|
|
490
|
+
for (const [byteStart, byteEnd] of joinedWithinPieces(ranges, pieceLength)) converted.push({ ...demand,
|
|
487
491
|
downloadInterval: { from: zone.owner.from, to: zone.owner.to }, byteStart, byteEnd });
|
|
488
492
|
};
|
|
489
493
|
// The bytes one segment needs are a fact of the file and the output, so
|
|
@@ -72,7 +72,13 @@ export class PacketIndex {
|
|
|
72
72
|
if (track.type !== "audio" || !isMp3Codec(track.codecId) ||
|
|
73
73
|
(interval?.trackIds && !interval.trackIds.includes(id)) || interval?.modes?.[id] === "copy") continue;
|
|
74
74
|
const records = track.packets;
|
|
75
|
-
|
|
75
|
+
// As in inputFor: records in time order are searched rather than read
|
|
76
|
+
// in full; one that starts too early to reach the interval is skipped
|
|
77
|
+
// below unless a record its preroll frames ahead does reach it.
|
|
78
|
+
const ordered = interval && records.ptsAscending;
|
|
79
|
+
const scanFrom = ordered ? Math.max(0, records.firstAtOrAfter(interval.from - records.longestDuration) - track.prerollFrames) : 0;
|
|
80
|
+
const scanTo = ordered ? records.firstAtOrAfter(interval.to) : records.length;
|
|
81
|
+
for (let position = scanFrom; position < scanTo; position++) {
|
|
76
82
|
const packet = records.at(position);
|
|
77
83
|
if (!packet.decodeDependencyUnknown || (interval &&
|
|
78
84
|
(packet.pts >= interval.to || (packet.pts < interval.from && packet.pts + packet.duration <= interval.from &&
|
|
@@ -174,7 +180,15 @@ export class PacketIndex {
|
|
|
174
180
|
const packets = track.packets;
|
|
175
181
|
const startAt = from - (track.type === "audio" && mode === "transcode" ? track.prerollSeconds : 0);
|
|
176
182
|
const selected = [];
|
|
177
|
-
|
|
183
|
+
// Records in time order are searched: only one starting within the
|
|
184
|
+
// longest duration before the interval can reach into it. Read in full,
|
|
185
|
+
// a two-hour soundtrack is 340 thousand records per interval, and the
|
|
186
|
+
// download map asked for two thousand intervals at once: the proxy's main
|
|
187
|
+
// thread stopped for minutes (Home Assistant 2026-10-10, torrent-tv/meta#166).
|
|
188
|
+
const ordered = packets.ptsAscending;
|
|
189
|
+
const scanFrom = ordered ? packets.firstAtOrAfter(startAt - packets.longestDuration) : 0;
|
|
190
|
+
const scanTo = ordered ? packets.firstAtOrAfter(to) : packets.length;
|
|
191
|
+
for (let index = scanFrom; index < scanTo; index++) {
|
|
178
192
|
const pts = packets.ptsAt(index);
|
|
179
193
|
if (pts < to && (pts >= startAt ||
|
|
180
194
|
((mode === "transcode" || track.type === "audio") && pts + packets.durationAt(index) > startAt))) selected.push(index);
|
|
@@ -17,6 +17,12 @@ export class PacketRecords {
|
|
|
17
17
|
#onDispose;
|
|
18
18
|
#disposed = false;
|
|
19
19
|
#prepaid = 0;
|
|
20
|
+
// Where the records first go back in time (-1 while they never do), the last
|
|
21
|
+
// time pushed, and the longest duration any record has had: what lets a
|
|
22
|
+
// reader find an interval by search instead of reading every record.
|
|
23
|
+
#descentAt = -1;
|
|
24
|
+
#lastPts = -Infinity;
|
|
25
|
+
#longest = 0;
|
|
20
26
|
|
|
21
27
|
constructor(memory = {}) {
|
|
22
28
|
const allocation = memory.forRecord?.(this) ?? memory;
|
|
@@ -77,9 +83,30 @@ export class PacketRecords {
|
|
|
77
83
|
this.#bytes = [...this.#blocks, ...this.#addresses].reduce((sum, buffer) => sum + buffer.length, 0);
|
|
78
84
|
}
|
|
79
85
|
this.#length = value;
|
|
86
|
+
if (this.#descentAt >= value) this.#descentAt = -1;
|
|
87
|
+
this.#lastPts = value ? this.ptsAt(value - 1) : -Infinity;
|
|
88
|
+
if (!value) this.#longest = 0;
|
|
80
89
|
if (previousBytes !== this.#bytes) this.#release(previousBytes - this.#bytes);
|
|
81
90
|
}
|
|
82
91
|
|
|
92
|
+
/** Whether every record's time is at least the one before it. */
|
|
93
|
+
get ptsAscending() { return this.#descentAt < 0; }
|
|
94
|
+
|
|
95
|
+
/** No record has lasted longer than this, though a removed one may have: a bound, not a measurement. */
|
|
96
|
+
get longestDuration() { return this.#longest; }
|
|
97
|
+
|
|
98
|
+
/** The first record whose time is at least `seconds`, `length` when there is none; only when the times ascend. */
|
|
99
|
+
firstAtOrAfter(seconds) {
|
|
100
|
+
if (!this.ptsAscending) throw new Error("Records whose times go back cannot be searched by time.");
|
|
101
|
+
let low = 0, high = this.#length;
|
|
102
|
+
while (low < high) {
|
|
103
|
+
const middle = (low + high) >>> 1;
|
|
104
|
+
if (this.ptsAt(middle) < seconds) low = middle + 1;
|
|
105
|
+
else high = middle;
|
|
106
|
+
}
|
|
107
|
+
return low;
|
|
108
|
+
}
|
|
109
|
+
|
|
83
110
|
push(packet) {
|
|
84
111
|
if (this.#disposed) throw new Error("Packet records were released.");
|
|
85
112
|
if (!Number.isFinite(packet.pts) || !Array.isArray(packet.ranges) ||
|
|
@@ -137,6 +164,9 @@ export class PacketRecords {
|
|
|
137
164
|
if (packet.streamId !== undefined) block.writeUInt32LE(packet.streamId, at + bytes - 4 - (packet.decodeFromIndex !== undefined ? 4 : 0));
|
|
138
165
|
if (packet.decodeFromIndex !== undefined) block.writeUInt32LE(packet.decodeFromIndex, at + bytes - 4);
|
|
139
166
|
this.#used += bytes;
|
|
167
|
+
if (this.#descentAt < 0 && packet.pts < this.#lastPts) this.#descentAt = this.#length;
|
|
168
|
+
this.#lastPts = packet.pts;
|
|
169
|
+
if (packet.duration > this.#longest) this.#longest = packet.duration;
|
|
140
170
|
return ++this.#length;
|
|
141
171
|
}
|
|
142
172
|
|
|
@@ -204,7 +234,9 @@ export class PacketRecords {
|
|
|
204
234
|
extendLastPresentation(seconds) {
|
|
205
235
|
if (!this.#length) return;
|
|
206
236
|
const [block, at] = this.#address(this.#length - 1);
|
|
207
|
-
block.
|
|
237
|
+
const duration = block.readDoubleLE(at + 8) + seconds;
|
|
238
|
+
block.writeDoubleLE(duration, at + 8);
|
|
239
|
+
if (duration > this.#longest) this.#longest = duration;
|
|
208
240
|
}
|
|
209
241
|
|
|
210
242
|
setDuration(index, seconds, derived = false) {
|
|
@@ -213,6 +245,7 @@ export class PacketRecords {
|
|
|
213
245
|
}
|
|
214
246
|
const [block, at] = this.#address(index);
|
|
215
247
|
block.writeDoubleLE(seconds, at + 8);
|
|
248
|
+
if (seconds > this.#longest) this.#longest = seconds;
|
|
216
249
|
const flags = block.readUInt32LE(at + 48);
|
|
217
250
|
block.writeUInt32LE(derived ? flags | 4 : flags & ~4, at + 48);
|
|
218
251
|
}
|
|
@@ -106,3 +106,29 @@ export function nearestFirst({ byteStart, byteEnd, parts }) {
|
|
|
106
106
|
}
|
|
107
107
|
return ranges;
|
|
108
108
|
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Byte ranges joined wherever the gap between two of them is shorter than a
|
|
112
|
+
* piece: the pieces they hold are the same, and there are fewer of them.
|
|
113
|
+
*
|
|
114
|
+
* A gap shorter than a piece cannot hold a whole piece, so every piece it
|
|
115
|
+
* touches is touched by a range beside it. The soundtrack of an AVI read from
|
|
116
|
+
* its own packets is one range per packet, interleaved with the picture's
|
|
117
|
+
* bytes: field 2026-10-10 (torrent-tv/meta#166), a map of 350 191 zones took
|
|
118
|
+
* 20.8 s to reach the torrent thread and grew its heap to 818 MB, for pieces of
|
|
119
|
+
* 4 MiB that the whole soundtrack touches anyway.
|
|
120
|
+
*
|
|
121
|
+
* @param {Array<[number, number]>} ranges - Inclusive byte ranges in ascending order.
|
|
122
|
+
* @param {number} pieceLength
|
|
123
|
+
* @returns {Array<[number, number]>} The same ranges when the piece length is not usable.
|
|
124
|
+
*/
|
|
125
|
+
export function joinedWithinPieces(ranges, pieceLength) {
|
|
126
|
+
if (!Number.isFinite(pieceLength) || pieceLength <= 0) return ranges;
|
|
127
|
+
const joined = [];
|
|
128
|
+
for (const [start, end] of ranges) {
|
|
129
|
+
const previous = joined.at(-1);
|
|
130
|
+
if (previous && start >= previous[0] && start - previous[1] - 1 < pieceLength) previous[1] = Math.max(previous[1], end);
|
|
131
|
+
else joined.push([start, end]);
|
|
132
|
+
}
|
|
133
|
+
return joined;
|
|
134
|
+
}
|