<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Ffmpeg on PHP Boy Scout</title><link>https://phpboyscout.uk/tags/ffmpeg/</link><description>Recent content in Ffmpeg on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Thu, 09 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://phpboyscout.uk/tags/ffmpeg/index.xml" rel="self" type="application/rss+xml"/><item><title>The afternoon the agent earned its keep</title><link>https://phpboyscout.uk/the-afternoon-the-agent-earned-its-keep/</link><pubDate>Thu, 09 Jul 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/the-afternoon-the-agent-earned-its-keep/</guid><description>&lt;img src="https://phpboyscout.uk/the-afternoon-the-agent-earned-its-keep/cover-the-afternoon-the-agent-earned-its-keep.png" alt="Featured image of post The afternoon the agent earned its keep" /&gt;&lt;p&gt;There&amp;rsquo;s a kind of bug that&amp;rsquo;s worse than a crash. A crash at least tells you &lt;em&gt;where&lt;/em&gt;. This one just&amp;hellip; stopped. I asked &lt;a class="link" href="https://phpboyscout.uk/introducing-afmpeg-and-ffmpeg-wasi/" &gt;ffmpeg-wasi&lt;/a&gt; to remux a clip into a Matroska file, no re-encoding, just repackage the same streams into an &lt;code&gt;.mkv&lt;/code&gt;, and it never came back. No error, no output. The test harness eventually gave up and shot it.&lt;/p&gt;
&lt;p&gt;The same job to an &lt;code&gt;.mp4&lt;/code&gt;? A dozen milliseconds. Change the file extension and a blink becomes a heat-death-of-the-universe operation.&lt;/p&gt;
&lt;p&gt;Normally this is the moment a war story starts costing me evenings. FFmpeg compiled to &lt;code&gt;wasm32-wasi&lt;/code&gt;, running under &lt;a class="link" href="https://wazero.io/" target="_blank" rel="noopener"
 &gt;wazero&lt;/a&gt;, reading and writing through an in-memory filesystem&amp;hellip; that&amp;rsquo;s a lot of layers, any of which could be the one that loops, and the traditional way to find out is to build test rigs for each layer by hand and lose a week to the plumbing. I know what that costs because I&amp;rsquo;ve paid it before.&lt;/p&gt;
&lt;p&gt;This time I didn&amp;rsquo;t pay it. The hang surfaced while an agent was implementing stream-copy for me, so rather than pull the work back onto my own bench, I told it to chase the bug too. What follows is the deduction it ran. My contribution was two decisions at two forks, and I&amp;rsquo;ll point at them as we pass.&lt;/p&gt;
&lt;h2 id="it-got-it-wrong-first"&gt;It got it wrong first
&lt;/h2&gt;&lt;p&gt;I want to start with the wrong turn, because it&amp;rsquo;s the most instructive part.&lt;/p&gt;
&lt;p&gt;The agent&amp;rsquo;s first theory was confident and detailed: it traced the guest&amp;rsquo;s filesystem calls, saw the Matroska muxer seeking back and forth rewriting its header, and declared the finalisation seek-backs the culprit, an I/O pattern my filesystem bridge couldn&amp;rsquo;t satisfy. Plausible. Specific. Wrong.&lt;/p&gt;
&lt;p&gt;And here&amp;rsquo;s the bit that sold me: it then designed the experiment that killed its own theory. Matroska has a streamable mode that writes without any seek-backs at all, so it flipped that on&amp;hellip; and the job still hung. Theory dead, evidence in hand, no sulking. I&amp;rsquo;ve watched humans (fine, me) defend a pet theory for two days past its expiry date. The agent binned its own in minutes and moved on to the thing you should always do when guessing stops working: bisect.&lt;/p&gt;
&lt;h2 id="ruling-out-the-world-one-build-at-a-time"&gt;Ruling out the world, one build at a time
&lt;/h2&gt;&lt;p&gt;This was my first fork. The agent asked whether to keep poking at it in place or to bring in native tooling, and I chose native. So it built a &lt;em&gt;real&lt;/em&gt; FFmpeg, same n8.1.2 source, same minimal configure flags, and started eliminating suspects:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;It tested&lt;/th&gt;
 &lt;th&gt;Result&lt;/th&gt;
 &lt;th&gt;Which ruled out&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Native FFmpeg, 64-bit, real files → mkv&lt;/td&gt;
 &lt;td&gt;works&lt;/td&gt;
 &lt;td&gt;the muxer / our configure flags&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;wasm backed by a &lt;strong&gt;real OS directory&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;hangs&lt;/td&gt;
 &lt;td&gt;the in-memory filesystem&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Native FFmpeg, &lt;strong&gt;32-bit&lt;/strong&gt; (&lt;code&gt;-m32&lt;/code&gt;) → mkv&lt;/td&gt;
 &lt;td&gt;works&lt;/td&gt;
 &lt;td&gt;a 32-bit integer overflow&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;wasm under wazero&amp;rsquo;s &lt;strong&gt;interpreter&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;hangs&lt;/td&gt;
 &lt;td&gt;a code-generation bug&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Sit with that table for a second. Two of those rows are &lt;em&gt;whole native builds of FFmpeg&lt;/em&gt;, one of them 32-bit with the toolchain to match, spun up as casually as you&amp;rsquo;d write a unit test. That&amp;rsquo;s the part that would have eaten my week. Each row walks an interesting suspect free, until the only thing left holding the bag is the plainest fact in the whole setup: this is running under WASI. Not the muxer. Not the maths. Not my filesystem. Not the runtime. The environment.&lt;/p&gt;
