AudioProxy.Peaks.Render (audio_proxy v0.4.0)

Copy Markdown View Source

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:

  1. Probe. AudioProxy.Ffprobe supplies both the argv and the mapping — the same ones /info answers with, so the duration this proxy reports and the duration it buckets by cannot drift apart. What differs is who runs the subprocess: /info blocks on Ffprobe.probe/2 because it is a request doing nothing else, while this process spawns the same argv and folds the chunks in handle_info/2, so it stays answerable to cancel/1 throughout. 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.
  2. Decode. The argv AudioProxy.Ffmpeg.Command builds for f:peaks: raw interleaved s16le on stdout, trimmed and downmixed. Each chunk is folded into AudioProxy.Peaks and 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

Types

What a peaks render needs beyond the decode argv.

Functions

Starts a peaks render.

Types

spec()

@type spec() :: keyword()

What a peaks render needs beyond the decode argv.

  • :options — the parsed AudioProxy.Options.t/0, for pts, ch and the trim the probe's duration has to be narrowed by.
  • :input — what ffmpeg and ffprobe read; a presigned URL, usually.
  • :protocols — the -protocol_whitelist set the probe runs under, from AudioProxy.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 means ffprobe from PATH, which is what production uses; tests pass a stand-in.

Functions

start_link(opts)

@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.