Extracting video frames in Rust, in-process — no CLI, no hand-written decode loop A Rust developer introduced ez-ffmpeg 0.15's FrameExtractor, which extracts video frames in-process without shelling out to ffmpeg or writing a manual decode loop. The tool provides a one-call API that returns tightly packed RGB bytes, handles color conversion correctly, and supports uniform sampling for VLM pipelines. Here is a task that comes up constantly. You are doing inference in Rust — candle, burn, ort — and you want to feed a video to a model. Step one is always the same: get the frames. And in Rust today, getting frames means one of two things. Either you shell out to Command::new "ffmpeg" , write PNGs to disk or parse -f rawvideo off a pipe, and read them back; or you reach for the low-level bindings and hand-write the send packet / receive frame decode loop — forty lines before the first pixel, and you own the EAGAIN handling and the YUV→RGB conversion yourself. The second path is hard enough that an entire cottage industry of "frame grabber" gists grew up around it: search "rust extract frame from video" and the authoritative answer is frequently someone's gist. A decade on, something this basic still has no comfortable standard answer. This post is the third path: ez-ffmpeg 0.15's FrameExtractor pulls frames in the same process , in one call, and hands you tightly packed RGB bytes ready for an ndarray view or a tensor. You will get runnable code, a sampling-strategy cheat sheet, and two things that actually earn their keep: why your thumbnails may have been subtly wrong on color this whole time, and how fast this path really is — with numbers you can reproduce. Show the ugly first. The shell-out path is cheapest when the output is a file, but the instant frames have to come back into memory they cross a process boundary — PNGs to disk and read back, or a raw pipe you slice into frames yourself. The hand-written decode loop keeps everything in-process, but the boilerplate runs like this: open the demuxer, find the video stream, build the decoder, pair up send packet / receive frame , service EAGAIN , then run each frame through swscale from YUV to RGB — every step its own opportunity to be wrong. And that last step hides a trap: many hand-rolled paths and some libraries apply BT.601 conversion coefficients to HD video too, which skews saturated reds and greens visibly — with no error to tell you. More on that trap below. Neither path is wrong. When the output is a file and the recipe exists, the CLI is still the right answer; when you need packet-level control, the raw bindings are irreplaceable. They just never made the specific thing — get a batch of frames, in-process, in one line, with the color right — comfortable. One dependency line it still links libav through ffmpeg-next, so FFmpeg 7.1–8.x has to be installed : dependencies ez-ffmpeg = "0.15" needs FFmpeg 7.1-8.x installed links libav Pull 32 frames to feed a VLM. Note UniformN — it takes N frames spread uniformly across the whole clip by presentation time, the primitive fixed-budget CLIP/VLM pipelines actually want: use ez ffmpeg::frame export::{FrameExtractor, Sampling}; // 32 frames evenly spread over the full duration, scaled to 224 wide // height derived from the aspect ratio . let frames = FrameExtractor::new "input.mp4" .sampling Sampling::UniformN 32 .width 224 .collect frames ?; // Vec