{"record":{"id":"5913618c4faa77e1","repo":"Hmbown/CodeWhale","slug":"ffmpeg-is-required-on-the-host-to-mux-harmony-snapshot","errorCode":null,"errorMessage":"ffmpeg is required on the host to mux harmony snapshot-series recordings","messagePattern":"ffmpeg is required on the host to mux harmony snapshot-series recordings","errorType":"exception","errorClass":"ExecError","httpStatus":null,"severity":"error","filePath":"crates/tui/plugins/computer-use/src/backends/harmonyos.mjs","lineNumber":274,"sourceCode":"    set_value: async ({ target, value }) => {\n      const b = await centerOf(target);\n      await uiInput([\"click\", String(b.cx), String(b.cy)]);\n      await new Promise((r) => setTimeout(r, 300));\n      await uiInput([\"inputText\", String(b.cx), String(b.cy), escDeviceText(String(value))]);\n      return { action_sent: true, strategy: \"uitest-element\" };\n    },\n    select_text: async () => { throw new ExecError(\"select_text is not exposed by uitest dumpLayout/uiInput on the harmony backend\"); },\n    perform_action: async ({ target, action }) => {\n      const b = await centerOf(target);\n      if (action === \"longClick\") return uiInput([\"longClick\", String(b.cx), String(b.cy)]);\n      return uiInput([\"click\", String(b.cx), String(b.cy)]);\n    },\n    read_clipboard: async () => { throw new ExecError(\"clipboard read is not exposed by hdc on current HarmonyOS builds\"); },\n    write_clipboard: async () => { throw new ExecError(\"clipboard write is not exposed by hdc on current HarmonyOS builds\"); },\n    cursor_position: async () => { throw new ExecError(\"cursor position does not exist on touch devices\"); },\n    recordingStart: async ({ intervalMs = 400 } = {}) => {\n      if (recording) throw new ExecError(`recording ${recording.id} already running`);\n      if (!(await have(\"ffmpeg\"))) throw new ExecError(\"ffmpeg is required on the host to mux harmony snapshot-series recordings\");\n      const id = crypto.randomBytes(4).toString(\"hex\");\n      const dir = fs.mkdtempSync(path.join(os.tmpdir(), `cu-rec-${id}-`));\n      const startedAt = new Date().toISOString();\n      const rec = recording = { id, dir, startedAt, intervalMs, seq: 0, controller: new AbortController() };\n      const tick = () => {\n        if (rec.stopped || rec.pending) return rec.pending;\n        rec.pending = (async () => {\n          const opts = { timeoutMs: 15_000, signal: rec.controller.signal };\n          const remote = `${DEVICE_TMP}-rec-${id}-${String(rec.seq).padStart(5, \"0\")}.jpeg`;\n          try {\n            await deviceOut([\"snapshot_display\", \"-f\", remote], opts);\n            await exec.pullFile(remote, path.join(dir, `f${String(rec.seq).padStart(5, \"0\")}.jpeg`), opts);\n            rec.seq++;\n          } finally { await shell([\"rm\", \"-f\", remote], opts).catch(() => {}); }\n        })().catch(() => {}).finally(() => { rec.pending = null; });\n        return rec.pending;\n      };\n      const signal = currentSignal();","sourceCodeStart":256,"sourceCodeEnd":292,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/plugins/computer-use/src/backends/harmonyos.mjs#L256-L292","documentation":"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.","triggerScenarios":"Calling `recordingStart()` on the HarmonyOS backend when ffmpeg is not installed or not on PATH (backends/harmonyos.mjs:274).","commonSituations":"Minimal CI containers or fresh machines without ffmpeg; ffmpeg installed for the user but not the process env (missing PATH); Docker images lacking ffmpeg.","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."],"exampleFix":"// before\nconst rec = await backend.recordingStart({ intervalMs: 400 }); // fails without ffmpeg\n// after\nconst { execSync } = await import(\"node:child_process\");\ntry { execSync(\"ffmpeg -version\", { stdio: \"ignore\" }); } catch { throw new Error(\"install ffmpeg first\"); }\nconst rec = await backend.recordingStart({ intervalMs: 400 });","handlingStrategy":"validation","validationCode":"const { execFile } = await import(\"node:child_process\");\nawait new Promise((res, rej) => execFile(\"ffmpeg\", [\"-version\"], (e) => e ? rej(new Error(\"ffmpeg missing\")) : res()));","typeGuard":"null","tryCatchPattern":"try { await backend.recordingStart({ intervalMs: 400 }); }\ncatch (e) {\n  if (String(e.message).includes(\"ffmpeg\")) throw new Error(\"Install ffmpeg: https://ffmpeg.org/download.html\");\n  throw e;\n}","preventionTips":["Check ffmpeg in environment setup/startup checks","Document ffmpeg as a host prerequisite for recording","Bake ffmpeg into CI/container images that record"],"tags":["missing-dependency","ffmpeg","recording"],"backgroundTag":"missing-dependency","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}