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.
- package/README.md +14 -5
- package/package.json +1 -1
- package/skills/easycaptionkit/SKILL.md +8 -3
- package/skills/easycaptionkit/scripts/easycaptionkit.js +12 -5
- package/skills/easycaptionkit/scripts/lib/boxes.js +139 -0
- package/skills/easycaptionkit/scripts/lib/caption.js +71 -40
- package/skills/easycaptionkit/scripts/lib/chrome.js +94 -42
- package/skills/easycaptionkit/scripts/lib/ffmpeg.js +97 -91
- package/skills/easycaptionkit/scripts/lib/look.js +1 -1
- package/skills/easycaptionkit/scripts/lib/programs.js +478 -0
- package/skills/easycaptionkit/scripts/lib/render.js +26 -7
- package/skills/easycaptionkit/scripts/lib/report.js +85 -21
- package/skills/easycaptionkit/scripts/lib/subtitles.js +47 -18
- package/skills/easycaptionkit/scripts/lib/version.js +1 -1
- package/skills/easycaptionkit/scripts/lib/words.js +8 -1
|
@@ -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
|
|
15
|
+
const programs = require("./programs");
|
|
16
16
|
const { track, trackFolder, untrackFolder, kill, sleep } = require("./running");
|
|
17
17
|
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
568
|
-
|
|
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(
|
|
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
|
|
575
|
-
const hasEncoder = (name) => new RegExp(`^\\s*V\\S*\\s+${name}\\s`, "m").test(encoders
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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 = {
|
|
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 };
|