@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.107.58",
3
+ "version": "2.107.60",
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; }
@@ -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
- 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
  }
@@ -98,7 +98,7 @@ export class OutputTimes {
98
98
  #host;
99
99
 
100
100
  /**
101
- * @param {object} host - `logger`, `runsOf`, `stopEncodeRun`, `planEncodersSoon`, `outputs`, `segmentDurationSec`
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
- if (member === session || runStartingAt(this.#host.runsOf(member), index) === null) {
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.stopEncodeRun(member, `#${index} really begins ${(trueStart - wasAt).toFixed(3)}s later than the table said`);
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" || 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";
@@ -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
- stopEncodeRun: (...args) => parts.encodeRuns.stopEncodeRun(...args),
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),