@torrent-tv/proxy 2.95.22 → 2.95.23
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseRange } from "../../utils/parse-range.js";
|
|
2
2
|
|
|
3
3
|
/** FFmpeg reads only original byte ranges already owned by its live run. */
|
|
4
|
-
export function handleEncodeInputGet(req, reply, { inputOf }) {
|
|
4
|
+
export function handleEncodeInputGet(req, reply, { inputOf, refused = () => {} }) {
|
|
5
5
|
const address = req.raw?.socket?.remoteAddress;
|
|
6
6
|
if (!["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(address)) return reply.code(403).send();
|
|
7
7
|
const token = Number(req.params.token), fileIndex = Number(req.params.fileIndex);
|
|
@@ -26,7 +26,12 @@ export function handleEncodeInputGet(req, reply, { inputOf }) {
|
|
|
26
26
|
// end of a content range). A cap of 1 MiB here ended every original-source
|
|
27
27
|
// segment longer than that on 6.1 (torrent-tv/meta#151).
|
|
28
28
|
const held = input.read(fileIndex, range.start, range.end, true);
|
|
29
|
-
if (!held)
|
|
29
|
+
if (!held) {
|
|
30
|
+
// FFmpeg asked for a byte the run was not given: the container named too
|
|
31
|
+
// little. Said, because FFmpeg itself reports only a failed seek or read.
|
|
32
|
+
refused({ token, fileIndex, start: range.start, runTag: input.runTag ?? null });
|
|
33
|
+
return reply.code(503).send({ error: "Original input bytes are outside the admitted ranges." });
|
|
34
|
+
}
|
|
30
35
|
return reply.code(206).header("Content-Length", String(held.bytes.length))
|
|
31
36
|
.header("Content-Range", `bytes ${range.start}-${held.end}/${length}`).send(held.bytes);
|
|
32
37
|
}
|
package/server.js
CHANGED
|
@@ -1147,7 +1147,8 @@ export async function startProxyServer({
|
|
|
1147
1147
|
})
|
|
1148
1148
|
);
|
|
1149
1149
|
app.get("/encode-input/:token/:fileIndex", (req, reply) =>
|
|
1150
|
-
handleEncodeInputGet(req, reply, { inputOf: token => outputParts.encodeRuns.originalInputOf(token)
|
|
1150
|
+
handleEncodeInputGet(req, reply, { inputOf: token => outputParts.encodeRuns.originalInputOf(token),
|
|
1151
|
+
refused: ({ fileIndex, start, runTag }) => logger.warn(`encode input: run ${runTag ?? "?"} asked for byte ${start} of file ${fileIndex}, which it was not given`) })
|
|
1151
1152
|
);
|
|
1152
1153
|
app.get("/stream", async (req, reply) =>
|
|
1153
1154
|
handleStreamGet(req, reply, {
|
|
@@ -478,7 +478,9 @@ export class EncodeRun {
|
|
|
478
478
|
if (line.length > 0) {
|
|
479
479
|
this.lastError = line;
|
|
480
480
|
const errorText = this.#stderrReadTail + String(chunk);
|
|
481
|
-
|
|
481
|
+
// A failed seek leaves the demuxer wherever it stood, so what follows is
|
|
482
|
+
// not the stretch asked for and is never published (torrent-tv/meta#151).
|
|
483
|
+
this.#inputTruncated ||= /Stream ends prematurely|Input\/output error|Error during demuxing|Seek failed/i.test(errorText);
|
|
482
484
|
this.#stderrReadTail = errorText.slice(-512);
|
|
483
485
|
this.logger.warn(`ffmpeg #${this.from}..#${this.to} of ${this.address}: ${line}`);
|
|
484
486
|
}
|
|
@@ -142,8 +142,9 @@ export class AviContainer extends Container {
|
|
|
142
142
|
const record = { dispose: () => this.#index?.dispose?.() };
|
|
143
143
|
const allocation = this.packetMemory?.forRecord?.(record, "index") ?? this.packetMemory;
|
|
144
144
|
const index = await readAviIndex({ read: this.readRange, fileSize: this.fileSize, movi, idx1, allocation,
|
|
145
|
-
streams: tracks.map(track => ({ type: track.type,
|
|
146
|
-
|
|
145
|
+
streams: tracks.map(track => ({ type: track.type, codecId: track.codecId, timeBase: track.timeBase,
|
|
146
|
+
sampleSize: track.sampleSize, blockAlign: track.blockAlign ?? 0, startUnits: track.startUnits ?? 0,
|
|
147
|
+
indexChunks: streams[track.trackNumber].indexChunks })) });
|
|
147
148
|
this.#index = index;
|
|
148
149
|
return index;
|
|
149
150
|
}
|
|
@@ -173,8 +174,12 @@ export class AviContainer extends Container {
|
|
|
173
174
|
if (!index) return { kind: "needs-index", reason: "source-has-no-avi-index" };
|
|
174
175
|
const tracks = await this.readTracks();
|
|
175
176
|
const { movi } = await this.#topLevel();
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
// Every audio and video stream, whichever the run outputs: FFmpeg's AVI
|
|
178
|
+
// demuxer seeks to the earliest position any stream's index gives for the
|
|
179
|
+
// keyframe time (`pos_min`, avi_read_seek) and reads every stream's packets
|
|
180
|
+
// in file order. Naming only the run's own tracks left a picture-only run
|
|
181
|
+
// asking for sound it was not given, and its seek failed (torrent-tv/meta#151).
|
|
182
|
+
const requested = tracks.filter(track => ["video", "audio"].includes(track.type));
|
|
178
183
|
const picture = ContainerTrack.firstUsable(tracks, "video");
|
|
179
184
|
const padded = ([start, end]) => [Math.max(0, start), Math.min(this.fileSize - 1, end + FFMPEG_INPUT_BUFFER_BYTES)];
|
|
180
185
|
const ranges = [[0, movi.start + 3], ...index.indexRanges, ...index.firstPackets(),
|
|
@@ -230,6 +235,7 @@ export class AviContainer extends Container {
|
|
|
230
235
|
track = new AudioTrack({ ...params, codecId: waveCodec(tag),
|
|
231
236
|
channels: strf.readUInt16LE(2), samplingFrequency: strf.readUInt32LE(4),
|
|
232
237
|
codecPrivateB64: strf.subarray(18).toString("base64") });
|
|
238
|
+
track.blockAlign = strf.readUInt16LE(12);
|
|
233
239
|
track.matroskaCodecId = "A_MS/ACM";
|
|
234
240
|
track.matroskaCodecPrivateB64 = (strf.length === 16
|
|
235
241
|
? Buffer.concat([strf, Buffer.alloc(2)]) : strf).toString("base64");
|
|
@@ -238,6 +244,7 @@ export class AviContainer extends Container {
|
|
|
238
244
|
track.startTimeSeconds = track.timeBase === null ? null : strh.readUInt32LE(28) * track.timeBase;
|
|
239
245
|
track.durationSeconds = track.timeBase === null ? null : strh.readUInt32LE(32) * track.timeBase;
|
|
240
246
|
track.sampleSize = strh.readUInt32LE(44);
|
|
247
|
+
track.startUnits = strh.readUInt32LE(28);
|
|
241
248
|
if (["h264", "hevc"].includes(track.codecId)) track.presentationCadenceSeconds = track.timeBase;
|
|
242
249
|
return track;
|
|
243
250
|
});
|
|
@@ -74,8 +74,9 @@ class StreamPackets {
|
|
|
74
74
|
* @param {number} params.fileSize
|
|
75
75
|
* @param {{ start: number, end: number }} params.movi - The first `movi` list (`start` at its type).
|
|
76
76
|
* @param {{ start: number, end: number } | null} params.idx1 - The `idx1` chunk's payload, if any.
|
|
77
|
-
* @param {Array<{ type: string, timeBase: number | null, sampleSize: number,
|
|
78
|
-
* indexChunks: Array<{ start: number, end: number }> }>} params.streams - In stream-number order
|
|
77
|
+
* @param {Array<{ type: string, codecId: string, timeBase: number | null, sampleSize: number, blockAlign: number,
|
|
78
|
+
* startUnits: number, indexChunks: Array<{ start: number, end: number }> }>} params.streams - In stream-number order,
|
|
79
|
+
* as the stream headers declare them (`strh` sample size and start, `WAVEFORMATEX` block alignment).
|
|
79
80
|
* @param {{ reserve?: (bytes: number) => boolean, release?: (bytes: number) => void }} params.allocation
|
|
80
81
|
* @returns {Promise<AviIndex | null>} Null when the file states no index.
|
|
81
82
|
*/
|
|
@@ -104,8 +105,38 @@ function reserve(allocation, count) {
|
|
|
104
105
|
return bytes;
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
/**
|
|
109
|
+
* A stream's clock as FFmpeg's AVI demuxer keeps it (avidec.c n8.1). The clock
|
|
110
|
+
* counts units: bytes where the stream has a sample size, otherwise blocks of
|
|
111
|
+
* the audio's `nBlockAlign` rounded up per packet (`get_duration`), otherwise
|
|
112
|
+
* one per packet, an empty one included. A packet's time is its units, divided
|
|
113
|
+
* by the sample size where there is one, times the time base. FFmpeg's header
|
|
114
|
+
* rules decide the sample size and the block alignment it uses; they are taken
|
|
115
|
+
* over exactly, since a time that disagrees with FFmpeg's moves the bytes a
|
|
116
|
+
* seek asks for out of the ranges named (field 2026-10-08: a two-hour MP3 AVI
|
|
117
|
+
* failed to seek at 384 s).
|
|
118
|
+
*/
|
|
119
|
+
function clockOf(stream) {
|
|
120
|
+
// The header's sample size is a signed 32-bit field to FFmpeg; the start is
|
|
121
|
+
// multiplied by it as read, before any correction, and a start beyond an hour
|
|
122
|
+
// is discarded.
|
|
123
|
+
const declared = (stream.sampleSize ?? 0) | 0;
|
|
124
|
+
const startUnits = (stream.startUnits ?? 0) * stream.timeBase > 3600 ? 0 : stream.startUnits ?? 0;
|
|
125
|
+
let sampleSize = stream.type === "video" || declared < 0 ? 0 : declared;
|
|
126
|
+
let blockAlign = 0;
|
|
127
|
+
if (stream.type === "audio") {
|
|
128
|
+
blockAlign = stream.blockAlign > 0 ? stream.blockAlign : 0;
|
|
129
|
+
if (sampleSize && blockAlign && sampleSize !== blockAlign) sampleSize = blockAlign;
|
|
130
|
+
if (["aac", "flac", "mp2"].includes(stream.codecId) && blockAlign <= 4) blockAlign = 0;
|
|
131
|
+
if ((stream.codecId === "aac" && (blockAlign === 1024 || blockAlign === 4096) && sampleSize === blockAlign) ||
|
|
132
|
+
(stream.codecId === "mp3" && blockAlign === 1152 && sampleSize === 1152)) sampleSize = 0;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
sampleSize,
|
|
136
|
+
units: startUnits * Math.max(1, declared),
|
|
137
|
+
advance(length) { this.units += sampleSize ? length : blockAlign ? Math.ceil(length / blockAlign) : 1; },
|
|
138
|
+
seconds() { return stream.timeBase * (sampleSize ? Math.floor(this.units / sampleSize) : this.units); }
|
|
139
|
+
};
|
|
109
140
|
}
|
|
110
141
|
|
|
111
142
|
async function readIdx1({ read, fileSize, movi, idx1, streams, allocation }) {
|
|
@@ -139,7 +170,7 @@ async function readIdx1({ read, fileSize, movi, idx1, streams, allocation }) {
|
|
|
139
170
|
const held = reserve(allocation, counts.reduce((sum, count) => sum + count, 0));
|
|
140
171
|
try {
|
|
141
172
|
const packets = counts.map(count => new StreamPackets(count));
|
|
142
|
-
const clocks = streams.map(
|
|
173
|
+
const clocks = streams.map(clockOf);
|
|
143
174
|
for await (const { bytes } of blocks(read, idx1.start, idx1.end, IDX1_ENTRY_BYTES)) {
|
|
144
175
|
for (let offset = 0; offset < bytes.length; offset += IDX1_ENTRY_BYTES) {
|
|
145
176
|
const chunkId = bytes.toString("ascii", offset, offset + 4);
|
|
@@ -151,8 +182,8 @@ async function readIdx1({ read, fileSize, movi, idx1, streams, allocation }) {
|
|
|
151
182
|
// FFmpeg takes the index flag alone, and every entry as a keyframe
|
|
152
183
|
// when no entry carries it (avi_read_idx1).
|
|
153
184
|
const keyframe = !anyKeyframe || !!(bytes.readUInt32LE(offset + 4) & AVIIF_KEYFRAME);
|
|
154
|
-
packets[id].push(start, length, clocks[id], keyframe);
|
|
155
|
-
clocks[id]
|
|
185
|
+
packets[id].push(start, length, clocks[id].seconds(), keyframe);
|
|
186
|
+
clocks[id].advance(length);
|
|
156
187
|
}
|
|
157
188
|
}
|
|
158
189
|
return new AviIndex({ packets, indexRanges: [[idx1.start - 8, fileSize - 1]], held, allocation });
|
|
@@ -202,9 +233,8 @@ async function readOpenDml({ read, fileSize, streams, allocation }) {
|
|
|
202
233
|
const held = reserve(allocation, counts.reduce((sum, count) => sum + count, 0));
|
|
203
234
|
try {
|
|
204
235
|
const packets = counts.map(count => new StreamPackets(count));
|
|
205
|
-
const clocks = streams.map(
|
|
236
|
+
const clocks = streams.map(clockOf);
|
|
206
237
|
for (const standard of parsed) {
|
|
207
|
-
const stream = streams[standard.id];
|
|
208
238
|
const tableStart = standard.start + 32;
|
|
209
239
|
const tableEnd = tableStart + standard.count * standard.entryBytes;
|
|
210
240
|
for await (const { bytes } of blocks(read, tableStart, tableEnd, standard.entryBytes)) {
|
|
@@ -212,8 +242,8 @@ async function readOpenDml({ read, fileSize, streams, allocation }) {
|
|
|
212
242
|
const start = standard.base + bytes.readUInt32LE(offset);
|
|
213
243
|
const encoded = bytes.readUInt32LE(offset + 4), length = encoded & 0x7fffffff;
|
|
214
244
|
if (length && (start < 8 || start + length > fileSize)) throw new Error("AVI OpenDML packet exceeds the file.");
|
|
215
|
-
packets[standard.id].push(start, length, clocks[standard.id], !(encoded & 0x80000000));
|
|
216
|
-
clocks[standard.id]
|
|
245
|
+
packets[standard.id].push(start, length, clocks[standard.id].seconds(), !(encoded & 0x80000000));
|
|
246
|
+
clocks[standard.id].advance(length);
|
|
217
247
|
}
|
|
218
248
|
}
|
|
219
249
|
}
|