@torrent-tv/proxy 2.107.58 → 2.107.60
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/EncodeRuns.js +18 -5
- package/services/encode/MatroskaInput.js +10 -23
- package/services/encode/OutputTimes.js +4 -3
- 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 +3 -1
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; }
|
|
@@ -1783,19 +1783,32 @@ export class EncodeRuns {
|
|
|
1783
1783
|
// disposed. A run that had already finished or failed is not among them,
|
|
1784
1784
|
// which is what keeps a stop from erasing how it actually ended.
|
|
1785
1785
|
for (const run of running) {
|
|
1786
|
-
// The run resumes itself if it was suspended — a stopped process does not
|
|
1787
|
-
// act on SIGTERM until it is continued — records the cause, and answers
|
|
1788
|
-
// its own exit. Nothing here has to null a field so that the exit is read
|
|
1789
|
-
// correctly, because there is no shared field left to misread.
|
|
1790
|
-
run.stop(reason);
|
|
1791
1786
|
// Nothing is cleared up from here. What this run left open is under its
|
|
1792
1787
|
// own working name, and the encoding layer removes it when the run's
|
|
1793
1788
|
// ending reaches it — one place, and it needs neither the stretch nor the
|
|
1794
1789
|
// init bytes this method used to fetch to judge a file by its contents.
|
|
1790
|
+
this.stopRun(run, reason);
|
|
1795
1791
|
}
|
|
1796
1792
|
this.#host.logger.info(`transcode ${session.id} ${running.length} encoder(s) stopped: ${reason}`);
|
|
1797
1793
|
}
|
|
1798
1794
|
|
|
1795
|
+
/**
|
|
1796
|
+
* Stop one run, for a stated reason, and nothing else of its output.
|
|
1797
|
+
*
|
|
1798
|
+
* For a caller that knows WHICH run is wrong: an output may hold a second run
|
|
1799
|
+
* near the viewer that is positioned correctly and must keep producing.
|
|
1800
|
+
*
|
|
1801
|
+
* @param {object} run - A live run of this class's.
|
|
1802
|
+
* @param {string} reason - Named in the log.
|
|
1803
|
+
* @returns {void}
|
|
1804
|
+
*/
|
|
1805
|
+
stopRun(run, reason) {
|
|
1806
|
+
// The run resumes itself if it was suspended — a stopped process does not
|
|
1807
|
+
// act on SIGTERM until it is continued — records the cause, and answers
|
|
1808
|
+
// its own exit.
|
|
1809
|
+
run.stop(reason);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1799
1812
|
/**
|
|
1800
1813
|
* What moving the encoder BACKWARDS costs, said out loud when it happens.
|
|
1801
1814
|
*
|
|
@@ -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
|
}
|
|
@@ -98,7 +98,7 @@ export class OutputTimes {
|
|
|
98
98
|
#host;
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* @param {object} host - `logger`, `runsOf`, `
|
|
101
|
+
* @param {object} host - `logger`, `runsOf`, `stopRun`, `planEncodersSoon`, `outputs`, `segmentDurationSec`
|
|
102
102
|
*/
|
|
103
103
|
constructor(host) {
|
|
104
104
|
this.#host = host;
|
|
@@ -454,7 +454,8 @@ export class OutputTimes {
|
|
|
454
454
|
// claimed. It converges: once the boundary holds the true time, the next
|
|
455
455
|
// reading agrees with it and the guard above returns before doing anything.
|
|
456
456
|
for (const member of this.#host.outputs.familyOf(session)) {
|
|
457
|
-
|
|
457
|
+
const misplaced = member === session ? null : runStartingAt(this.#host.runsOf(member), index);
|
|
458
|
+
if (misplaced === null) {
|
|
458
459
|
continue;
|
|
459
460
|
}
|
|
460
461
|
this.#host.logger.info(
|
|
@@ -474,7 +475,7 @@ export class OutputTimes {
|
|
|
474
475
|
// which this function has just written it into and which every session of
|
|
475
476
|
// the file shares. It used to be passed as an argument from here, so only
|
|
476
477
|
// a run started by this line ever had it.
|
|
477
|
-
this.#host.
|
|
478
|
+
this.#host.stopRun(misplaced, `#${index} really begins ${(trueStart - wasAt).toFixed(3)}s later than the table said`);
|
|
478
479
|
}
|
|
479
480
|
this.#host.planEncodersSoon();
|
|
480
481
|
}
|
|
@@ -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";
|
|
@@ -336,7 +337,7 @@ export function wireOutputs({
|
|
|
336
337
|
parts.outputTimes = new OutputTimes({
|
|
337
338
|
logger,
|
|
338
339
|
runsOf: (...args) => parts.encodeRuns.runsOf(...args),
|
|
339
|
-
|
|
340
|
+
stopRun: (...args) => parts.encodeRuns.stopRun(...args),
|
|
340
341
|
planEncodersSoon: (...args) => parts.encodeRuns.planEncodersSoon(...args),
|
|
341
342
|
get outputs() { return parts.outputs; },
|
|
342
343
|
get segmentDurationSec() { return parts.segmentDurationSec; },
|
|
@@ -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),
|