rivet/spec/policy.rs
1//! Policy enums — how video/audio codec, container, muxer, output-mode, color,
2//! bit-depth, encode/decode distribution, chunk-seam handling, and GPU family
3//! are selected. All types are `pub` and re-exported from the parent `spec`
4//! module so callers reach them as `rivet::spec::VideoCodecPolicy`, etc.
5
6use codec::frame::VideoCodec;
7
8/// Output **video** codec policy — the video analogue of [`AudioCodecPolicy`].
9/// Selects which codec the encoder produces:
10/// - `Av1` *(default)* — royalty-clean (AV1 + Opus in MP4 = zero royalty exposure).
11/// - `H264` / `H265` — for legacy-player compatibility; they carry the
12/// patent-licensing obligations AV1 was chosen to avoid.
13///
14/// All three work for single-file MP4 **and** CMAF/HLS. Resolve to the
15/// encoder/muxer's [`VideoCodec`] with [`VideoCodecPolicy::codec`].
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
17pub enum VideoCodecPolicy {
18 #[default]
19 Av1,
20 H264,
21 H265,
22}
23
24impl VideoCodecPolicy {
25 /// Resolve to the low-level [`VideoCodec`] the encoder + muxer consume.
26 pub fn codec(self) -> VideoCodec {
27 match self {
28 VideoCodecPolicy::Av1 => VideoCodec::Av1,
29 VideoCodecPolicy::H264 => VideoCodec::H264,
30 VideoCodecPolicy::H265 => VideoCodec::H265,
31 }
32 }
33}
34
35/// Output **audio** codec policy — how the source audio track is handled.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub enum AudioCodecPolicy {
38 /// Passthrough AAC / Opus / AC-3 / E-AC-3 verbatim; transcode MP3 /
39 /// Vorbis to Opus; drop anything else.
40 #[default]
41 Auto,
42 /// Keep/produce Opus: passthrough Opus, transcode everything else to Opus.
43 ForceOpus,
44 /// Drop audio entirely (video-only output).
45 Drop,
46}
47
48/// Deprecated alias for [`AudioCodecPolicy`] (renamed for symmetry with
49/// [`VideoCodecPolicy`]).
50#[deprecated(since = "0.1.5", note = "renamed to AudioCodecPolicy")]
51pub type AudioPolicy = AudioCodecPolicy;
52
53/// Output container.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
55pub enum Container {
56 /// Plain MP4 (ISO-BMFF), one self-contained file.
57 #[default]
58 Mp4,
59 /// Fragmented MP4 (CMAF) — `moof`+`mdat` segments, for HLS/DASH.
60 Cmaf,
61}
62
63/// Muxer — how the container bytes are assembled.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
65pub enum Muxer {
66 /// `Av1Mp4Muxer` — a single faststart MP4 with interleaved A/V.
67 #[default]
68 Mp4File,
69 /// `CmafVideoMuxer` + `CmafAudioMuxer` + HLS playlists.
70 CmafHls,
71}
72
73/// The high-level shape of the output.
74#[derive(Debug, Clone, PartialEq)]
75pub enum OutputMode {
76 /// One self-contained file per rung.
77 SingleFile,
78 /// Segmented CMAF + HLS: a media playlist per rung, a shared audio
79 /// rendition, and a master playlist. `segment_seconds` is the target
80 /// segment length (segments still break on keyframes).
81 Hls { segment_seconds: f32 },
82}
83
84impl Default for OutputMode {
85 fn default() -> Self {
86 OutputMode::SingleFile
87 }
88}
89
90/// How the decode pump selects its GPU — the decode-side counterpart to
91/// [`EncodePolicy`]. A sum type so the modes stay mutually exclusive: you can't
92/// accidentally ask for "a specific GPU **and** the fastest".
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
94pub enum DecodePolicy {
95 /// Follow the encode policy: the first device of the selected family/set,
96 /// round-robin for per-rung pumps. The default.
97 #[default]
98 Auto,
99 /// Pin decode to this physical GPU index (e.g. decode on an iGPU while the
100 /// dGPUs encode).
101 SpecificGpu(u32),
102 /// Benchmark every decode-capable GPU on a short prefix of the input before
103 /// the job and pin the pump to the fastest. The engine resolves this to
104 /// `SpecificGpu` once the winner is known; a no-op on single-GPU hosts.
105 FastestGpu,
106}
107
108impl DecodePolicy {
109 /// The concrete pinned GPU index, if any. `Auto` and an unresolved
110 /// `FastestGpu` both return `None`, so the engine follows the encode policy.
111 pub fn gpu_index(self) -> Option<u32> {
112 match self {
113 DecodePolicy::SpecificGpu(i) => Some(i),
114 DecodePolicy::Auto | DecodePolicy::FastestGpu => None,
115 }
116 }
117
118 /// Whether the engine should benchmark decoders and resolve a fastest GPU.
119 pub fn is_fastest(self) -> bool {
120 matches!(self, DecodePolicy::FastestGpu)
121 }
122}
123
124impl std::str::FromStr for DecodePolicy {
125 type Err = String;
126
127 /// Parse `auto` / `fastest` / a GPU index — the `--decode-gpu` value space.
128 fn from_str(s: &str) -> Result<Self, Self::Err> {
129 match s.trim().to_ascii_lowercase().as_str() {
130 "" | "auto" => Ok(DecodePolicy::Auto),
131 "fastest" => Ok(DecodePolicy::FastestGpu),
132 other => other.parse::<u32>().map(DecodePolicy::SpecificGpu).map_err(|_| {
133 format!("decode-gpu must be 'auto', 'fastest', or a GPU index; got '{other}'")
134 }),
135 }
136 }
137}
138
139/// Selects how a job's encode work is distributed across the host's GPUs.
140///
141/// Applies to both the single-file and HLS paths: `AllGpus` runs the multi-GPU
142/// engine (decode once, chunk each rung across every GPU, stitch); `SingleGpu`
143/// constrains the GPU pool to one device and (for single-file) takes the serial
144/// encode path with no chunk overhead.
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
146pub enum EncodePolicy {
147 /// Use **all** available GPUs (the multi-GPU lease-pool engine). For
148 /// single-file this chunk-encodes each rung across the GPUs and stitches
149 /// the packets; it falls back to single-GPU serial encode when only one
150 /// GPU is present or the frame count is unknown. This is the default.
151 #[default]
152 AllGpus,
153 /// Use a **single** GPU. `None` picks the first available GPU; `Some(i)`
154 /// pins to GPU index `i`. Single-file uses the serial encode path.
155 SingleGpu(Option<u32>),
156 /// Use every GPU of one **vendor family** (and only that family) — e.g.
157 /// `Family(GpuFamily::Nvidia)` on a host with an NVIDIA discrete + an
158 /// integrated AMD/Intel GPU uses just the NVIDIA cards. With more than one
159 /// device in the family, single-file chunks across them like `AllGpus`.
160 Family(GpuFamily),
161}
162
163/// A GPU vendor family, for constraining encode to one vendor's devices.
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum GpuFamily {
166 Nvidia,
167 Amd,
168 Intel,
169}
170
171/// How the multi-GPU **single-file** path keeps quality consistent across the
172/// chunk seams it stitches into one continuous video.
173///
174/// Only relevant when more than one GPU encodes a single file (the `AllGpus` /
175/// `Family` policies on a multi-GPU host); single-GPU hosts, `SingleGpu`, and
176/// HLS (whose segments are independent by design) are unaffected. AMD (AMF) and
177/// Intel (QSV) chunks are already constant-QP, so their seams are quality-flat
178/// — this chiefly governs **NVENC**, which otherwise runs VBR per chunk and can
179/// leave a mild quality step at the ~2 s boundaries.
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
181pub enum ChunkSeamMode {
182 /// Default. Chunk across GPUs for throughput; each chunk uses its encoder's
183 /// normal rate control (VBR on NVENC). Fastest; NVENC may show mild quality
184 /// steps at the seams on complex content.
185 #[default]
186 Parallel,
187 /// Chunk across GPUs but force **constant-QP** so the seams are
188 /// quality-flat, keeping the multi-GPU speedup. The QP is derived from the
189 /// `QualityTarget` (via the per-encoder tuning CQ), so quality still tracks
190 /// the target — the hand-rolled NVENC sets a real const-QP rather than a
191 /// preset default. AMD/QSV are unchanged (already constant-QP).
192 ParallelConstQp,
193 /// Encode the whole file with **one encoder** — seam-free and
194 /// `QualityTarget`-accurate, at the cost of the multi-GPU single-file
195 /// speedup. (Like `SingleGpu`, but leaves multi-GPU in place for HLS jobs.)
196 Serial,
197}
198
199/// Output **color** policy — the gamut (which colors are representable) and the
200/// transfer curve (SDR vs HDR), plus whether to tonemap an HDR source down. This
201/// is the *color* half of the decision; bit depth is the separate [`BitDepth`]
202/// half (though the HDR variants here imply 10-bit on their own).
203///
204/// The decode pump never tonemaps on its own — this policy decides.
205///
206/// Glossary (the jargon these variants use):
207/// - **BT.709** — the standard HD / SDR color gamut. What the vast majority of
208/// video uses; "SDR" output means BT.709.
209/// - **BT.2020** — the *wide* gamut used by HDR: more saturated, deeper colors.
210/// - **PQ** (SMPTE ST 2084) — the HDR10 transfer curve (absolute brightness, up
211/// to 10,000 nits).
212/// - **HLG** (ARIB STD-B67) — the broadcast-friendly HDR transfer curve
213/// (relative brightness; degrades gracefully on SDR screens).
214/// - **tonemap** — squeeze an HDR signal's brightness/gamut down into SDR so it
215/// looks right on ordinary (BT.709, 8-bit) screens.
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
217pub enum ColorPolicy {
218 /// **SDR out.** Tonemap HDR (PQ / HLG) sources down to 8-bit **BT.709** SDR;
219 /// SDR sources pass through unchanged. The default — maximally web-compatible.
220 /// (Convenience builder: [`super::OutputSpec::web_sdr`].)
221 #[default]
222 TonemapToSdr,
223 /// **Verbatim.** Keep the source's gamut, transfer, and bit depth as-is — no
224 /// tonemap, no re-signaling. An HDR source stays HDR (needs a 10-bit
225 /// encoder); an SDR source stays SDR. (Builder: [`super::OutputSpec::passthrough`].)
226 Passthrough,
227 /// **HDR10 out.** Force **BT.2020** gamut + **PQ** transfer, 10-bit. Sets
228 /// 10-bit on its own, so you do *not* also need [`BitDepth::TenBit`].
229 /// (Builder: [`super::OutputSpec::hdr10`].)
230 Hdr10,
231 /// **HLG out.** Force **BT.2020** gamut + **HLG** transfer, 10-bit. Implies
232 /// 10-bit. (Builder: [`super::OutputSpec::hlg`].)
233 Hlg,
234}
235
236impl ColorPolicy {
237 /// Whether the decode pump tonemaps HDR→SDR under this policy.
238 pub fn tonemaps(self) -> bool {
239 matches!(self, ColorPolicy::TonemapToSdr)
240 }
241
242 /// Whether this policy signals HDR (PQ/HLG) in the output bitstream.
243 pub fn is_hdr(self) -> bool {
244 matches!(self, ColorPolicy::Hdr10 | ColorPolicy::Hlg)
245 }
246}
247
248/// Output **bit depth** — bits per sample. The on-disk pixel format is *derived*
249/// from this (the encoder is always AV1 4:2:0, the web-safe chroma subsampling):
250/// 8-bit → **`yuv420p`**, 10-bit → **`yuv420p10le`** (`le` = little-endian 16-bit
251/// words holding 10 valid bits). Bit depth is one axis; gamut + SDR/HDR transfer
252/// is the orthogonal [`ColorPolicy`] axis.
253///
254/// You rarely set this by hand: `Auto` derives it from the color policy.
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
256pub enum BitDepth {
257 /// Derive depth from the [`ColorPolicy`]: 8-bit for an SDR tonemap, 10-bit
258 /// for HDR (`Hdr10` / `Hlg`), the source's own depth for `Passthrough`. The
259 /// default — the right choice almost always.
260 #[default]
261 Auto,
262 /// Force **8-bit** 4:2:0 (`yuv420p`) — universal web compatibility.
263 EightBit,
264 /// Force **10-bit** 4:2:0 (`yuv420p10le`) — higher precision (banding-free
265 /// gradients), and required by the HDR policies. Needs a 10-bit-capable
266 /// encoder: NVENC (`nvidia`), AMF (`amd`), QSV (`qsv`), or `ffmpeg`.
267 TenBit,
268}