easycaptionkit 0.1.1 → 0.1.3

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.
@@ -12,49 +12,86 @@ const fs = require("node:fs");
12
12
  const os = require("node:os");
13
13
  const path = require("node:path");
14
14
  const { spawn, spawnSync } = require("node:child_process");
15
- const { onPath, given, namedNothing } = require("./ffmpeg");
15
+ const programs = require("./programs");
16
16
  const { track, trackFolder, untrackFolder, kill, sleep } = require("./running");
17
17
 
18
- // A Chrome named in the environment is found as FFMPEG_PATH is: a bare
19
- // name on PATH, a path from the folder the run starts in.
20
- function findChrome() {
21
- const named = process.env.CHROME_PATH;
22
- if (named) return given(named);
23
- const mac = [
24
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
25
- "/Applications/Chromium.app/Contents/MacOS/Chromium",
26
- "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
27
- "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
28
- path.join(os.homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
29
- ];
30
- const win = [
31
- path.join(process.env.PROGRAMFILES || "C:\\Program Files", "Google\\Chrome\\Application\\chrome.exe"),
32
- path.join(process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)", "Google\\Chrome\\Application\\chrome.exe"),
33
- path.join(process.env.LOCALAPPDATA || "", "Google\\Chrome\\Application\\chrome.exe"),
34
- path.join(process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)", "Microsoft\\Edge\\Application\\msedge.exe"),
35
- ];
36
- const listed = process.platform === "darwin" ? mac : process.platform === "win32" ? win : [];
37
- for (const file of listed) if (file && fs.existsSync(file)) return file;
38
- for (const name of ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "chrome", "microsoft-edge", "brave-browser"]) {
39
- const found = onPath(name);
40
- if (found) return found;
41
- }
42
- return "";
43
- }
18
+ const LIBASS = "--libass has ffmpeg draw the captions without Chrome.";
44
19
 
45
- const MISSING = "No Chrome was found to draw the captions with. Install Google Chrome or Chromium, set CHROME_PATH to it, or pass --libass to have ffmpeg draw them.";
20
+ // Chrome as a run looks for it and asks it its version (see
21
+ // programs.find()): the path CHROME_PATH gives, or a name on PATH, as
22
+ // FFMPEG_PATH is read; else where macOS and Windows install it, and on
23
+ // PATH by each of its names. It answers with a version number, as "Google
24
+ // Chrome 155.0.8059.84". On Windows, where Chrome prints no version, it is
25
+ // not asked, and taken once nothing keeps it from running.
26
+ const CHROME = {
27
+ name: "Chrome",
28
+ variable: "CHROME_PATH",
29
+ args: ["--version"],
30
+ answers: (stdout) => /\d+\.\d+/.test(stdout),
31
+ names: ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "chrome", "microsoft-edge", "brave-browser"],
32
+ first: () => {
33
+ const mac = [
34
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
35
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
36
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
37
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
38
+ path.join(os.homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
39
+ ];
40
+ const win = [
41
+ path.join(process.env.PROGRAMFILES || "C:\\Program Files", "Google\\Chrome\\Application\\chrome.exe"),
42
+ path.join(process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)", "Google\\Chrome\\Application\\chrome.exe"),
43
+ path.join(process.env.LOCALAPPDATA || "", "Google\\Chrome\\Application\\chrome.exe"),
44
+ path.join(process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)", "Microsoft\\Edge\\Application\\msedge.exe"),
45
+ ];
46
+ const listed = process.platform === "darwin" ? mac : process.platform === "win32" ? win : [];
47
+ return listed.filter(Boolean).map((file) => ({ file, name: "Chrome", where: "installed on this machine" }));
48
+ },
49
+ ask: process.platform !== "win32",
50
+ none: `easycaptionkit needs Chrome to draw the captions, and none was found. Install Google Chrome or Chromium, or set CHROME_PATH to one. ${LIBASS}`,
51
+ advice: {
52
+ variable: `Set CHROME_PATH to a Chrome that runs, or unset it and install Google Chrome or Chromium. ${LIBASS}`,
53
+ found: `Install Google Chrome or Chromium, or set CHROME_PATH to a Chrome that runs. ${LIBASS}`,
54
+ late: `Run easycaptionkit again. ${LIBASS}`,
55
+ },
56
+ };
46
57
 
