<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yeauty YE</title>
    <description>The latest articles on DEV Community by Yeauty YE (@yeauty).</description>
    <link>https://dev.to/yeauty</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2932171%2Fe3eedc22-29d9-4208-a808-b30b6d3116b7.png</url>
      <title>DEV Community: Yeauty YE</title>
      <link>https://dev.to/yeauty</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yeauty"/>
    <language>en</language>
    <item>
      <title>The state of media processing in Rust (2026): what each crate actually covers</title>
      <dc:creator>Yeauty YE</dc:creator>
      <pubDate>Tue, 28 Jul 2026 19:54:48 +0000</pubDate>
      <link>https://dev.to/yeauty/the-state-of-media-processing-in-rust-2026-what-each-crate-actually-covers-2k2c</link>
      <guid>https://dev.to/yeauty/the-state-of-media-processing-in-rust-2026-what-each-crate-actually-covers-2k2c</guid>
      <description>&lt;p&gt;Every few months someone asks how to decode video or pull audio samples in Rust, and the thread converges on the same advice: just run the ffmpeg binary. This post is a map of why — six paths through the ecosystem as of July 2026, what each one actually covers, how it is maintained, and where each one honestly wins. All numbers are monthly downloads or GitHub stars as verified in July 2026; where I could not verify a number, the table shows a dash instead of a guess.&lt;/p&gt;

&lt;p&gt;A note on method: all six paths get the same treatment — where it wins, the honest limitation, a one-line verdict — and the limitation column does not soften for whichever path is newest. One disambiguation up front: the Rust crate &lt;code&gt;ez-ffmpeg&lt;/code&gt; discussed below is unrelated to the JavaScript "Ez FFmpeg" that once made the HN front page; same name, different thing.&lt;br&gt;
One more ground rule: there are no benchmarks in this piece — runnable benchmarks later beat adjectives now.&lt;/p&gt;
&lt;h2&gt;
  
  
  The landscape at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Execution model&lt;/th&gt;
&lt;th&gt;Coverage highlights (verified)&lt;/th&gt;
&lt;th&gt;Key limitations (verified)&lt;/th&gt;
&lt;th&gt;Data point (July 2026)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;std::process::Command&lt;/code&gt; + ffmpeg CLI&lt;/td&gt;
&lt;td&gt;subprocess&lt;/td&gt;
&lt;td&gt;everything the CLI can do; every recipe on the internet applies as-is&lt;/td&gt;
&lt;td&gt;ship the binary; data re-enters your process via stdout/temp files&lt;/td&gt;
&lt;td&gt;FFmpeg 8.0 is out; its Whisper support hit the HN front page (1033 points)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ffmpeg-sidecar&lt;/td&gt;
&lt;td&gt;subprocess&lt;/td&gt;
&lt;td&gt;iterate any video "as if it were an array of raw RGB frames"; no libav linking&lt;/td&gt;
&lt;td&gt;still a child process; an ffmpeg executable must be present at runtime&lt;/td&gt;
&lt;td&gt;~131K downloads/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ffmpeg-next&lt;/td&gt;
&lt;td&gt;in-process (low-level bindings)&lt;/td&gt;
&lt;td&gt;the full libav control plane; the ecosystem's foundation&lt;/td&gt;
&lt;td&gt;"maintenance-only mode"; hand-written decode loops (~40 lines before the first frame); third fork in a lineage&lt;/td&gt;
&lt;td&gt;1.28M downloads/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rsmpeg&lt;/td&gt;
&lt;td&gt;in-process (safe-wrapper style)&lt;/td&gt;
&lt;td&gt;tracks FFmpeg 6/7/8 (0.18.0 added 8.0)&lt;/td&gt;
&lt;td&gt;slow cadence: roughly 11 months without major movement&lt;/td&gt;
&lt;td&gt;877 stars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;video-rs&lt;/td&gt;
&lt;td&gt;in-process (high-level)&lt;/td&gt;
&lt;td&gt;RGB ndarrays in ten lines; README encodes rainbow.mp4 from ndarray frames; &lt;code&gt;seek_to_frame&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;no audio API; self-described "still a work-in-progress… some parts not flushed out", "Use with caution"; successor rave "not ready for use yet"&lt;/td&gt;
&lt;td&gt;~32K downloads/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;symphonia + rubato&lt;/td&gt;
&lt;td&gt;in-process (pure Rust)&lt;/td&gt;
&lt;td&gt;demux + decode audio, resample to 16 kHz; symphonium does it in one call; mutter feeds whisper-rs in five lines&lt;/td&gt;
&lt;td&gt;HE-AAC and Opus listed unfinished; no AC-3; video out of scope&lt;/td&gt;
&lt;td&gt;symphonia ~1.07M downloads/month, 0.6.0 (May 2026); whisper-rs ~170K/month (maintained on Codeberg)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ac-ffmpeg&lt;/td&gt;
&lt;td&gt;in-process&lt;/td&gt;
&lt;td&gt;touches all three domains (video, audio, encoding)&lt;/td&gt;
&lt;td&gt;stays at the packet-loop level; no release in 14 months&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ez-ffmpeg&lt;/td&gt;
&lt;td&gt;in-process (runtime)&lt;/td&gt;
&lt;td&gt;API and internal scheduling modeled 1:1 on the FFmpeg CLI — command-line knowledge transfers as-is; 0.15 runs pasted ffmpeg commands in-process (&lt;code&gt;cli&lt;/code&gt; facade, verified subset); FrameExtractor / SampleExtractor / VideoWriter as first-class APIs; color-tag-correct YUV→RGB by default&lt;/td&gt;
&lt;td&gt;still links libav — the build pain stays; new APIs marked experimental; early-stage&lt;/td&gt;
&lt;td&gt;~340 stars&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A dash means I could not verify it, and nothing in this article depends on it.&lt;/p&gt;

&lt;p&gt;One boundary note: gstreamer-rs is a real option too — ~564K downloads a month and actively maintained — but it is neither FFmpeg-based nor a few-lines-of-code style of API, so it sits outside this FFmpeg-centered map.&lt;/p&gt;
&lt;h2&gt;
  
  
  Path 1: &lt;code&gt;std::process::Command&lt;/code&gt; + the ffmpeg binary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Where it wins.&lt;/strong&gt; Thirty years of FFmpeg knowledge exists in command-line form, and all of it applies unchanged. The CLI cookbook culture is alive and huge — "FFmpeg by Example" alone scored 920 points on HN. No FFI, no linking, no version matrix. On rustcc, a Chinese Rust forum, a thread on these bindings landed on shelling out as "the most reliable option, with far fewer pitfalls" (translation mine) — not a joke, but the rational endpoint of a lot of scar tissue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest limitation.&lt;/strong&gt; You ship an ffmpeg binary with your app, and the moment data has to come back into your process — frames, PCM samples — you are parsing stdout or juggling temp files. jaredly's issue from 2015 describes the pattern exactly: "rendering to images then using ffmpeg on the cli". A decade later, plenty of people still work that way. Not because it is good — because the alternatives were never paved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict: when the output is a file and the recipe exists, still the right answer. When data must land in memory, the tax meter starts.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Path 2: ffmpeg-sidecar — the subprocess, made livable
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Where it wins.&lt;/strong&gt; ~131K downloads a month, and a self-description that gets straight to the point: treat any video "as if it were an array of raw RGB frames". It turns shelling out into a structured API — you write an iterator, not a stdout parser. And because it talks to ffmpeg over stdin/stdout, it never links libav: the entire build/link/platform circus does not apply to you. If you want frames without touching FFI, this is the most comfortable path in the list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest limitation.&lt;/strong&gt; It is still a child process. An ffmpeg executable has to exist wherever you deploy, and every frame crosses a process boundary. In-process filter graphs and fine-grained control are outside this model. Whether the boundary matters for your workload is a measurement, not an opinion — this survey quotes no numbers for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict: if a child process is acceptable, the most comfortable way to get frames.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Path 3: the raw bindings — ffmpeg-next and rsmpeg
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Where it wins.&lt;/strong&gt; When you need the full libav control plane — custom IO, obscure containers, packet-level surgery — nothing else covers that surface. &lt;code&gt;ffmpeg-next&lt;/code&gt; sits at 1.28M downloads a month; ez-ffmpeg itself links libav through it. That number is the measure of how much of the ecosystem stands on this layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest limitation.&lt;/strong&gt; Start with maintenance: ffmpeg-next is frozen by policy, not abandoned — the maintainer's own words are "maintenance-only mode for the most part… Any PR to improve existing API is unlikely to be merged" — and the crate sits in a fork lineage already three generations long: ffmpeg → ffmpeg-next → ffmpeg-the-third. Then the daily cost: decoding a frame means hand-writing the send_packet/receive_frame ceremony, roughly forty lines before you see pixels, and even "transcode audio and video together" can send you searching — one developer's verdict after going through the examples: "Examples… do not include transcoding of audio+video". When kornel &lt;a href="https://users.rust-lang.org/t/best-mediaplayer-for-rust/97573/4" rel="noopener noreferrer"&gt;wrote&lt;/a&gt; "avoid ffmpeg… all incomplete and poorly maintained", this category's overall state is what he was describing.&lt;/p&gt;

