Skip to content

Latest commit

 

History

History
415 lines (322 loc) · 20.5 KB

File metadata and controls

415 lines (322 loc) · 20.5 KB

Configuring a transcode — the complete OutputSpec guide

Everything you can configure for a rivet job lives on one struct, OutputSpec. You build it (constructor + chained with_* setters), optionally validate it, then run it. This page documents every knob; for the internals see pipeline & architecture, and for the CLI equivalents see the CLI reference.

use rivet::{OutputSpec, Rung, Quality, AudioCodecPolicy, EncodePolicy,
            ChunkSeamMode, PerceptualTarget, run_job_blocking, fn_sink};
use rivet::progress::RungProgress;
use std::sync::Arc;

// A 3-rung single-file ladder, fully specified.
let spec = OutputSpec::single_file(vec![
    Rung::new(1920, 1080).with_quality(Quality::crf(28)),
    Rung::new(1280, 720).with_quality(Quality::target(PerceptualTarget::Standard)),
    Rung::new(854, 480),                              // default quality
])
.with_audio(AudioCodecPolicy::Auto)                        // passthrough / transcode / drop
.with_max_frame_rate(30.0)                            // cap output cadence
.web_sdr()                                            // color preset: BT.709 8-bit SDR
.encode_policy(EncodePolicy::AllGpus)                 // chunk-encode across every GPU
.chunk_seam_mode(ChunkSeamMode::ParallelConstQp);     // keep seams quality-flat

spec.validate()?;                                     // fail fast on incoherent specs

let bytes = std::fs::read("input.mkv")?;
let sink = Arc::new(fn_sink(|p: RungProgress| {
    println!("{:<6} {:?} {:>5.1}%  {} frames", p.label, p.status, p.percent, p.frames_done);
}));
let out = run_job_blocking(&bytes, &spec, Some("out_dir".as_ref()), sink)?;

Just want one file in, one file out? Skip the spec entirely: rivet::transcode_file("in.mkv", "out.mp4")? uses sensible defaults (source-resolution single rung, AAC/Opus passthrough, 8-bit SDR, all GPUs).


1. Construct — the output shape

Constructor Output
OutputSpec::single_file(rungs) One self-contained faststart MP4 per rung (video + audio; AV1 by default — set with_video_codec for H.264/H.265).
OutputSpec::hls(rungs, segment_seconds) A segmented CMAF/HLS package: master.m3u8 + an audio rendition group + video/<h>p/{init.mp4, seg-*.m4s, playlist.m3u8} per rung, segment-aligned for clean ABR.

rungs is a Vec<Rung> (next section). segment_seconds is the HLS target segment length (segments still break on keyframes). The constructor wires the matching Container + Muxer + OutputMode for you.


2. The ladder — rungs & quality

A Rung is one rendition: a target size + a per-rung Quality.

Rung::new(1280, 720)                       // auto label "720p", default quality
    .with_quality(Quality::crf(28))        // or .with_quality(Quality::target(..))
    .with_label("hd")                      // override the auto label
Rung method Effect
Rung::new(width, height) A rung at width × height; label auto-set to "<short-side>p", default quality.
.with_quality(Quality) Set the per-rung encoder quality.
.with_label(impl Into<String>) Override the auto label.
.short_side() The "p" number (min(width, height)).

Public fields: width, height, label, quality.

Quality

Quality::crf(28)                           // constant rate factor (lower = better)
Quality::target(PerceptualTarget::High)    // perceptual target instead of a CRF
Quality field Type Meaning
crf Option<u8> Constant rate factor, encoder-native (rav1e/NVENC 0..=255). None → derive from target.
speed_preset Option<u8> Encoder-native speed preset. None → derive from tier.
target QualityTarget Perceptual target (used when crf is None).
tier SpeedTier Speed/efficiency tier (used when speed_preset is None).
keyframe_interval Option<u32> GOP length in frames. None2 × fps (a 2-second GOP).
overrides EncodeOverrides Backend-agnostic per-rung knobs layered on the target/tier — a quality shift in libaom-CQ steps, tiles, reference frames, lookahead, B-frames. Inert by default.

Quality::crf / Quality::target are the two constructors; set the rest with struct-update syntax, e.g. Quality { tier: Speed::Archive, keyframe_interval: Some(120), ..Quality::crf(30) }, or .with_overrides(EncodeOverrides { .. }).

VMAF as the target. QualityTarget::Vmaf(n) aims every rung at a VMAF score; codec::encode::tuning turns it into each backend's quantiser through calibrated anchor tables, so Vmaf(93) means the same perceived quality on NVENC, QSV, AMF and rav1e (docs/av1-tuning-research.md has the tables and docs/av1-tuning-methodology.md how to re-calibrate for a new encoder). On every surface it is the word vmaf=93--target, target= on the socket, target in the API/manifest, target=vmaf=93 in the policy grammar. Whether a target delivers its VMAF is measured, not assumed: bench/ scores a ladder against its source with libvmaf.

