mp4-to-mcap
v0.2.0
Published
Convert an MP4 video file to an MCAP file by repackaging its H.264/H.265 bitstream into foxglove.CompressedVideo messages, re-encoding only when needed to strip B-frames that Foxglove's video panel can't play back.
Downloads
40
Readme
mp4-to-mcap
Convert an MP4 video file to an MCAP file. Each video frame is
repackaged into a foxglove.CompressedVideo message, so the result plays back in
Foxglove Studio or any other Foxglove-schema-aware MCAP viewer.
npx mp4-to-mcap input.mp4Converting |████████████████████████████████████████| 100% | 300/300 frames | ETA: 0s
Wrote 300 h264 frames (1920x1080, 10.00s) to input.mcap (42.31 MB)Why "no transcoding" (usually)
The video bitstream inside your MP4 is already H.264 or H.265 encoded. In the common
case, this tool doesn't decode and re-encode it — it demuxes the MP4 container in pure
JS (no ffmpeg, no protoc), converts each sample from MP4's length-prefixed NAL format
to the Annex-B start-code
format foxglove.CompressedVideo expects, and writes one message per frame. The encoded
pixels never change.
On every keyframe, the track's SPS/PPS (and VPS, for H.265) parameter sets are prepended so the bitstream is self-decodable from that point — the same thing a camera-facing encoder does natively.
The one exception is B-frames — see below. When the source uses them, this tool falls
back to a real transcode via a bundled ffmpeg binary before repackaging.
Install
npm install -g mp4-to-mcapOr run it once with npx mp4-to-mcap ... without installing.
Usage
mp4-to-mcap <input.mp4> [options]
Options:
-o, --output <path> output .mcap file path (default: <input> with .mcap extension)
-t, --topic <topic> MCAP topic for the video channel (default: "/camera/h264")
--frame-id <id> frame_id recorded in each CompressedVideo message (default: "")
-q, --quiet suppress the progress bar
-V, --version output the version number
-h, --help display helpAs a library
import { convertMp4ToMcap } from "mp4-to-mcap";
const result = await convertMp4ToMcap("input.mp4", "output.mcap", {
topic: "/camera/h264",
onProgress: (framesWritten, totalFrames) => {
console.log(`${framesWritten}/${totalFrames}`);
},
});Limitations
- Codecs: only H.264 (
avc1/avc3) and H.265 (hvc1/hev1) video tracks are supported — the whole point of this tool is repackaging an existing bitstream, so other codecs (VP9, AV1, ...) aren't handled. - B-frames: Foxglove's video panel has no reorder buffer / lookahead, so it can't play
back a stream where presentation order differs from decode order (a documented
limitation of
foxglove.CompressedVideoitself). When this tool detects B-frames (sample.cts !== sample.dts), it prints a warning and automatically re-encodes the video without them (ffmpeg -bf 0) using a bundled binary fromffmpeg-staticbefore repackaging. Two things follow from that:- It's a real transcode, not a lossless remux: the frame is decoded and re-compressed, which costs a generation of quantization loss on top of whatever the source already lost, plus B-frames' own compression-efficiency advantage (they're typically the cheapest frame type to encode), so expect a modestly larger and/or slightly lower-fidelity output than the source. Frame count, frame rate, resolution, and colorspace are unaffected.
ffmpeg-staticbundles a real per-platformffmpegbinary, so installing this package downloads one (tens of MB) and is noticeably heavier than a pure-JS install. That binary (and thelibx264/libx265encoders it links) is GPL-licensed; this package's own MIT license covers only its own code.
- Audio and other tracks in the source MP4 are ignored; only the first video track is converted.
- The whole input file is read into memory.
The foxglove.CompressedVideo convention
This isn't a bespoke format — it's the same schema and per-frame-message structure that
coScene's own MCAP video tooling (encoders that produce .mini.mcap files, OSS video
extraction pipelines) already uses. If you're working in that ecosystem, output from this
tool is drop-in compatible.
License
MIT