47
- // The variable that names this run's Chrome, when it names no program:
48
- // what it names, as a sentence without its end. Empty otherwise.
49
- function chromeNamedNothing() {
50
- return namedNothing("CHROME_PATH");
58
+ // The Chrome a run would take, by what is at its place alone, not run, or
59
+ // "" where there is none.
60
+ const findChrome = () => programs.locate(CHROME);
61
+
62
+ // The Chrome that answered, for the CHROME_PATH, the PATH and the folder
63
+ // the run works with now: a run asks it once, and every Chrome it starts
64
+ // to draw is that one. A failure is not kept, so a later call asks again.
65
+ let checked = null;
66
+
67
+ // The Chrome a run draws with, found and asked its version before any work,
68
+ // so one that cannot be started or does not answer as Chrome stops the run
69
+ // with what the system or the program said, not once it begins to draw.
70
+ async function chromeReady(limit = programs.LIMIT) {
71
+ let here = "";
72
+ try {
73
+ here = process.cwd();
74
+ } catch {
75
+ // A folder taken away since: only PATH's relative entries read from it.
76
+ }
77
+ const key = JSON.stringify([process.env.CHROME_PATH, process.env.PATH, here]);
78
+ if (checked && checked.key === key) return checked.file;
79
+ const { file } = await programs.find(CHROME, limit);
80
+ checked = { key, file };
81
+ return file;
51
82
  }
52
83
 
53
- // Why there is no Chrome: what the variable names, or that none is on the
54
- // machine.
55
- function missingChrome() {
56
- const nothing = chromeNamedNothing();
57
- return nothing ? `${nothing}. Set it to the path of Chrome or Chromium, or pass --libass to have ffmpeg draw the captions.` : MISSING;
84
+ // For a run that draws without Chrome and uses it for its pictures alone:
85
+ // why there is no Chrome to use, as words that follow "since", or "" when
86
+ // it answers.
87
+ async function chromeProblem() {
88
+ try {
89
+ await chromeReady();
90
+ return "";
91
+ } catch (err) {
92
+ if (!err.unusable) throw err;
93
+ return err.why;
94
+ }
58
95
  }
59
96
 
60
97
  // An error of Chrome's own, not of a file or of ffmpeg: `start` when Chrome
@@ -225,9 +262,17 @@ function whyStopped(stderr, temp) {
225
262
  return text;
226
263
  }
227
264
 
265
+ // Starts the Chrome a run checked before any work (see chromeReady()),
266
+ // asking it first where it was not. A start the system refuses says what
267
+ // the system said, never its code alone.
228
268
  async function launch({ width = 1920, height = 1080 } = {}) {
229
- const chrome = findChrome();
230
- if (!chrome) throw failed(missingChrome(), { start: true });
269
+ let chrome;
270
+ try {
271
+ chrome = await chromeReady();
272
+ } catch (err) {
273
+ throw failed(err.message, { start: true });
274
+ }
275
+ const refused = (err) => failed(programs.startFailure(chrome, err, "Chrome").message, { start: true });
231
276
  const temp = chromeTemp();
232
277
  clearOldProfiles();
233
278
  // A profile Chrome cannot have is a Chrome that cannot start.
@@ -271,13 +316,20 @@ async function launch({ width = 1920, height = 1080 } = {}) {
271
316
  child = track(spawn(chrome, args, { stdio: ["ignore", "ignore", "pipe", "pipe", "pipe"], env: { ...process.env, TMPDIR: temp, CHROME_CONFIG_HOME: path.join(profile, "config") } }), stop);
272
317
  } catch (err) {
273
318
  drop();
274
- throw failed(`Chrome could not start: ${err.message}`, { start: true });
319
+ throw refused(err);
275
320
  }
276
321
  // Settles when Chrome is gone, whether it exited or never started.
277
322
  const gone = new Promise((resolve) => {
278
323
  child.once("exit", resolve);
279
324
  child.once("error", resolve);
280
325
  });
326
+ // With no file descriptor left for them Node makes no pipes, and says
327
+ // why a moment later.
328
+ if (!child.stdio || !child.stderr || !child.stdio[3] || !child.stdio[4]) {
329
+ const err = await new Promise((resolve) => child.once("error", resolve));
330
+ drop();
331
+ throw refused(err);
332
+ }
281
333
  let stderr = "";
282
334
  child.stderr.on("data", (d) => (stderr = (stderr + d).slice(-8000)));
283
335
  const told = new Promise((resolve) => child.stderr.once("close", resolve));
@@ -312,7 +364,7 @@ async function launch({ width = 1920, height = 1080 } = {}) {
312
364
  fall(dead);
313
365
  });
314
366
  child.on("error", (err) => {
315
- dead = failed(`Chrome could not start: ${err.message}`, { start: true });
367
+ dead = refused(err);
316
368
  for (const waiting of pending.values()) waiting.reject(dead);
317
369
  pending.clear();
318
370
  fall(dead);
@@ -421,4 +473,4 @@ async function openPage(browser, file, { width, height, transparent = false }) {
421
473
  return page;
422
474
  }
423
475
 
424
- module.exports = { findChrome, missingChrome, chromeNamedNothing, tempProblem, launch, evaluate, openPage, whyStopped, chromeTemp, clearOldProfiles, MISSING };
476
+ module.exports = { CHROME, findChrome, chromeReady, chromeProblem, tempProblem, launch, evaluate, openPage, whyStopped, chromeTemp, clearOldProfiles };
@@ -8,82 +8,69 @@ const fs = require("node:fs");
8
8
  const path = require("node:path");
9
9
  const { spawn } = require("node:child_process");
10
10
  const { track } = require("./running");
11
+ const programs = require("./programs");
12
+
13
+ const { exe } = programs;
11
14
 
12
15
  // Where package managers put it, for an agent or app whose PATH is short.
13
16
  const KNOWN = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/snap/bin"];
14
17
 
15
- // The first program by a name on PATH, in a folder `skip` does not pass
16
- // over.
17
- function onPath(name, { skip = () => false } = {}) {
18
- for (const dir of String(process.env.PATH || "").split(path.delimiter)) {
19
- if (!dir || skip(dir)) continue;
20
- const file = path.join(dir, name);
21
- try {
22
- fs.accessSync(file, fs.constants.X_OK);
23
- if (fs.statSync(file).isFile()) return file;
24
- } catch {
25
- // Not here.
26
- }
27
- }
28
- return "";
29
- }
30
-
31
- const exe = (name) => (process.platform === "win32" ? `${name}.exe` : name);
18
+ const INSTALL = "brew install ffmpeg, sudo apt install ffmpeg or winget install ffmpeg";
32
19
 
33
- const bare = (value) => !(value.includes("/") || (process.platform === "win32" && value.includes("\\")));
34
- const isFile = (file) => {
35
- try {
36
- return fs.statSync(file).isFile();
37
- } catch {
38
- return false;
39
- }
20
+ // ffmpeg as a run looks for it and asks it its version (see programs.find()).
21
+ const FFMPEG = {
22
+ name: "ffmpeg",
23
+ variable: "FFMPEG_PATH",
24
+ args: ["-version"],
25
+ answers: (stdout) => /^ffmpeg version\b/m.test(stdout),
26
+ names: [exe("ffmpeg")],
27
+ known: KNOWN,
28
+ none: `easycaptionkit needs ffmpeg, and none was found. Install it (${INSTALL}) or set FFMPEG_PATH to one.`,
29
+ advice: {
30
+ variable: `Set FFMPEG_PATH to an ffmpeg that runs, or unset it and install one (${INSTALL}).`,
31
+ found: `Install ffmpeg (${INSTALL}) or set FFMPEG_PATH to an ffmpeg that runs.`,
32
+ late: "Run easycaptionkit again.",
33
+ },
40
34
  };
41
35
 
42
- // A program named in the environment, as a whole path: a path is taken
43
- // from the folder the run started in, so a program run in another folder
44
- // (the encode runs in its work folder) still finds it, and a bare name is
45
- // looked up on PATH, as a shell would. Empty when it is not there.
46
- function given(value) {
47
- if (bare(value)) return onPath(value) || onPath(exe(value));
48
- const file = path.resolve(value);
49
- return isFile(file) ? file : "";
50
- }
51
-
52
- // What an environment variable that names a program names, when that is
53
- // no program: "FFMPEG_PATH is /opt/ffmpeg, and there is no file there".
54
- // Empty when it is not set or names one.
55
- function namedNothing(variable) {
56
- const value = process.env[variable];
57
- if (!value || given(value)) return "";
58
- const what = bare(value) ? "and no program by that name is on PATH" : fs.existsSync(path.resolve(value)) ? "a folder, not a program" : "and there is no file there";
59
- return `${variable} is ${value}, ${what}`;
60
- }
61
-
62
- function findFfmpeg() {
63
- if (process.env.FFMPEG_PATH) return given(process.env.FFMPEG_PATH);
64
- return onPath(exe("ffmpeg")) || (process.platform === "win32" ? "" : KNOWN.map((d) => path.join(d, "ffmpeg")).find((f) => fs.existsSync(f)) || "");
65
- }
36
+ // ffprobe, which sits next to ffmpeg in every package: the one next to the
37
+ // ffmpeg at `ffmpeg`, when that is not ffmpeg's own file, else as ffmpeg is
38
+ // looked for.
39
+ const FFPROBE = (ffmpeg) => ({
40
+ name: "ffprobe",
41
+ variable: "FFPROBE_PATH",
42
+ args: ["-version"],
43
+ answers: (stdout) => /^ffprobe version\b/m.test(stdout),
44
+ names: [exe("ffprobe")],
45
+ known: KNOWN,
46
+ first: () => (ffmpeg ? [{ file: path.join(path.dirname(ffmpeg), exe("ffprobe")), name: "ffprobe", where: `next to ${ffmpeg}`, next: true }].filter((place) => place.file !== ffmpeg) : []),
47
+ none: `easycaptionkit needs ffprobe, and none was found${ffmpeg ? ` next to ${ffmpeg} or` : ""} on PATH. Install ffmpeg, which comes with ffprobe (${INSTALL}), or set FFPROBE_PATH to one.`,
48
+ advice: {
49
+ variable: `Set FFPROBE_PATH to an ffprobe that runs, or unset it and install ffmpeg, which comes with ffprobe (${INSTALL}).`,
50
+ next: "Set FFPROBE_PATH to an ffprobe that runs.",
51
+ found: `Install ffmpeg, which comes with ffprobe (${INSTALL}), or set FFPROBE_PATH to an ffprobe that runs.`,
52
+ late: "Run easycaptionkit again.",
53
+ },
54
+ });
66
55
 
67
- // ffprobe sits next to ffmpeg in every package.
68
- function findFfprobe(ffmpeg) {
69
- if (process.env.FFPROBE_PATH) return given(process.env.FFPROBE_PATH);
70
- const next = ffmpeg ? path.join(path.dirname(ffmpeg), exe("ffprobe")) : "";
71
- if (next && fs.existsSync(next)) return next;
72
- return onPath(exe("ffprobe")) || (process.platform === "win32" ? "" : KNOWN.map((d) => path.join(d, "ffprobe")).find((f) => fs.existsSync(f)) || "");
56
+ // ffmpeg and ffprobe, each found and asked its version before any work,
57
+ // as { ffmpeg, ffprobe, version, places }: their files, what ffmpeg said,
58
+ // which capabilities() reads, and where each was found, for the words of
59
+ // a later failure. ffprobe is looked for next to the ffmpeg that answered.
60
+ // A program that cannot be started or does not answer as itself stops the
61
+ // run there, as programs.find() says, ffmpeg's first.
62
+ async function ready(limit = programs.LIMIT) {
63
+ const ffmpeg = await programs.find(FFMPEG, limit);
64
+ const ffprobe = await programs.find(FFPROBE(ffmpeg.file), limit);
65
+ return { ffmpeg: ffmpeg.file, ffprobe: ffprobe.file, version: ffmpeg.stdout, places: { ffmpeg: ffmpeg.place, ffprobe: ffprobe.place } };
73
66
  }
74
67
 
75
- const MISSING = "ffmpeg and ffprobe were not found. Install them (brew install ffmpeg, sudo apt install ffmpeg or winget install ffmpeg) or set FFMPEG_PATH and FFPROBE_PATH to them.";
76
-
77
- function tools() {
78
- for (const [variable, name] of [["FFMPEG_PATH", "ffmpeg"], ["FFPROBE_PATH", "ffprobe"]]) {
79
- const nothing = namedNothing(variable);
80
- if (nothing) throw new Error(`${nothing}. Set it to the path of ${name}, or unset it to use the one on PATH.`);
81
- }
82
- const ffmpeg = findFfmpeg();
83
- const ffprobe = findFfprobe(ffmpeg);
84
- if (!ffmpeg) throw new Error(MISSING);
85
- if (!ffprobe) throw new Error(`ffprobe was not found next to ${ffmpeg} or on PATH. It comes with ffmpeg: install it, or set FFPROBE_PATH to it.`);
86
- return { ffmpeg, ffprobe };
68
+ // ffprobe alone, for a run that reads a video and starts no ffmpeg: found
69
+ // and asked as ready() does, next to the ffmpeg the run would take, which
70
+ // is not run, so nothing at ffmpeg's path stops it.
71
+ async function readyProbe(limit = programs.LIMIT) {
72
+ const ffprobe = await programs.find(FFPROBE(programs.locate(FFMPEG)), limit);
73
+ return { ffprobe: ffprobe.file, places: { ffprobe: ffprobe.place } };
87
74
  }
88
75
 
89
76
  // What ffmpeg prints once it has given up, after the lines that say why:
@@ -92,7 +79,9 @@ function tools() {
92
79
  // ffmpeg 7 and newer with the others, and ffmpeg 8 says last that its
93
80
  // encoder never opened and that it wrote nothing, after a filter that
94
81
  // could not start, or that filtering failed, after the scaler said which
95
- // frame it could not convert.
82
+ // frame it could not convert. ffmpeg 6.1 says it could not start a simple
83
+ // filtergraph, the one -vf gives, after the line that names the option
84
+ // its filter refused.
96
85
  const CLOSING = [
97
86
  /^Could not open encoder before EOF/,
98
87
  /^Error while filtering\b/,
@@ -104,6 +93,7 @@ const CLOSING = [
104
93
  /^Error initializing complex filters\.?$/,
105
94
  /^Error opening filters!?$/,
106
95
  /^Exiting normally/,
96
+ /^Error initializing a simple filtergraph$/,
107
97
  /^(?:Invalid argument|Operation not permitted|Cannot allocate memory|Invalid data found when processing input|Unknown error occurred)$/,
108
98
  /^Error opening (?:input|output) files?\b/,
109
99
  /^Error selecting an encoder$/,
@@ -149,20 +139,42 @@ function whyFailed({ code, signal, stderr }, who = "it") {
149
139
  // error, not a hang, unless what `busy` gives, as the size and the time of
150
140
  // the file it writes, changed in that time: ffmpeg prints nothing while it
151
141
  // writes a whole MP4 again to move its index to the front. With `binary`,
152
- // stdout is the bytes it wrote, not text.
142
+ // stdout is the bytes it wrote, not text. The programs are the ones a run
143
+ // checked before any work, so one that cannot be started now says what
144
+ // the system said, or that the folder it was to run in is gone, never the
145
+ // system's code alone: Node throws some of those errors at once and gives
146
+ // the others a moment later.
153
147
  function run(binary, args, { quiet = 120_000, onStdout = null, cwd = undefined, busy = null, binary: bytes = false } = {}) {
154
148
  return new Promise((resolve, reject) => {
149
+ const failed = (err) => {
150
+ if (!/^spawn\b/.test(err.syscall || "")) return err;
151
+ // A start that fails for want of the folder it runs in is no fault of
152
+ // the program's.
153
+ if (cwd && !programs.enterable(cwd)) return new Error(`${path.basename(binary)} could not be started in ${cwd}, a folder that is not there any more or may not be entered`);
154
+ return programs.startFailure(binary, err);
155
+ };
155
156
  let child;
156
157
  try {
157
158
  child = track(spawn(binary, args, { stdio: ["ignore", "pipe", "pipe"], cwd }));
158
159
  } catch (err) {
159
- reject(err);
160
+ reject(failed(err));
160
161
  return;
161
162
  }
162
163
  let stdout = "";
163
164
  const chunks = [];
164
165
  let stderr = "";
165
166
  let timer = null;
167
+ child.on("error", (err) => {
168
+ clearTimeout(timer);
169
+ reject(failed(err));
170
+ });
171
+ child.on("close", (code, signal) => {
172
+ clearTimeout(timer);
173
+ resolve({ code, signal, stdout: bytes ? Buffer.concat(chunks) : stdout, stderr });
174
+ });
175
+ // With no file descriptor left for them Node makes no pipes, and says
176
+ // why a moment later.
177
+ if (!child.stdout || !child.stderr) return;
166
178
  const arm = () => {
167
179
  clearTimeout(timer);
168
180
  const before = busy ? busy() : null;
@@ -184,14 +196,6 @@ function run(binary, args, { quiet = 120_000, onStdout = null, cwd = undefined,
184
196
  arm();
185
197
  stderr = (stderr + d).slice(-40_000);
186
198
  });
187
- child.on("error", (err) => {
188
- clearTimeout(timer);
189
- reject(err.code === "ENOENT" ? new Error(MISSING) : err);
190
- });
191
- child.on("close", (code, signal) => {
192
- clearTimeout(timer);
193
- resolve({ code, signal, stdout: bytes ? Buffer.concat(chunks) : stdout, stderr });
194
- });
195
199
  });
196
200
  }
197
201
 
@@ -564,15 +568,21 @@ const round3 = (value) => Math.round(value * 1000) / 1000;
564
568
 
565
569
  // ─── what this ffmpeg can do ──────────────────────────────────────────────
566
570
 
567
- async function capabilities(bins) {
568
- const [about, filters, encoders] = await Promise.all([run(bins.ffmpeg, ["-hide_banner", "-version"], { quiet: 20_000 }), run(bins.ffmpeg, ["-hide_banner", "-filters"], { quiet: 20_000 }), run(bins.ffmpeg, ["-hide_banner", "-encoders"], { quiet: 20_000 })]);
571
+ // What this ffmpeg can do: its version number, from what it said when
572
+ // ready() asked it, and its filters and encoders, each asked of it and
573
+ // read only once it ended well (see programs.probe()), so an ffmpeg that
574
+ // fails or is stopped when asked is said to, not taken for one with no
575
+ // H.264 encoder, no overlay filter or no libass.
576
+ async function capabilities(bins, version) {
577
+ const asked = (args, what) => programs.probe(FFMPEG, bins.places.ffmpeg, ["-hide_banner", ...args], what);
578
+ const [filters, encoders] = await Promise.all([asked(["-filters"], "its filters"), asked(["-encoders"], "its encoders")]);
569
579
  // 4.4 is the oldest ffmpeg the kit is tried on: Ubuntu 22.04's. A build
570
580
  // from git says no number and is newer.
571
- const v = /ffmpeg version n?(\d+)\.(\d+)/.exec(about.stdout);
581
+ const v = /ffmpeg version n?(\d+)\.(\d+)/.exec(version);
572
582
  const atLeast = (major, minor) => !v || Number(v[1]) > major || (Number(v[1]) === major && Number(v[2]) >= minor);
573
583
  if (!atLeast(4, 4)) throw new Error(`ffmpeg 4.4 or newer is needed, and this one is ${v[1]}.${v[2]}. Install a newer one (brew install ffmpeg, or a static build) or set FFMPEG_PATH to it.`);
574
- const hasFilter = (name) => new RegExp(`^\\s*\\S+\\s+${name}\\s`, "m").test(filters.stdout);
575
- const hasEncoder = (name) => new RegExp(`^\\s*V\\S*\\s+${name}\\s`, "m").test(encoders.stdout);
584
+ const hasFilter = (name) => new RegExp(`^\\s*\\S+\\s+${name}\\s`, "m").test(filters);
585
+ const hasEncoder = (name) => new RegExp(`^\\s*V\\S*\\s+${name}\\s`, "m").test(encoders);
576
586
  let encoder = null;
577
587
  // No B-frames: with them the file starts behind an edit list, and a tool
578
588
  // that reads frames without it is two frames off the captions' times.
@@ -581,11 +591,7 @@ async function capabilities(bins) {
581
591
  else if (hasEncoder("libopenh264")) encoder = { name: "libopenh264", args: ["-c:v", "libopenh264", "-b:v", "12M"] };
582
592
  else if (hasEncoder("h264_mf")) encoder = { name: "h264_mf", args: ["-c:v", "h264_mf", "-b:v", "12M"] };
583
593
  // Whether x264 here writes 10 bits, which an HDR video needs to stay HDR.
584
- let tenBit = false;
585
- if (encoder && encoder.name === "libx264") {
586
- const help = await run(bins.ffmpeg, ["-hide_banner", "-h", "encoder=libx264"], { quiet: 20_000 });
587
- tenBit = /yuv420p10le/.test(help.stdout);
588
- }
594
+ const tenBit = Boolean(encoder && encoder.name === "libx264" && /yuv420p10le/.test(await asked(["-h", "encoder=libx264"], "about its libx264")));
589
595
  const setparams = hasFilter("setparams");
590
596
  return {
591
597
  // -fps_mode came in 5.1, in the place of -vsync.
@@ -594,10 +600,10 @@ async function capabilities(bins) {
594
600
  libass: hasFilter("ass"),
595
601
  setparams,
596
602
  // The names setparams takes for each of its options.
597
- colorNames: setparams ? optionNames((await run(bins.ffmpeg, ["-hide_banner", "-h", "filter=setparams"], { quiet: 20_000 })).stdout) : {},
603
+ colorNames: setparams ? optionNames(await asked(["-h", "filter=setparams"], "about its setparams filter")) : {},
598
604
  encoder,
599
605
  tenBit,
600
- aac: /^\s*A\S*\s+aac\s/m.test(encoders.stdout),
606
+ aac: /^\s*A\S*\s+aac\s/m.test(encoders),
601
607
  };
602
608
  }
603
609
 
@@ -925,7 +931,7 @@ const rateBase = (rate) => {
925
931
  // The captions show on the video's own frames. Where it has too few for
926
932
  // them, as fillFor says, `fill` gives it frames at a steady rate first.
927
933
  async function burn({ bins, caps, video: v, output, overlay = null, ass = null, assMatrix = "", assFull = false, fontsDir = "", fill = null, onProgress = null, quiet = 180_000 }) {
928
- if (!caps.encoder) throw new Error("This ffmpeg has no H.264 encoder (libx264, VideoToolbox or OpenH264), so it cannot write the captioned MP4");
934
+ if (!caps.encoder) throw new Error("This ffmpeg has no H.264 encoder (libx264, VideoToolbox, OpenH264 or Media Foundation), so it cannot write the captioned MP4");
929
935
  const color = colorOf(v, caps.colorNames);
930
936
  // An HDR video stays HDR where x264 writes 10 bits.
931
937
  const deep = v.hdr && caps.tenBit;
@@ -1029,4 +1035,4 @@ async function frameAt(bins, file, t, output, { width = 0, retag = "", untag = "
1029
1035
  return output;
1030
1036
  }
1031
1037
 
1032
- module.exports = { tools, findFfmpeg, findFfprobe, given, namedNothing, probe, readVideo, packetsOf, frameTimes, rateOf, steadyRate, fillFor, capabilities, burn, frameAt, run, whyFailed, optionNames, onPath, MISSING, colorOf, prepareChain, filterPath, libassFilter, hdrLevel, fineBase, matrixLine, as601, libassFollows, assColors, pixelFormat };
1038
+ module.exports = { FFMPEG, FFPROBE, ready, readyProbe, onPath: programs.onPath, probe, readVideo, packetsOf, frameTimes, rateOf, steadyRate, fillFor, capabilities, burn, frameAt, run, whyFailed, optionNames, colorOf, prepareChain, filterPath, libassFilter, hdrLevel, fineBase, matrixLine, as601, libassFollows, assColors, pixelFormat };
@@ -568,4 +568,4 @@ function chooseLook(options = {}) {
568
568
  };
569
569
  }
570
570
 
571
- module.exports = { chooseLook, readBrand, readFontsCss, parseColor, contrast, luminance, highlightMode, brandHighlight, variableFace, machineFont, fallbackFont, sameName, DEFAULT_HIGHLIGHT };
571
+ module.exports = { chooseLook, readBrand, readFontsCss, parseColor, contrast, luminance, highlightMode, brandHighlight, variableFace, machineFont, fallbackFont, fcName, sameName, DEFAULT_HIGHLIGHT };