GOP. OutputSpec::gop (with_gop, CLI --gop, key gop) sets the keyframe cadence for every rung — and, on the multi-GPU single-file path, the chunk grid, since a chunk is a whole number of GOPs. For HLS the segment grid is segment_seconds; a GOP shorter than the segment adds keyframes inside it, a longer one is silently the segment. A rung's own Quality::keyframe_interval wins over the spec-wide value.

Per-rung policy — with_rung_policy(RungPolicy)

A ladder wants different knobs at different positions: softer going down (the same quantizer at a quarter of the resolution is a far finer quantizer in terms of what an eye can resolve), one tile below 4K, more reference frames. Rather than hand-setting overrides on every rung, give the spec a RungPolicy and the engine resolves it against each rung's position before encoding, layering the rung's own overrides on top (the rung-specific knob wins; quality deltas accumulate):

use codec::encode::tuning::RungPolicy;

// The measured recommendation: +2 steps softer per rung going down, no top
// bonus, one tile below 4K, three reference frames.
let spec = OutputSpec::hls(rungs, 4.0).with_rung_policy(RungPolicy::recommended());

// Or the text grammar (what `--encode-policy` and the settings key take):
let policy: RungPolicy = "qstep=2;top:q=-2;short<=2159:tiles=1x1;any:refs=3".parse()?;

The grammar: rules separated by ;, each selector:key=value,..., later wins; selectors any/top/below_top/step=N/short<=N/short>=N; keys q, tiles (CxR), gop, lookahead, bframes, refs, multipass, grain, speed, target (vmaf=N allowed); qstep=N alone is the compounding per-rung step. An empty policy — the default — changes nothing. LadderPolicy is the recommendation as numbers, for tuning one of them.

  • QualityTarget (re-exported as PerceptualTarget): VisuallyLossless, High, Standard, Low, Vmaf(u8) (target a specific VMAF score).
  • SpeedTier (re-exported as Speed): Draft (fastest), Standard, Archive (slowest/most efficient).

Auto ladder

Don't want to hand-write rungs? Derive a standard ABR ladder from the source:

let rungs = rivet::standard_ladder(source_w, source_h, /* max_short_side */ 1080);
let spec = OutputSpec::single_file(rungs);

It snaps to standard short sides (2160/1440/1080/720/480/360/240), preserves aspect ratio, even-aligns dims, and caps the top rung.


3. Audio — with_audio(AudioCodecPolicy)

AudioCodecPolicy Behavior
Auto (default) Passthrough AAC / Opus / AC-3 / E-AC-3 verbatim; transcode MP3 / Vorbis → Opus; drop anything else.
ForceOpus Always produce Opus (passthrough Opus, transcode everything else).
Drop Video-only output.
spec.with_audio(AudioCodecPolicy::ForceOpus)

4. Color & bit depth

Two orthogonal axes. Most callers use a preset; the low-level setters are there when you need them.

spec.web_sdr()       // BT.709 8-bit SDR, tonemap any HDR source down (the default)
spec.hdr10()         // BT.2020 + PQ, 10-bit, no tonemap
spec.hlg()           // BT.2020 + HLG, 10-bit, no tonemap
spec.passthrough()   // keep the source's color + bit depth verbatim

Under the presets are exactly two methods:

Method Sets Values
with_color(ColorPolicy) gamut + transfer + tonemap decision TonemapToSdr (default) · Passthrough · Hdr10 · Hlg
with_bit_depth(BitDepth) bits per sample Auto (default — follow the color policy) · EightBit · TenBit

There is intentionally no with_gamut / with_transfer / with_color_spaceColorPolicy bundles them because only a few combinations are web-safe:

  • Gamut = the color primaries (which colors are representable): BT.709 (standard SDR) or BT.2020 (wide, for HDR).
  • Transfer = the transfer function / EOTF (the curve mapping stored values ↔ light, i.e. the brightness response): SDR gamma (~2.2/2.4), PQ (SMPTE ST 2084, absolute brightness — HDR10), or HLG (ARIB STD-B67, relative — broadcast HDR).
ColorPolicy Gamut Transfer Bit depth (with Auto) Tonemap
TonemapToSdr BT.709 gamma 8-bit HDR → SDR
Passthrough source source source no
Hdr10 BT.2020 PQ 10-bit no
Hlg BT.2020 HLG 10-bit no

The on-disk pixel format follows from bit depth: 8-bit → yuv420p, 10-bit → yuv420p10le (4:2:0). HDR needs a 10-bit encoder (nvidia, amd, or qsv — the software fallback is 8-bit); validate() rejects an HDR request a build can't produce. HDR is tagged in the container via colr/mdcv/clli atoms.