&lt;p&gt;One counterintuitive fact about this layer: even a wrapper sold on safety has shipped public APIs annotated "might trigger undefined behavior" (zmwangx/rust-ffmpeg#225). A safe skin over an FFI skeleton is an engineering promise, not a proof.&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;rsmpeg&lt;/code&gt; specifically: 877 stars, hosted under the larksuite organization, supporting FFmpeg 6, 7 and 8 (0.18.0 added 8.0). The cadence is slow — roughly 11 months without major movement — but calling it dormant overstates it. If you need a low-level wrapper that tracks current FFmpeg majors, it is the fresher one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict: as a foundation, indispensable; as a daily API, you pay full price.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Path 4: video-rs — high-level video, self-described work in progress
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Where it wins.&lt;/strong&gt; For years this has been the one serious high-level option (~32K downloads/month). PyAV-style decode/encode has been there since 2023: RGB ndarrays within ten lines, a README whose example encodes rainbow.mp4 from ndarray frames, and a &lt;code&gt;seek_to_frame&lt;/code&gt; (frame accuracy undeclared). If you want a PyAV-like video experience in Rust today, this is the most convenient existing answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest limitation.&lt;/strong&gt; The limitations are written in its own README: "still a work-in-progress… some parts not flushed out", "Use with caution". There is no audio API. The official successor, rave, aims to move off FFmpeg entirely — and is "not ready for use yet".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict: strong on video, and video only; no audio plus self-declared WIP keeps it a specialist.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Path 5: symphonia + rubato — pure Rust until the codec wall
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Where it wins.&lt;/strong&gt; Pure Rust can pull the audio track out of an MP4/MKV and resample it to 16 kHz (symphonia + rubato; symphonium does both in one call; mutter feeds whisper-rs in five lines). The decode-and-resample chain is Rust top to bottom — of the six paths here, the only one that involves no C artifact at any point: no libav to link, no ffmpeg binary to ship. If a clean dependency tree is the goal, this path owns it. One correction to a rumor while I am here: whisper-rs is archived on GitHub but actively maintained on &lt;a href="https://codeberg.org/tazz4843/whisper-rs" rel="noopener noreferrer"&gt;Codeberg&lt;/a&gt;, at ~170K downloads a month — do not file it under dead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest limitation.&lt;/strong&gt; "Any video, any codec, one line" hits a codec wall: symphonia still lists HE-AAC and Opus as unfinished and has no AC-3 at all — which is exactly where an FFmpeg backend remains irreplaceable. And video is out of scope by design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict: inside its codec table, the cleanest dependency story on this list; outside it, you are back to FFmpeg.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Path 6: ez-ffmpeg — the in-process runtime
&lt;/h2&gt;

&lt;p&gt;The project's stated aim: "the actively-developed high-level in-process FFmpeg runtime for Rust."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The core is not any single feature — it is the ergonomics of moving an FFmpeg command into Rust unchanged.&lt;/strong&gt; One builder chain reads like one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nn"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input.mkv"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nn"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"output.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.set_video_codec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"libx264"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;// -c:v libx264&lt;/span&gt;
            &lt;span class="nf"&gt;.set_video_codec_opt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"crf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"23"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// -crf 23&lt;/span&gt;
            &lt;span class="nf"&gt;.set_video_codec_opt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"preset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"fast"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// -preset fast&lt;/span&gt;
            &lt;span class="nf"&gt;.set_audio_codec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"aac"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;               &lt;span class="c1"&gt;// -c:a aac&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
    &lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
    &lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migration is mechanical, because nothing has to be relearned: seconds become microseconds (&lt;code&gt;-ss 10&lt;/code&gt; → &lt;code&gt;set_start_time_us(10_000_000)&lt;/code&gt;); option names work verbatim minus the leading dash (&lt;code&gt;crf&lt;/code&gt;, &lt;code&gt;preset&lt;/code&gt;, &lt;code&gt;movflags&lt;/code&gt; go through &lt;code&gt;set_*_opt&lt;/code&gt; straight into FFmpeg's AVOption system — whatever name the CLI accepts, this accepts); the string after &lt;code&gt;-vf&lt;/code&gt; drops into &lt;code&gt;filter_desc&lt;/code&gt; character for character; stream selection uses the CLI's own specifier syntax (&lt;code&gt;0:a:0&lt;/code&gt;). Years of command-line muscle memory stay valid, line for line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;As of 0.15 even the translation step is optional — the command can be pasted in as-is.&lt;/strong&gt; With the &lt;code&gt;cli&lt;/code&gt; feature enabled, &lt;code&gt;from_cli&lt;/code&gt; takes a whole ffmpeg command string, parses it, builds the pipeline, and runs it inside your own process — no subprocess:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;cli&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;from_cli&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;from_cli&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ffmpeg -i input.mp4 -c:v libx264 -crf 28 -preset veryfast -c:a aac -y output.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attitude behind it is the part worth noting. Commands sort into three classes: &lt;strong&gt;verified shapes&lt;/strong&gt; may run (six today, each reconciled against the real ffmpeg CLI by a semantic golden suite); &lt;strong&gt;unverified&lt;/strong&gt; ones are translated but never executed; &lt;strong&gt;unrecognized&lt;/strong&gt; ones are rejected on the spot, anchored to the offending token. A sibling function, &lt;code&gt;emit_rust_code&lt;/code&gt;, turns the same command into a compile-ready builder program you can extend and maintain. It would rather refuse you clearly than "run, but come out subtly different from ffmpeg."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The isomorphism does not stop at the API surface — it runs down to the source.&lt;/strong&gt; The internal scheduler is modeled on the ffmpeg CLI's own architecture: fftools' &lt;code&gt;ffmpeg_demux.c&lt;/code&gt; / &lt;code&gt;ffmpeg_dec.c&lt;/code&gt; / &lt;code&gt;ffmpeg_filter.c&lt;/code&gt; / &lt;code&gt;ffmpeg_enc.c&lt;/code&gt; / &lt;code&gt;ffmpeg_mux.c&lt;/code&gt; / &lt;code&gt;ffmpeg_sched.c&lt;/code&gt; / &lt;code&gt;sync_queue.c&lt;/code&gt; map file for file to &lt;code&gt;demux_task&lt;/code&gt; / &lt;code&gt;dec_task&lt;/code&gt; / &lt;code&gt;filter_task&lt;/code&gt; / &lt;code&gt;enc_task&lt;/code&gt; / &lt;code&gt;mux_task&lt;/code&gt; / &lt;code&gt;ffmpeg_scheduler&lt;/code&gt; / &lt;code&gt;sync_queue&lt;/code&gt;. Two practical consequences: when behavior has to match the CLI, there is a line-by-line reference to diff against — a divergence can be traced to its fftools counterpart; and if you have ever wanted to understand FFmpeg's internals, this codebase reads as a typed, ownership-annotated companion to fftools — a far gentler slope than the C.&lt;/p&gt;

&lt;p&gt;On the feature side: history first, then the new parts. Even in 0.13, ez-ffmpeg let you intercept and rewrite every frame via FrameFilter — the repository ships custom tile/volume filter examples. 0.14 adds the dedicated FrameExtractor / SampleExtractor / VideoWriter — turning "possible" into "one line", with color-tag-correct YUV→RGB by default. Concretely: FrameExtractor pulls decoded frames with sampling policies (EveryNth / UniformN / Keyframes); SampleExtractor hands you 16 kHz mono f32 ready for whisper-rs; VideoWriter pushes frames you rendered yourself into an encoding pipeline (write / write_owned), with filter-graph validation; its output surface covers files and RTMP live targets (the live-target claim is API-surface and module-doc backed, not exercised against a live server here). The color detail deserves its own sentence: HD video converts to RGB per BT.709 by default, whereas &lt;code&gt;to_ndarray('rgb24')&lt;/code&gt; in older PyAV releases applied BT.601 across the board, which skews saturated colors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who should not use ez-ffmpeg
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If linking FFmpeg is your blocker, ez-ffmpeg does not remove it.&lt;/strong&gt; It links libav through ffmpeg-next — the very layer criticized above, whose frozen maintenance and fork-lineage risk it inherits wholesale. The developer who wrote "I've spent the last 12 hours… willing to spend another week" fighting the build would fight the same build here. What it removes is the hundred-line pipeline after linking succeeds — not the linking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The three new APIs are marked experimental in the README.&lt;/strong&gt; That word is there on purpose. The &lt;code&gt;cli&lt;/code&gt; facade has boundaries of its own: it is an optional feature you must enable, execution currently requires an FFmpeg 7.1 runtime (anything else fails before any I/O), and the subset is &lt;em&gt;deliberately narrow&lt;/em&gt; — only those six verified shapes run; two-pass encoding, complex filtergraphs and hardware acceleration are outside it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is early-stage: ~340 stars, and effectively one maintainer&lt;/strong&gt; — bus factor is a real risk. A safe Rust API surface is not the same thing as freedom from C CVEs — libav is still underneath, and the wrapper carries its own unsafe glue: the "safe skin over an FFI skeleton" line above applies to this crate too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No parity claims&lt;/strong&gt; with decord's GPU batch decoding or torchaudio's multi-backend DSP.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If a pure-Rust supply chain is the goal, symphonia is your answer — not ez-ffmpeg.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Verdict: if you can already write the ffmpeg command, this is the lowest-migration-cost way to run it inside your own process; when frames and samples must land in memory with FFmpeg-grade format coverage, it is also the shortest path. When linking is the problem, nothing on this layer saves you.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The set-difference that 0.14 targets
&lt;/h2&gt;

&lt;p&gt;Let me state the full picture first, so nothing here reads as a strawman: Rust has never been unable to hand you decoded frames — video-rs iterates RGB ndarrays in ten lines, and ffmpeg-sidecar turns any video into an RGB frame iterator (via subprocess). What was missing is a one-liner with sampling policies and correct color handling inside a single in-process, FFmpeg-grade runtime.&lt;/p&gt;

&lt;p&gt;So the set-difference looks like this: in Python that's three libraries — PyAV, decord, torchaudio. In Rust, video-rs skips audio, symphonia skips video, sidecar forks a process. ez-ffmpeg 0.14 is the first to put all three jobs inside one in-process FFmpeg runtime.&lt;/p&gt;

&lt;p&gt;And the scope of that claim, stated precisely: as far as I can verify (July 2026), ez-ffmpeg is the only actively-maintained in-process FFmpeg runtime in Rust where decoded-RGB export, 16 kHz f32 audio export, and frame-push encoding are all first-class APIs: video-rs has no audio API at all and self-declares WIP (its successor rave is "not ready for use yet"), while ac-ffmpeg covers all three domains but at packet-loop level, with no release in 14 months. gstreamer-rs's appsink/appsrc can genuinely do all three in-process too — but through a different pipeline model, not an FFmpeg runtime, which is exactly why the boundary note above places it on a different map.&lt;/p&gt;

&lt;p&gt;Two honest footnotes. First, PyAV is itself a set of libav bindings — ez-ffmpeg runs on the same engine. So the claim is not "Rust rewrote PyAV"; it is the same engine wearing a native high-level Rust face, without raw FFI and without a child process. Second, why this gap is worth closing now: candle, burn, ort, tract and whisper-rs are all growing, and what they need is frames and audio as data — while step one of feeding them, today, is often a line of shell. For a sense of how absurd the gap can get: someone implemented a decord-style video reader in Rust — video_reader-rs — and ships it only on PyPI. You cannot &lt;code&gt;cargo add&lt;/code&gt; it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When shelling out is the right answer
&lt;/h2&gt;

&lt;p&gt;A selection guide that funnels every case toward one crate is an ad. For these, the plain CLI is the right answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The output is a file and the recipe exists.&lt;/strong&gt; Transcodes, clips, one-off batch jobs — the ML audio world's step one is still &lt;code&gt;ffmpeg -i in.mp4 -ar 16000 -ac 1 -c:a pcm_s16le out.wav&lt;/code&gt;. If that line is your whole requirement, do not add a crate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your environment ships the ffmpeg binary anyway.&lt;/strong&gt; The deployment cost is already paid; no need to also pay the linking cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want process isolation.&lt;/strong&gt; A crashed transcode job that cannot take down your service is a feature of the subprocess model, not a defect (an engineering judgment, not a quote).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your team knows the CLI, not the libav API, and the task is not on a hot path.&lt;/strong&gt; The migration cost exceeds the benefit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flip side: the moment frames or samples must land in your process memory — feeding a model, real-time processing, logic interleaved with decoding — the CLI starts charging: stdout parsing, temp WAV files, mismatched parameters. jaredly's pattern, running from 2015 to today, is that tax's ten-year invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking by task
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Output is a file, recipe exists → &lt;strong&gt;CLI&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Frames without FFI or linking → &lt;strong&gt;ffmpeg-sidecar&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Full libav control plane → &lt;strong&gt;ffmpeg-next&lt;/strong&gt; (tracking newer FFmpeg majors → &lt;strong&gt;rsmpeg&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;Video frames only, WIP acceptable → &lt;strong&gt;video-rs&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Audio only, pure Rust, codecs within the support table → &lt;strong&gt;symphonia + rubato&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You already write ffmpeg commands and want them in-process at minimal migration cost → &lt;strong&gt;ez-ffmpeg&lt;/strong&gt; (option names, filter strings and stream specifiers are all CLI syntax)&lt;/li&gt;
&lt;li&gt;Frames, samples and frame-push all in-process, FFmpeg-grade coverage → &lt;strong&gt;ez-ffmpeg&lt;/strong&gt; (remember both caveats: the linking pain stays, and the new APIs are experimental)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every path on this map wins somewhere; none of them replaces the others. Pick by task, not by tribe.&lt;/p&gt;

&lt;p&gt;The Rust crate &lt;code&gt;ez-ffmpeg&lt;/code&gt; lives at github.com/YeautyYE/ez-ffmpeg.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ffmpeg</category>
      <category>cli</category>
    </item>
    <item>
      <title>In-process HLS packaging in Rust: fMP4 segments, an ABR ladder, no ffmpeg subprocess</title>
      <dc:creator>Yeauty YE</dc:creator>
      <pubDate>Mon, 27 Jul 2026 19:35:53 +0000</pubDate>
      <link>https://dev.to/yeauty/in-process-hls-packaging-in-rust-fmp4-segments-an-abr-ladder-no-ffmpeg-subprocess-jbj</link>
      <guid>https://dev.to/yeauty/in-process-hls-packaging-in-rust-fmp4-segments-an-abr-ladder-no-ffmpeg-subprocess-jbj</guid>
      <description>&lt;p&gt;&lt;code&gt;ez-ffmpeg&lt;/code&gt; is a Rust crate (no relation to the JavaScript "Ez FFmpeg" project) that runs FFmpeg pipelines inside your process — no subprocess, no hand-written decode loop. As of 0.14 its HLS ladder recipe writes fragmented-MP4 segments as well as MPEG-TS, switched by one enum value, and the crate is on &lt;a href="https://crates.io/crates/ez-ffmpeg" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt; and &lt;a href="https://github.com/YeautyYE/ez-ffmpeg" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. This article packages HLS both ways, verifies every artifact with ffprobe, and walks through two design decisions that only become visible when something fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first decision: fMP4 or TS
&lt;/h2&gt;

&lt;p&gt;If you are packaging HLS yourself, the container question comes before any code. A decision rather than a definition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick fMP4&lt;/strong&gt; when you target modern players (hls.js, AVPlayer, ExoPlayer) on a new project. It is the container CMAF standardized on, so HLS and DASH can share one set of segments; header data lives in a single &lt;code&gt;init.mp4&lt;/code&gt; instead of being repeated in every segment; and if you ever head toward LL-HLS, fMP4 is the ground it is built on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick TS&lt;/strong&gt; when legacy set-top boxes and older smart TVs are a hard requirement. TS also has an operational property fMP4 gives up: every segment is self-contained and independently playable, which makes single-file inspection one step shorter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Need both? The code below switches with one enum value.&lt;/p&gt;

&lt;p&gt;Everything here was compiled and run against FFmpeg 7.1.3 + ez-ffmpeg 0.15; the outputs quoted are real, none are mocked up. What the crate does not fix, stated up front: installing and linking FFmpeg. It sits on the libav libraries, so FFmpeg 7.1–8.x must be present. What it removes is everything after the link succeeds — the hand-written demux/decode/filter/encode/mux loop, or the &lt;code&gt;Command::new("ffmpeg")&lt;/code&gt; subprocess and its stderr scraping.&lt;/p&gt;

&lt;h2&gt;
  
  
  The minimal runnable path: single-bitrate fMP4
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;ez-ffmpeg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.15"&lt;/span&gt;   &lt;span class="c"&gt;# Rust &amp;gt;= 1.80, FFmpeg 7.1-8.x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The hls muxer writes into an existing directory; it does not mkdir by default.&lt;/span&gt;
    &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create_dir_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hls_single"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nn"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nn"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hls_single/index.m3u8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;.set_format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hls"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                &lt;span class="c1"&gt;// -f hls&lt;/span&gt;
                &lt;span class="nf"&gt;.set_video_codec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"libx264"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;.set_audio_codec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"aac"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="c1"&gt;// Fixed 6 s GOP at 30 fps, no scene-cut keyframes: segment&lt;/span&gt;
                &lt;span class="c1"&gt;// boundaries can only land on keyframes, so pin them down.&lt;/span&gt;
                &lt;span class="nf"&gt;.set_video_codec_opt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"g"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"180"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;.set_video_codec_opt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sc_threshold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;.set_format_opt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hls_time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"6"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;// target segment length&lt;/span&gt;
                &lt;span class="nf"&gt;.set_format_opt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hls_playlist_type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"vod"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;// full playlist, ENDLIST at the end&lt;/span&gt;
                &lt;span class="nf"&gt;.set_format_opt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hls_segment_type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"fmp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;// .m4s instead of .ts&lt;/span&gt;
                &lt;span class="nf"&gt;.set_format_opt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hls_fmp4_init_filename"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"init.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;.set_format_opt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hls_segment_filename"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"hls_single/seg_%05d.m4s"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wrote hls_single/index.m3u8"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The input is a 12-second lavfi-generated test clip (640x360, 30 fps, H.264+AAC). Afterwards &lt;code&gt;hls_single/&lt;/code&gt; holds four files — &lt;code&gt;index.m3u8&lt;/code&gt;, &lt;code&gt;init.mp4&lt;/code&gt;, &lt;code&gt;seg_00000.m4s&lt;/code&gt;, &lt;code&gt;seg_00001.m4s&lt;/code&gt; — and the playlist reads, verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="init.mp4"
#EXTINF:6.000000,
seg_00000.m4s
#EXTINF:6.000000,
seg_00001.m4s
#EXT-X-ENDLIST
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twelve seconds split into exactly two 6.000000-second segments, with &lt;code&gt;EXT-X-MAP&lt;/code&gt; pointing at the one init segment — the structural signature of fMP4 HLS. The &lt;code&gt;#EXT-X-VERSION:7&lt;/code&gt; is the muxer's doing: &lt;code&gt;EXT-X-MAP&lt;/code&gt; requires protocol version 6+, and FFmpeg writes 7 for fMP4 output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping the options (and the step I got wrong first)
&lt;/h2&gt;

&lt;p&gt;If you know the CLI, the options map one to one — same names, minus the dash. Container options go through &lt;code&gt;set_format_opt&lt;/code&gt;, encoder options through &lt;code&gt;set_video_codec_opt&lt;/code&gt;; both feed FFmpeg's own AVOption system.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ffmpeg CLI&lt;/th&gt;
&lt;th&gt;ez-ffmpeg&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-f hls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.set_format("hls")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-hls_time 6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.set_format_opt("hls_time", "6")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-hls_playlist_type vod&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.set_format_opt("hls_playlist_type", "vod")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-hls_segment_type fmp4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.set_format_opt("hls_segment_type", "fmp4")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-hls_fmp4_init_filename init.mp4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.set_format_opt("hls_fmp4_init_filename", "init.mp4")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-hls_segment_filename ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.set_format_opt("hls_segment_filename", "...")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-g 180 -sc_threshold 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.set_video_codec_opt("g", "180")&lt;/code&gt; etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those two GOP lines are the step I got wrong. My first version left them out, and the segment durations came out as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#EXTINF:8.333333,
seg_00000.m4s
#EXTINF:3.666667,
seg_00001.m4s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hls_time=6&lt;/code&gt; is a &lt;em&gt;target&lt;/em&gt;, not a command. The muxer can only cut at keyframes, and x264's default keyint is 250 — at 30 fps the first cut opportunity is at 8.33 s, so a request for 6 becomes the reality of 8.33 + 3.67. To make the cuts land on 6, make the keyframes land on 6: &lt;code&gt;g = 6 x 30 = 180&lt;/code&gt;, plus &lt;code&gt;sc_threshold=0&lt;/code&gt; so scene-cut detection cannot insert extra ones. With those two lines, you get the 6.000000 + 6.000000 playlist above.&lt;/p&gt;

&lt;p&gt;On the builder path this trap is yours to find. The ladder recipe in the next section turns it into a validation rule instead: &lt;code&gt;segment_duration&lt;/code&gt; must be an integer multiple of the GOP length, and a violation fails before &lt;code&gt;run()&lt;/code&gt; does any work — rather than producing a drifted playlist you discover in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Probe before you build
&lt;/h3&gt;

&lt;p&gt;FFmpeg builds differ widely — distro packages, vcpkg, and source builds each trim differently. Instead of failing mid-pipeline, ask up front:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// FFmpeg builds differ in what got compiled in. The probe answers for&lt;/span&gt;
    &lt;span class="c1"&gt;// *this* linked build, before any job is wired up.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nn"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;is_muxer_available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hls"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;eprintln!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"linked FFmpeg build has no hls muxer"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;process&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Segments go through the file protocol; probe it the same way. For a&lt;/span&gt;
    &lt;span class="c1"&gt;// stream published elsewhere you would probe "http" or "srt" here.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nn"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;is_output_protocol_available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;eprintln!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"linked FFmpeg build cannot write files"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;process&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hls muxer + file output protocol: available"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;is_muxer_available&lt;/code&gt; answers exactly one question — is this muxer compiled into the linked build — and deliberately does not promise that the network or TLS backends a job needs at runtime are also there. One namespace trap worth knowing: muxer names and protocol names are separate worlds. The muxer called &lt;code&gt;srt&lt;/code&gt; is the SubRip subtitle format, unrelated to the SRT streaming protocol; protocols go through &lt;code&gt;is_output_protocol_available&lt;/code&gt;. Missing &lt;em&gt;encoders&lt;/em&gt; are a different path: since 0.14, &lt;code&gt;build()&lt;/code&gt; fails with &lt;code&gt;encoder 'libx264' is not available in the linked FFmpeg build&lt;/code&gt; — the error names the encoder instead of a generic not-found.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ABR ladder: one decode, N renditions
&lt;/h2&gt;

&lt;p&gt;A single bitrate is a buffering spinner for anyone on a weak connection. ABR means encoding the same content at several resolution/bitrate pairs and letting the player switch. Done by hand that is: a &lt;code&gt;split&lt;/code&gt; filter fan-out, a &lt;code&gt;scale&lt;/code&gt; per branch, fixed-GOP keyframe alignment per branch, one hls muxer per branch, and a hand-assembled master playlist. In 0.14 that orchestration is a recipe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;HlsLadder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HlsSegmentType&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;HlsLadder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input.mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"hls_out"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.rendition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;854&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;480&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1400k"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.rendition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;640&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"800k"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.segment_duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.segment_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;HlsSegmentType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fmp4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wrote hls_out/master.m3u8 and per-rendition playlists"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                     ┌─▶ scale 854x480 ─▶ x264 (GOP-aligned) ─▶ hls muxer ─▶ 480p/…
input.mp4 ─▶ decode ─▶ split
                     └─▶ scale 640x360 ─▶ x264 (GOP-aligned) ─▶ hls muxer ─▶ 360p/…
after every rendition succeeds ─▶ write master.m3u8 (BANDWIDTH = video+audio × 1.1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it wires for you: the &lt;code&gt;[0:v]split=2[s0][s1];[s0]scale=854:480,...&lt;/code&gt; filtergraph; per-rendition &lt;code&gt;g&lt;/code&gt;/&lt;code&gt;keyint_min&lt;/code&gt;/&lt;code&gt;sc_threshold=0&lt;/code&gt; plus closed-GOP x264 params, so every rendition's keyframe PTS sequence coincides and segments stay switchable across bitrates; and a master playlist whose &lt;code&gt;BANDWIDTH&lt;/code&gt; folds video + audio bitrate plus 10% muxing overhead — an engineering estimate from the nominal bitrates (the RFC defines the field as peak segment bit rate) — the number players use to pick a variant, so under-reporting it makes players overestimate their headroom. Swap &lt;code&gt;HlsSegmentType::Fmp4&lt;/code&gt; for &lt;code&gt;MpegTs&lt;/code&gt; (or delete the line — TS is the default) and the same code is a TS ladder; the master's &lt;code&gt;EXT-X-VERSION&lt;/code&gt; switches between 7 and 3 accordingly.&lt;/p&gt;

&lt;p&gt;One wording matter, stated the way the module docs state it: this is &lt;strong&gt;fMP4 HLS as a structural claim&lt;/strong&gt; — init segment, &lt;code&gt;EXT-X-MAP&lt;/code&gt;, &lt;code&gt;.m4s&lt;/code&gt; segments, probe-able end to end — not a claim of CMAF compliance or Apple validation.&lt;/p&gt;

&lt;p&gt;The recipe's boundaries, copied from its docs rather than hidden: input must be constant-frame-rate (probed from the file when possible; callback inputs need an explicit &lt;code&gt;.fps(30, 1)&lt;/code&gt;); one video stream plus at most one audio stream; VOD only; no encryption, no audio groups, no live/event playlists; and the master carries no &lt;code&gt;CODECS&lt;/code&gt; attribute (strict validators will flag that). And one Apple-specific trap: Apple requires the &lt;code&gt;hvc1&lt;/code&gt; sample-entry tag for HEVC, FFmpeg's MP4/fMP4 muxer writes &lt;code&gt;hev1&lt;/code&gt; by default — the muxer's codec-tag choice, independent of which encoder produced the stream — and this recipe has no per-stream codec-tag override. Prefer H.264 for Apple targets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two craftsmanship details (the part that is not in the README)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Directories are created only after a successful build.&lt;/strong&gt; Before 0.14, the ladder created its output tree first and built the job second — so a configuration rejected at build time (the classic case: a linked FFmpeg without libx264) left a trail of empty rendition directories. The order is now: wire every rendition &lt;code&gt;Output&lt;/code&gt; as pure configuration, let &lt;code&gt;build()&lt;/code&gt; resolve encoders, and only mkdir once it has accepted the job. File I/O is deferred to &lt;code&gt;start()&lt;/code&gt; anyway, so creating directories earlier bought nothing but litter. Verified by asking for an encoder that does not exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;HlsLadder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HlsSegmentType&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Deliberately request an encoder this FFmpeg build does not have.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;HlsLadder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input.mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"hls_reject"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.rendition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;640&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"800k"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.segment_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;HlsSegmentType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fmp4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.video_codec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"libx264_that_is_not_here"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"run() -&amp;gt; {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="nf"&gt;.err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"hls_reject/ exists after the failed build: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;path&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hls_reject"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Open output error: encoder 'libx264_that_is_not_here' is not
available in the linked FFmpeg build — ..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;hls_reject&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The master playlist follows the same rule: its text is computed up front but written only after the transcode succeeds, so a failed run leaves no dangling &lt;code&gt;master.m3u8&lt;/code&gt;. One ordering consequence to know (per the docs and code order; not exercised with a callback input here): a one-shot callback-backed input is consumed by the build step, which happens before directory creation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows paths are parsed twice — and the two parsers disagree.&lt;/strong&gt; A path handed to FFmpeg is interpreted once by the OS file APIs (which on Windows accept both &lt;code&gt;/&lt;/code&gt; and &lt;code&gt;\&lt;/code&gt;) and once by the hls muxer's own string splitting, which only knows &lt;code&gt;/&lt;/code&gt;. hlsenc.c derives the fMP4 init segment's output directory with &lt;code&gt;strrchr(m3u8_name, '/')&lt;/code&gt; — no DOS-path handling on that route, even though the master-URL code elsewhere in the same file does carry a &lt;code&gt;\&lt;/code&gt; fallback. The consequence: pass a &lt;code&gt;\&lt;/code&gt;-separated path on Windows and &lt;code&gt;init.mp4&lt;/code&gt; lands silently &lt;em&gt;outside&lt;/em&gt; the rendition directory, so the &lt;code&gt;EXT-X-MAP&lt;/code&gt; the player fetches is a 404. The 0.14 fix normalizes every path that enters an FFmpeg option string to forward slashes — lossless, since Windows file APIs accept &lt;code&gt;/&lt;/code&gt; — with one deliberate exception: verbatim &lt;code&gt;\\?\&lt;/code&gt; paths are prefix-sensitive, rewriting their separators can change which object they name, so they pass through untouched (prefer regular paths for fMP4 output on Windows). The contract is frozen in the test suite: the expected option strings are literal &lt;code&gt;"out/720p/index.m3u8"&lt;/code&gt;-style values, forward slashes on every platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  The run, verbatim
&lt;/h2&gt;

&lt;p&gt;The ladder's output tree (12 s input, 4 s segments, 3 per rendition):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hls_out/
├── master.m3u8
├── 480p/
│   ├── index.m3u8
│   ├── init.mp4          (1.4 KB)
│   └── seg_00000..2.m4s  (3 segments)
└── 360p/
    ├── index.m3u8
    ├── init.mp4
    └── seg_00000..2.m4s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;init.mp4&lt;/code&gt; per rendition is the expected layout — each rendition is its own hls muxer instance, and each media playlist references only its own init segment. The master:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#EXTM3U
#EXT-X-VERSION:7
#EXT-X-STREAM-INF:BANDWIDTH=1020800,RESOLUTION=640x360
360p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1680800,RESOLUTION=854x480
480p/index.m3u8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ffprobe acceptance. A bare &lt;code&gt;.m4s&lt;/code&gt; is not readable on its own — the headers live in the init segment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ffprobe hls_out/480p/seg_00000.m4s
&lt;span class="go"&gt;... trun track id unknown, no tfhd was found
... error reading header
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Concatenate the init in front and it is a complete stream — which is what "single init segment" means operationally, and a step TS simply does not have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ffprobe &lt;span class="nt"&gt;-show_entries&lt;/span&gt; &lt;span class="nv"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;duration &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    "concat:hls_out/480p/init.mp4|hls_out/480p/seg_00000.m4s"
duration=4.023023
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Probing &lt;code&gt;hls_out/master.m3u8&lt;/code&gt; directly reads both renditions (h264 854x480 + aac, h264 640x360 + aac), and the two renditions' &lt;code&gt;EXTINF&lt;/code&gt; sequences are byte-identical (&lt;code&gt;diff&lt;/code&gt; is empty) — the fixed-GOP alignment doing its job.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to use this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Large-scale distribution or a live platform&lt;/strong&gt;: use a dedicated packager (Shaka Packager) or CDN-side just-in-time packaging. Encryption, DRM, audio groups, subtitle renditions, LL-HLS — none of that is here; this recipe is a minimal viable set for VOD ABR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A one-off conversion&lt;/strong&gt;: &lt;code&gt;ffmpeg -i in.mp4 -f hls ...&lt;/code&gt; on the command line is shorter. Writing a Rust program for a single run is a detour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where in-process earns its keep&lt;/strong&gt;: packaging as part of program logic — packaging user uploads on demand, emitting HLS from programmatically generated content, or running upload/accounting/notification in the same process right after the packaging step, without managing a child process and parsing its stderr.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;You now have the minimal fMP4 path, a TS/fMP4-switchable ABR ladder, the probe-before-build pattern, and two design decisions that only show up on the failure path. &lt;code&gt;examples/hls_conversion&lt;/code&gt; and &lt;code&gt;examples/hls_abr_ladder&lt;/code&gt; in the repository (&lt;a href="https://github.com/YeautyYE/ez-ffmpeg" rel="noopener noreferrer"&gt;github.com/YeautyYE/ez-ffmpeg&lt;/a&gt;) are the runnable versions.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ffmpeg</category>
      <category>hls</category>
      <category>stream</category>
    </item>
    <item>
      <title>Pushing your own frames into an FFmpeg pipeline in Rust (headless frame source)</title>
      <dc:creator>Yeauty YE</dc:creator>
      <pubDate>Mon, 27 Jul 2026 05:19:31 +0000</pubDate>
      <link>https://dev.to/yeauty/pushing-your-own-frames-into-an-ffmpeg-pipeline-in-rust-headless-frame-source-bmp</link>
      <guid>https://dev.to/yeauty/pushing-your-own-frames-into-an-ffmpeg-pipeline-in-rust-headless-frame-source-bmp</guid>
      <description>&lt;p&gt;&lt;code&gt;ez-ffmpeg&lt;/code&gt; is a Rust crate that runs FFmpeg pipelines inside your process — linked libav libraries behind a high-level API, no subprocess (and no relation to the JavaScript "Ez FFmpeg" project). The narrow claim of this post: its &lt;code&gt;VideoWriter&lt;/code&gt; (added in 0.14) takes frames you render in Rust — one tightly packed byte buffer per frame — and pushes them through a real filter → encode → mux pipeline into an MP4, shown below as a complete listing that compiles, runs, and is ffprobe-verified verbatim. It's on &lt;a href="https://crates.io/crates/ez-ffmpeg" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt; and &lt;a href="https://github.com/YeautyYE/ez-ffmpeg" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here's how old this itch is. In 2015, the original rust-ffmpeg binding received its issue number one (&lt;a href="https://github.com/meh/rust-ffmpeg/issues/1" rel="noopener noreferrer"&gt;meh/rust-ffmpeg#1&lt;/a&gt;). The author, jaredly, wanted the most ordinary thing: he had frames drawn in code and wanted them encoded into a video file. He "couldn't find a way to: open a file as writer; encode video frames to it". His workaround: "rendering to a bunch of images and then using ffmpeg on the cli". A decade later, Rust code that renders, simulates, or visualizes still mostly ships that 2015 workaround.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workaround, costed
&lt;/h2&gt;

&lt;p&gt;Here is the image-sequence detour in its modern form (PPM keeps it dependency-free — a three-line text header over raw RGB is a legal image):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Step 1: render every frame to its own image file on disk.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rgb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WIDTH&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;HEIGHT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;render_gradient_rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WIDTH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HEIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;File&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"frame_{i:04}.ppm"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nd"&gt;write!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"P6&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;{WIDTH} {HEIGHT}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;255&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// PPM needs no image crate&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="nf"&gt;.write_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Step 2: hope the ffmpeg on PATH exists and agrees with your flags.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ffmpeg"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.args&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-framerate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"30"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"frame_%04d.ppm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s"&gt;"-c:v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mpeg4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-q:v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"images.mp4"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;.status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works — I ran it, 120 frames in, 120 frames out. It also taxes you three ways. Disk: a 640×360, 30 fps, 4-second animation writes 120 PPM files totalling 80 MB of intermediates for a final MP4 of 213 KiB, and the intermediate size scales with resolution and duration. Naming: &lt;code&gt;frame_{i:04}&lt;/code&gt; and &lt;code&gt;frame_%04d&lt;/code&gt; are the same implicit contract written in two languages; change the zero-padding on one side and ffmpeg silently stops at the first filename that doesn't match. Failure surface: step one fails as a Rust &lt;code&gt;Result&lt;/code&gt;, step two fails as an exit code plus stderr text — "encoder not found" and "frame 37 is corrupt" both arrive as log lines for you to fish out.&lt;/p&gt;

&lt;p&gt;Fairness demands the third old way on the table too: skip the disk entirely and pipe raw frames into ffmpeg's stdin — &lt;code&gt;ffmpeg -f rawvideo -pix_fmt rgba -s 640x360 -framerate 30 -i - out.mp4&lt;/code&gt;. Zero temp files, zero naming contract, and a full pipe blocks the writer, so backpressure comes free (ffmpeg-sidecar wraps exactly this pattern). That kills the disk tax — but the other two stand: failures are still an exit code plus stderr text, the right ffmpeg binary still has to exist at runtime, and a structurally invalid filter graph only blows up mid-run instead of being rejected up front. The API below earns its keep on that second half — typed errors, open()-time validation, no subprocess lifecycle — not on saving temp files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The in-process version
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your render loop              VideoWriter pipeline (same process)
┌────────────┐ write()/      ┌─────────────┐   ┌────────┐   ┌─────────┐
│ render(buf)│─write_owned──▶│bounded queue│──▶│ filters│──▶│ encoder │─▶ gradient.mp4
└────────────┘ blocks if full└─────────────┘   └────────┘   └─────────┘
   a frame-source worker sits where a decoder would; end = explicit in-band EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same animation, pushed straight into the pipeline. One dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;ez-ffmpeg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.15"&lt;/span&gt;   &lt;span class="c"&gt;# needs FFmpeg 7.1-8.x installed (links libav)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complete listing — copy it whole:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VideoWriter&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;WIDTH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;640&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HEIGHT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FPS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SECONDS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Name the encoder explicitly. With a bare "gradient.mp4" the linked&lt;/span&gt;
    &lt;span class="c1"&gt;// FFmpeg build picks the container default (H.264 when libx264 is&lt;/span&gt;
    &lt;span class="c1"&gt;// compiled in, otherwise mpeg4 at a low default bitrate).&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gradient.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.set_video_codec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mpeg4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.set_video_qscale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;VideoWriter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WIDTH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HEIGHT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.pixel_format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rgba"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// the default, spelled out&lt;/span&gt;
        &lt;span class="nf"&gt;.fps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FPS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// The writer tells you the exact byte count of one tightly packed frame.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="nf"&gt;.frame_size&lt;/span&gt;&lt;span class="p"&gt;()];&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FPS&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;SECONDS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;render_gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WIDTH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HEIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;FPS&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="nf"&gt;.write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// copies the slice; `frame` is reusable&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="nf"&gt;.finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// drains the encoder, writes the trailer, reports errors&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wrote {total} frames to gradient.mp4"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cd"&gt;/// Fills `buf` (tightly packed RGBA) with a gradient that drifts right over time.&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;render_gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;fy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;fx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="n"&gt;fx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;255.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// red wave, drifting&lt;/span&gt;
            &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;255.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// green: vertical ramp&lt;/span&gt;
            &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;fx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;255.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// blue: horizontal ramp&lt;/span&gt;
            &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// opaque&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two honest caveats first. Installing and linking FFmpeg is not solved here — the crate sits on the libav libraries, FFmpeg 7.1–8.x must be present, and that pain is unchanged; what disappears is the subprocess, the 80 MB of intermediates, and the stderr fishing, not the FFmpeg dependency itself. And this API is marked &lt;strong&gt;experimental&lt;/strong&gt; in the docs: new in 0.14, its surface may still be refined in minor releases.&lt;/p&gt;

&lt;p&gt;Call by call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Output::from("gradient.mp4").set_video_codec("mpeg4").set_video_qscale(5)&lt;/code&gt;&lt;/strong&gt; — the destination is the crate's ordinary &lt;code&gt;Output&lt;/code&gt;, so encoder, container, bitrate, codec options, and format options (&lt;code&gt;movflags&lt;/code&gt;…) are all configured at that layer. &lt;code&gt;mpeg4&lt;/code&gt; keeps this listing runnable on FFmpeg builds without libx264; swap in &lt;code&gt;set_video_codec("libx264")&lt;/code&gt; for H.264. Do name an encoder: with a bare path the linked build picks the container default, which on a libx264-less build means mpeg4 at a low default bitrate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;VideoWriter::builder(WIDTH, HEIGHT)&lt;/code&gt;&lt;/strong&gt; — width and height are the only required parameters, positional so they cannot be forgotten.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.pixel_format("rgba")&lt;/code&gt;&lt;/strong&gt; — the default, spelled out. Any non-hardware &lt;code&gt;AVPixelFormat&lt;/code&gt; name works (&lt;code&gt;rgb24&lt;/code&gt;, &lt;code&gt;gray8&lt;/code&gt;, &lt;code&gt;yuv420p&lt;/code&gt;, &lt;code&gt;nv12&lt;/code&gt;, …); frames are tightly packed, planes concatenated in descriptor order (&lt;code&gt;yuv420p&lt;/code&gt; = all of Y, then U, then V). Hardware formats like &lt;code&gt;cuda&lt;/code&gt; are rejected with a typed error — a CPU byte buffer cannot fill them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.fps(FPS, 1)&lt;/code&gt;&lt;/strong&gt; — constant frame rate as a rational; NTSC rates like &lt;code&gt;fps(30000, 1001)&lt;/code&gt; work. Every written frame advances exactly &lt;code&gt;den/num&lt;/code&gt; seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.open(output)?&lt;/code&gt;&lt;/strong&gt; — validates the configuration (dimensions, fps, pixel format, filter shape, and that the output actually consumes video), builds the pipeline, and starts it without waiting for a first frame. An unknown encoder name fails right here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;writer.frame_size()&lt;/code&gt;&lt;/strong&gt; — the exact byte count one frame must have: &lt;code&gt;av_image_get_buffer_size(pix_fmt, w, h, 1)&lt;/code&gt;. A wrong-sized buffer gets a typed &lt;code&gt;InvalidSize { expected, got }&lt;/code&gt;, not a garbled picture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;writer.write(&amp;amp;frame)?&lt;/code&gt;&lt;/strong&gt; — pushes one frame by copying the borrowed slice, so the same allocation serves the whole loop. Blocks when the internal queue is full; more on that below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;writer.finish()?&lt;/code&gt;&lt;/strong&gt; — closes ingress, drains the encoder, finalizes the container, and returns the pipeline's first error. This is the result path.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it works: the four contracts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ownership: &lt;code&gt;write&lt;/code&gt; copies, &lt;code&gt;write_owned&lt;/code&gt; moves.&lt;/strong&gt; From the verification build, the contrast in twelve lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// write(): you keep the buffer. The slice is copied into the pipeline,&lt;/span&gt;
    &lt;span class="c1"&gt;// so one allocation serves the whole run.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="nf"&gt;.frame_size&lt;/span&gt;&lt;span class="p"&gt;()];&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;2u8&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="nf"&gt;.fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="nf"&gt;.write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// write_owned(): the Vec moves into the pipeline — no borrow-copy on the&lt;/span&gt;
    &lt;span class="c1"&gt;// caller side. Use it when each frame is born as its own Vec anyway.&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;4u8&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;owned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="nf"&gt;.frame_size&lt;/span&gt;&lt;span class="p"&gt;()];&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="nf"&gt;.write_owned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owned&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both paths still pay one copy at the far end: the frame-source worker fills the bytes plane-by-plane into a pooled, aligned &lt;code&gt;AVFrame&lt;/code&gt; — that copy is not skippable. Two corners worth knowing: a &lt;code&gt;Vec&lt;/code&gt; handed to &lt;code&gt;write_owned&lt;/code&gt; is queued as-is, so spare &lt;code&gt;capacity&lt;/code&gt; beyond its length stays allocated while it waits; and &lt;code&gt;write&lt;/code&gt; takes &lt;code&gt;&amp;amp;mut self&lt;/code&gt;, so the total frame order is fixed at compile time — the writer is &lt;code&gt;Send&lt;/code&gt; (move it to a dedicated producer thread) but deliberately not &lt;code&gt;Sync&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is no demuxer and no decoder behind this. The builder assembles a pipeline whose filtergraph (&lt;code&gt;buffersrc → [your filter chain or null] → buffersink&lt;/code&gt;) is fed by a frame-source worker sitting exactly where a decoder would sit. End of stream is an explicit in-band marker, which is why filters that buffer everything (&lt;code&gt;reverse&lt;/code&gt;, &lt;code&gt;tpad&lt;/code&gt;) still flush correctly at the end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backpressure: the queue is bounded, &lt;code&gt;write&lt;/code&gt; waits.&lt;/strong&gt; The ingress queue is bounded by frame count, defaulting to &lt;code&gt;max(1, min(4, 64 MiB / frame_size))&lt;/code&gt; — four frames of 1080p RGBA, two of 4K RGBA. When rendering outruns encoding, &lt;code&gt;write&lt;/code&gt; blocks instead of buffering your animation in RAM. A pipeline that dies mid-run does not leave you parked: a blocked &lt;code&gt;write&lt;/code&gt; re-checks pipeline status every 100 ms and returns &lt;code&gt;PushError::PipelineClosed&lt;/code&gt;. That error is a signpost, not a verdict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;pushed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="nf"&gt;.frame_size&lt;/span&gt;&lt;span class="p"&gt;()];&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="nf"&gt;.write_owned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pushed&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="c1"&gt;// Not an error by itself: the pipeline stopped taking frames.&lt;/span&gt;
            &lt;span class="c1"&gt;// finish() holds the verdict — the real failure, or Ok when the&lt;/span&gt;
            &lt;span class="c1"&gt;// job simply completed (as the frame limit does here).&lt;/span&gt;
            &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;PushError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;PipelineClosed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Concretely: put &lt;code&gt;set_max_video_frames(3)&lt;/code&gt; on the &lt;code&gt;Output&lt;/code&gt; and the pipeline completes itself after three frames — subsequent writes report &lt;code&gt;PipelineClosed&lt;/code&gt;, &lt;code&gt;finish()&lt;/code&gt; returns &lt;code&gt;Ok&lt;/code&gt;, and the file holds exactly 3 frames (verified). If instead an encoder genuinely fails mid-stream, the same &lt;code&gt;finish()&lt;/code&gt; hands you the real error. The test suite pins this: &lt;code&gt;worker_failure_unblocks_write_and_finish_reports&lt;/code&gt; opens an encoder that rejects the frame size only at first-frame time, and asserts that &lt;code&gt;write&lt;/code&gt; unblocks and &lt;code&gt;finish&lt;/code&gt; reports — the writer never hangs against a dead consumer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timing: constant frame rate, no per-frame PTS.&lt;/strong&gt; That is the explicit v1 scope: no variable frame rate, no audio. What you do get is a correct tail: 120 frames at 30 fps probe as duration 4.000000, not a clipped 119/30 — the explicit EOF marker closes the stream at the accumulated frame-end time, so the last frame keeps its full duration (test: &lt;code&gt;eof_preserves_final_frame_duration&lt;/code&gt;, covered for integral and NTSC rates).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teardown: &lt;code&gt;finish&lt;/code&gt; is the verdict, &lt;code&gt;Drop&lt;/code&gt; is the safety net, &lt;code&gt;abort&lt;/code&gt; is the discard.&lt;/strong&gt; &lt;code&gt;finish()&lt;/code&gt; is the only path that returns the &lt;code&gt;Result&lt;/code&gt;. Forgetting it loses no frames: &lt;code&gt;Drop&lt;/code&gt; performs the same close-drain-finalize (test: &lt;code&gt;drop_without_finish_keeps_all_frames&lt;/code&gt; — push 10, drop the writer, the file probes 10), but an error is only logged, and the drop can block while the encoder drains. When you don't want the file at all, &lt;code&gt;abort()&lt;/code&gt; discards the export; the partial output is not guaranteed playable (test: &lt;code&gt;abort_returns_cleanly&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;One more gate worth knowing about: &lt;code&gt;filter_desc&lt;/code&gt; accepts any single-input single-output video chain (&lt;code&gt;"hue=s=0"&lt;/code&gt; verifiably turns solid red frames gray; &lt;code&gt;"pad=ceil(iw/2)*2:ceil(ih/2)*2"&lt;/code&gt; is the odd-dimensions remedy), and graphs that cannot work are rejected at &lt;code&gt;open()&lt;/code&gt; with typed errors instead of surfacing as a mid-run hang — a stranded pad, a disconnected graph, or an output unreachable from the input each get their own error. The program output for &lt;code&gt;filter_desc("split")&lt;/code&gt;, verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rejected at open(): Video writer error: filter_desc must have exactly one video input pad and one video output pad; found 1 input pad(s) (1 video) and 2 output pad(s) (2 video)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compositing over generated sources is still allowed when the pushed frames reach the output — &lt;code&gt;"color=c=red:s=64x48[bg];[in][bg]overlay=shortest=1"&lt;/code&gt; runs and ends on input EOF, the same rule the CLI applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The listing above, then ffprobe, output verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ffprobe -v error -select_streams v:0 -show_entries \
    stream=codec_name,width,height,r_frame_rate,nb_frames,duration \
    -of default=noprint_wrappers=1 gradient.mp4
codec_name=mpeg4
width=640
height=360
r_frame_rate=30/1
duration=4.000000
nb_frames=120
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;120 frames in, 120 frames out, duration exact. No intermediate files, no subprocess, typed errors end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the ecosystem already had
&lt;/h2&gt;

&lt;p&gt;'Rust can't encode your own frames into an MP4' is false — video-rs's README literally encodes rainbow.mp4 from ndarray frames (and video-rs is itself FFmpeg-based, via ffmpeg-next), opencv-rust has VideoWriter, and openh264 + the mp4 crate do it in pure Rust. So the defensible claim is narrower and concrete: what this API adds over those is the contract work — filter chains validated at &lt;code&gt;open()&lt;/code&gt; instead of failing mid-run, explicit backpressure and teardown semantics, typed errors end to end — plus an output surface beyond files. Not "nobody else can encode frames."&lt;/p&gt;

&lt;p&gt;The filter-graph validation you saw above. On live targets, this post's evidence stops at the API surface: the module documentation lists RTMP targets among supported destinations, and an &lt;code&gt;Output::from("rtmp://…").set_format("flv")&lt;/code&gt; writer session compile-checks — but I did not run one against a live server here, so treat that half of the claim as documented-and-type-checked, not exercised.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should not use this
&lt;/h2&gt;

&lt;p&gt;The split is by scenario, and each path has its win. The images already exist on disk and you convert once: &lt;code&gt;ffmpeg -framerate 30 -i frame_%04d.png out.mp4&lt;/code&gt; in a terminal wins, and writing a Rust program for it is a detour. You need GStreamer-grade live pipeline orchestration — dynamic topology, multi-branch fan-out, fine-grained clocking: the gstreamer crate's mature pipeline model is the right tool, not this. You need variable frame rate, per-frame PTS, or an audio track muxed in: v1 does not cover any of those (video only, constant rate; stream maps are rejected; and since pushed frames are raw, video stream-copy does not apply — frames are always encoded). And if your frames come out of another video file rather than your own code, that's a transcode — the crate's ordinary &lt;code&gt;Input&lt;/code&gt;/&lt;code&gt;Output&lt;/code&gt; job covers it without &lt;code&gt;VideoWriter&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What's left — render loops, simulation output, procedural video, anywhere the frames are born in your code and the destination is a file — is exactly what this API is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the first build costs
&lt;/h2&gt;

&lt;p&gt;The prerequisite is the same one the whole crate carries: FFmpeg 7.1–8.x installed where the linker can find it, because ez-ffmpeg links the libav libraries rather than shelling out. That cost predates this crate and survives it; budget your first &lt;code&gt;cargo build&lt;/code&gt; accordingly. The writer itself opens no input format context, so there is no version-specific probing behavior across supported FFmpeg releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;Your render loop now emits an MP4 (or MKV, or anything the linked build muxes) directly, with a filter chain if you want one, backpressure managing memory, and failures arriving as typed &lt;code&gt;Result&lt;/code&gt;s. In the repository, &lt;code&gt;examples/frames_to_video&lt;/code&gt; (a plasma animation) and &lt;code&gt;examples/bouncing_balls&lt;/code&gt; (a gravity toy with real per-frame state) run as-is: &lt;a href="https://github.com/YeautyYE/ez-ffmpeg" rel="noopener noreferrer"&gt;github.com/YeautyYE/ez-ffmpeg&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ffmpeg</category>
      <category>cli</category>
      <category>media</category>
    </item>
    <item>
      <title>Extracting video frames in Rust, in-process — no CLI, no hand-written decode loop</title>
      <dc:creator>Yeauty YE</dc:creator>
      <pubDate>Sat, 25 Jul 2026 22:52:39 +0000</pubDate>
      <link>https://dev.to/yeauty/extracting-video-frames-in-rust-in-process-no-cli-no-hand-written-decode-loop-3393</link>
      <guid>https://dev.to/yeauty/extracting-video-frames-in-rust-in-process-no-cli-no-hand-written-decode-loop-3393</guid>
      <description>&lt;p&gt;Here is a task that comes up constantly. You are doing inference in Rust — candle, burn, &lt;code&gt;ort&lt;/code&gt; — 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 &lt;code&gt;Command::new("ffmpeg")&lt;/code&gt;, write PNGs to disk or parse &lt;code&gt;-f rawvideo&lt;/code&gt; off a pipe, and read them back; or you reach for the low-level bindings and hand-write the &lt;code&gt;send_packet&lt;/code&gt; / &lt;code&gt;receive_frame&lt;/code&gt; decode loop — forty lines before the first pixel, and you own the &lt;code&gt;EAGAIN&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;This post is the third path: &lt;code&gt;ez-ffmpeg&lt;/code&gt; 0.15's &lt;code&gt;FrameExtractor&lt;/code&gt; pulls frames &lt;strong&gt;in the same process&lt;/strong&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two old ways: cross a process, or hand-write the decode loop
&lt;/h2&gt;

&lt;p&gt;Show the ugly first. The shell-out path is cheapest when the output is a file, but the instant frames have to come &lt;em&gt;back&lt;/em&gt; 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 &lt;code&gt;send_packet&lt;/code&gt; / &lt;code&gt;receive_frame&lt;/code&gt;, service &lt;code&gt;EAGAIN&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;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 — &lt;em&gt;get a batch of frames, in-process, in one line, with the color right&lt;/em&gt; — comfortable.&lt;/p&gt;

&lt;h2&gt;
  
  
  FrameExtractor: a batch of frames in one line
&lt;/h2&gt;

&lt;p&gt;One dependency line (it still links libav through ffmpeg-next, so FFmpeg 7.1–8.x has to be installed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;ez-ffmpeg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.15"&lt;/span&gt;   &lt;span class="c"&gt;# needs FFmpeg 7.1-8.x installed (links libav)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pull 32 frames to feed a VLM. Note &lt;code&gt;UniformN&lt;/code&gt; — it takes N frames spread uniformly across the whole clip by presentation time, the primitive fixed-budget CLIP/VLM pipelines actually want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;frame_export&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FrameExtractor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Sampling&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// 32 frames evenly spread over the full duration, scaled to 224 wide&lt;/span&gt;
&lt;span class="c1"&gt;// (height derived from the aspect ratio).&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;FrameExtractor&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.sampling&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Sampling&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;UniformN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;.width&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;224&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.collect_frames&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Vec&amp;lt;VideoFrame&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;frames&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// f.as_bytes() is tightly packed, top-down RGB24 with no row padding —&lt;/span&gt;
    &lt;span class="c1"&gt;// width*height*3 bytes, ready for an ndarray view or a tensor.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="nf"&gt;.width&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="nf"&gt;.height&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="nf"&gt;.as_bytes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// your_preprocess(rgb, w, h) ...&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"frame #{:&amp;gt;2} pts={:?}us {}x{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="nf"&gt;.index&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="nf"&gt;.pts_us&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things worth pointing at. &lt;code&gt;new(input)&lt;/code&gt; takes a path, a URL, or anything convertible into an &lt;code&gt;Input&lt;/code&gt;; the packed layout defaults to &lt;code&gt;Rgb24&lt;/code&gt; and also offers &lt;code&gt;Rgba32&lt;/code&gt; / &lt;code&gt;Gray8&lt;/code&gt;. &lt;code&gt;width(224)&lt;/code&gt; sets only the width and derives the height from the source aspect ratio (internally &lt;code&gt;scale=224:-2&lt;/code&gt;); pin the other side with &lt;code&gt;height&lt;/code&gt;, or give both for an exact size. &lt;code&gt;collect_frames()&lt;/code&gt; gathers a &lt;code&gt;Vec&lt;/code&gt;; swap in &lt;code&gt;frames()&lt;/code&gt; for a streaming iterator (&lt;code&gt;Iterator&amp;lt;Item = Result&amp;lt;VideoFrame&amp;gt;&amp;gt;&lt;/code&gt;) that decodes and yields as you consume, without holding every frame in memory — reach for it when you export dense frames from a long file.&lt;/p&gt;

&lt;p&gt;The shape of this API is not incidental — it deliberately tracks the FFmpeg CLI. The builder chain reads like a command; &lt;code&gt;start_time_us&lt;/code&gt; / &lt;code&gt;duration_us&lt;/code&gt; map to &lt;code&gt;-ss&lt;/code&gt; / &lt;code&gt;-t&lt;/code&gt; (seconds become microseconds); the filter graph it assembles internally is the same &lt;code&gt;scale=…,format=rgb24&lt;/code&gt; shape you would type on the command line, plus explicit color-management parameters. That "move the CLI into your Rust process unchanged" throughline is an argument this series' ecosystem survey makes at length; here it is enough to say your command-line muscle memory stays valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sampling: how many frames, and which ones
&lt;/h2&gt;

&lt;p&gt;Half of frame extraction is deciding &lt;em&gt;which&lt;/em&gt; frames. &lt;code&gt;Sampling&lt;/code&gt; lays the common strategies out:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Typical use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;All&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;every decoded frame (default)&lt;/td&gt;
&lt;td&gt;dense export, per-frame analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EveryNth(n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one frame in every n&lt;/td&gt;
&lt;td&gt;decimate by frame count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EverySec(k)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one frame per k seconds (float)&lt;/td&gt;
&lt;td&gt;timeline scrubbing, preview strips&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KeyframesOnly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;keyframes only&lt;/td&gt;
&lt;td&gt;shot/scene proxies, &lt;strong&gt;fast at decode time&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UniformN(n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;exactly n&lt;/strong&gt; frames spread by time&lt;/td&gt;
&lt;td&gt;VLM / CLIP fixed-budget input&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two deserve a note. &lt;code&gt;KeyframesOnly&lt;/code&gt; pins the decoder option &lt;code&gt;skip_frame=nokey&lt;/code&gt;, so non-keyframes are skipped &lt;em&gt;during&lt;/em&gt; decode — not decoded and then discarded, not decoded at all — which is where its speed comes from. &lt;code&gt;UniformN(n)&lt;/code&gt; guarantees &lt;strong&gt;exactly n frames&lt;/strong&gt;: an input too short to hold n distinct frames pads by repeating nearby frames (each repeat keeps its source frame's &lt;code&gt;pts_us&lt;/code&gt;), so the "give me exactly 32" a model pipeline was promised is always honored, with no padding logic on the caller's side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Color: HD converts as BT.709 by default, and that is not a footnote
&lt;/h2&gt;

&lt;p&gt;Back to the color trap. YUV→RGB needs a coefficient set: SD video uses BT.601, HD video uses BT.709, and picking the wrong one skews saturated colors. Many "decode to RGB" shortcuts apply BT.601 to everything — fine for SD, wrong for 1080p, and silent about it; you only see it if you put the result on screen.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FrameExtractor&lt;/code&gt; converts per the frame's own color tags by default (&lt;code&gt;ColorPolicy::Tagged&lt;/code&gt;): an HD frame tagged BT.709 converts as BT.709 — no guessing, no one-size-fits-all. This is the behavior in the module I am most willing to stand behind. For contrast: &lt;code&gt;to_ndarray('rgb24')&lt;/code&gt; in &lt;em&gt;older&lt;/em&gt; PyAV releases applied BT.601 across the board (that qualifier matters — newer PyAV changed it; do not quote it as current). For untagged frames there is &lt;code&gt;TaggedOrResolutionGuess&lt;/code&gt; (guess by resolution: BT.709 at height ≥ 720), or &lt;code&gt;Force { matrix, range }&lt;/code&gt; to pin one interpretation for the whole run. Most people never touch the default tier — but knowing it gets this right for you is worth the paragraph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should not use it
&lt;/h2&gt;

&lt;p&gt;The usual rule: point outward first, not just at my own crate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The thumbnail is a file on disk, the recipe exists, and you convert once.&lt;/strong&gt; &lt;code&gt;ffmpeg -i in.mp4 -vf "select=..." -vsync vfr out_%03d.png&lt;/code&gt; is a one-liner; pulling in a crate for it is a detour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want frames but will not touch FFI or linking, and a subprocess is fine.&lt;/strong&gt; &lt;code&gt;ffmpeg-sidecar&lt;/code&gt; (~131K downloads/month) wraps any video as an RGB frame iterator, links no libav, and is genuinely pleasant — at the cost of an ffmpeg binary at runtime and frames crossing a process boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video only, WIP acceptable.&lt;/strong&gt; &lt;code&gt;video-rs&lt;/code&gt; iterates RGB ndarrays in ten lines (~32K downloads/month); it has no audio API and self-describes as work-in-progress, but it is strong at its single subject. To be clear: Rust has &lt;strong&gt;never&lt;/strong&gt; been unable to hand you decoded frames — several paths can. &lt;code&gt;FrameExtractor&lt;/code&gt;'s difference is doing it in-process, with sampling policies and color management, in one line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want decord-style GPU batch decoding.&lt;/strong&gt; That is a different capability tier; no parity is claimed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And three plain facts to close on. One: the compile-and-link pain, ez-ffmpeg removes none of it — it still links libav, and the linking gauntlet is unchanged. Two: the project is ~340 stars, still early-stage, and a "safe Rust API" is not the same as "no C CVEs" — libav is underneath. Three: the whole &lt;code&gt;frame_export&lt;/code&gt; module — &lt;code&gt;FrameExtractor&lt;/code&gt; along with its siblings &lt;code&gt;SampleExtractor&lt;/code&gt; (audio → 16 kHz PCM) and &lt;code&gt;VideoWriter&lt;/code&gt; (frame-push encoding) — is still marked experimental in the docs: 0.15 settled the behavioral semantics (default precision, sampling), but the API shape may still shift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Frames never leave the process, one call gets them, the color follows the tags, and the conversion stays on swscale's fast path — the decade-old "render to images, stitch with the CLI" cottage industry can retire. If you are doing ML in Rust, extracting frames should not send you back out to a subprocess.&lt;/p&gt;

&lt;p&gt;Runnable examples are all in the repo: &lt;code&gt;examples/uniform_thumbnails&lt;/code&gt; (UniformN into a 4×3 contact sheet), &lt;code&gt;examples/extract_rgb_frames&lt;/code&gt; (RGB to PPM), &lt;code&gt;examples/keyframe_thumbnails&lt;/code&gt; (keyframe proxies), &lt;code&gt;examples/frame_sampling&lt;/code&gt; (the sampling strategies side by side). The crate lives at &lt;a href="https://github.com/YeautyYE/ez-ffmpeg" rel="noopener noreferrer"&gt;github.com/YeautyYE/ez-ffmpeg&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;An open question I would genuinely like answered: in your Rust ML pipeline today, is frame extraction a shell-out, a hand-written decode loop, or something else? And if I've gotten anything above wrong, tell me and I'll fix it.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ffmpeg</category>
      <category>frame</category>
      <category>media</category>
    </item>
    <item>
      <title>A CLI-compat layer that refuses half your commands, on purpose</title>
      <dc:creator>Yeauty YE</dc:creator>
      <pubDate>Sat, 25 Jul 2026 08:18:42 +0000</pubDate>
      <link>https://dev.to/yeauty/a-cli-compat-layer-that-refuses-half-your-commands-on-purpose-8lo</link>
      <guid>https://dev.to/yeauty/a-cli-compat-layer-that-refuses-half-your-commands-on-purpose-8lo</guid>
      <description>&lt;p&gt;Disclosure first: I maintain the Rust crate &lt;code&gt;ez-ffmpeg&lt;/code&gt; (no relation to the JavaScript "Ez FFmpeg" that once hit the HN front page). That is why the "who should not use it" section is the honest one.&lt;/p&gt;

&lt;p&gt;What you type into a search box is never "how do I initialize an AVFormatContext" — it is "ffmpeg extract audio command." You already know the command; what stops you in a Rust project is the mapping: which API does this command correspond to? For the last year this article would have been a hand-written lookup table — you find your row, you translate &lt;code&gt;-c:v libx264 -crf 23&lt;/code&gt; into builder calls one flag at a time. ez-ffmpeg 0.15's &lt;code&gt;cli&lt;/code&gt; feature does something less tedious and more interesting: &lt;strong&gt;paste the command string, and it either runs it in-process (&lt;code&gt;from_cli&lt;/code&gt;) or translates it into compile-ready Rust (&lt;code&gt;emit_rust_code&lt;/code&gt;).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The interesting part is not that it translates — it is the &lt;em&gt;attitude&lt;/em&gt; with which it translates. It sorts every command into three classes: &lt;strong&gt;verified&lt;/strong&gt; (safe to run), &lt;strong&gt;unverified&lt;/strong&gt; (generates scaffolding code with a loud warning, and refuses to execute), and &lt;strong&gt;unrecognized&lt;/strong&gt; (rejected on the spot with a token-anchored, typed error). It would rather refuse you clearly than "run, but come out subtly different from ffmpeg" — because that second failure mode is the one that destroys trust. This post is about that contract: how to use it, and why it is built this way.&lt;/p&gt;

&lt;h2&gt;
  
  
  from_cli: paste the command, run it in-process
&lt;/h2&gt;

&lt;p&gt;Enable the &lt;code&gt;cli&lt;/code&gt; feature (libav is still linked underneath, so FFmpeg 7.1–8.x must be installed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;ez-ffmpeg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"cli"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A transcode command, pasted in and run — no subprocess:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;cli&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;from_cli&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;from_cli&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ffmpeg -i input.mp4 -c:v libx264 -crf 28 -preset veryfast -c:a aac -y output.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran exactly this against a real audio+video clip: &lt;code&gt;from_cli&lt;/code&gt; parses, classifies, builds the pipeline, and &lt;code&gt;output.mp4&lt;/code&gt; lands — ffprobe confirms H.264 video plus AAC audio, duration intact. The whole chain runs in your own process: no &lt;code&gt;Command::new("ffmpeg")&lt;/code&gt;, no stderr scraping. The string form handles quotes and backslash escapes by POSIX rules; to avoid quoting entirely, pass argv directly with &lt;code&gt;from_cli_args(&amp;amp;["-i", "input.mp4", ...])&lt;/code&gt; — zero quoting ambiguity, the shape &lt;code&gt;ffmpeg.wasm&lt;/code&gt; also chose.&lt;/p&gt;

&lt;p&gt;One prerequisite up front: &lt;strong&gt;execution currently requires an FFmpeg 7.1 runtime.&lt;/strong&gt; Link any other version (including 8.x) and it fails with &lt;code&gt;UnverifiedRuntimeProfile&lt;/code&gt; before any I/O — deliberately, for reasons the "gates" section below covers.&lt;/p&gt;

&lt;h2&gt;
  
  
  emit_rust_code: or translate it to code
&lt;/h2&gt;

&lt;p&gt;Sometimes you do not want it to run the command for you — you want the builder code, to extend and maintain. Same command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;cli&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;emit_rust_code&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;emit_rust_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"ffmpeg -i in.mkv -c:v libx264 -crf 23 -preset fast -c:a aac -y out.mp4"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{code}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What prints is a complete, compile-ready program (real output — pinned byte-for-byte by the repository's tests):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Generated from an ffmpeg command by the ez-ffmpeg CLI-compat emitter.&lt;/span&gt;
&lt;span class="c1"&gt;// command: ffmpeg -i in.mkv -c:v libx264 -crf 23 -preset fast -c:a aac -y out.mp4&lt;/span&gt;
&lt;span class="c1"&gt;// dialect: ffmpeg 7.1 command line; manifest: r4; crate: ez-ffmpeg 0.15.0; cargo features: none required&lt;/span&gt;
&lt;span class="c1"&gt;// status: verified shape V1 (H.264/AAC transcode (crf + preset)) — verified by the manifest-driven semantic golden suite (oracle: Transcode) against the ffmpeg CLI; ...&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"in.mkv"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nn"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"out.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;.set_video_codec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"libx264"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// -c:v libx264&lt;/span&gt;
                &lt;span class="nf"&gt;.set_audio_codec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"aac"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// -c:a aac&lt;/span&gt;
                &lt;span class="nf"&gt;.set_video_codec_opt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"crf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"23"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// -crf 23&lt;/span&gt;
                &lt;span class="nf"&gt;.set_video_codec_opt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"preset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"fast"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// -preset fast&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the header comments: the original command, the dialect version (ffmpeg 7.1), the manifest revision, the crate version, and the line that matters most — this shape's &lt;em&gt;verification status&lt;/em&gt;. This one is labeled &lt;code&gt;verified shape V1&lt;/code&gt;: it passed a semantic golden suite that compares stream identity, codecs, dimensions, durations, and playlist topology against the actual ffmpeg CLI. The translation is not "looks about right"; it is "reconciled against the oracle."&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract: classify everything, approximate nothing
&lt;/h2&gt;

&lt;p&gt;The design premise is stated plainly in the module docs: &lt;strong&gt;broad CLI compatibility is a non-goal.&lt;/strong&gt; The ffmpeg CLI is ~14k lines of option machinery whose semantics shift with every release; chasing it wholesale produces the "runs, but subtly different" failures that destroy trust. So it does the opposite — every argv token must classify against a versioned compatibility manifest, or the &lt;em&gt;entire&lt;/em&gt; command is rejected with a token-anchored typed diagnostic. Nothing is dropped, nothing is guessed. Three outcomes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;① Verified shape → runs.&lt;/strong&gt; V1 above. Six verified shapes today (V1–V6): H.264/AAC transcode, re-encoded clip, audio extract, single-frame thumbnail, scaled transcode, single-rendition VOD HLS — each backed by a semantic golden. Six sounds small; what the number means is that every one of them is reconciled. Widening the subset costs one golden lane per shape, and the manifest grows release by release — breadth is bought with verification, never with approximation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;② Unverified shape → translated, never executed.&lt;/strong&gt; For example, a transcode that names codecs but omits crf/preset. &lt;code&gt;emit_rust_code&lt;/code&gt; still hands you code — but the header flips to a loud warning (real output):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// status: UNVERIFIED SCAFFOLDING — manifest entry U19 (audio+video codec selection only).&lt;/span&gt;
&lt;span class="c1"&gt;// This shape has no semantic golden. The code below compiles against the&lt;/span&gt;
&lt;span class="c1"&gt;// ez-ffmpeg builder API, but its behavior has NOT been checked against the&lt;/span&gt;
&lt;span class="c1"&gt;// ffmpeg CLI and must not be treated as a faithful translation. Review every&lt;/span&gt;
&lt;span class="c1"&gt;// call before use; in-process execution (from_cli / from_cli_args) refuses&lt;/span&gt;
&lt;span class="c1"&gt;// this shape.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;from_cli&lt;/code&gt; refuses to run it — the error's first line is &lt;code&gt;command shape is not verified for execution&lt;/code&gt;, followed by the parsed options. It gives you scaffolding as a starting point, but says plainly: "I have not reconciled this one — review it yourself."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;③ Unrecognized → rejected on the spot, anchored to the token.&lt;/strong&gt; A command with an option outside the subset — say &lt;code&gt;ffmpeg -i in.mp4 -bogus 1 -y out.mp4&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unsupported option `-bogus` (token #2, output #0)
  this option is not in the CLI-compat subset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not "ignore that option and run anyway" — the whole command is rejected, and it tells you which token broke it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few honest gates
&lt;/h2&gt;

&lt;p&gt;Beyond the three-way sort, a handful of "refuse rather than fudge" decisions say the most about this layer's character:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-y&lt;/code&gt; is mandatory.&lt;/strong&gt; Omit it and you get &lt;code&gt;missing mandatory `-y`&lt;/code&gt;. The reasoning is concrete: without &lt;code&gt;-y&lt;/code&gt; the ffmpeg CLI prompts before overwriting, and this library always creates/truncates output — it cannot reproduce that prompt, so it requires you to make the overwrite explicit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No shell emulation.&lt;/strong&gt; The string form does POSIX word-splitting only. A pipe in the command? A tokenize error anchored to the exact byte: &lt;code&gt;shell operator; pipes, redirects, command lists and subshells are not ffmpeg options&lt;/code&gt; — rejected, never emulated. It translates ffmpeg, not your shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strict AVOptions.&lt;/strong&gt; CLI-initiated pipelines run in strict mode: an option no component consumed fails the whole command (fftools &lt;code&gt;check_avoptions&lt;/code&gt; parity), instead of the default builder path's warning. "That parameter silently did nothing" is not something you can miss here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime-profile gate.&lt;/strong&gt; Execution additionally requires a verified runtime profile of the linked FFmpeg (currently 7.1 only; 8.1 joins once its version-matched golden lane passes). Anything else fails with &lt;code&gt;UnverifiedRuntimeProfile&lt;/code&gt; before any I/O.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The command layout is fixed too: exactly one &lt;code&gt;-i&lt;/code&gt; input and one output path, in canonical order; the &lt;code&gt;-&lt;/code&gt;/stdin/stdout pseudo-paths are excluded — pipe I/O is process wiring, not part of the in-process subset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond the subset: the builder is the full surface
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;from_cli&lt;/code&gt; / &lt;code&gt;emit_rust_code&lt;/code&gt; are an &lt;em&gt;on-ramp&lt;/em&gt;, not the whole road. The subset covers the most common shapes; the full surface is the builder API itself. And migrating from the CLI to the builder needs only three conversion rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Seconds become microseconds&lt;/strong&gt;: &lt;code&gt;-ss 10&lt;/code&gt; → &lt;code&gt;set_start_time_us(10_000_000)&lt;/code&gt; (the &lt;code&gt;_us&lt;/code&gt; suffix is the reminder).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Option names work verbatim, minus the dash&lt;/strong&gt;: &lt;code&gt;-crf 23&lt;/code&gt; → &lt;code&gt;set_video_codec_opt("crf", "23")&lt;/code&gt;, &lt;code&gt;-movflags +faststart&lt;/code&gt; → &lt;code&gt;set_format_opt("movflags", "faststart")&lt;/code&gt; — straight into FFmpeg's AVOption system; whatever name the CLI accepts, this accepts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter strings copy over unchanged&lt;/strong&gt;: the string after &lt;code&gt;-vf&lt;/code&gt; drops into &lt;code&gt;.filter_desc(...)&lt;/code&gt; character for character.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A watermark, for instance — &lt;code&gt;-filter_complex "overlay=10:10"&lt;/code&gt; is multiple &lt;code&gt;.input()&lt;/code&gt; calls mapping to &lt;code&gt;[0]&lt;/code&gt;/&lt;code&gt;[1]&lt;/code&gt;, filter string copied as-is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nn"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"logo.png"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.filter_desc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[0:v][1:v]overlay=10:10"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"watermarked.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full mapping of ~50 CLI options and patterns (with each one's implementation and known gaps) lives in the &lt;a href="https://docs.rs/ez-ffmpeg/latest/ez_ffmpeg/" rel="noopener noreferrer"&gt;CLI-to-API mapping on docs.rs&lt;/a&gt;. Emitted scaffolding plus that table is the complete "any common command → maintainable Rust" path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should not use it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-off jobs: use the CLI.&lt;/strong&gt; Transcode one file, try one filter, verify one parameter — a terminal command is the shortest path; a Rust program is the detour. This layer is for commands you run repeatedly in a program, or want to grow into Rust code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The full power of ffmpeg: &lt;code&gt;from_cli&lt;/code&gt; cannot give it.&lt;/strong&gt; It is a &lt;em&gt;deliberately narrow&lt;/em&gt; trusted subset — six verified shapes, a 7.1 runtime. Two-pass encoding, complex &lt;code&gt;-filter_complex&lt;/code&gt;, hardware acceleration, device capture are outside the subset and rejected outright. For the full CLI surface, shell out, or write the builder directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three honest facts&lt;/strong&gt;: the compile/link-libav pain is untouched by this layer; &lt;code&gt;cli&lt;/code&gt; is an optional feature you must enable; and the CLI-compat layer is young — the manifest grows release by release.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Paste the command and it either runs it for you or hands you the code — but it never pretends to have translated something it did not verify. A tool that will say "I have not reconciled this one; review it yourself" is more trustworthy than one that runs anything. Hand-mapping CLI to API is now only needed outside the subset.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;cli&lt;/code&gt; feature is exercised in the repo's &lt;code&gt;examples/cli_emitted_*&lt;/code&gt; (the pinned translations of all six verified shapes) and the CLI-to-API mapping on docs.rs. The crate lives at &lt;a href="https://github.com/YeautyYE/ez-ffmpeg" rel="noopener noreferrer"&gt;github.com/YeautyYE/ez-ffmpeg&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;An open question: which ffmpeg command do you most want to paste straight into Rust and run? Drop it in the comments — and if I've gotten anything above wrong, tell me and I'll fix it.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>rust</category>
      <category>cli</category>
      <category>media</category>
    </item>
    <item>
      <title>Feed audio to Whisper in Rust without ever shelling out to ffmpeg</title>
      <dc:creator>Yeauty YE</dc:creator>
      <pubDate>Fri, 24 Jul 2026 10:39:33 +0000</pubDate>
      <link>https://dev.to/yeauty/feed-audio-to-whisper-in-rust-without-ever-shelling-out-to-ffmpeg-4if5</link>
      <guid>https://dev.to/yeauty/feed-audio-to-whisper-in-rust-without-ever-shelling-out-to-ffmpeg-4if5</guid>
      <description>&lt;p&gt;&lt;code&gt;ez-ffmpeg&lt;/code&gt; is a Rust crate that runs FFmpeg pipelines inside your process — linked libav libraries behind a high-level API, no subprocess (and no relation to the JavaScript "Ez FFmpeg" project). The narrow claim of this post: its 0.14 &lt;code&gt;SampleExtractor&lt;/code&gt; turns a video file into the 16 kHz mono f32 PCM that Whisper-family models consume, in one call, verified end-to-end against whisper-rs 0.16 on a real MP4 — full program, real timings, and the verbatim transcript below. It's on &lt;a href="https://crates.io/crates/ez-ffmpeg" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt; and &lt;a href="https://github.com/YeautyYE/ez-ffmpeg" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here's the itch. Speech-to-text in Rust is in decent shape — whisper-rs is maintained, candle and ort keep growing — yet if your input is an ordinary MP4 rather than 16 kHz mono PCM, nearly every setup guide in the ML-audio ecosystem starts with the same shell line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;.mp4 &lt;span class="nt"&gt;-ar&lt;/span&gt; 16000 &lt;span class="nt"&gt;-ac&lt;/span&gt; 1 &lt;span class="nt"&gt;-c&lt;/span&gt;:a pcm_s16le out.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step one of your "pure Rust" transcription pipeline is to leave Rust. Even "FFmpeg 8.0 adds Whisper support" making the HN front page doesn't change that for you: that news serves CLI users, while a Rust program wants decoded samples in its own memory, not one more command to fork.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline everyone actually ships
&lt;/h2&gt;

&lt;p&gt;Whisper's input contract is fixed: 16 kHz, mono, f32 samples normalized to [-1, 1] — that's the &lt;code&gt;&amp;amp;[f32]&lt;/code&gt; in whisper-rs's &lt;code&gt;full()&lt;/code&gt;. Your MP4 most likely holds 44.1 or 48 kHz stereo AAC. So the standard recipe forks ffmpeg and cleans up afterwards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The "leave Rust first" pipeline: subprocess + temp file + wav parsing.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;process&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ffmpeg"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.args&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"in.mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-ar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"16000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-ac"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s"&gt;"-c:a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pcm_s16le"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"out.wav"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;.status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// hope ffmpeg is on PATH, hope the flags are right&lt;/span&gt;
&lt;span class="c1"&gt;// ...then open out.wav, skip the header, convert i16 -&amp;gt; f32 by hand&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works, and it taxes you four ways. Deployment: every target machine and container image needs an &lt;code&gt;ffmpeg&lt;/code&gt; executable, and its version is now your problem. Errors: a failed subprocess hands you an exit code and a pile of stderr text — distinguishing "file not found" from "no audio stream" means regexing log lines instead of matching on a typed error. Files: a temporary wav has to be written, named, and cleaned up, and concurrent jobs must not collide. Format: &lt;code&gt;pcm_s16le&lt;/code&gt; comes back as i16, so you divide by 32768 yourself. Crates like ffmpeg-sidecar wrap this subprocess dance neatly, but the first three taxes are inherent to the model: there's still an external binary at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  One call, in-process
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;in.mp4 ─▶ demux ─▶ decode(AAC) ─▶ resample(16 kHz mono f32) ─▶ Vec&amp;lt;f32&amp;gt;
          └────── SampleExtractor::for_whisper(), one call ──────┘
Vec&amp;lt;f32&amp;gt; ─▶ whisper-rs full() ─▶ transcript   (no subprocess, no temp file)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SampleExtractor&lt;/code&gt; collapses demux → decode → resample → downmix into a single in-process call — internally it's a normal FFmpeg pipeline with a sink on the end that hands the samples back to you. &lt;code&gt;for_whisper&lt;/code&gt; presets exactly the contract above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;SampleExtractor&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;for_whisper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"in.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.collect_samples&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two honest caveats before the code. First, installing and linking FFmpeg is not solved here — the crate sits on the libav libraries, so FFmpeg 7.1–8.x must be present, and that pain is unchanged. What goes away is the subprocess, the temp file, and the stderr scraping — not the FFmpeg dependency itself. Second, the frame/sample export API is marked &lt;strong&gt;experimental&lt;/strong&gt; in the README and may be reshaped in a future minor release.&lt;/p&gt;

&lt;p&gt;The complete program, two phases — extract PCM, feed whisper-rs 0.16:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[package]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"mp4-to-text"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;edition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2021"&lt;/span&gt;

&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;ez-ffmpeg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.14"&lt;/span&gt;    &lt;span class="c"&gt;# needs FFmpeg 7.1-8.x installed (links libav)&lt;/span&gt;
&lt;span class="py"&gt;whisper-rs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.16"&lt;/span&gt;   &lt;span class="c"&gt;# needs cmake + C/C++ toolchain (builds whisper.cpp)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;frame_export&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SampleExtractor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;whisper_rs&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FullParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SamplingStrategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WhisperContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WhisperContextParameters&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Phase 1: decode the video's audio track into 16 kHz mono f32 PCM.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;t_extract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;SampleExtractor&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;for_whisper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"jfk.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.collect_samples&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;extract_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t_extract&lt;/span&gt;&lt;span class="nf"&gt;.elapsed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.as_secs_f64&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1000.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pcm samples : {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pcm&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"pcm duration: {:.3} s (len / 16000.0, mono)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;pcm&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;16_000.0&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"extract time: {extract_ms:.1} ms"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Phase 2: transcribe the PCM with whisper.cpp via whisper-rs.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;t_whisper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;WhisperContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_with_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"ggml-tiny.en.bin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nn"&gt;WhisperContextParameters&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="nf"&gt;.create_state&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;FullParams&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SamplingStrategy&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Greedy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;best_of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="nf"&gt;.set_language&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="nf"&gt;.set_print_special&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="nf"&gt;.set_print_progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="nf"&gt;.set_print_realtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="nf"&gt;.set_print_timestamps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="nf"&gt;.full&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;n_segments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="nf"&gt;.full_n_segments&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;n_segments&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="nf"&gt;.get_segment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="nf"&gt;.push_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="nf"&gt;.to_str_lossy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;whisper_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t_whisper&lt;/span&gt;&lt;span class="nf"&gt;.elapsed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.as_secs_f64&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1000.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"whisper time: {whisper_ms:.1} ms"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"transcript  : {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="nf"&gt;.trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call by call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SampleExtractor::for_whisper("jfk.mp4")&lt;/code&gt;&lt;/strong&gt; — a thin preset, equivalent to &lt;code&gt;SampleExtractor::new(...).sample_rate(16000).channels(Mono)&lt;/code&gt;. It is worth pointing out that the base builder's defaults &lt;em&gt;preserve the source&lt;/em&gt;: source sample rate and channel layout pass through untouched, and only the sample format is pinned to packed f32. Resampling and downmixing are opt-in, so music and analysis users don't get silently converted to 16 kHz; an ASR preset is where that normalization belongs, explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.collect_samples()?&lt;/code&gt;&lt;/strong&gt; — runs the pipeline and returns one flat interleaved &lt;code&gt;Vec&amp;lt;f32&amp;gt;&lt;/code&gt;. Mono means samples == frames, so &lt;code&gt;len() / 16000.0&lt;/code&gt; is the duration — the program prints exactly that. The whole track sits in memory, so for hours-long input switch the same builder to &lt;code&gt;.samples()?&lt;/code&gt;: a streaming iterator that yields chunks as they decode, carries rate/channels/timestamp metadata on each chunk, keeps a small bounded number in flight (default 4), and aborts the run cleanly if you drop it early. Errors have defined landing spots too: option mistakes and "no audio stream" surface when you start the run, mid-decode failures arrive as the iterator's terminal error — typed, not scraped from stderr. &lt;code&gt;start_time_us&lt;/code&gt; / &lt;code&gt;duration_us&lt;/code&gt; carve out a time window when you chunk long audio for the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WhisperContext::new_with_params&lt;/code&gt; + &lt;code&gt;create_state&lt;/code&gt;&lt;/strong&gt; — loads the ggml model from disk and allocates inference state; most of the "whisper time" below lives here and in &lt;code&gt;full&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;FullParams::new(SamplingStrategy::Greedy { best_of: 1 })&lt;/code&gt;&lt;/strong&gt; — the cheapest decoding strategy; &lt;code&gt;set_language(Some("en"))&lt;/code&gt; matches the English-only tiny.en model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The five &lt;code&gt;set_print_*(false)&lt;/code&gt; calls&lt;/strong&gt; — whisper.cpp prints transcription progress to stderr by default, so an embedded library behaves like a chatty CLI; these turn that off. Model loading logs another ~30 lines through a separate path; &lt;code&gt;whisper_rs::install_logging_hooks&lt;/code&gt; silences those.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The segment loop&lt;/strong&gt; — &lt;code&gt;full()&lt;/code&gt;, &lt;code&gt;full_n_segments()&lt;/code&gt;, &lt;code&gt;get_segment(i)&lt;/code&gt;, &lt;code&gt;to_str_lossy()&lt;/code&gt; — is the whisper-rs 0.16 API. It looks unremarkable, and it is exactly where most published examples stop compiling. That deserves its own section.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The trap nobody documents: whisper-rs 0.16 API drift
&lt;/h2&gt;

&lt;p&gt;The ez-ffmpeg half of this program gave me nothing to write about: the documented one-liner compiled and ran correctly on the first try. Every wall I hit was on the whisper-rs half — starting with my first draft, which followed an older tutorial and failed to compile: one method not found, one &lt;code&gt;?&lt;/code&gt; where &lt;code&gt;?&lt;/code&gt; no longer applies. Checking the current docs turned up three changes that most pre-0.16 blog posts trip over:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;full()&lt;/code&gt; returns &lt;code&gt;Result&amp;lt;(), _&amp;gt;&lt;/code&gt; — apply &lt;code&gt;?&lt;/code&gt; and move on; don't expect a number back.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;full_n_segments()&lt;/code&gt; returns a plain &lt;code&gt;i32&lt;/code&gt; — a trailing &lt;code&gt;?&lt;/code&gt; is now a compile error.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;full_get_segment_text()&lt;/code&gt; is gone — use &lt;code&gt;get_segment(i)&lt;/code&gt;, which returns &lt;code&gt;Option&amp;lt;WhisperSegment&amp;gt;&lt;/code&gt;, then &lt;code&gt;to_str()&lt;/code&gt; / &lt;code&gt;to_str_lossy()&lt;/code&gt; for the text.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The segment loop in the listing above is the current, correct form. The general lesson I took: for a fast-moving crate, trust the method signatures on docs.rs for the exact version in your lockfile, and treat blog posts as intent, not syntax. One more orientation note: whisper-rs's GitHub repository is archived, but the project is not dead — it moved to Codeberg and is maintained there. Most of the tutorials a search engine surfaces predate these changes. (A caveat on my own account: I reconstructed this API history from a single migration, so treat it as one data point, not a changelog.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;Test input: an 11-second clip of JFK's "ask not" line packed into an MP4 (h264 video + AAC audio track). Model: ggml-tiny.en.bin, 77,704,715 bytes. Two consecutive runs produced identical sample counts and identical transcripts; here is the second run's output, verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pcm samples : 176128
pcm duration: 11.008 s (len / 16000.0, mono)
extract time: 7.8 ms
whisper time: 635.6 ms
transcript  : And so my fellow Americans ask not what your country can do for you ask what you can do for your country.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transcript is correct, whole sentence, first pass. The numbers reconcile too: 176,128 ÷ 16,000 = 11.008 s. The source wav was 11.00 s; the extra ≈8 ms is AAC encoder priming/padding introduced when the clip was remuxed — alignment samples the codec pads at the edges of the track, entirely normal. Which is why duration checks against extracted PCM should assert ≈, never == — an exact-equality assertion dies on those 8 ms.&lt;/p&gt;

&lt;p&gt;Timings, with the full caveat attached: one machine, an 8-core/16-thread Ryzen 9 5900HX, CPU-only, two consecutive runs. This is a smoke test, not a benchmark.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Run 1 (cold)&lt;/th&gt;
&lt;th&gt;Run 2 (warm)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PCM extraction (&lt;code&gt;SampleExtractor&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;16.3 ms&lt;/td&gt;
&lt;td&gt;7.8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whisper phase (incl. loading the 77 MB model from disk)&lt;/td&gt;
&lt;td&gt;1463.9 ms&lt;/td&gt;
&lt;td&gt;635.6 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two readings. The cold/warm gap in the whisper row is mostly the 77 MB model file being in the OS cache the second time — don't read it as inference getting faster. And for this 11-second clip, extraction is single-digit-to-low-double-digit milliseconds while the model dominates — the PCM step is no longer the thing worth optimizing in this pipeline. Batch throughput and long-file behavior weren't measured here, so no numbers are claimed for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should not use this
&lt;/h2&gt;

&lt;p&gt;Pure Rust can pull the audio track out of an MP4/MKV and resample it to 16 kHz (symphonia + rubato; symphonium does both in one call; mutter feeds whisper-rs in five lines). But "any video, any codec, one line" hits a codec wall: symphonia still lists HE-AAC and Opus as unfinished and has no AC-3 at all — which is exactly where an FFmpeg backend remains irreplaceable.&lt;/p&gt;

&lt;p&gt;So the split is by scenario, and each path has its win. One file, one time: type the ffmpeg command in a terminal; writing a Rust program for a run-once job is a detour. Inputs you control (WAV, FLAC, ordinary AAC) and a zero-C-dependency build as a goal: symphonia + rubato is the right road, and linking FFmpeg buys you nothing. Inputs you don't control — user uploads in whatever container and codec they arrive in — FFmpeg's codec coverage is the point, and that is the case for an FFmpeg-backed crate like ez-ffmpeg. And keep one asterisk in view: even with a pure-Rust audio path, whisper-rs itself compiles whisper.cpp, so "no C toolchain anywhere" was never on the table for this pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the first build costs
&lt;/h2&gt;

&lt;p&gt;Honesty about the build, since the Cargo.toml comments above compress it: whisper-rs builds whisper.cpp from source, which needs cmake and a C/C++ toolchain, and the first build takes minutes — it is not hung. On PEP 668 distros (Ubuntu 24.04 and later), &lt;code&gt;pip install --user cmake&lt;/code&gt; is refused by the system Python; install cmake inside a venv or just use your distro's cmake package. The ez-ffmpeg side needs FFmpeg 7.1–8.x present for linking, as covered earlier — that prerequisite predates this crate and survives it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;Your transcription pipeline now has one subprocess, one temp file, and one hand-rolled i16→f32 conversion less, and its failures arrive as typed &lt;code&gt;Result&lt;/code&gt;s. From here: switch to &lt;code&gt;samples()&lt;/code&gt; plus &lt;code&gt;start_time_us&lt;/code&gt;/&lt;code&gt;duration_us&lt;/code&gt; to stream and chunk long recordings, or hand the same &lt;code&gt;Vec&amp;lt;f32&amp;gt;&lt;/code&gt; to other audio models via candle or ort — 16 kHz mono f32 is the common handshake shape.&lt;/p&gt;

&lt;p&gt;The code lives at &lt;a href="https://github.com/YeautyYE/ez-ffmpeg" rel="noopener noreferrer"&gt;github.com/YeautyYE/ez-ffmpeg&lt;/a&gt;; &lt;code&gt;examples/extract_whisper_pcm&lt;/code&gt; in the repository runs as-is.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>whisper</category>
      <category>ffmpeg</category>
    </item>
    <item>
      <title>🚀 Double Your Performance with One Line of Code? The Memory Superpower Every Rust Developer Should Know!</title>
      <dc:creator>Yeauty YE</dc:creator>
      <pubDate>Wed, 02 Jul 2025 11:42:11 +0000</pubDate>
      <link>https://dev.to/yeauty/double-your-performance-with-one-line-of-code-the-memory-superpower-every-rust-developer-should-1g93</link>
      <guid>https://dev.to/yeauty/double-your-performance-with-one-line-of-code-the-memory-superpower-every-rust-developer-should-1g93</guid>
      <description>&lt;p&gt;In the world of high-performance Rust programming, you often hear about &lt;em&gt;hardcore&lt;/em&gt; optimization techniques: asynchronous magic with Tokio, blazing-fast SIMD instructions, lock-free concurrency, and more.&lt;/p&gt;

&lt;p&gt;Yet there’s one &lt;strong&gt;often-overlooked tool&lt;/strong&gt; lurking in your stack that can deliver a massive, sometimes &lt;em&gt;game-changing&lt;/em&gt; performance boost with &lt;em&gt;minimal&lt;/em&gt; effort:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;The Memory Allocator.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, really—by simply swapping out Rust’s default allocator, your application can achieve &lt;strong&gt;multi-fold throughput improvements&lt;/strong&gt; and &lt;strong&gt;significantly lower latency&lt;/strong&gt; in high-concurrency or large-data workloads. This isn’t hype—it’s backed by serious benchmarks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Authoritative Benchmark Highlights&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.microsoft.com/en-us/research/uploads/prod/2019/06/mimalloc-tr-v1.pdf" rel="noopener noreferrer"&gt;&lt;strong&gt;Microsoft mimalloc report&lt;/strong&gt;&lt;/a&gt;: Under heavy multithreaded workloads on Linux, mimalloc delivered &lt;strong&gt;5.3× faster average performance&lt;/strong&gt; compared to glibc malloc, while cutting &lt;strong&gt;RSS memory usage by ~50%&lt;/strong&gt;. More performance, less cost.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/tpn/pdfs/blob/master/A%20Scalable%20Concurrent%20malloc%20Implementation%20for%20FreeBSD%20%28jemalloc%29.pdf" rel="noopener noreferrer"&gt;&lt;strong&gt;jemalloc research paper&lt;/strong&gt;&lt;/a&gt;: In real-world tests on a 4-core server, glibc malloc achieved only &lt;strong&gt;15% of jemalloc’s throughput&lt;/strong&gt;—a night-and-day difference for latency-sensitive services.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this post, you’ll discover &lt;strong&gt;why allocators matter so much&lt;/strong&gt;, what makes modern allocators radically faster, and how you can unlock their benefits with &lt;strong&gt;literally one line of code&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 1. Memory Allocators: Your Program’s &lt;em&gt;Invisible Power Broker&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Whenever you write something like &lt;code&gt;Vec::with_capacity(100)&lt;/code&gt;, you’re calling the allocator: the engine that manages heap memory behind the scenes.&lt;/p&gt;

&lt;p&gt;Rust’s default allocator is usually the system allocator—e.g., glibc malloc on Linux. It’s reliable and general-purpose, but under concurrency, it can quickly become a bottleneck.&lt;/p&gt;




&lt;h3&gt;
  
  
  ❌ The Problem with Traditional Allocators: &lt;em&gt;Global Lock Contention&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;Under high concurrency, all threads compete for &lt;strong&gt;a single global lock&lt;/strong&gt; to allocate or free memory. It’s like the entire country trying to buy train tickets from &lt;em&gt;one window&lt;/em&gt; during the holidays:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      +-------------------------------------------+
      |      Traditional Allocator (glibc malloc) |
      |          One Big Global Lock 🔒           |
      +-------------------------------------------+
          ^          ^          ^          ^
          |          |          |          |
      [Thread1]  [Thread2]   [Thread3]   [Thread4]
      (waiting)  (waiting)   (holding)   (waiting)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: threads pile up, CPUs spend more time context switching than doing real work, and throughput plummets.&lt;/p&gt;




&lt;h3&gt;
  
  
  ✅ Modern Allocators to the Rescue: &lt;em&gt;Thread-Local Caches&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;Allocators like &lt;code&gt;jemalloc&lt;/code&gt; and &lt;code&gt;mimalloc&lt;/code&gt; assign each thread its own fast, lock-free cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+------------------+ +------------------+ +------------------+
| Thread 1 Cache   | | Thread 2 Cache   | | Thread 3 Cache   |
| (No locks here!) | | (No locks here!) | | (No locks here!) |
+------------------+ +------------------+ +------------------+
        |                  |                  |
        +------------------+------------------+
                           |
                           v
             +-----------------------------+
             |    Global Memory Pool       |
             |   (accessed infrequently)   |
             +-----------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most allocations happen &lt;strong&gt;without any locking&lt;/strong&gt;, so threads can operate at full speed. Only when the local cache runs out does it fetch more memory from the global pool.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 2. Why Does Swapping Allocators Make You So Much Faster?
&lt;/h2&gt;

&lt;p&gt;Let’s break down the core reasons:&lt;/p&gt;




&lt;h3&gt;
  
  
  🔒 2.1 Escaping the &lt;em&gt;Global Lock Hell&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;This is the biggest win. With thread-local caches, modern allocators eliminate lock contention, unleashing the full power of multi-core CPUs.&lt;/p&gt;

&lt;p&gt;In languages like Rust or Go that thrive on concurrency, this difference is especially dramatic.&lt;/p&gt;




&lt;h3&gt;
  
  
  🧩 2.2 Fighting Memory Fragmentation
&lt;/h3&gt;

&lt;p&gt;Frequent allocations and deallocations can leave memory full of tiny gaps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Memory State: | Used | Free(1) | Used | Free(2) | Used |
              +------+--------+------+--------+------+

New Request: "I need 3 contiguous blocks..."

Result: Even though total free space &amp;gt;3, allocation fails—classic fragmentation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modern allocators prevent this by using &lt;strong&gt;binning&lt;/strong&gt;: pre-categorizing memory into size classes (e.g., 8B, 16B, 32B) and reusing those buckets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+------------------------------------------------------+
|        Modern Allocator Memory Pools (Size Classes)  |
+------------------------------------------------------+
| [8B Bin][16B Bin][32B Bin][64B Bin][...more bins...] |
+------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This not only reduces fragmentation but also improves cache locality and speeds up memory access.&lt;/p&gt;




&lt;h3&gt;
  
  
  ⚡ 2.3 Optimizing Large Allocations
&lt;/h3&gt;

&lt;p&gt;Traditional allocators often call the kernel (&lt;code&gt;mmap&lt;/code&gt;) every time you need a large chunk of memory—an expensive syscall.&lt;/p&gt;

&lt;p&gt;Modern allocators instead &lt;strong&gt;reserve large arenas up front&lt;/strong&gt; and manage them in user space, reducing system call overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔧 3. Rust Made It Simple: One Line to Swap Your Allocator
&lt;/h2&gt;

&lt;p&gt;Using a faster allocator in Rust is ridiculously easy. For example, to enable &lt;code&gt;mimalloc&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add it to &lt;code&gt;Cargo.toml&lt;/code&gt;:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;mimalloc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Declare the global allocator in your &lt;code&gt;main.rs&lt;/code&gt;:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[global_allocator]&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;GLOBAL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;mimalloc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MiMalloc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;mimalloc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MiMalloc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// No other code changes needed!&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. Build and run—your program is now supercharged.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;em&gt;Note&lt;/em&gt;: You can only have one global allocator per binary. But what if you need to support different allocators per platform without messy &lt;code&gt;#[cfg]&lt;/code&gt; flags everywhere? There’s a better way.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ 4. The Ultimate Solution: &lt;code&gt;auto-allocator&lt;/code&gt;—No More Conditional Compilation Hell
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/YeautyYE/auto-allocator" rel="noopener noreferrer"&gt;&lt;code&gt;auto-allocator&lt;/code&gt;&lt;/a&gt; is a smart Rust library that &lt;strong&gt;auto-detects your target platform&lt;/strong&gt; and picks the best allocator automatically. Whether you’re building high-concurrency servers, mobile apps, WebAssembly frontends, or embedded devices, it just works.&lt;/p&gt;

&lt;p&gt;🌍 &lt;strong&gt;Platform-Aware Optimizations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux/Windows/macOS&lt;/strong&gt;: Enables &lt;code&gt;mimalloc&lt;/code&gt; for up to 6× throughput improvements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS&lt;/strong&gt;: Uses Apple’s optimized &lt;code&gt;libmalloc&lt;/code&gt; for stability and performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android&lt;/strong&gt;: Switches to &lt;code&gt;scudo&lt;/code&gt; for efficient and secure allocation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebAssembly&lt;/strong&gt;: Retains the default allocator for maximum compatibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedded (&lt;code&gt;no_std&lt;/code&gt;)&lt;/strong&gt;: Selects &lt;code&gt;embedded-alloc&lt;/code&gt; for resource-constrained environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How It Works&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Auto-Allocator uses a &lt;em&gt;two-stage optimization pipeline&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🛠️ Compile Time                     ⚡ Runtime                    ✅ Final Result
┌────────────────────────┐          ┌───────────────────────┐     ┌─────────────────────────┐
│ Platform Detection.    │          │ CPU Core Analysis     │     │                         │
│                        │─────────▶│                       │     │                         │
│ Feature Analysis       │          │ Memory Detection      │────▶│ Best Allocator Selected │
│                        │─────────▶│                       │     │                         │
│ Compiler Capabilities  │          │ Hardware Optimization │     │                         │
└────────────────────────┘          └───────────────────────┘     └─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ &lt;em&gt;90% of decisions happen at compile time—no runtime overhead.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Usage:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add it to your &lt;code&gt;Cargo.toml&lt;/code&gt;:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;auto-allocator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Import in &lt;code&gt;main.rs&lt;/code&gt;:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="n"&gt;auto_allocator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;auto_allocator&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get_allocator_info&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"Using allocator: {:?} | Reason: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="py"&gt;.allocator_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="py"&gt;.reason&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"Created Vec with {} elements—allocation optimized automatically!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✨ That’s it. One &lt;code&gt;use&lt;/code&gt; statement—done.&lt;/p&gt;

&lt;p&gt;For additional security (e.g., guard pages, canary checks), enable the secure feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;auto-allocator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"secure"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🏁 5. Wrapping Up: Time for a &lt;em&gt;Free&lt;/em&gt; Performance Upgrade
&lt;/h2&gt;

&lt;p&gt;By swapping your memory allocator, you can unlock:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚀 &lt;strong&gt;Higher throughput&lt;/strong&gt;: Handle more traffic on the same hardware.&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;Lower latency and costs&lt;/strong&gt;: Faster response times and reduced memory footprint.&lt;/li&gt;
&lt;li&gt;🧩 &lt;strong&gt;Better stability&lt;/strong&gt;: Less fragmentation over long-running workloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you care about performance, this is one of the &lt;em&gt;highest ROI optimizations you’ll ever make.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👇 Give it a try—add &lt;code&gt;auto-allocator&lt;/code&gt; to your &lt;code&gt;Cargo.toml&lt;/code&gt; today and see the results yourself!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/YeautyYE/auto-allocator" rel="noopener noreferrer"&gt;👉 GitHub Repository – auto-allocator&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>memory</category>
      <category>allocator</category>
      <category>mimalloc</category>
    </item>
    <item>
      <title>Implementing FFmpeg Filters with Rust: A New Approach to Video and Audio Processing</title>
      <dc:creator>Yeauty YE</dc:creator>
      <pubDate>Wed, 09 Apr 2025 12:11:51 +0000</pubDate>
      <link>https://dev.to/yeauty/implementing-ffmpeg-filters-with-rust-a-new-approach-to-video-and-audio-processing-1hk4</link>
      <guid>https://dev.to/yeauty/implementing-ffmpeg-filters-with-rust-a-new-approach-to-video-and-audio-processing-1hk4</guid>
      <description>&lt;p&gt;Hey there, fellow developers! If you’ve ever wrestled with FFmpeg’s C API in Rust—or shuddered at the thought of memory leaks and &lt;code&gt;unsafe&lt;/code&gt; blocks—this one’s for you. Today, we’re diving into &lt;code&gt;ez-ffmpeg&lt;/code&gt;, a Rust library that makes implementing custom video and audio filters a breeze. Think safety, simplicity, and yes, even GPU acceleration—all wrapped in Rust’s ergonomic goodness. Ready to level up your multimedia game? Let’s get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters: The Pain of Traditional FFmpeg
&lt;/h2&gt;

&lt;p&gt;FFmpeg is a powerhouse for multimedia processing—encoding, decoding, filtering, you name it. But using its C API directly in Rust? That’s a different story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complexity Overload&lt;/strong&gt;: Manual memory management can feel like defusing a bomb—one wrong move, and boom, leaks or crashes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety Risks&lt;/strong&gt;: Foreign Function Interface (FFI) means &lt;code&gt;unsafe&lt;/code&gt; blocks, inviting bugs that Rust usually shields us from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steep Learning Curve&lt;/strong&gt;: Custom filters demand deep FFmpeg knowledge—filter graphs, frame handling—not exactly beginner-friendly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enter &lt;code&gt;ez-ffmpeg&lt;/code&gt;. It’s like a friendly guide that takes you from “FFmpeg newbie” to “filter pro” without the headaches. Whether you’re tweaking live streams, augmenting ML datasets, or spicing up game visuals, this library’s got your back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You’ll Learn
&lt;/h2&gt;

&lt;p&gt;We’ll walk through three killer examples:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Brighten Up&lt;/strong&gt;: A CPU-based brightness filter for YUV420 video.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go Gray&lt;/strong&gt;: A GPU-accelerated grayscale filter with OpenGL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn It Down&lt;/strong&gt;: An audio volume adjustment filter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Plus, we’ll explore real-world use cases and why &lt;code&gt;ez-ffmpeg&lt;/code&gt; is a game-changer. Let’s dive in!&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started: Setup
&lt;/h2&gt;

&lt;p&gt;First, add &lt;code&gt;ez-ffmpeg&lt;/code&gt; to your &lt;code&gt;Cargo.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;ez-ffmpeg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For GPU fun later, enable the &lt;code&gt;opengl&lt;/code&gt; feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;ez-ffmpeg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"opengl"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll need FFmpeg 7.0+ and Rust 1.80.0+ installed. Got that? Great—let’s code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1: Brighten Your Video (YUV420)
&lt;/h2&gt;

&lt;p&gt;Most videos use YUV420, so let’s start with a brightness filter that tweaks the Y (luminance) component.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code
&lt;/h3&gt;

&lt;p&gt;Here’s how we define the filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;core&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;frame_filter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FrameFilter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;core&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Frame&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;core&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;frame_filter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FrameFilterContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ffmpeg_sys_next&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;AVMediaType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AVPixelFormat&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;BrightnessFilter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;FrameFilter&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;BrightnessFilter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;media_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AVMediaType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;AVMediaType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AVMEDIA_TYPE_VIDEO&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;filter_frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;FrameFilterContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Frame&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="nf"&gt;.format&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nn"&gt;AVPixelFormat&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AV_PIX_FMT_YUV420P&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unsupported pixel format"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;y_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="nf"&gt;.get_data_mut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.ok_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to get Y plane"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;y_data&lt;/span&gt;&lt;span class="nf"&gt;.iter_mut&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;new_y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.increment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here’s how to run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FramePipelineBuilder&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ffmpeg_sys_next&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AVMediaType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame_pipeline_builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FramePipelineBuilder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;AVMediaType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AVMEDIA_TYPE_VIDEO&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;brightness_filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BrightnessFilter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame_pipeline_builder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame_pipeline_builder&lt;/span&gt;&lt;span class="nf"&gt;.filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"brightness"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;brightness_filter&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nn"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"output.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.add_frame_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame_pipeline_builder&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What’s Happening?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Filter Logic&lt;/strong&gt;: We grab the Y plane, bump each value by 20, and clamp it between 0-255.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline&lt;/strong&gt;: We plug the filter into a video processing pipeline, transforming &lt;code&gt;input.mp4&lt;/code&gt; into a brighter &lt;code&gt;output.mp4&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run it, and bam—your video’s glowing!&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 2: Grayscale with GPU Power (OpenGL)
&lt;/h2&gt;

&lt;p&gt;For real-time magic, let’s use the GPU. This grayscale filter leverages OpenGL for speed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Shader
&lt;/h3&gt;

&lt;p&gt;Here’s the fragment shader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight glsl"&gt;&lt;code&gt;&lt;span class="cp"&gt;#version 330 core
&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="kt"&gt;vec2&lt;/span&gt; &lt;span class="n"&gt;TexCoord&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;vec4&lt;/span&gt; &lt;span class="n"&gt;FragColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;uniform&lt;/span&gt; &lt;span class="kt"&gt;sampler2D&lt;/span&gt; &lt;span class="n"&gt;texture1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;vec4&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texture1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TexCoord&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;gray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;299&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;587&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;114&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FragColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;vec4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FramePipelineBuilder&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;opengl&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;OpenGLFrameFilter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ffmpeg_sys_next&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AVMediaType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;fragment_shader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;r#"
    #version 330 core
    in vec2 TexCoord;
    out vec4 FragColor;
    uniform sampler2D texture1;

    void main() {
        vec4 color = texture(texture1, TexCoord);
        float gray = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
        FragColor = vec4(gray, gray, gray, color.a);
    }"#&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;OpenGLFrameFilter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_simple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fragment_shader&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame_pipeline_builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FramePipelineBuilder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;AVMediaType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AVMEDIA_TYPE_VIDEO&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame_pipeline_builder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame_pipeline_builder&lt;/span&gt;&lt;span class="nf"&gt;.filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"opengl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nn"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"output.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.add_frame_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame_pipeline_builder&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why It Rocks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt;: GPU acceleration handles big frames fast—perfect for live streams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity&lt;/strong&gt;: No fiddly CPU loops; the shader does the heavy lifting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your video’s now a stylish black-and-white masterpiece.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 3: Lower the Volume (Audio)
&lt;/h2&gt;

&lt;p&gt;Let’s switch gears to audio with a volume adjustment filter.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;core&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;frame_filter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FrameFilter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;core&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Frame&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;core&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;frame_filter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FrameFilterContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ffmpeg_sys_next&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;AVMediaType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AVSampleFormat&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;VolumeFilter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;FrameFilter&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;VolumeFilter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;media_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AVMediaType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;AVMediaType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AVMEDIA_TYPE_AUDIO&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;filter_frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;FrameFilterContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Frame&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="nf"&gt;.format&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nn"&gt;AVSampleFormat&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AV_SAMPLE_FMT_S16&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unsupported sample format"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="nf"&gt;.get_data_mut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.ok_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to get audio data"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_raw_parts_mut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="nf"&gt;.as_mut_ptr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="nb"&gt;i16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="nf"&gt;.iter_mut&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;new_sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.gain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;32768.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;32767.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_sample&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FramePipelineBuilder&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ffmpeg_sys_next&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AVMediaType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame_pipeline_builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FramePipelineBuilder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;AVMediaType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AVMEDIA_TYPE_AUDIO&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;volume_filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VolumeFilter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame_pipeline_builder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame_pipeline_builder&lt;/span&gt;&lt;span class="nf"&gt;.filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"volume"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;volume_filter&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nn"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"output.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.add_frame_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame_pipeline_builder&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gain Factor&lt;/strong&gt;: &lt;code&gt;gain: 0.5&lt;/code&gt; halves the volume (range: -32768 to 32767).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety&lt;/strong&gt;: Rust keeps us crash-free while tweaking samples.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now your audio’s perfectly chilled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Shines: Use Cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live Streaming&lt;/strong&gt;: Add effects like brightness or grayscale in real time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Machine Learning&lt;/strong&gt;: Transform video frames for data augmentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gaming&lt;/strong&gt;: Dynamic video effects for immersive experiences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio Tweaks&lt;/strong&gt;: Fine-tune sound for podcasts or videos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surveillance&lt;/strong&gt;: Process feeds for motion detection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With &lt;code&gt;ez-ffmpeg&lt;/code&gt;, you skip FFmpeg’s filter graph maze and get straight to the fun stuff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;ez-ffmpeg&lt;/code&gt; Wins
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Safety First&lt;/strong&gt;: Rust’s guarantees mean no memory woes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ease of Use&lt;/strong&gt;: Ergonomic APIs lower the entry barrier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: CPU or GPU, pick your speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexibility&lt;/strong&gt;: Video, audio, real-time—handle it all.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What’s Next?
&lt;/h2&gt;

&lt;p&gt;This is just the start. Explore &lt;code&gt;ez-ffmpeg&lt;/code&gt;’s hardware encoding, streaming, or metadata tools. Check out the &lt;a href="https://github.com/YeautyYE/ez-ffmpeg" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; for more examples and docs.&lt;/p&gt;

&lt;p&gt;So, what do you think? Ready to ditch the C API and go Rust? Drop a comment—I’d love to hear your ideas or projects!&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ffmpeg</category>
    </item>
    <item>
      <title>How to Easily Implement RTMP Live Streaming in Rust: A Practical Guide</title>
      <dc:creator>Yeauty YE</dc:creator>
      <pubDate>Fri, 04 Apr 2025 08:06:00 +0000</pubDate>
      <link>https://dev.to/yeauty/how-to-easily-implement-rtmp-live-streaming-in-rust-a-practical-guide-4ed1</link>
      <guid>https://dev.to/yeauty/how-to-easily-implement-rtmp-live-streaming-in-rust-a-practical-guide-4ed1</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;With the rapid growth of live streaming, RTMP (Real-Time Messaging Protocol) has become a standard choice for broadcasting real-time media content. However, directly working with low-level APIs to implement RTMP streaming can be complex, error-prone, and may introduce memory safety issues, posing significant challenges for developers.&lt;/p&gt;

&lt;p&gt;This article explores how you can leverage Rust’s safety, efficiency, and developer-friendly features to simplify RTMP live streaming. We'll discuss common streaming scenarios, technical challenges, and provide practical solutions with ready-to-use examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use Rust for RTMP Streaming?
&lt;/h2&gt;

&lt;p&gt;Common pain points when implementing RTMP streaming include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex low-level APIs with a steep learning curve.&lt;/li&gt;
&lt;li&gt;Memory management and safety risks when directly interfacing with FFmpeg APIs.&lt;/li&gt;
&lt;li&gt;High demands for real-time performance and stability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rust, by its very design, addresses these challenges effectively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Offers zero-cost abstractions and built-in memory safety.&lt;/li&gt;
&lt;li&gt;Performance comparable to C/C++ with a significantly improved developer experience.&lt;/li&gt;
&lt;li&gt;Rich ecosystem and seamless integration with existing C/C++ libraries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thus, Rust is an excellent choice for developing robust and efficient RTMP streaming applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common RTMP Streaming Scenarios
&lt;/h2&gt;

&lt;p&gt;Based on real-world requirements, RTMP streaming usually falls into two scenarios:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Streaming to Public Platforms
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Case:&lt;/strong&gt; Platforms like Twitch, YouTube, or Facebook Live for large audience broadcasts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Requirements:&lt;/strong&gt; Stability, low latency, ease of use.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Local or Internal Streaming
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Case:&lt;/strong&gt; Suitable for internal live streams, testing, development, or LAN video monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Requirements:&lt;/strong&gt; Easy deployment, flexibility, quick setup, and convenient debugging.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solution: Rust with ez-ffmpeg
&lt;/h2&gt;

&lt;p&gt;To address these challenges and requirements, we introduce &lt;code&gt;ez-ffmpeg&lt;/code&gt;, a Rust library providing a clean abstraction over FFmpeg, significantly simplifying RTMP streaming through user-friendly interfaces and automatic memory management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technical Highlights:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FFI Bindings:&lt;/strong&gt; Safe and efficient interfacing with native FFmpeg APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Memory Management:&lt;/strong&gt; Eliminates manual memory handling, enhancing safety and productivity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ergonomic Design:&lt;/strong&gt; Chainable method calls, improving developer experience.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick Start Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Environment Setup
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Install FFmpeg
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;macOS:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Windows:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vcpkg &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rust Dependency
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;ez-ffmpeg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"rtmp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scenario 1: Stream to Public RTMP Servers
&lt;/h3&gt;

&lt;p&gt;The following example shows how to push a local video file to public RTMP platforms (e.g., YouTube, Twitch):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"video.mp4"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rtmp://your-platform-address/app/stream_key"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nn"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scenario 2: Embedded Local RTMP Server
&lt;/h3&gt;

&lt;p&gt;Ideal for local testing and quick setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;rtmp&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;embed_rtmp_server&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;EmbedRtmpServer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;EmbedRtmpServer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"localhost:1935"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="nf"&gt;.create_rtmp_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"test-stream"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"video.mp4"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nn"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clients can access the stream using the URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rtmp://localhost:1935/test-app/test-stream
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By leveraging Rust and the ez-ffmpeg library, developers can easily and safely implement RTMP streaming applications. Whether targeting public platforms or setting up local streaming environments, this approach makes development faster and more reliable.&lt;/p&gt;

&lt;p&gt;🔗 Project Repository: &lt;a href="https://github.com/YeautyYE/ez-ffmpeg" rel="noopener noreferrer"&gt;https://github.com/YeautyYE/ez-ffmpeg&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ffmpeg</category>
      <category>rtmp</category>
    </item>
    <item>
      <title>Efficient Video Processing in Rust: Leveraging Hardware Acceleration for High-Resolution Content</title>
      <dc:creator>Yeauty YE</dc:creator>
      <pubDate>Wed, 02 Apr 2025 07:03:04 +0000</pubDate>
      <link>https://dev.to/yeauty/efficient-video-processing-in-rust-leveraging-hardware-acceleration-for-high-resolution-content-53fe</link>
      <guid>https://dev.to/yeauty/efficient-video-processing-in-rust-leveraging-hardware-acceleration-for-high-resolution-content-53fe</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;Introduction&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In the world of video processing, the rise of 4K, 8K, and even higher-resolution content has pushed traditional CPU-based approaches to their limits. Whether it’s video editing, live streaming, or format conversion, developers often grapple with excessive CPU usage under heavy workloads. Hardware acceleration steps in as a game-changer, offloading encoding and decoding tasks to GPUs or specialized chips, boosting performance while freeing up CPU resources for a smoother user experience. Rust, with its blend of performance and safety, offers a robust ecosystem for tackling these demands—tools like &lt;a href="https://github.com/YeautyYE/ez-ffmpeg" rel="noopener noreferrer"&gt;&lt;code&gt;ez-ffmpeg&lt;/code&gt;&lt;/a&gt; make hardware-accelerated video processing accessible and efficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Problem: Why Hardware Acceleration Matters&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Picture this: you’re building a video transcoding tool to convert a 4K clip into H.264 format. Relying solely on the CPU, the process could take minutes, with usage spiking to 100%, slowing down everything else on the system. Or consider a real-time streaming app—high latency and dropped frames can ruin the viewer’s experience. These challenges highlight a key truth: modern video processing often exceeds what CPUs alone can handle. By tapping into GPUs or dedicated hardware like Intel Quick Sync or Nvidia NVENC, hardware acceleration slashes processing times and eases system strain, delivering results in a fraction of the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How It Works: Implementing Hardware Acceleration&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;At its core, hardware acceleration leverages hardware-specific APIs (think VideoToolbox, CUDA, or Direct3D) and codecs to streamline video workflows. In Rust, FFmpeg serves as a powerful foundation, and higher-level wrappers simplify its use. Here’s a quick example of hardware-accelerated transcoding in Rust:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"test.mp4"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"output.mp4"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Example for macOS: Use VideoToolbox for hardware acceleration&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;.set_hwaccel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"videotoolbox"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="nf"&gt;.set_video_codec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"h264_videotoolbox"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nn"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Breaking It Down&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;set_hwaccel("videotoolbox")&lt;/code&gt;&lt;/strong&gt;: Activates VideoToolbox on macOS for decoding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;set_video_codec("h264_videotoolbox")&lt;/code&gt;&lt;/strong&gt;: Sets the H.264 codec with VideoToolbox for encoding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outcome&lt;/strong&gt;: The input &lt;code&gt;test.mp4&lt;/code&gt; is transcoded to &lt;code&gt;output.mp4&lt;/code&gt; with hardware acceleration, far outpacing CPU-only methods.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Cross-Platform Flexibility: Adapting to Different Hardware&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Hardware acceleration varies across platforms and devices, posing a common hurdle for developers. Fortunately, it’s adaptable to diverse setups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: Use Direct3D 12 Video Acceleration (d3d12va) for decoding and Media Foundation for encoding:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;  &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;.set_hwaccel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"d3d12va"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="nf"&gt;.set_video_codec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"h264_mf"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nvidia GPUs&lt;/strong&gt;: Pair CUDA decoding with NVENC encoding:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;  &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;.set_hwaccel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cuda"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.set_video_codec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"h264_cuvid"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="nf"&gt;.set_video_codec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"h264_nvenc"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Heads-Up&lt;/strong&gt;: Availability depends on your hardware and system. Nvidia GPUs require proper drivers, while VideoToolbox needs Apple hardware. Always verify compatibility before diving in.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Wrapping Up&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;From speeding up video workflows to cutting resource usage, hardware acceleration is a must-have in today’s development toolkit. Rust’s clean APIs and strong ecosystem make it a breeze to harness this power for high-performance video processing. If you’re on the hunt for a practical solution, check out open-source projects like &lt;a href="https://github.com/YeautyYE/ez-ffmpeg" rel="noopener noreferrer"&gt;&lt;code&gt;ez-ffmpeg&lt;/code&gt;&lt;/a&gt;—it’s a solid launchpad for Rust developers looking to dive in.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Explore More&lt;/strong&gt;: &lt;a href="https://github.com/YeautyYE/ez-ffmpeg" rel="noopener noreferrer"&gt;ez-ffmpeg&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ffmpeg</category>
    </item>
    <item>
      <title>Mastering Video Effects in Rust: Unleashing FFmpeg and OpenGL Like a Pro</title>
      <dc:creator>Yeauty YE</dc:creator>
      <pubDate>Mon, 31 Mar 2025 13:51:27 +0000</pubDate>
      <link>https://dev.to/yeauty/mastering-video-effects-in-rust-unleashing-ffmpeg-and-opengl-like-a-pro-1fl0</link>
      <guid>https://dev.to/yeauty/mastering-video-effects-in-rust-unleashing-ffmpeg-and-opengl-like-a-pro-1fl0</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;Introduction: The Video Effects Struggle Is Real&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Video effects are everywhere—think short-form video filters, live-stream beauty enhancements, cinematic color grading, or even AI-generated dynamic visuals. They’re the secret sauce that hooks viewers. But let’s be honest: building them as a developer can feel like a nightmare. Sound familiar?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bulk Processing Woes&lt;/strong&gt;: Need to slap a blur on hundreds of videos? Manual tools like Premiere or After Effects laugh in your face—way too slow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FFmpeg Command-Line Hell&lt;/strong&gt;: Sure, it’s powerful, but those endless cryptic flags? One typo, and you’re debugging for hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C API Madness&lt;/strong&gt;: Calling FFmpeg’s C API directly? Good luck with FFI headaches and memory leaks that haunt your dreams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Effects Bottleneck&lt;/strong&gt;: Want real-time wave distortions or cyberpunk flickers? CPU-only processing crawls, and GPU acceleration feels like a distant dream.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re a Rust developer, you’ve likely hit these walls. In this post, I’ll show you how to wield Rust, FFmpeg, and OpenGL to craft video effects with elegance and speed—basic filters to hardcore dynamic visuals, all in a few minutes. Let’s dive in.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Why Rust + FFmpeg + OpenGL? The Holy Trinity of Video Magic&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Rust brings safety and performance. FFmpeg is the Swiss Army knife of multimedia. OpenGL unlocks GPU power. Together, they’re a dream team for video effects—efficient, scalable, and developer-friendly. Here’s how to make them work for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Get Started: Video Effects in 3 Steps&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Let’s say you’ve got an &lt;code&gt;input.mp4&lt;/code&gt; and want to add a Gaussian blur, saving it as &lt;code&gt;output.mp4&lt;/code&gt;. Here’s the no-nonsense breakdown:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Set Up FFmpeg&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Not installed yet? It’s quick:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  brew &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  vcpkg &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg
  &lt;span class="c"&gt;# First-time vcpkg users: set the VCPKG_ROOT env variable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;2. Add Rust Dependencies&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;In your &lt;code&gt;Cargo.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;ez-ffmpeg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;3. Write the Code&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Blur that video in a handful of lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;// Input file&lt;/span&gt;
        &lt;span class="nf"&gt;.filter_desc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"boxblur=10:10"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Gaussian blur&lt;/span&gt;
        &lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"output.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;// Output file&lt;/span&gt;
        &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it, and boom—&lt;code&gt;output.mp4&lt;/code&gt; has a slick blur. Chained API calls, no FFmpeg flag memorization required. Done.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;5 Real-World Use Cases and How to Nail Them&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Video effects vary wildly by project. Here are five practical scenarios with code snippets every dev can relate to:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Privacy Blur: Gaussian Effect&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nf"&gt;.filter_desc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"boxblur=10:10"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem&lt;/strong&gt;: Live streams or videos need obscured backgrounds or sensitive areas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result&lt;/strong&gt;: Smooth, professional blur—privacy secured, visuals intact.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Retro Vibes: Black-and-White Filter&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nf"&gt;.filter_desc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"format=gray"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem&lt;/strong&gt;: Need a nostalgic look for a video reel, but manual grading takes forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result&lt;/strong&gt;: Instant monochrome magic, old-school feels in one line.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Cinematic Polish: Color Grading&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nf"&gt;.filter_desc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"curves=all='0/0 0.5/0.75 1/1'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem&lt;/strong&gt;: Post-production color tweaks are a slog, especially in bulk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result&lt;/strong&gt;: Film-like brightness and contrast, effortlessly applied.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Pixel Art Style: Mosaic&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nf"&gt;.filter_desc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"scale=iw/10:ih/10,scale=iw*10:ih*10"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem&lt;/strong&gt;: Want a stylized look or subtle obfuscation without heavy tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result&lt;/strong&gt;: Pixelated charm, perfect for art or privacy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. Sci-Fi Twist: Negative (Night Vision)&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nf"&gt;.filter_desc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"negate"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem&lt;/strong&gt;: Craving a futuristic vibe but stuck on implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result&lt;/strong&gt;: Colors inverted, night-vision style—done in a snap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These FFmpeg filters are lightweight, fast, and ideal for batch jobs or quick prototypes.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Level Up: Dynamic Effects with OpenGL&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Static filters not cutting it? Crank things up with OpenGL and GLSL shaders for real-time, GPU-accelerated effects.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Use Case 1: Wavy Distortion&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;GLSL shader (&lt;code&gt;fragment.glsl&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight glsl"&gt;&lt;code&gt;&lt;span class="cp"&gt;#version 330 core
&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="kt"&gt;vec2&lt;/span&gt; &lt;span class="n"&gt;TexCoord&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;vec4&lt;/span&gt; &lt;span class="n"&gt;FragColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;uniform&lt;/span&gt; &lt;span class="kt"&gt;sampler2D&lt;/span&gt; &lt;span class="n"&gt;screenTexture&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;uniform&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;playTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;vec2&lt;/span&gt; &lt;span class="n"&gt;uv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TexCoord&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playTime&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mo"&gt;02&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Wavy motion&lt;/span&gt;
    &lt;span class="kt"&gt;vec3&lt;/span&gt; &lt;span class="n"&gt;texColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screenTexture&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;FragColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;vec4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rust code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;opengl&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;opengl_frame_filter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;OpenGLFrameFilter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AVMediaType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;frame_pipeline_builder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FramePipelineBuilder&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;fragment_shader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;include_str!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"../fragment.glsl"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame_pipeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FramePipelineBuilder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;AVMediaType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AVMEDIA_TYPE_VIDEO&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;OpenGLFrameFilter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_simple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fragment_shader&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame_pipeline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame_pipeline&lt;/span&gt;&lt;span class="nf"&gt;.filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wave"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nn"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"output.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.add_frame_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame_pipeline&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem&lt;/strong&gt;: Static effects feel flat; users want motion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result&lt;/strong&gt;: A rippling wave effect—think water or dreamy sequences.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Use Case 2: Cyberpunk Flicker&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;GLSL shader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight glsl"&gt;&lt;code&gt;&lt;span class="cp"&gt;#version 330 core
&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="kt"&gt;vec2&lt;/span&gt; &lt;span class="n"&gt;TexCoord&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;vec4&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;uniform&lt;/span&gt; &lt;span class="kt"&gt;sampler2D&lt;/span&gt; &lt;span class="n"&gt;screenTexture&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;uniform&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;playTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;vec3&lt;/span&gt; &lt;span class="n"&gt;texColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;texture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screenTexture&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TexCoord&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;r_shift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playTime&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;g_shift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playTime&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;b_shift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playTime&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;texColor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;r_shift&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;texColor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;g_shift&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;texColor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;b_shift&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playTime&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;texColor&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;vec4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem&lt;/strong&gt;: Short-form content needs eye-catching flair; traditional tools are clunky.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result&lt;/strong&gt;: Pulsing colors and flickers—pure cyberpunk energy.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Why This Combo Wins&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Process hundreds of videos with code, not clicks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer Joy&lt;/strong&gt;: Chained APIs beat command-line chaos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: GPU via OpenGL smokes CPU-only workflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety&lt;/strong&gt;: Rust handles memory, sparing you C’s nightmares.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Wrap-Up: Your New Rust Superpower&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Whether it’s filters for TikTok-style apps, live-stream enhancements, or AI video pipelines, Rust with FFmpeg and OpenGL delivers. Professional effects in a few lines, GPU speed boosts, and no low-level grunt work—every dev’s dream.&lt;/p&gt;

&lt;p&gt;Curious to dig deeper? Check out this open-source gem:&lt;br&gt;&lt;br&gt;
🔗 &lt;a href="https://github.com/YeautyYE/ez-ffmpeg" rel="noopener noreferrer"&gt;https://github.com/YeautyYE/ez-ffmpeg&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ffmpeg</category>
    </item>
    <item>
      <title>Simplify Video Frame Rate Adjustment in Rust: A Practical Guide for Developers</title>
      <dc:creator>Yeauty YE</dc:creator>
      <pubDate>Fri, 28 Mar 2025 15:44:09 +0000</pubDate>
      <link>https://dev.to/yeauty/simplify-video-frame-rate-adjustment-in-rust-a-practical-guide-for-developers-20gm</link>
      <guid>https://dev.to/yeauty/simplify-video-frame-rate-adjustment-in-rust-a-practical-guide-for-developers-20gm</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Adjusting video frame rates (FPS) is a common task in video processing, whether to ensure smooth playback or meet specific platform requirements. For instance, converting a 60 FPS video to 30 FPS might be necessary for compatibility or efficiency. While the FFmpeg command-line tool (&lt;code&gt;ffmpeg -i input.mp4 -r 30 output.mp4&lt;/code&gt;) is a popular choice, integrating this functionality directly into Rust projects can offer more control and automation.&lt;/p&gt;

&lt;p&gt;In Rust, calling FFmpeg’s C libraries via FFI (Foreign Function Interface) provides access to its full capabilities, though it can be complex and time-consuming. For simpler tasks like frame rate adjustments, &lt;code&gt;ez-ffmpeg&lt;/code&gt; offers a more straightforward approach. Built on top of FFI, it provides a user-friendly interface that streamlines common video processing tasks, making it a practical option for developers looking to avoid the intricacies of direct FFI calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  FFI vs. ez-ffmpeg: Choosing the Right Tool
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Challenges of FFI
&lt;/h3&gt;

&lt;p&gt;FFI allows direct interaction with FFmpeg’s C libraries, offering maximum flexibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full Control&lt;/strong&gt;: Access every feature of FFmpeg, ideal for advanced use cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complexity&lt;/strong&gt;: Requires managing structs, pointers, and memory manually, which can lead to verbose and error-prone code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steep Learning Curve&lt;/strong&gt;: Developers need a solid understanding of FFmpeg’s low-level API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, adjusting frame rates via FFI involves configuring &lt;code&gt;AVCodecContext&lt;/code&gt; and &lt;code&gt;AVFrame&lt;/code&gt;, where mistakes can easily lead to issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  How ez-ffmpeg Helps
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ez-ffmpeg&lt;/code&gt; is designed to simplify common tasks without replacing FFI entirely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ease of Use&lt;/strong&gt;: Its API mirrors FFmpeg command-line syntax, making it intuitive for those familiar with the tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety&lt;/strong&gt;: Handles memory management automatically, reducing risks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus&lt;/strong&gt;: Lets developers concentrate on application logic rather than FFmpeg internals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While FFI is better for intricate, custom workflows, &lt;code&gt;ez-ffmpeg&lt;/code&gt; shines in scenarios where simplicity and speed are priorities, such as basic frame rate adjustments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started: Adjust Video Frame Rates in Rust
&lt;/h2&gt;

&lt;p&gt;Let’s say you need to convert a 60 FPS video to 30 FPS. Here’s how &lt;code&gt;ez-ffmpeg&lt;/code&gt; can help.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Install FFmpeg
&lt;/h3&gt;

&lt;h4&gt;
  
  
  macOS:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Windows:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vcpkg &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg
&lt;span class="c"&gt;# Set the VCPKG_ROOT environment variable if using vcpkg for the first time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Add the Rust Dependency
&lt;/h3&gt;

&lt;p&gt;In your &lt;code&gt;Cargo.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;ez-ffmpeg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Code Examples
&lt;/h3&gt;

&lt;p&gt;Below are two ways to adjust frame rates using &lt;code&gt;ez-ffmpeg&lt;/code&gt;:&lt;/p&gt;

&lt;h4&gt;
  
  
  Method 1: Set Frame Rate via Output
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ffmpeg_sys_next&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AVRational&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Input file&lt;/span&gt;
        &lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"output.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.set_framerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AVRational&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;den&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt; &lt;span class="c1"&gt;// Set to 30 FPS&lt;/span&gt;
        &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Method 2: Use a Filter to Set Frame Rate
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;ez_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;FfmpegContext&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test.mp4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Input file&lt;/span&gt;
        &lt;span class="nf"&gt;.filter_desc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fps=30"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Use "fps" filter to set 30 FPS&lt;/span&gt;
        &lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"output.mp4"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;// Output file&lt;/span&gt;
        &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When to Use Each Method
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Method 1&lt;/strong&gt;: Straightforward for basic frame rate changes. Best when that’s the only adjustment needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method 2&lt;/strong&gt;: More flexible, allowing frame rate changes alongside other filters (e.g., resizing). Ideal for combined operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both methods produce a 30 FPS &lt;code&gt;output.mp4&lt;/code&gt; from &lt;code&gt;test.mp4&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;For Rust developers working with video, &lt;code&gt;ez-ffmpeg&lt;/code&gt; provides a convenient way to handle tasks like frame rate adjustments. It builds on FFI to offer a simpler, safer interface for common operations, complementing the power of direct FFmpeg calls. While it’s not a solution for every scenario, it can be a valuable tool in your toolkit, especially when ease of use is a priority.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Learn more&lt;/strong&gt;: &lt;a href="https://github.com/YeautyYE/ez-ffmpeg" rel="noopener noreferrer"&gt;ez-ffmpeg on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ffmpeg</category>
      <category>github</category>
    </item>
  </channel>
</rss>
