Hmbown/CodeWhale · error · ExecError
ffmpeg is required on the host to mux harmony…
Error message
ffmpeg is required on the host to mux harmony snapshot-series recordings
What it means
HarmonyOS has no CLI screen recorder, so the backend records by taking snapshot frames and muxing them into an mp4 with ffmpeg on the host machine at stop time. If `ffmpeg` is not found on the host PATH (checked via `have("ffmpeg")`), starting a recording would produce frames that can never be assembled, so it fails at start.
Solutions
- Install ffmpeg on the host machine (e.g. `apt install ffmpeg`, `brew install ffmpeg`).
- Ensure ffmpeg is on the PATH of the process running the backend.
- If containerized, rebuild the image to include ffmpeg.
Example fix
// before
const rec = await backend.recordingStart({ intervalMs: 400 }); // fails without ffmpeg
// after
const { execSync } = await import("node:child_process");
try { execSync("ffmpeg -version", { stdio: "ignore" }); } catch { throw new Error("install ffmpeg first"); }
const rec = await backend.recordingStart({ intervalMs: 400 }); Defensive patterns
Strategy: validation
Validate before calling
const { execFile } = await import("node:child_process");
await new Promise((res, rej) => execFile("ffmpeg", ["-version"], (e) => e ? rej(new Error("ffmpeg missing")) : res())); Type guard
null
Try / catch
try { await backend.recordingStart({ intervalMs: 400 }); }
catch (e) {
if (String(e.message).includes("ffmpeg")) throw new Error("Install ffmpeg: https://ffmpeg.org/download.html");
throw e;
} Prevention
- Check ffmpeg in environment setup/startup checks
- Document ffmpeg as a host prerequisite for recording
- Bake ffmpeg into CI/container images that record
When it happens
Trigger: Calling `recordingStart()` on the HarmonyOS backend when ffmpeg is not installed or not on PATH (backends/harmonyos.mjs:274).
Common situations: Minimal CI containers or fresh machines without ffmpeg; ffmpeg installed for the user but not the process env (missing PATH); Docker images lacking ffmpeg.
Understand the failure class
Background: "not installed", "pip install", "required for": how missing-dependency errors surface across open-source libraries — this error's family across 34 libraries.
Related errors
- ffmpeg mux failed
- AT-SPI walk failed — is python3-pyatspi installed and the…
- Cannot continue agent
- choose one available display for recording
- durationSec must be positive
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/5913618c4faa77e1.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/harmonyos.mjs:274
set_value: async ({ target, value }) => {
const b = await centerOf(target);
await uiInput(["click", String(b.cx), String(b.cy)]);
await new Promise((r) => setTimeout(r, 300));
await uiInput(["inputText", String(b.cx), String(b.cy), escDeviceText(String(value))]);
return { action_sent: true, strategy: "uitest-element" };
},
select_text: async () => { throw new ExecError("select_text is not exposed by uitest dumpLayout/uiInput on the harmony backend"); },
perform_action: async ({ target, action }) => {
const b = await centerOf(target);
if (action === "longClick") return uiInput(["longClick", String(b.cx), String(b.cy)]);
return uiInput(["click", String(b.cx), String(b.cy)]);
},
read_clipboard: async () => { throw new ExecError("clipboard read is not exposed by hdc on current HarmonyOS builds"); },
write_clipboard: async () => { throw new ExecError("clipboard write is not exposed by hdc on current HarmonyOS builds"); },
cursor_position: async () => { throw new ExecError("cursor position does not exist on touch devices"); },
recordingStart: async ({ intervalMs = 400 } = {}) => {
if (recording) throw new ExecError(`recording ${recording.id} already running`);
if (!(await have("ffmpeg"))) throw new ExecError("ffmpeg is required on the host to mux harmony snapshot-series recordings");
const id = crypto.randomBytes(4).toString("hex");
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `cu-rec-${id}-`));
const startedAt = new Date().toISOString();
const rec = recording = { id, dir, startedAt, intervalMs, seq: 0, controller: new AbortController() };
const tick = () => {
if (rec.stopped || rec.pending) return rec.pending;
rec.pending = (async () => {
const opts = { timeoutMs: 15_000, signal: rec.controller.signal };
const remote = `${DEVICE_TMP}-rec-${id}-${String(rec.seq).padStart(5, "0")}.jpeg`;
try {
await deviceOut(["snapshot_display", "-f", remote], opts);
await exec.pullFile(remote, path.join(dir, `f${String(rec.seq).padStart(5, "0")}.jpeg`), opts);
rec.seq++;
} finally { await shell(["rm", "-f", remote], opts).catch(() => {}); }
})().catch(() => {}).finally(() => { rec.pending = null; });
return rec.pending;
};
const signal = currentSignal();View on GitHub (pinned to 73e0f67d83)