5b. Output codec — with_video_codec(...)

VideoCodecPolicy (the video analogue of AudioCodecPolicy) is Av1 (default), H264, or H265. It resolves to the encoder/muxer's low-level VideoCodec via VideoCodecPolicy::codec().

use rivet::VideoCodecPolicy;

let spec = OutputSpec::single_file(rungs).with_video_codec(VideoCodecPolicy::H264);

AV1 is the royalty-clean default (AV1 + Opus in MP4 = zero royalty exposure); H.264 / H.265 are for legacy-player compatibility and carry the patent-licensing obligations AV1 was chosen to avoid. All three work for single-file MP4 and CMAF/HLS — the muxer emits av01/avc1/avc3/hvc1/ hev1 sample entries with the matching config box and CODECS= string. H.265 encodes 8- or 10-bit (Main / Main 10 4:2:0) on NVENC + QSV — hardware- validated on RTX 3090 and Intel Arc — so with_bit_depth(TenBit) / a HDR ColorPolicy works for H.265 too. H.264 is 8-bit only: there is no hardware Hi10P profile on NVENC (no High 10 GUID) or QSV (no AVC High 10 in oneVPL), so a 10-bit H.264 request is capability-rejected, not down-converted. The encoder backend is chosen per GPU vendor: NVENC + QSV encode H.264/H.265; AMF and the software encoder currently reject them (a follow-up). The same string vocabulary (av1/h264/h265) drives the CLI --codec, the codec= settings key, the batch manifest codec:, and the HTTP codec field.


5. Frame rate — with_max_frame_rate(fps)

Cap the output cadence; the source cadence is otherwise preserved.

spec.with_max_frame_rate(30.0)   // never exceed 30 fps

6. Video filters — with_filters(...)

Per-frame transforms — geometry (crop, pad, flip, rotate, grayscale), an image overlay (PNG logo/watermark with alpha), and colour (invert, brightness, contrast, saturation) — applied to the decoded source once, before per-rung scaling, so a filter applies to every rendition. spec.filters is a list of codec::filter::VideoFilter:

spec.with_filters(vec![
    VideoFilter::Crop { w: 1920, h: 1080, x: None, y: None },
    VideoFilter::Overlay { image: "logo.png".into(), x: 24, y: 24 },
]);
// or parse the equivalent ffmpeg-style string form:
spec.with_filters(codec::filter::parse_chain("crop=1920:1080,overlay=logo.png:24:24")?);

See Video filters for the full filter set, the string + structured-object forms, and per-surface usage.


7. GPU selection — encode_policy(...), decode_policy(...)

Which cards, and how the decode and the encode are laid across them. Two questions, two enums, and each enum answers its question whole — so no two settings can contradict each other ("pin decode to card 2" and "split the decode across every card" are not both sayable).

Method Effect
encode_policy(EncodePolicy) The encode plan (below).
with_gpu_index(u32) Shorthand for encode_policy(SingleGpu(Some(idx))).
decode_policy(DecodePolicy) The decode plan (below).

EncodePolicy — which cards encode, and how the work is laid across them:

Variant Meaning
AllGpus (default) Every capable card, ladder-scheduled: one worker per card, each serving every rung and taking the next chunk of whichever rung is furthest behind. A card idles only when the whole job is out of work, and a ladder deeper than the GPU count still costs one decode. Measured faster than the pinned shape.
PerRung Every capable card, each worker pinned to its own rungs (rung i to worker i mod workers) — "one rung, one GPU" when the ladder fits the pool. Predictable placement, and a rung's chunks all come off one card, at the cost of cards idling when their rungs are blocked. For benchmarking against AllGpus, and for hosts where placement matters more than throughput.
SingleGpu(Option<u32>) One card — pinned to Some(i), or the first with None — one encoder per rung, serial. Single-file output is seam-free by construction (there are no chunks); HLS runs one worker.
Family(GpuFamily) Every card of one vendor (GpuFamily::{Nvidia, Amd, Intel}), ladder-scheduled — e.g. ignore an integrated GPU.

DecodePolicy — which card(s) decode, and whether the decode is one pump or split into ranges:

Variant Meaning
Auto (default) Split the decode into one range per decode-capable card of the encode set, where the source allows (an un-spliced H.264/H.265 input whose keyframes fall on chunk boundaries — see plan_decode_ranges); whole otherwise. The cards decode different stretches of the source at the same time; the numbering stays continuous across the join and the output is byte-identical to a whole-source decode.
Whole One decoder for the whole source, on the first capable card of the encode set. What every job did before ranges existed; the control arm of any comparison.
SpecificGpu(u32) One decoder pinned to that card (e.g. decode on an iGPU while the dGPUs encode). Never split — a split on one card is no split.
FastestGpu Benchmark every decode-capable card on a short prefix of the input and put one decoder on the quickest. A no-op on single-GPU hosts.
Ranges(usize) Split into up to this many ranges, round-robin over the capable cards. More ranges than cards is legal (several pumps share a card) and is how the split is exercised on a one-card host.

