Build1 publisher3 min readPublished
On-demand audio variants make the first listener wait on ffmpeg
The audioproxy-rails gem signs variant options into an audio tag's src and lets one container transcode on first request. That deletes the variants table and puts a failed encode inside a live request.
The Engineer · Build desk

What happened
- The post starts from an app where users upload 40-minute WAV files and the product needs a 30-second preview, a waveform, MP3 downloads, Opus streaming and loudness normalisation.
- The conventional build is a variant model with an attachment per variant, a job that shells out to ffmpeg, a status column because jobs fail, and a rake task to backfill when settings change.
- audioproxy puts one container between storage and users, renders a variant the first time it is requested, streams bytes while ffmpeg is still encoding, and writes the result to a store you own.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- cost The render cost moves onto the first person who loads the page, and it is paid at page-load time instead of before anyone looked.
- constraint Disk-backed storage pins the proxy to the same filesystem as the app, because AP_LOCAL_ROOT and the Disk service root have to be the same directory.
- exposure The signing key is shared between app and proxy, so a leaked pair lets an outsider commission transcodes on your CPU with valid signatures.
- decision Error handling has to be designed for the response path: a dead encoder is now a bad HTTP response rather than a row you can inspect and retry.
One helper call does the conversion. `audioproxy_audio_tag @recording.audio, format: "opus", bitrate: 96` returns an `<audio>` element whose `src` points at the proxy [15]. The attachment becomes a source string, the options become a path, and the path gets signed [17]. The first path segment is that signature, computed over everything after it, so an edited URL will not make the proxy transcode something nobody asked for [16]. `f:` carries the format and `br:` the bitrate in kbps, and the source sits at the end as a base64url-encoded `s3://` or `local://` URI [16].
Both halves of the deployment read the same two secrets: the app signs with `AP_KEY` and `AP_SALT`, and the proxy verifies with them [11]. The sample key in the post is 64 hex characters and the salt is 32 [9], so 32 bytes of key and 16 of salt [10]. Anyone holding the pair can sign a URL for a 40-minute source at whatever bitrate they choose, and the container will encode it [1][16].
The tightest coupling in the sample env file is `AP_LOCAL_ROOT`, which has to name the same directory as your Disk service's `root` [12]. In Compose that is a shared volume, and in development the post mounts the audio directory into the proxy read only [19], which is one fewer way to lose the originals. The variant store in the sample is `file:///var/cache/audioproxy`, and `AP_SERVE_MODE=proxy` beside it is not optional [13]. The proxy refuses to boot in its default redirect mode and says why: "AP_SERVE_MODE=redirect serves cache hits via presigned URLs, which a file:// AP_VARIANT_STORE cannot produce; set AP_SERVE_MODE=proxy or use a store that can presign" [14]. Failing at boot is better than redirecting to a URL that cannot exist.
The queue design needs four pieces: a variant model, an ffmpeg job, a status column and a backfill task [3]. Julian Rubisch, who wrote the post, has built it at least twice, and said of it: "It works, but it's a lot of code that has nothing to do with the product." [4] The proxy version needs the gem and three configuration values [8][23], and the status column goes because nothing is rendered in advance [7].
Failures do not go with it. The column exists because jobs fail [3], and an ffmpeg process that dies now dies inside a request a person is waiting on [6]. Rubisch writes that the render starts on the first request and streams bytes while ffmpeg is still encoding [6], so time to first byte can be short even when the encode is long. The post does not give timings for that first request.
Two things have to be true before the tally transfers to your app. The first request for each variant has to tolerate an ffmpeg start on a 40-minute WAV [1]. And concurrent first requests for the same variant must not each start their own encode.
In my view this is the right trade for a feed of 30-second previews [2], where a file is uploaded once, read many times, and the first reader is usually whoever uploaded it. For a download button, where somebody clicks and expects an MP3 [2], I would keep the job and the status column, for the reason the post gives for having one in the first place: jobs fail [3].
What to watch
- Whether the gem or the proxy documents what happens when several first requests for the same variant arrive at once.
- Whether the published option rules cover the waveform and the loudness normalisation in the requirements list, neither of which the helper example demonstrates.
- A redirect-mode deployment against an S3 variant store that can presign, and what the cache-hit path costs there.