&lt;h2 id="the-environment-was-missing-a-number"&gt;The environment was missing a number
&lt;/h2&gt;&lt;p&gt;WASI is a smaller world than POSIX. No threads, no network, and, it turns out, no &lt;code&gt;/dev/urandom&lt;/code&gt;. Most code never notices. Right up until it does.&lt;/p&gt;
&lt;p&gt;With the bisection result in hand, the agent went and read the Matroska muxer&amp;rsquo;s source. Every Matroska track carries a unique random identifier, so at startup the muxer seeds a pseudo-random number generator via libavutil&amp;rsquo;s &lt;code&gt;av_get_random_seed()&lt;/code&gt;. In a build configured as tightly as ours, that function has exactly &lt;strong&gt;one&lt;/strong&gt; source of real entropy compiled in: reading &lt;code&gt;/dev/urandom&lt;/code&gt;. The Windows crypto API, &lt;code&gt;arc4random&lt;/code&gt;, OpenSSL, libgcrypt&amp;hellip; all configured out. Just the one device.&lt;/p&gt;
&lt;p&gt;Which WASI doesn&amp;rsquo;t have. So the read fails, and the function falls back to its last resort, &lt;a class="link" href="https://github.com/FFmpeg/FFmpeg/blob/n8.1.2/libavutil/random_seed.c#L75" target="_blank" rel="noopener"
 &gt;&lt;code&gt;get_generic_seed()&lt;/code&gt;&lt;/a&gt;: a genuinely clever routine that manufactures entropy out of thin air by reading &lt;code&gt;clock()&lt;/code&gt; in a tight loop and harvesting the jitter in how it advances. It&amp;rsquo;s a lovely trick. It has one requirement. The clock has to advance.&lt;/p&gt;
