@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.107.58",
3
+ "version": "2.107.59",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -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(input => matroskaTrackIdentity(input.track)));
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(input => matroskaTrackIdentity(input.track))) !== declarations) {
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
- export function matroskaTrackIdentity(track) {
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 (/^[VA]_/.test(codec)) return codec;
148
- if (/^pcm_[su](?:16|24|32|64)le$/.test(codec) || /^pcm_[su]8(?:le|be)?$/.test(codec)) return "A_PCM/INT/LIT";
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" || codec === "h265" || codec === "x265") {
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) && !MPEG4_CODECS.has(track.codecId.toUpperCase()) && !INTRA_PICTURE_CODECS.has(track.codecId.toUpperCase())) {
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 (!MPEG4_CODECS.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));
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" || !MPEG4_CODECS.has(track.codecId.toUpperCase())) {
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 VIDEO_CODECS.get(id) ?? FOURCC_CODECS.get(id.toLowerCase()) ?? id.toLowerCase();
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
- const known = CODEC_NAMES.get(codecId);
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),