Both apply to HLS and to multi-GPU single-file alike (single-file's unit is a chunk of several GOPs stitched back into one MP4 — see §8 for the seams). CLI: --encode all|per-rung|single|gpu:N|family:VENDOR, --decode auto|whole|fastest|gpu:N|ranges:N; settings keys encode, decode; the older --gpu / --single-gpu / --gpu-family / --decode-gpu still work as spellings of the same choices.

spec.encode_policy(EncodePolicy::Family(rivet::GpuFamily::Nvidia))
    .decode_policy(DecodePolicy::SpecificGpu(0));   // decode on GPU 0, encode on the NVIDIA cards

spec.encode_policy(EncodePolicy::PerRung)              // one rung, one GPU
    .decode_policy(DecodePolicy::Whole);              // one decoder, e.g. to A/B the split

8. Chunk seams — chunk_seam_mode(ChunkSeamMode)

Only relevant when multiple GPUs encode a single file: each rung is chunked at GOP boundaries, encoded in parallel, and stitched. Each chunk is an independent IDR-led GOP so it always plays, but per-chunk rate control can step quality at the ~2 s seams. This knob governs that (chiefly for NVENC, which otherwise runs VBR per chunk; AMD/QSV chunks are already constant-QP):

ChunkSeamMode Seams Speed
Parallel (default) possible mild NVENC steps fastest (all GPUs)
ParallelConstQp flat (forced constant-QP, quality still tracks the target) fast (all GPUs)
Serial none (one encoder for the whole file) slower; HLS still uses every GPU

Single-GPU hosts, --gpu/SingleGpu, and HLS jobs are unaffected (HLS segments are independent by design).


9. Validate — validate()

spec.validate()?;

Rejects incoherent specs before any work starts: no rungs, zero/odd dimensions, container/muxer/mode mismatch, HDR with forced 8-bit, or 10-bit/HDR on a build with no 10-bit encoder (queryable at runtime via codec::encode::build_output_caps()).


10. Run it

Function Use
rivet::transcode_file(input, output) One file → one file, default spec. Returns a TranscodeOutcome.
rivet::transcode_bytes(&bytes, ..) The in-memory variant.
rivet::run_job_blocking(&bytes, &spec, out_dir, sink) Run a full OutputSpec synchronously. out_dir: Option<&Path> (the HLS/multi-rung asset root; None = temp dir). Returns JobOutput.
rivet::run_job(&bytes, &spec, out_dir, sink).await The async variant (drive from a Tokio runtime).
rivet::probe_file(path) / probe_bytes(&bytes) Inspect without transcoding → MediaInfo.

Progress

Both run_job* take a ProgressSink that streams a uniform RungProgress per rung — label, status (RungStatus: PendingRunningCompleted/Failed), percent, frames_done, segment + byte counters. Wire it however you like:

use std::sync::Arc;

// a closure
let sink = Arc::new(rivet::fn_sink(|p| println!("{} {:.0}%", p.label, p.percent)));

// or a Tokio channel (async)
let (tx, mut rx) = tokio::sync::mpsc::channel(64);
let sink = Arc::new(rivet::channel_sink(tx));

Full method reference

OutputSpec Signature Section
single_file (Vec<Rung>) -> Self 1
hls (Vec<Rung>, f32) -> Self 1
with_audio (AudioCodecPolicy) -> Self 3
with_max_frame_rate (f64) -> Self 5
with_color (ColorPolicy) -> Self 4
with_bit_depth (BitDepth) -> Self 4
web_sdr / hdr10 / hlg / passthrough () -> Self 4
with_gpu_index (u32) -> Self 6
encode_policy (EncodePolicy) -> Self 7
decode_policy (DecodePolicy) -> Self 7
chunk_seam_mode (ChunkSeamMode) -> Self 7
with_rung_policy (RungPolicy) -> Self 2
validate (&self) -> Result<()> 8
tonemaps (&self) -> bool (does this spec tonemap?)
resolve_output (ColorMetadata, PixelFormat) -> (ColorMetadata, PixelFormat) (resolve color/depth vs a source)

All OutputSpec fields are pub, so anything above can also be set directly (spec.color = ColorPolicy::Hdr10;): mode, video_codec, audio, container, muxer, rungs, max_frame_rate, gpu_index, encode_policy, decode_policy, color, bit_depth, chunk_seam_mode, rung_policy. The builders are the recommended path (they keep linked fields — e.g. gpu_index and encode_policy — in sync).