&lt;p&gt;Under WASI, &lt;code&gt;clock()&lt;/code&gt; doesn&amp;rsquo;t advance. The seeding call never returns. The muxer hangs before it has written a single byte, and the &lt;code&gt;.mp4&lt;/code&gt; muxer never asks for randomness at all, which is the entire reason it was fine all along.&lt;/p&gt;
&lt;p&gt;The agent didn&amp;rsquo;t leave that as a theory either. It wired &lt;code&gt;av_get_random_seed()&lt;/code&gt; into a trivial diagnostic op, one that does nothing else&amp;hellip; and that hung too. Root cause, demonstrated in isolation.&lt;/p&gt;
&lt;h2 id="a-device-not-a-patch"&gt;A device, not a patch
&lt;/h2&gt;&lt;p&gt;The fix isn&amp;rsquo;t a patch to FFmpeg. It&amp;rsquo;s &lt;a class="link" href="https://gitlab.com/phpboyscout/afmpeg/-/blob/b9b7d5e/internal/vfs/random.go#L32-40" target="_blank" rel="noopener"
 &gt;a device&lt;/a&gt;. My filesystem bridge already fabricates a couple of the POSIX fixtures a guest expects (&lt;code&gt;/tmp&lt;/code&gt; routed to scratch space, &lt;code&gt;/dev/null&lt;/code&gt; as a discard sink), so &lt;code&gt;/dev/urandom&lt;/code&gt; is &lt;a class="link" href="https://afmpeg.phpboyscout.uk/explanation/components/vfs-bridge/#why-devurandom-is-load-bearing" target="_blank" rel="noopener"
 &gt;the same trick&lt;/a&gt; with real randomness behind it, backed by Go&amp;rsquo;s &lt;code&gt;crypto/rand&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Read fills buf with random bytes; a short read never happens (crypto/rand&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// fills the whole slice or errors), matching /dev/urandom&amp;#39;s contract.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;randFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;experimentalsys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Errno&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;experimentalsys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EIO&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now the muxer&amp;rsquo;s very first entropy read succeeds, the seed returns immediately, and the clever-but-doomed clock loop is never reached. The &lt;code&gt;.mkv&lt;/code&gt; remux that had been hanging forever finishes in ten milliseconds. And the fix isn&amp;rsquo;t Matroska-specific: any format that wants a random seed now gets one for free.&lt;/p&gt;
&lt;h2 id="what-the-afternoon-actually-cost"&gt;What the afternoon actually cost
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the arithmetic that made me want to write this up.&lt;/p&gt;
&lt;p&gt;From the first observed hang to a verified fix was a little over an hour. That hour included the wrong theory and the experiment that disproved it, two complete native builds of FFmpeg (one of them 32-bit), runs across two wazero execution modes, a read through libavutil&amp;rsquo;s entropy code, the diagnostic op that proved the root cause in isolation, and the fix itself. The agent even went looking for prior art and couldn&amp;rsquo;t find anyone who&amp;rsquo;d written this failure up&amp;hellip; a genuinely undocumented corner of WASI, run to ground between lunch and tea.&lt;/p&gt;
&lt;p&gt;By hand? Most of a week, and the bulk of it spent on build plumbing rather than thinking: getting a matching native FFmpeg to configure, wrangling a 32-bit toolchain, wiring the trace points. None of that work is hard. It&amp;rsquo;s just &lt;em&gt;time&lt;/em&gt;, and it&amp;rsquo;s exactly the kind of time a solo developer doesn&amp;rsquo;t have lying around, which is how deep bugs like this end up parked on a &amp;ldquo;someday&amp;rdquo; list with the &lt;code&gt;.mkv&lt;/code&gt; button greyed out.&lt;/p&gt;
&lt;p&gt;My whole part was two forks (dig in place, or go native? and later: document it before touching anything else) and reading the result. The judgement stayed mine. The legwork didn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve &lt;a class="link" href="https://phpboyscout.uk/the-off-switch-was-never-a-button/" &gt;compared these agents to Golden Retrievers before&lt;/a&gt;, and I stand by it: eager, tireless, loyal to their training, occasionally proud of a shredded cushion. A dog that plays fetch all day is good company. But this one went down the burrow after something I couldn&amp;rsquo;t see, and came back with the exact rabbit.&lt;/p&gt;
&lt;p&gt;Good boy.&lt;/p&gt;</description></item><item><title>Introducing afmpeg and ffmpeg-wasi: FFmpeg with no install, no CGO, no disk</title><link>https://phpboyscout.uk/introducing-afmpeg-and-ffmpeg-wasi/</link><pubDate>Sun, 28 Jun 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/introducing-afmpeg-and-ffmpeg-wasi/</guid><description>&lt;img src="https://phpboyscout.uk/introducing-afmpeg-and-ffmpeg-wasi/cover-introducing-afmpeg-and-ffmpeg-wasi.png" alt="Featured image of post Introducing afmpeg and ffmpeg-wasi: FFmpeg with no install, no CGO, no disk" /&gt;&lt;p&gt;&lt;a class="link" href="https://keryx.phpboyscout.uk" target="_blank" rel="noopener"
 &gt;keryx&lt;/a&gt; renders short promo reels, and the way it does that, today, is the way nearly everything does: it shells out to the &lt;code&gt;ffmpeg&lt;/code&gt; binary. Which is fine, until you ask it to render a project that doesn&amp;rsquo;t exist on disk. keryx can work on an in-memory project, a repo cloned straight into RAM with no local checkout, and the moment it tries to hand that to &lt;code&gt;ffmpeg&lt;/code&gt;, the whole thing falls over. The binary wants real files in a real directory. There aren&amp;rsquo;t any.&lt;/p&gt;
&lt;p&gt;I went looking for a way out and didn&amp;rsquo;t find one I could live with. The bindings that use &lt;code&gt;purego&lt;/code&gt;/&lt;code&gt;dlopen&lt;/code&gt; are immature and still need the host&amp;rsquo;s &lt;code&gt;libav&lt;/code&gt; libraries installed. The CGO bindings to &lt;code&gt;libav&lt;/code&gt; are mature and can absolutely work in memory, but they&amp;rsquo;re CGO, and CGO takes away the thing I most want from a Go program: a clean static cross-compile to a single binary that runs anywhere. The famous &lt;code&gt;ffmpeg.wasm&lt;/code&gt; is an &lt;em&gt;emscripten&lt;/em&gt; build aimed at the browser, which is the opposite target from a server-side Go tool. And the one existing WASI-capable build pins FFmpeg 5.1, which is end-of-life, and I&amp;rsquo;m not shipping an out-of-support media decoder whose entire job is parsing untrusted files. Every road had a tollbooth I wasn&amp;rsquo;t willing to pay.&lt;/p&gt;
&lt;p&gt;So I built two new things instead, and because they&amp;rsquo;re so closely tied I&amp;rsquo;m introducing them together.&lt;/p&gt;
&lt;h2 id="ffmpeg-wasi-current-ffmpeg-sandboxed-cgo-free"&gt;ffmpeg-wasi: current FFmpeg, sandboxed, CGO-free
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://ffmpeg-wasi.phpboyscout.uk" target="_blank" rel="noopener"
 &gt;ffmpeg-wasi&lt;/a&gt; (&lt;a class="link" href="https://gitlab.com/phpboyscout/ffmpeg-wasi" target="_blank" rel="noopener"
 &gt;repo&lt;/a&gt;) is the foundation, and it&amp;rsquo;s the harder of the two to build. It takes FFmpeg&amp;rsquo;s media libraries, the &lt;code&gt;libav*&lt;/code&gt; family, and builds them to &lt;code&gt;wasm32-wasi&lt;/code&gt;, then drives them with a small purpose-built engine, producing a single &lt;code&gt;.wasm&lt;/code&gt; artifact that runs anywhere a WASI runtime does. No native FFmpeg install, no C toolchain at deploy time, no shelling out to a binary. It&amp;rsquo;s built to run under &lt;a class="link" href="https://wazero.io/" target="_blank" rel="noopener"
 &gt;wazero&lt;/a&gt;, the zero-dependency pure-Go WebAssembly runtime, so a Go program can transcode, filter and mux media embedded, sandboxed, and CGO-free, still cross-compiling to one static binary.&lt;/p&gt;
&lt;p&gt;The interesting bit, the reason this didn&amp;rsquo;t already exist, is a wall that FFmpeg 7.0 put up. The 7.x series rewrote the &lt;em&gt;command-line tool&lt;/em&gt; to be mandatorily multithreaded, and a pure-Go WASI runtime can&amp;rsquo;t run that, because the threading model it needs (&lt;code&gt;wasi-threads&lt;/code&gt;, spawning real threads) isn&amp;rsquo;t something wazero does. Every project trying to get &lt;em&gt;current&lt;/em&gt; FFmpeg into WASI hits that wall, which is exactly why the existing build froze at the last single-threaded CLI, 5.1, and went EOL there. ffmpeg-wasi goes under the wall instead of over it: it doesn&amp;rsquo;t compile the CLI at all. It links the &lt;code&gt;libav*&lt;/code&gt; &lt;em&gt;libraries&lt;/em&gt; directly, which build single-threaded without complaint, and drives them with its own engine. That&amp;rsquo;s the move nobody else has made, and it&amp;rsquo;s the whole reason this can track current, maintained FFmpeg rather than a frozen one.&lt;/p&gt;
&lt;h2 id="afmpeg-the-pure-go-binding-that-lives-in-memory"&gt;afmpeg: the pure-Go binding that lives in memory
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://afmpeg.phpboyscout.uk" target="_blank" rel="noopener"
 &gt;afmpeg&lt;/a&gt; (&lt;a class="link" href="https://gitlab.com/phpboyscout/afmpeg" target="_blank" rel="noopener"
 &gt;repo&lt;/a&gt;) was the catalyst for all this, and it&amp;rsquo;s the part a Go developer actually touches. It&amp;rsquo;s a small, idiomatic Go API (&lt;code&gt;New&lt;/code&gt;, &lt;code&gt;Run&lt;/code&gt;, &lt;code&gt;Probe&lt;/code&gt;, &lt;code&gt;Close&lt;/code&gt;) sitting on top of the ffmpeg-wasi artifact, with one important twist: its I/O is bridged to an &lt;a class="link" href="https://github.com/spf13/afero" target="_blank" rel="noopener"
 &gt;&lt;code&gt;afero.Fs&lt;/code&gt;&lt;/a&gt;. afero isn&amp;rsquo;t in the standard library, but it&amp;rsquo;s the filesystem abstraction a great deal of Go already reaches for when it wants to swap a real disk for something else, and that &amp;ldquo;something else&amp;rdquo; is exactly the point here. The inputs and outputs of a media job can live entirely in memory, or in any afero backend you like, and &lt;code&gt;ffmpeg&lt;/code&gt; is none the wiser. keryx gets to render its in-memory project without ever touching the disk, which was the whole reason I started.&lt;/p&gt;
&lt;p&gt;This isn&amp;rsquo;t a roadmap post. Both projects are released and run today: afmpeg is at &lt;code&gt;v0.4.0&lt;/code&gt;, ffmpeg-wasi at &lt;code&gt;n8.1.2-1&lt;/code&gt; (current FFmpeg, not the EOL 5.1), and between them they do real in-memory transcodes, verified end to end, WAV to AAC and H.264 rescaled and re-encoded with x264. Stripped of keryx&amp;rsquo;s reel-specific filtergraph, the shape a caller actually deals with is about this small:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Compile the ffmpeg-wasi module once, then reuse the runtime.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;afmpeg&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="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;afmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithModuleFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;ffmpeg-wasi-lgpl.wasm&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// The whole job lives in memory: no temp dir, nothing on disk.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;afero&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewMemMapFs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;afero&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;in.wav&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="nx"&gt;o644&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Drive it with the ffmpeg arguments you already know...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rt&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="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;-i&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;in.wav&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;-c:a&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;aac&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;out.m4a&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ExitCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;render failed: %w (%s)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ...then read the finished file straight back out of memory.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;afero&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;out.m4a&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;.wasm&lt;/code&gt; itself is a published release artifact you pin by SHA-256 (or let afmpeg fetch and verify for you with &lt;code&gt;WithModuleURL&lt;/code&gt;), so the licence boundary stays explicit and nothing surprising ends up in your binary.&lt;/p&gt;
&lt;p&gt;It doesn&amp;rsquo;t do everything yet, and I&amp;rsquo;d rather say so than let you find out the hard way. Single input to single output and &lt;code&gt;Probe&lt;/code&gt; work now; the full multi-pad &lt;code&gt;filter_complex&lt;/code&gt; and multi-output muxing are the next thing on the bench. But the part that nobody had cracked, getting current FFmpeg to run sandboxed, pure-Go, over a virtual filesystem with nothing on disk, is done, and you can pull it today.&lt;/p&gt;
&lt;h2 id="why-its-two-repos"&gt;Why it&amp;rsquo;s two repos
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a reason these are separate projects rather than one, and it&amp;rsquo;s about licences, not tidiness. The moment you compile FFmpeg you&amp;rsquo;re handling LGPL and GPL code, and I wanted that boundary to be obvious rather than smeared across one repo where nobody&amp;rsquo;s quite sure what&amp;rsquo;s covered by what. So the build tooling and the &lt;code&gt;(L)GPL&lt;/code&gt; &lt;code&gt;.wasm&lt;/code&gt; artifacts live in ffmpeg-wasi, with no grey areas, and afmpeg stays a clean permissive layer on top of the published artifact. I&amp;rsquo;m also shipping both LGPL and GPL builds of the artifact, so anyone who just wants the output and doesn&amp;rsquo;t fancy doing their own FFmpeg build can pick the licence that suits them and get on with it.&lt;/p&gt;
&lt;p&gt;Both repos are public, so you can rebuild or relink either one yourself, and the doc sites are now linked in the nav up top.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a stack of stories behind these two already, the threading wall and the spike that found the way under it, the afero-to-WASI filesystem bridge, the day I built a reel-shaped API and then reverted it before merge for being too narrow, the licence decision in full. I&amp;rsquo;ll be writing those up as the work lands. For now this is the headline: I needed an FFmpeg I could embed in a Go program with no install, no CGO, and no disk, couldn&amp;rsquo;t buy one off the shelf, so I built it in the open, and as of today it&amp;rsquo;s there to pull.&lt;/p&gt;</description></item></channel></rss>