Hmbown/CodeWhale · error · ExecError

ffmpeg mux failed

Error message

ffmpeg mux failed: ${(r.stderr || "").slice(0, 300)}

What it means

After frames are captured, `recordingStop` invokes ffmpeg to mux the snapshot-series JPEGs into an mp4 (libx264, yuv420p, up to 180s). If ffmpeg exits non-zero, the backend throws with the first 300 bytes of ffmpeg's stderr and passes the raw result along, so the failure cause comes from ffmpeg itself (bad frames, unsupported codec build, disk full, etc.).

Solutions

  1. Read the stderr excerpt in the error message to identify the ffmpeg failure and address it directly.
  2. Verify the installed ffmpeg supports libx264 (`ffmpeg -encoders | grep libx264`) and reinstall a full build if not.
  3. Check disk space and write permissions on the recordings directory (CODEWHALE_CU_RECORDINGS_DIR or ~/.codewhale-cu/recordings).
  4. Retry the recording with a smaller intervalMs and confirm frames are being captured (recordingStatus shows frames count).

Example fix

// before
// error surfaced: ffmpeg mux failed: Unknown encoder 'libx264'
// after
// install a full ffmpeg build, e.g.: apt-get install -y ffmpeg (includes libx264)
const result = await backend.recordingStop({ id });
Defensive patterns

Strategy: try-catch

Validate before calling

import { execFileSync } from "node:child_process";
const enc = execFileSync("ffmpeg", ["-hide_banner", "-encoders"], { encoding: "utf8" });
if (!enc.includes("libx264")) throw new Error("ffmpeg lacks libx264");

Type guard

null

Try / catch

try { await backend.recordingStop({ id }); }
catch (e) {
  if (String(e.message).startsWith("ffmpeg mux failed")) {
    console.error("ffmpeg stderr:", String(e.message).slice("ffmpeg mux failed: ".length));
    // inspect disk space, codec support, frame files, then retry recording
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling `recordingStop({ id })` where the captured frame sequence cannot be encoded: zero/corrupt frames, an ffmpeg build without libx264, disk full or unwritable output dir, or ffmpeg timing out (backends/harmonyos.mjs:315).

Common situations: Recording a session where the screen never produced a frame; minimal ffmpeg builds lacking libx264; recordings directory on a read-only mount; intervalMs so high that fps rounds to an invalid value.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/3d2783e8891da36c. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/backends/harmonyos.mjs:315

        await tick();
        throwIfAborted(signal);
        if (rec.stopped) throw new ExecError("Harmony recording closed during startup");
        rec.timer = setInterval(tick, Math.max(150, intervalMs));
      } catch (err) { await stopFrames(); throw err; }
      finally { signal?.removeEventListener("abort", abort); }
      return { id, mode: "snapshot-series", intervalMs, startedAt, note: "no HarmonyOS CLI screen recorder; frames are muxed into mp4 on stop" };
    },
    recordingStop: async ({ id }) => {
      if (!recording || recording.id !== id) throw new ExecError(`unknown recording "${id}"`);
      const { dir, seq, startedAt, intervalMs } = await stopFrames();
      const dirOut = process.env.CODEWHALE_CU_RECORDINGS_DIR || path.join(os.homedir(), ".codewhale-cu", "recordings");
      fs.mkdirSync(dirOut, { recursive: true });
      const out = path.join(dirOut, `rec-${id}.mp4`);
      const fps = Math.max(1, Math.min(15, Math.round(1000 / Math.max(150, intervalMs))));
      const r = await run("ffmpeg", ["-y", "-loglevel", "error", "-framerate", String(fps), "-i", path.join(dir, "f%05d.jpeg"), "-c:v", "libx264", "-pix_fmt", "yuv420p", out], { timeoutMs: 180_000 });
      const bytes = fs.existsSync(out) ? fs.statSync(out).size : 0;
      try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
      if (r.code !== 0) throw new ExecError(`ffmpeg mux failed: ${(r.stderr || "").slice(0, 300)}`, r);
      return { id, mode: "snapshot-series", frames: seq, fps, file: out, bytes, startedAt, stoppedAt: new Date().toISOString() };
    },
    recordingStatus: ({ id }) => recording && recording.id === id
      ? { id, running: true, mode: "snapshot-series", frames: recording.seq, startedAt: recording.startedAt }
      : { id, running: false },
    recordingList: async () => {
      const dir = process.env.CODEWHALE_CU_RECORDINGS_DIR || path.join(os.homedir(), ".codewhale-cu", "recordings");
      const out = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => /\.(mp4|mov|jpeg|png)$/i.test(f)).map((f) => {
        const st = fs.statSync(path.join(dir, f));
        return { file: path.join(dir, f), bytes: st.size, modifiedAt: st.mtime.toISOString() };
      }).sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt)).slice(0, 50) : [];
      return { dir, recordings: out, running: recording ? [recording.id] : [] };
    },
  };
}

export default { create };

View on GitHub (pinned to 73e0f67d83)