A peaks render: probe, decode, reduce, serialize — behind
AudioProxy.Ffmpeg.Render's contract.
f:peaks is a format, not a separate resource, so everything downstream of
here is unchanged: AudioProxy.RenderCoordinator coalesces it, the
write-back tee stores it, AudioProxy.Plugs.RenderAction streams it, and the
next request for the same URL is a HIT. That only works because this process
is indistinguishable from a render — same three messages, same ack/2 cast,
same cancel/1 call, same "exactly one terminal message, then stop".
Two subprocesses, one render
A peaks render runs ffprobe and then ffmpeg, both as ordinary
AudioProxy.Ffmpeg.Render children:
- Probe.
AudioProxy.Ffprobesupplies both the argv and the mapping — the same ones/infoanswers with, so the duration this proxy reports and the duration it buckets by cannot drift apart. What differs is who runs the subprocess:/infoblocks onFfprobe.probe/2because it is a request doing nothing else, while this process spawns the same argv and folds the chunks inhandle_info/2, so it stays answerable tocancel/1throughout. Bucket boundaries are a function of the total sample count, and the alternative to knowing it up front is buffering the whole decode — memory instead of a header read. - Decode. The argv
AudioProxy.Ffmpeg.Commandbuilds forf:peaks: raw interleaveds16leon stdout, trimmed and downmixed. Each chunk is folded intoAudioProxy.Peaksand dropped; the PCM is never retained and never reaches the coordinator.
Both inherit the pipeline's kill discipline and AP_RENDER_TIMEOUT — worth
stating plainly, because that budget now covers two subprocesses in sequence
rather than one. A probe that hangs is a :timeout classified exactly like
a decode that hangs, and a source that 404s fails at the probe with
%{class: :not_found}, which is the status the client would have got either
way.
One chunk, at the end
The reduction is streaming, but the output is not: pts pairs are a few
kilobytes and both serializations carry a header describing the whole body,
so they are emitted as a single chunk followed by {:done, _, _}. That is
why ack/2 is accepted and ignored — there is no second chunk for a
high-water mark to hold back.
Failure
Anything the two renders report is forwarded verbatim, so the classes the
HTTP layer maps are the pipeline's own. What this module classifies itself is
the probe's output, which ffprobe can write successfully while still
describing something no waveform can be drawn from: a file with no audio
stream, or one whose duration nothing can determine. Both are :undecodable,
so both are a 415 — the source cannot yield this variant, permanently, and
that is the client's business rather than a server fault to retry.
Note what this is not claiming: f:mp3 on the same file currently answers
500, because ffmpeg's diagnostic there is "Output file does not contain any
stream" and AudioProxy.Ffmpeg.Render's classifiers do not match it. That
gap is the audio path's and predates peaks; peaks answering 500 as well would
have been consistency with a bug rather than with a contract.
A probe this proxy could not read is different again and stays
:render_failed: ffprobe exited cleanly and wrote something unparseable,
which says nothing about the source and everything about the install.
The request-side deadline is tighter here than for audio
Worth knowing before tuning AP_RENDER_TIMEOUT.
AudioProxy.Plugs.RenderAction bounds a render by a mailbox deadline of
AP_RENDER_TIMEOUT + 1s that restarts on every message. For an audio
render that is an idle timeout, because chunks arrive continuously, and the
pipeline's own timer always fires first — which is what that module's
moduledoc claims.
Peaks send exactly one chunk, at the end, so nothing resets that clock
between {:rendering, _} and completion: it is a total budget. Meanwhile
each inner render gets a timer of its own, so the pipeline would tolerate up
to twice the configured timeout across probe and decode. A peaks render
taking longer than AP_RENDER_TIMEOUT + 1s is therefore ended by the request
loop as a 504 with no ffmpeg diagnostic behind it, while neither subprocess
considers itself late.
At the 300 s default this needs a five-minute peaks render and no realistic source reaches it, which is why it is documented rather than designed around. Giving the two subprocesses one shared budget is the fix if it ever bites.
Summary
Functions
Starts a peaks render.
Types
@type spec() :: keyword()
What a peaks render needs beyond the decode argv.
:options— the parsedAudioProxy.Options.t/0, forpts,chand the trim the probe's duration has to be narrowed by.:input— what ffmpeg and ffprobe read; a presigned URL, usually.:protocols— the-protocol_whitelistset the probe runs under, fromAudioProxy.Ffmpeg.Command.protocols/1. Required, because this pipeline builds its own probe argv: the decode's whitelist is already inside:args, and a probe spawned without one would be the single route in the proxy that reads a source with every ffmpeg protocol available to it.:probe_executable— the ffprobe binary. Unset meansffprobefromPATH, which is what production uses; tests pass a stand-in.
Functions
@spec start_link(keyword()) :: GenServer.on_start()
Starts a peaks render.
Takes AudioProxy.Ffmpeg.Render.start_link/1's options — :args,
:consumer, :executable — plus the :peaks keyword list described in
spec/0.