@torrent-tv/proxy 2.107.58 → 2.107.59
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/services/encode/AdmittedInput.js +6 -6
- package/services/encode/EncodeInputs.js +4 -2
- package/services/encode/MatroskaInput.js +10 -23
- package/services/encode/decode-cost-fit.js +1 -1
- package/services/media/container/AviContainer.js +4 -4
- package/services/media/playback-declarations.js +2 -15
- package/services/media/tracks/AudioTrack.js +2 -47
- package/services/media/tracks/codec-names.js +83 -0
- package/services/server/wire-outputs.js +2 -0
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import { pcmNormalization, normalizePcmSlices } from "./pcm-normalization.js";
|
|
|
5
5
|
const INPUT_WRITE_BATCH_BYTES = 64 * 1024;
|
|
6
6
|
|
|
7
7
|
/** Acquire complete source ranges before any process can consume the input. */
|
|
8
|
-
export async function admitInput({ sources, reserve, readRanges }) {
|
|
8
|
+
export async function admitInput({ sources, reserve, readRanges, matroskaIdOf }) {
|
|
9
9
|
if (!Array.isArray(sources) || sources.length === 0 || typeof reserve !== "function" || typeof readRanges !== "function") {
|
|
10
10
|
throw new TypeError("Admission requires resolved source inputs, memory reservation and an available-only reader.");
|
|
11
11
|
}
|
|
@@ -41,7 +41,7 @@ export async function admitInput({ sources, reserve, readRanges }) {
|
|
|
41
41
|
pts: packet.pts - (source.timeShiftSeconds ?? 0),
|
|
42
42
|
...(packet.dts === undefined ? {} : { dts: packet.dts - (source.timeShiftSeconds ?? 0) }) })) };
|
|
43
43
|
tracks.push(shifted);
|
|
44
|
-
hash.update(matroskaTrackIdentity(input.track));
|
|
44
|
+
hash.update(matroskaTrackIdentity(input.track, matroskaIdOf));
|
|
45
45
|
hash.update(JSON.stringify(shifted.packets.map(packet => [packet.pts, packet.dts ?? null,
|
|
46
46
|
packet.duration, packet.keyframe === true, packet.ranges, packet.discardPaddingSeconds ?? 0,
|
|
47
47
|
packet.bitOffset ?? null, packet.bitLength ?? null])));
|
|
@@ -82,8 +82,8 @@ export async function admitInput({ sources, reserve, readRanges }) {
|
|
|
82
82
|
}
|
|
83
83
|
retained = true;
|
|
84
84
|
return {
|
|
85
|
-
kind: "result", bytes, fingerprint: hash.digest("hex"), originSeconds, tracks, readPacket,
|
|
86
|
-
stream: options => matroskaInput({ tracks,
|
|
85
|
+
kind: "result", matroskaIdOf, bytes, fingerprint: hash.digest("hex"), originSeconds, tracks, readPacket,
|
|
86
|
+
stream: options => matroskaInput({ tracks, matroskaIdOf,
|
|
87
87
|
readPacket: (input, packet) => normalized.has(packet) ? [normalized.get(packet)] : packetSlices(input, packet),
|
|
88
88
|
originSeconds, ...options }),
|
|
89
89
|
release: () => {
|
|
@@ -112,13 +112,13 @@ export async function admitInput({ sources, reserve, readRanges }) {
|
|
|
112
112
|
export async function writeAdmittedInput(input, stdin, { next = null, waiting = null, onFirstInputBytes = null } = {}) {
|
|
113
113
|
if (input?.kind !== "result") throw new TypeError("Only complete input may be written to an encoder.");
|
|
114
114
|
const originSeconds = input.originSeconds;
|
|
115
|
-
const declarations = JSON.stringify(input.tracks.map(
|
|
115
|
+
const declarations = JSON.stringify(input.tracks.map(track => matroskaTrackIdentity(track.track, input.matroskaIdOf)));
|
|
116
116
|
const seen = new Map();
|
|
117
117
|
let firstInputBytesNoted = false;
|
|
118
118
|
let current = input, includeHeader = true;
|
|
119
119
|
try {
|
|
120
120
|
while (current) {
|
|
121
|
-
if (JSON.stringify(current.tracks.map(
|
|
121
|
+
if (JSON.stringify(current.tracks.map(track => matroskaTrackIdentity(track.track, current.matroskaIdOf))) !== declarations) {
|
|
122
122
|
throw new Error("Consecutive encoder input changed its declared tracks.");
|
|
123
123
|
}
|
|
124
124
|
for (let index = 0; index < current.tracks.length; index++) {
|
|
@@ -67,10 +67,12 @@ export class EncodeInputs {
|
|
|
67
67
|
*/
|
|
68
68
|
#stretchCopied;
|
|
69
69
|
#observeWait;
|
|
70
|
+
#matroskaIdOf;
|
|
70
71
|
|
|
71
72
|
constructor({ resolve, readRanges, openWholeFile = null, heldRanges = null, reviseBudget, changed, failed, capacity = () => null, urgent = () => true,
|
|
72
73
|
rankAt = () => null, demandChanged = () => {}, openOf = () => null, log = () => {}, now = Date.now, stretchCopied = () => {},
|
|
73
|
-
observeWait = null }) {
|
|
74
|
+
observeWait = null, matroskaIdOf }) {
|
|
75
|
+
this.#matroskaIdOf = matroskaIdOf;
|
|
74
76
|
this.#stretchCopied = stretchCopied;
|
|
75
77
|
this.#observeWait = observeWait;
|
|
76
78
|
this.#openOf = openOf;
|
|
@@ -461,7 +463,7 @@ export class EncodeInputs {
|
|
|
461
463
|
}
|
|
462
464
|
if (!result) return;
|
|
463
465
|
} else {
|
|
464
|
-
result = await admitInput({ sources: resolved.sources, readRanges: this.#read, reserve: this.#reserveFor(request) });
|
|
466
|
+
result = await admitInput({ sources: resolved.sources, readRanges: this.#read, matroskaIdOf: this.#matroskaIdOf, reserve: this.#reserveFor(request) });
|
|
465
467
|
if (result.kind === "result") Object.assign(result, { from: request.from, to: request.from });
|
|
466
468
|
}
|
|
467
469
|
if (this.#requests.get(request.key) !== request) { result.release?.(); return; }
|
|
@@ -2,12 +2,12 @@ import { avcPacketSlices } from "./annex-b-input.js";
|
|
|
2
2
|
import { bitPayloadSlices } from "./bit-payload.js";
|
|
3
3
|
|
|
4
4
|
/** Emit already admitted packets as one timestamped Matroska input. */
|
|
5
|
-
export async function* matroskaInput({ tracks, readPacket, originSeconds = 0, includeHeader = true, seen = new Map() }) {
|
|
5
|
+
export async function* matroskaInput({ tracks, readPacket, originSeconds = 0, includeHeader = true, seen = new Map(), matroskaIdOf }) {
|
|
6
6
|
if (!Array.isArray(tracks) || tracks.length === 0 || typeof readPacket !== "function" || !Number.isFinite(originSeconds)) {
|
|
7
7
|
throw new TypeError("A Matroska input requires tracks, admitted packets and a timeline origin.");
|
|
8
8
|
}
|
|
9
9
|
if (includeHeader) {
|
|
10
|
-
const declarations = tracks.map((input, position) => trackElement(input.track, position + 1));
|
|
10
|
+
const declarations = tracks.map((input, position) => trackElement(input.track, position + 1, matroskaIdOf));
|
|
11
11
|
yield element("1a45dfa3", Buffer.concat([
|
|
12
12
|
uintElement("4286", 1), uintElement("42f7", 1), uintElement("42f2", 4), uintElement("42f3", 8),
|
|
13
13
|
textElement("4282", "matroska"), uintElement("4287", 4), uintElement("4285", 2)
|
|
@@ -107,10 +107,10 @@ export async function* matroskaInput({ tracks, readPacket, originSeconds = 0, in
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
function trackElement(track, number) {
|
|
110
|
+
function trackElement(track, number, matroskaIdOf) {
|
|
111
111
|
const type = track.type === "video" ? 1 : track.type === "audio" ? 2 : null;
|
|
112
112
|
if (type === null) throw new Error("Admitted input carries an unsupported track type.");
|
|
113
|
-
const codec = track.matroskaCodecId ?? matroskaCodec(track.codecId);
|
|
113
|
+
const codec = track.matroskaCodecId ?? matroskaCodec(track.codecId, matroskaIdOf);
|
|
114
114
|
const fields = [uintElement("d7", number), uintElement("73c5", number), uintElement("83", type),
|
|
115
115
|
uintElement("9c", 0), textElement("86", codec)];
|
|
116
116
|
const privateBytes = track.matroskaCodecPrivateB64 ?? track.codecPrivateB64;
|
|
@@ -133,9 +133,10 @@ function trackElement(track, number) {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
/** Stable identity of every declaration emitted into the input stream. */
|
|
136
|
-
|
|
136
|
+
/** @param {(codec: string) => string | undefined} matroskaIdOf - Names a codec's Matroska id; handed in, since this layer reads no other. */
|
|
137
|
+
export function matroskaTrackIdentity(track, matroskaIdOf) {
|
|
137
138
|
return JSON.stringify([
|
|
138
|
-
track.type, track.matroskaCodecId ?? matroskaCodec(track.codecId),
|
|
139
|
+
track.type, track.matroskaCodecId ?? matroskaCodec(track.codecId, matroskaIdOf),
|
|
139
140
|
track.matroskaCodecPrivateB64 ?? track.codecPrivateB64 ?? "",
|
|
140
141
|
track.language ?? "", track.packetFraming ?? "", track.width ?? null, track.height ?? null,
|
|
141
142
|
track.samplingFrequency ?? null, track.channels ?? null, track.bitDepth ?? null,
|
|
@@ -143,23 +144,9 @@ export function matroskaTrackIdentity(track) {
|
|
|
143
144
|
]);
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
function matroskaCodec(codec) {
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
if (/^pcm_[su](?:16|24|32|64)be$/.test(codec)) return "A_PCM/INT/BIG";
|
|
150
|
-
if (/^pcm_f(?:32|64)(?:le|be)$/.test(codec)) return "A_PCM/FLOAT/IEEE";
|
|
151
|
-
const declared = new Map([
|
|
152
|
-
["avc1", "V_MPEG4/ISO/AVC"], ["avc3", "V_MPEG4/ISO/AVC"], ["h264", "V_MPEG4/ISO/AVC"],
|
|
153
|
-
["hvc1", "V_MPEGH/ISO/HEVC"], ["hev1", "V_MPEGH/ISO/HEVC"], ["hevc", "V_MPEGH/ISO/HEVC"],
|
|
154
|
-
["av01", "V_AV1"], ["av1", "V_AV1"], ["vp9", "V_VP9"], ["vp8", "V_VP8"],
|
|
155
|
-
["mjpeg", "V_MJPEG"], ["ffv1", "V_FFV1"], ["theora", "V_THEORA"], ["jpeg2000", "V_JPEG2000"],
|
|
156
|
-
["mpeg1video", "V_MPEG1"], ["mpeg2video", "V_MPEG2"], ["mpeg4", "V_MPEG4/ISO/ASP"],
|
|
157
|
-
["FMP4", "V_MPEG4/ISO/ASP"], ["XVID", "V_MPEG4/ISO/ASP"], ["DIVX", "V_MPEG4/ISO/ASP"],
|
|
158
|
-
["mp4a", "A_AAC"], ["aac", "A_AAC"], ["mp3", "A_MPEG/L3"], ["mp2", "A_MPEG/L2"], ["mp1", "A_MPEG/L1"],
|
|
159
|
-
["ac-3", "A_AC3"], ["ac3", "A_AC3"], ["ec-3", "A_EAC3"], ["eac3", "A_EAC3"],
|
|
160
|
-
["Opus", "A_OPUS"], ["opus", "A_OPUS"], ["fLaC", "A_FLAC"], ["flac", "A_FLAC"], ["dts", "A_DTS"],
|
|
161
|
-
["vorbis", "A_VORBIS"], ["alac", "A_ALAC"], ["truehd", "A_TRUEHD"]
|
|
162
|
-
]).get(codec);
|
|
147
|
+
function matroskaCodec(codec, matroskaIdOf) {
|
|
148
|
+
if (typeof matroskaIdOf !== "function") throw new TypeError("A Matroska input is given the lookup that names codec ids.");
|
|
149
|
+
const declared = matroskaIdOf(codec);
|
|
163
150
|
if (!declared) throw new Error(`Admitted codec ${codec} has no Matroska declaration.`);
|
|
164
151
|
return declared;
|
|
165
152
|
}
|
|
@@ -334,7 +334,7 @@ export function decodeFamilyOf(source) {
|
|
|
334
334
|
const codec = typeof source?.codec === "string" ? source.codec.trim().toLowerCase() : "";
|
|
335
335
|
const depth = Number(source?.bitDepth);
|
|
336
336
|
const tenBit = Number.isFinite(depth) && depth >= 10;
|
|
337
|
-
if (codec === "hevc"
|
|
337
|
+
if (codec === "hevc") {
|
|
338
338
|
return tenBit ? "hevc10" : "hevc";
|
|
339
339
|
}
|
|
340
340
|
// ffmpeg's own names. Only what has a clip set: `msmpeg4v3` (DivX 3) is a
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { FFMPEG_STREAM_SEARCH_SECONDS } from "./ffmpeg-stream-search.js";
|
|
11
11
|
import { Container } from "./Container.js";
|
|
12
|
+
import { MPEG4_FOURCCS } from "../tracks/codec-names.js";
|
|
12
13
|
import { VideoTrack } from "../tracks/VideoTrack.js";
|
|
13
14
|
import { AudioTrack } from "../tracks/AudioTrack.js";
|
|
14
15
|
import { ContainerTrack } from "../tracks/ContainerTrack.js";
|
|
@@ -22,7 +23,6 @@ import { ac3Frame } from "./ac3-frame.js";
|
|
|
22
23
|
import { RetainedReads } from "./RetainedReads.js";
|
|
23
24
|
import { OutsideReadableEdges, edgeReader, emptyWorkTags, text, textList, yearOf } from "./work-tags.js";
|
|
24
25
|
|
|
25
|
-
const MPEG4_CODECS = new Set(["FMP4", "XVID", "DIVX", "DX50", "MP4V", "M4S2", "MP4S"]);
|
|
26
26
|
/** FFmpeg's input buffer, filled ahead of what its demuxer parses (`IO_BUFFER_SIZE`, libavformat/aviobuf.c n8.1). */
|
|
27
27
|
const FFMPEG_INPUT_BUFFER_BYTES = 32768;
|
|
28
28
|
/** Picture codecs whose every picture stands alone, so decoding order is showing order. */
|
|
@@ -473,7 +473,7 @@ export class AviContainer extends Container {
|
|
|
473
473
|
const clocks = new Map();
|
|
474
474
|
for (const track of tracks.filter(track => ["video", "audio"].includes(track.type))) {
|
|
475
475
|
if (!(track.timeBase > 0)) throw new Error("AVI stream time base is invalid.");
|
|
476
|
-
if (track.type === "video" && !["h264", "hevc"].includes(track.codecId) && !
|
|
476
|
+
if (track.type === "video" && !["h264", "hevc"].includes(track.codecId) && !MPEG4_FOURCCS.has(track.codecId.toUpperCase()) && !INTRA_PICTURE_CODECS.has(track.codecId.toUpperCase())) {
|
|
477
477
|
throw new Error("AVI video without an index requires elementary picture timing.");
|
|
478
478
|
}
|
|
479
479
|
if (track.type === "audio" && !(track.sampleSize > 0) && !MPEG_AUDIO_CODECS.has(track.codecId)) throw new Error("AVI variable-size audio requires elementary frame timing.");
|
|
@@ -651,7 +651,7 @@ export class AviContainer extends Container {
|
|
|
651
651
|
if (length) await this.#appendPacket(state.index, state.timings, track, { pts, duration, keyframe: true, ranges: [[start, end - 1]] });
|
|
652
652
|
else if (track.type === "video") state.index.extendLastPresentation(trackId, track.timeBase);
|
|
653
653
|
state.clocks.set(trackId, pts + duration);
|
|
654
|
-
if (!
|
|
654
|
+
if (!MPEG4_FOURCCS.has(track.codecId.toUpperCase()) && !["h264", "hevc"].includes(track.codecId) && !MPEG_AUDIO_CODECS.has(track.codecId) && (track.type === "audio" || length)) state.index.coverThrough(trackId, Math.max(0, pts + duration));
|
|
655
655
|
}
|
|
656
656
|
state.at = next;
|
|
657
657
|
state.index.flushPending();
|
|
@@ -687,7 +687,7 @@ export class AviContainer extends Container {
|
|
|
687
687
|
}
|
|
688
688
|
return;
|
|
689
689
|
}
|
|
690
|
-
if (track.type !== "video" || !
|
|
690
|
+
if (track.type !== "video" || !MPEG4_FOURCCS.has(track.codecId.toUpperCase())) {
|
|
691
691
|
index.append(track.trackNumber, packet);
|
|
692
692
|
return;
|
|
693
693
|
}
|
|
@@ -1,33 +1,20 @@
|
|
|
1
1
|
import { AudioTrack } from "./tracks/AudioTrack.js";
|
|
2
2
|
import { TextSubtitleTrack } from "./tracks/TextSubtitleTrack.js";
|
|
3
3
|
import { ContainerTrack } from "./tracks/ContainerTrack.js";
|
|
4
|
+
import { videoCodecName } from "./tracks/codec-names.js";
|
|
4
5
|
|
|
5
|
-
const VIDEO_CODECS = new Map([
|
|
6
|
-
["V_MPEG4/ISO/AVC", "h264"], ["avc1", "h264"], ["avc3", "h264"],
|
|
7
|
-
["V_MPEGH/ISO/HEVC", "hevc"], ["hvc1", "hevc"], ["hev1", "hevc"],
|
|
8
|
-
["V_MPEG1", "mpeg1video"], ["V_MPEG2", "mpeg2video"],
|
|
9
|
-
["V_MPEG4/ISO/ASP", "mpeg4"], ["V_MPEG4/ISO/SP", "mpeg4"], ["mp4v", "mpeg4"],
|
|
10
|
-
["V_VP8", "vp8"], ["V_VP9", "vp9"], ["vp09", "vp9"],
|
|
11
|
-
["V_AV1", "av1"], ["av01", "av1"], ["V_MJPEG", "mjpeg"],
|
|
12
|
-
["V_FFV1", "ffv1"], ["V_THEORA", "theora"]
|
|
13
|
-
]);
|
|
14
6
|
const SUBTITLE_CODECS = new Map([
|
|
15
7
|
["S_TEXT/UTF8", "subrip"], ["S_TEXT/ASS", "ass"], ["S_TEXT/SSA", "ssa"],
|
|
16
8
|
["S_TEXT/WEBVTT", "webvtt"], ["wvtt", "webvtt"], ["tx3g", "mov_text"],
|
|
17
9
|
["S_HDMV/PGS", "hdmv_pgs_subtitle"], ["S_VOBSUB", "dvd_subtitle"]
|
|
18
10
|
]);
|
|
19
|
-
const FOURCC_CODECS = new Map([
|
|
20
|
-
["mjpg", "mjpeg"], ["jpeg", "mjpeg"], ["xvid", "mpeg4"],
|
|
21
|
-
["divx", "mpeg4"], ["dx50", "mpeg4"], ["mpg2", "mpeg2video"]
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
11
|
function videoCodecOf(track) {
|
|
25
12
|
let id = track.codecId;
|
|
26
13
|
if (id === "V_MS/VFW/FOURCC" && track.codecPrivateB64) {
|
|
27
14
|
const header = Buffer.from(track.codecPrivateB64, "base64");
|
|
28
15
|
if (header.length >= 20) id = header.toString("ascii", 16, 20);
|
|
29
16
|
}
|
|
30
|
-
return
|
|
17
|
+
return videoCodecName(id);
|
|
31
18
|
}
|
|
32
19
|
|
|
33
20
|
/** One playback inventory, taken from the same declarations used for input. */
|
|
@@ -11,40 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { codecParametersOf, peakKbpsOf } from "./audio-bound.js";
|
|
14
|
+
import { audioCodecName } from "./codec-names.js";
|
|
14
15
|
import { ContainerTrack } from "./ContainerTrack.js";
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
* Codec identifiers as containers write them, against the name ffmpeg prints.
|
|
18
|
-
*
|
|
19
|
-
* Needed because the browser decides whether it can play a soundtrack from that
|
|
20
|
-
* name, and for a sidecar file there is no ffmpeg banner to read it from — the
|
|
21
|
-
* track came from the container's own table, where Matroska writes `A_AC3` and
|
|
22
|
-
* MP4 writes `ac-3` for the thing ffmpeg calls `ac3`. Only what a soundtrack can
|
|
23
|
-
* actually be is listed; an identifier not here is reported as it was written,
|
|
24
|
-
* which the browser treats as one it does not know and therefore transcodes.
|
|
25
|
-
*/
|
|
26
|
-
const CODEC_NAMES = new Map([
|
|
27
|
-
["A_AAC", "aac"],
|
|
28
|
-
["A_AC3", "ac3"],
|
|
29
|
-
["A_EAC3", "eac3"],
|
|
30
|
-
["A_DTS", "dts"],
|
|
31
|
-
["A_FLAC", "flac"],
|
|
32
|
-
["A_OPUS", "opus"],
|
|
33
|
-
["A_VORBIS", "vorbis"],
|
|
34
|
-
["A_TRUEHD", "truehd"],
|
|
35
|
-
["A_MPEG/L3", "mp3"],
|
|
36
|
-
["A_MPEG/L2", "mp2"],
|
|
37
|
-
["A_ALAC", "alac"],
|
|
38
|
-
["mp4a", "aac"],
|
|
39
|
-
["ac-3", "ac3"],
|
|
40
|
-
["ec-3", "eac3"],
|
|
41
|
-
["alac", "alac"],
|
|
42
|
-
["opus", "opus"],
|
|
43
|
-
["Opus", "opus"],
|
|
44
|
-
["fLaC", "flac"],
|
|
45
|
-
["flac", "flac"]
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
17
|
/**
|
|
49
18
|
* Extensions of raw elementary streams, against the codec they carry.
|
|
50
19
|
*
|
|
@@ -126,21 +95,7 @@ export class AudioTrack extends ContainerTrack {
|
|
|
126
95
|
}
|
|
127
96
|
const codecId = typeof track?.codecId === "string" ? track.codecId.trim() : "";
|
|
128
97
|
if (codecId.length > 0) {
|
|
129
|
-
|
|
130
|
-
if (known) {
|
|
131
|
-
return known;
|
|
132
|
-
}
|
|
133
|
-
// Matroska allows a suffix — `A_AAC/MPEG4/LC`, `A_PCM/INT/LIT` — so the
|
|
134
|
-
// family is what the first two segments say.
|
|
135
|
-
const family = codecId.split("/").slice(0, 2).join("/");
|
|
136
|
-
const byFamily = CODEC_NAMES.get(family) ?? CODEC_NAMES.get(codecId.split("/")[0]);
|
|
137
|
-
if (byFamily) {
|
|
138
|
-
return byFamily;
|
|
139
|
-
}
|
|
140
|
-
if (codecId.startsWith("A_PCM")) {
|
|
141
|
-
return "pcm";
|
|
142
|
-
}
|
|
143
|
-
return codecId.toLowerCase();
|
|
98
|
+
return audioCodecName(codecId);
|
|
144
99
|
}
|
|
145
100
|
return CODEC_BY_EXTENSION.get(extension) ?? "";
|
|
146
101
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The one table that names codecs.
|
|
3
|
+
*
|
|
4
|
+
* Containers write a codec as a Matroska id (`V_MPEG4/ISO/AVC`), an MP4 sample
|
|
5
|
+
* entry (`avc1`) or an AVI FOURCC (`XVID`); the rest of the proxy speaks the
|
|
6
|
+
* names ffmpeg prints (`h264`, `mpeg4`). Every translation lives here, so a
|
|
7
|
+
* FOURCC cannot be named one thing for the inventory and another for the
|
|
8
|
+
* container that reads it (torrent-tv/meta#284). Pure: imports nothing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** FOURCCs that are MPEG-4 Part 2 (ASP/SP) whatever their case. */
|
|
12
|
+
export const MPEG4_FOURCCS = new Set(["FMP4", "XVID", "DIVX", "DX50", "MP4V", "M4S2", "MP4S"]);
|
|
13
|
+
|
|
14
|
+
const VIDEO_NAMES = new Map([
|
|
15
|
+
["V_MPEG4/ISO/AVC", "h264"], ["avc1", "h264"], ["avc3", "h264"],
|
|
16
|
+
["V_MPEGH/ISO/HEVC", "hevc"], ["hvc1", "hevc"], ["hev1", "hevc"],
|
|
17
|
+
["V_MPEG1", "mpeg1video"], ["V_MPEG2", "mpeg2video"], ["mpg2", "mpeg2video"],
|
|
18
|
+
["V_MPEG4/ISO/ASP", "mpeg4"], ["V_MPEG4/ISO/SP", "mpeg4"],
|
|
19
|
+
["V_VP8", "vp8"], ["V_VP9", "vp9"], ["vp09", "vp9"],
|
|
20
|
+
["V_AV1", "av1"], ["av01", "av1"], ["V_MJPEG", "mjpeg"], ["mjpg", "mjpeg"], ["jpeg", "mjpeg"],
|
|
21
|
+
["V_FFV1", "ffv1"], ["V_THEORA", "theora"]
|
|
22
|
+
].map(([id, name]) => [id.toLowerCase(), name]));
|
|
23
|
+
|
|
24
|
+
/** The ffmpeg name of a video track from its codec id or FOURCC, in any case; unknown ids come back lower-cased. */
|
|
25
|
+
export function videoCodecName(id) {
|
|
26
|
+
const key = String(id ?? "").toLowerCase();
|
|
27
|
+
if (MPEG4_FOURCCS.has(key.toUpperCase())) return "mpeg4";
|
|
28
|
+
return VIDEO_NAMES.get(key) ?? key;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const AUDIO_NAMES = new Map([
|
|
32
|
+
["A_AAC", "aac"], ["A_AC3", "ac3"], ["A_EAC3", "eac3"], ["A_DTS", "dts"], ["A_FLAC", "flac"],
|
|
33
|
+
["A_OPUS", "opus"], ["A_VORBIS", "vorbis"], ["A_TRUEHD", "truehd"], ["A_MPEG/L3", "mp3"],
|
|
34
|
+
["A_MPEG/L2", "mp2"], ["A_ALAC", "alac"],
|
|
35
|
+
["mp4a", "aac"], ["ac-3", "ac3"], ["ec-3", "eac3"], ["alac", "alac"],
|
|
36
|
+
["opus", "opus"], ["Opus", "opus"], ["fLaC", "flac"], ["flac", "flac"]
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The ffmpeg name of a soundtrack from its container id. Matroska allows a
|
|
41
|
+
* suffix (`A_AAC/MPEG4/LC`, `A_PCM/INT/LIT`), so the family is what the first
|
|
42
|
+
* two segments say. An id not listed is reported as written, lower-cased.
|
|
43
|
+
*/
|
|
44
|
+
export function audioCodecName(codecId) {
|
|
45
|
+
const id = String(codecId ?? "").trim();
|
|
46
|
+
const known = AUDIO_NAMES.get(id);
|
|
47
|
+
if (known) return known;
|
|
48
|
+
const family = id.split("/").slice(0, 2).join("/");
|
|
49
|
+
const byFamily = AUDIO_NAMES.get(family) ?? AUDIO_NAMES.get(id.split("/")[0]);
|
|
50
|
+
if (byFamily) return byFamily;
|
|
51
|
+
if (id.startsWith("A_PCM")) return "pcm";
|
|
52
|
+
return id.toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const MATROSKA_IDS = new Map([
|
|
56
|
+
["h264", "V_MPEG4/ISO/AVC"], ["hevc", "V_MPEGH/ISO/HEVC"], ["av1", "V_AV1"], ["vp9", "V_VP9"],
|
|
57
|
+
["vp8", "V_VP8"], ["mjpeg", "V_MJPEG"], ["ffv1", "V_FFV1"], ["theora", "V_THEORA"],
|
|
58
|
+
["jpeg2000", "V_JPEG2000"], ["mpeg1video", "V_MPEG1"], ["mpeg2video", "V_MPEG2"], ["mpeg4", "V_MPEG4/ISO/ASP"],
|
|
59
|
+
["aac", "A_AAC"], ["mp3", "A_MPEG/L3"], ["mp2", "A_MPEG/L2"], ["mp1", "A_MPEG/L1"],
|
|
60
|
+
["ac3", "A_AC3"], ["eac3", "A_EAC3"], ["opus", "A_OPUS"], ["flac", "A_FLAC"], ["dts", "A_DTS"],
|
|
61
|
+
["vorbis", "A_VORBIS"], ["alac", "A_ALAC"], ["truehd", "A_TRUEHD"]
|
|
62
|
+
]);
|
|
63
|
+
// Spellings an input may carry besides the canonical name, taken exactly: a
|
|
64
|
+
// codec outside this list has no Matroska declaration, and `M4S2` or `xvid`
|
|
65
|
+
// stay refused because an input's key follows its declared track identity.
|
|
66
|
+
const MATROSKA_SPELLINGS = [
|
|
67
|
+
...["avc1", "avc3", "hvc1", "hev1", "av01"].map(spelling => [spelling, videoCodecName(spelling)]),
|
|
68
|
+
...["FMP4", "XVID", "DIVX"].map(spelling => [spelling, "mpeg4"]),
|
|
69
|
+
...["mp4a", "ac-3", "ec-3", "Opus", "fLaC"].map(spelling => [spelling, audioCodecName(spelling)])
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The exact Matroska CodecID a canonical codec name is declared as, or
|
|
74
|
+
* `undefined` when there is none. Reverse of the names above, never wider.
|
|
75
|
+
*/
|
|
76
|
+
export function matroskaIdOf(codec) {
|
|
77
|
+
if (/^[VA]_/.test(codec)) return codec;
|
|
78
|
+
if (/^pcm_[su](?:16|24|32|64)le$/.test(codec) || /^pcm_[su]8(?:le|be)?$/.test(codec)) return "A_PCM/INT/LIT";
|
|
79
|
+
if (/^pcm_[su](?:16|24|32|64)be$/.test(codec)) return "A_PCM/INT/BIG";
|
|
80
|
+
if (/^pcm_f(?:32|64)(?:le|be)$/.test(codec)) return "A_PCM/FLOAT/IEEE";
|
|
81
|
+
const spelled = MATROSKA_SPELLINGS.find(([spelling]) => spelling === codec);
|
|
82
|
+
return MATROSKA_IDS.get(spelled ? spelled[1] : codec);
|
|
83
|
+
}
|
|
@@ -12,6 +12,7 @@ import { EventEmitter } from "node:events";
|
|
|
12
12
|
import { writeJsonAtomic } from "../../utils/atomic-json.js";
|
|
13
13
|
import { logger } from "../../utils/logger.js";
|
|
14
14
|
import { KeyframeTables } from "../media/KeyframeTables.js";
|
|
15
|
+
import { matroskaIdOf } from "../media/tracks/codec-names.js";
|
|
15
16
|
import { bandOf, WaitLedger } from "../encode/OutputWaitLedger.js";
|
|
16
17
|
import { readEndedChildrenCpuSeconds, readMachineState, readProcessCpuSeconds, readProxyCpuSeconds, readSystemCpu, shareOfMachine } from "../encode/host-load.js";
|
|
17
18
|
import { minimumBufferFrom } from "../torrent/supply-margin.js";
|
|
@@ -1197,6 +1198,7 @@ export function wireOutputs({
|
|
|
1197
1198
|
parts.encodeInputs = encodeInputs ?? (typeof encodeInputResolution?.resolve === "function" ? new EncodeInputs({
|
|
1198
1199
|
resolve: (...args) => encodeInputResolution.resolve(...args),
|
|
1199
1200
|
readRanges: readEncodeRanges,
|
|
1201
|
+
matroskaIdOf,
|
|
1200
1202
|
openWholeFile: openEncodeWholeFile,
|
|
1201
1203
|
heldRanges: readEncodeHeldRanges,
|
|
1202
1204
|
stretchCopied: (...args) => encodeInputResolution.stretchCopied(...args),
|