@torrent-tv/proxy 2.97.17 → 2.97.18
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
|
@@ -556,6 +556,9 @@ export async function startProxyServer({
|
|
|
556
556
|
const known = segmentSourceRanges.get(rangesKey);
|
|
557
557
|
if (known) { convert(known); continue; }
|
|
558
558
|
if (waiting.has(zone.outputKey)) continue;
|
|
559
|
+
// What is resolved so far is stated before a lower zone is worked out
|
|
560
|
+
// (DownloadMaps, torrent-tv/meta#178).
|
|
561
|
+
map.workingOut?.(zone.priority, converted);
|
|
559
562
|
if (direct) {
|
|
560
563
|
// Asked per zone too: a soundtrack of an AVI is read from its own
|
|
561
564
|
// packets, which the same index states (torrent-tv/meta#166).
|
|
@@ -43,6 +43,8 @@ export class AviContainer extends Container {
|
|
|
43
43
|
/** The index alone: undefined until read, null when the file states none. */
|
|
44
44
|
#index = undefined;
|
|
45
45
|
#layout = null;
|
|
46
|
+
/** The first RIFF's `idx1`: undefined until looked for, null when there is none. */
|
|
47
|
+
#idx1Chunk = undefined;
|
|
46
48
|
|
|
47
49
|
async #readHeaders() {
|
|
48
50
|
if (this.#headers) return this.#headers;
|
|
@@ -124,17 +126,40 @@ export class AviContainer extends Container {
|
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
|
|
127
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* The first RIFF's `movi` list, found without reading past it: the chunk
|
|
131
|
+
* after `movi` lies where the first RIFF ends, which on a cold start is a
|
|
132
|
+
* piece of its own off the swarm.
|
|
133
|
+
*/
|
|
128
134
|
async #topLevel() {
|
|
129
135
|
if (this.#layout) return this.#layout;
|
|
130
136
|
const header = await this.readRange(0, 11);
|
|
131
|
-
const
|
|
132
|
-
const
|
|
137
|
+
const end = 8 + header.readUInt32LE(4);
|
|
138
|
+
const before = await riffChunks(this.readRange, 12, end, "movi");
|
|
139
|
+
const movi = before.find(chunk => chunk.id === "LIST" && chunk.type === "movi");
|
|
133
140
|
if (!movi) throw new Error("AVI media list is absent.");
|
|
134
|
-
this.#layout = { movi,
|
|
141
|
+
this.#layout = { movi, end, before };
|
|
135
142
|
return this.#layout;
|
|
136
143
|
}
|
|
137
144
|
|
|
145
|
+
/**
|
|
146
|
+
* The first RIFF's `idx1` chunk, which follows `movi`. Read only for a file
|
|
147
|
+
* with no OpenDML index, as FFmpeg reads it: a loaded OpenDML index marks
|
|
148
|
+
* the index loaded and `avi_load_index` is not called (avidec.c n6.1 and
|
|
149
|
+
* n8.1, `read_odml_index`). Looked for on every file, it cost a cold
|
|
150
|
+
* two-hour OpenDML AVI a round trip to the swarm: its index's own piece was
|
|
151
|
+
* asked for 4.4 s later than it could have been (Home Assistant 2026-10-12,
|
|
152
|
+
* torrent-tv/meta#178).
|
|
153
|
+
*/
|
|
154
|
+
async #idx1() {
|
|
155
|
+
if (this.#idx1Chunk !== undefined) return this.#idx1Chunk;
|
|
156
|
+
const { movi, end, before } = await this.#topLevel();
|
|
157
|
+
const after = movi.end + ((movi.end - movi.start) & 1);
|
|
158
|
+
const chunks = [...before, ...(after < end ? await riffChunks(this.readRange, after, end) : [])];
|
|
159
|
+
this.#idx1Chunk = chunks.find(chunk => chunk.id === "idx1") ?? null;
|
|
160
|
+
return this.#idx1Chunk;
|
|
161
|
+
}
|
|
162
|
+
|
|
138
163
|
/**
|
|
139
164
|
* What the index states, read once; null when the file states no index.
|
|
140
165
|
* A shortage of bytes or memory is thrown and nothing is kept, so the next
|
|
@@ -144,7 +169,8 @@ export class AviContainer extends Container {
|
|
|
144
169
|
if (this.#index !== undefined) return this.#index;
|
|
145
170
|
const tracks = await this.readTracks();
|
|
146
171
|
const { streams } = await this.#readHeaders();
|
|
147
|
-
const { movi
|
|
172
|
+
const { movi } = await this.#topLevel();
|
|
173
|
+
const idx1 = streams.some(stream => stream.indexChunks.length) ? null : await this.#idx1();
|
|
148
174
|
const record = { dispose: () => this.#index?.dispose?.() };
|
|
149
175
|
const allocation = this.packetMemory?.forRecord?.(record, "index") ?? this.packetMemory;
|
|
150
176
|
const index = await readAviIndex({ read: this.readRange, fileSize: this.fileSize, movi, idx1, allocation,
|
|
@@ -74,8 +74,28 @@ export class DownloadMaps {
|
|
|
74
74
|
}
|
|
75
75
|
if (changed) await this.#emit(file);
|
|
76
76
|
if (revision !== file.revision) return;
|
|
77
|
+
const isCurrent = () => this.#files.get(`${sourceKey}:${fileIndex}`) === file && revision === file.revision;
|
|
78
|
+
// WHAT IS RESOLVED IS STATED WHILE THE REST IS STILL BEING RESOLVED. A pass
|
|
79
|
+
// works the zones out in the order the swarm is to fetch them, and
|
|
80
|
+
// published only at its end it kept the first bytes from the swarm for as
|
|
81
|
+
// long as the far end of the film took: 14.6 s on a cold two-hour AVI, the
|
|
82
|
+
// swarm fetching nothing for 17.5 s while the encoder waited for its next
|
|
83
|
+
// piece (Home Assistant 2026-10-12, torrent-tv/meta#178). The swarm
|
|
84
|
+
// fetches the highest group still missing, so before the pass works out a
|
|
85
|
+
// zone of a lower priority than any before it, what it has resolved is
|
|
86
|
+
// published, with the zones still retained for what it has not reached. A
|
|
87
|
+
// pass that works nothing out — every pass over a film already worked out
|
|
88
|
+
// — states nothing early.
|
|
89
|
+
let stated = Number.POSITIVE_INFINITY;
|
|
90
|
+
const workingOut = (priority, zones) => {
|
|
91
|
+
if (!(priority < stated)) return;
|
|
92
|
+
stated = priority;
|
|
93
|
+
if (zones.length === 0 || !isCurrent()) return;
|
|
94
|
+
this.#setZones(file, withRetained(zones, retained));
|
|
95
|
+
this.#emit(file).catch(() => undefined);
|
|
96
|
+
};
|
|
77
97
|
const resolved = await this.#resolvePlayback({ sourceKey, fileIndex, durationSeconds, zones: file.sourceZones,
|
|
78
|
-
isCurrent
|
|
98
|
+
isCurrent, workingOut });
|
|
79
99
|
if (revision !== file.revision) return;
|
|
80
100
|
this.#setZones(file, resolved);
|
|
81
101
|
return this.#emit(file);
|
|
@@ -333,6 +353,19 @@ export class DownloadMaps {
|
|
|
333
353
|
}
|
|
334
354
|
}
|
|
335
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Zones resolved so far, with the retained zones of every segment they do not
|
|
358
|
+
* yet resolve again: a segment's bytes are stated once, by its newest reading.
|
|
359
|
+
*/
|
|
360
|
+
function withRetained(resolved, retained) {
|
|
361
|
+
const keyOf = zone => typeof zone.outputKey === "string" && Number.isSafeInteger(zone.index) ? `${zone.outputKey}:${zone.index}` : null;
|
|
362
|
+
const resolvedKeys = new Set(resolved.map(keyOf).filter(key => key !== null));
|
|
363
|
+
return [...resolved, ...retained.filter(zone => {
|
|
364
|
+
const key = keyOf(zone);
|
|
365
|
+
return key === null || !resolvedKeys.has(key);
|
|
366
|
+
})];
|
|
367
|
+
}
|
|
368
|
+
|
|
336
369
|
/** @param {number} since */
|
|
337
370
|
function emptyStats(since) {
|
|
338
371
|
return { since, sent: 0, unchanged: 0, zones: [], deliveredMs: [], appliedMs: [],
|