Skip to main content

oxideav_h265/
encoder.rs

1//! H.265 / HEVC encoder — write-side building blocks.
2//!
3//! The encode stack mirrors the decode stack bottom-up:
4//!
5//! * [`bitwriter::BitWriter`] — the MSB-first RBSP bit sink (`u(n)`,
6//!   `ue(v)` / `se(v)`, `rbsp_trailing_bits()`).
7//! * [`nal`] — §7.4.1.1 emulation-prevention escaping, the §7.3.1.2
8//!   NAL header, and Annex B framing.
9//! * [`cabac::CabacEncoder`] — the §9.3.5 arithmetic encoding engine
10//!   (the spec's informative encoder that "matches the arithmetic
11//!   decoding engine described in clause 9.3.4.3").
12//!
13//! Every layer is pinned against the crate's own decode side: bit
14//! fields re-read through [`crate::bitreader::BitReader`], NAL units
15//! re-walked through [`crate::nal`], and CABAC bin streams re-decoded
16//! through [`crate::cabac::CabacEngine`].
17
18// internal — exposed for tests/fuzz; not part of the stable API
19#[doc(hidden)]
20pub mod bitwriter;
21// internal — exposed for tests/fuzz; not part of the stable API
22mod aq;
23#[doc(hidden)]
24pub mod cabac;
25pub mod ctu;
26pub mod hrd;
27pub mod inter;
28pub mod intra;
29pub mod loopfilter;
30pub mod pyramid;
31// internal — exposed for tests/fuzz; not part of the stable API
32#[cfg(test)]
33mod ccp_streams;
34#[doc(hidden)]
35pub mod nal;
36#[cfg(test)]
37mod palette_streams;
38pub mod pcm;
39pub mod rate;
40#[cfg(test)]
41mod rdpcm_streams;
42#[cfg(test)]
43mod scc_streams;
44// internal — exposed for tests/fuzz; not part of the stable API
45#[doc(hidden)]
46pub mod residual;
47
48/// Registry-encoder coding mode, selected by the `mode` codec option.
49#[derive(Debug)]
50enum EncodeMode {
51    /// PCM-only IDR bootstrap: bit-exact lossless, every CU a
52    /// §7.3.8.7 PCM block. The default (`mode` absent or `"pcm"`).
53    Pcm,
54    /// Real CABAC intra coding ([`intra::encode_idr_intra_au`]) at
55    /// the carried `SliceQpY` (`mode = "intra"`, `qp` option, default
56    /// 26) with the carried §8.7 in-loop filters (`deblock` / `sao`
57    /// options); the `bitrate` option swaps the constant QP for a
58    /// per-frame ABR election.
59    Intra {
60        /// Constant `SliceQpY` when `rc` is off.
61        qp: i32,
62        /// Quadtree-coder geometry (the `ctb` option; `None` keeps
63        /// the fixed-geometry bootstrap coder).
64        tree: Option<ctu::TreeCfg>,
65        /// CTU-level rate feedback (the `cturc` option).
66        ctu_rc: bool,
67        /// The §8.7 in-loop filter switches.
68        lf: loopfilter::LoopFilterCfg,
69        /// ABR controller (the `bitrate` / `fps` options).
70        rc: Option<rate::RateController>,
71        /// Spatial adaptive-quantization strength (the `aq` option,
72        /// 0..=3; nonzero signals per-CTB `cu_qp_delta`).
73        aq: u8,
74        /// §E.2.1 VUI timing declaration (an explicit `fps` option).
75        timing: Option<(u32, u32)>,
76        /// HRD signalling (the `hrd` option): the §E.2.2 schedule the
77        /// SPS VUI declares plus the Annex C clock that emits and
78        /// enforces the per-AU §D.2.2 / §D.2.3 SEI.
79        hrd: Option<(hrd::HrdSignalCfg, hrd::HrdClock)>,
80    },
81    /// Low-delay inter coding (`mode = "inter"`): `IDR, P, P, …`
82    /// GOPs through [`inter::LowDelayPEncoder`] (`qp` option, default
83    /// 26; `gop` option = GOP length in frames, default 0 = a single
84    /// leading IDR).
85    Inter(inter::LowDelayPEncoder),
86    /// Hierarchical-B coding (`mode = "inter"` + `pyramid = <gop>`):
87    /// dyadic B pyramids through [`pyramid::PyramidEncoder`] —
88    /// out-of-order coding, so packets arrive in bursts per mini-GOP
89    /// and [`Encoder::flush`] emits the low-delay tail.
90    Pyramid {
91        /// The buffering pyramid encoder.
92        enc: pyramid::PyramidEncoder,
93        /// `log2(gop)` — the dts-behind-pts reorder delay in frames.
94        delay: i64,
95        /// The caller-supplied pts of each pushed frame, by display
96        /// index (`None` ⇒ the display index itself).
97        pts_by_display: Vec<Option<i64>>,
98        /// Decode-order packet counter (drives dts).
99        decode_count: i64,
100    },
101}
102
103use std::collections::VecDeque;
104
105use oxideav_core::{
106    CodecId, CodecParameters, Encoder, Error, Frame, Packet, PixelFormat, Result, TimeBase,
107};
108
109/// H.265 registry encoder behind the [`oxideav_core::Encoder`]
110/// contract. Every input frame becomes one Annex B access unit — a
111/// self-contained IDR (`VPS + SPS + PPS + IDR_N_LP`) in the two intra
112/// modes and at inter-mode GOP starts, or a `TRAIL_R` P slice inside
113/// an inter-mode GOP. Three coding modes, selected by the `mode`
114/// codec option:
115///
116/// * `"pcm"` (default) — the PCM-only bootstrap: every coding unit a
117///   §7.3.8.7 PCM block, bit-exact lossless.
118/// * `"intra"` — real CABAC intra coding (§8.4 prediction + forward
119///   transform/quant + §7.3.8 syntax) at the `qp` option's
120///   `SliceQpY` (0..=51, default 26).
121/// * `"inter"` — low-delay `IDR, P, P, …` coding (§8.5 inter
122///   prediction with per-CTU skip / merge / AMVP / intra decisions)
123///   at the `qp` option's `SliceQpY`; the `gop` option sets the IDR
124///   period in frames (default 0 = a single leading IDR); the
125///   `bslices` option (`"1"` / `"true"`) codes the non-IDR frames as
126///   low-delay B slices; the `amp` option switches to the AMP stream
127///   configuration (`MinCbSizeY == 8`, `amp_enabled_flag == 1`, the
128///   four asymmetric PART_2NxnU/2NxnD/nLx2N/nRx2N shapes competing in
129///   the inter CU ladder); the `pyramid` option (a power of two in
130///   2..=16) switches to hierarchical-B coding — dyadic mini-GOPs
131///   coded out of display order with per-layer QP offsets — the
132///   `pyramidstep` option (0..=6, default 1) sets the per-layer step
133///   of that hierarchical allocation (`SliceQpY = base + layer *
134///   step`, composing with `aq`'s per-CTB deltas on top); packets
135///   then arrive in bursts per mini-GOP (dts trails pts by
136///   `log2(pyramid)` frames) and `flush()` emits the tail. `pyramid`
137///   excludes the `gop` / `bslices` options.
138///
139/// The `"intra"` and `"inter"` modes accept the §8.7 in-loop filter
140/// options `deblock` and `sao` (`"1"` / `"true"` to enable): the
141/// encoder then signals the filters, elects them per slice / CTB
142/// against distortion, and reconstructs through them (the reference
143/// path a conforming decoder holds).
144///
145/// The `bitrate` option (bits per second, optional `k` / `M` suffix)
146/// switches the `"intra"` / `"inter"` modes from constant QP to
147/// average-bitrate coding: [`rate::RateControlCfg`] semantics, with
148/// the `fps` option (`"25"` or `"30000/1001"`, default 25) fixing the
149/// frame rate the budget is spread over and an explicit `qp` option
150/// seeding the starting QP. `fps` also sets the packet time base.
151/// `bufsize` (bits; requires `bitrate`) adds a VBV constraint: no
152/// access unit may exceed the modelled decoder buffer (frames are
153/// re-encoded at a higher QP when needed) — on the low-delay, intra
154/// AND pyramid paths, the last with per-access-unit accounting at
155/// each decode instant across the mini-GOP burst.
156///
157/// The `aq` option (1..=3, `"intra"` / `"inter"` modes) enables
158/// spatial adaptive quantization: per-CTB QP offsets from luma
159/// activity, signalled through §7.3.8.10 `cu_qp_delta`.
160///
161/// The `hrd` option (`"1"` / `"true"`; requires `bitrate`, `bufsize`
162/// AND an explicit `fps`) declares and enforces HRD conformance: the
163/// SPS VUI gains a §E.2.2 `hrd_parameters( )` delivery schedule (NAL
164/// HRD, one CPB at the target rate and VBV size, VBR, fixed picture
165/// rate), every IRAP access unit carries a §D.2.2 buffering-period
166/// SEI, every access unit a §D.2.3 pic-timing SEI (the pyramid's
167/// `pic_dpb_output_delay` carries its reorder schedule), and coded
168/// sizes are additionally capped by an exact Annex C CPB replay so
169/// the emitted stream satisfies the §C.4 conformance conditions.
170/// The `cbr` option (requires `hrd`) switches the schedule to
171/// constant-bit-rate delivery (`cbr_flag == 1`): back-to-back
172/// eq. C-3 arrivals, mid-stream initial delays held to the
173/// two-sided eq. C-19 bound, and §7.3.4 filler-data NAL units
174/// padding underruns so the CPB cannot overflow (needs a `bufsize`
175/// of at least two frame intervals of the bitrate).
176///
177/// 4:2:0 8-bit, dimensions multiples of 16.
178pub struct H265Encoder {
179    codec_id: CodecId,
180    output_params: CodecParameters,
181    width: usize,
182    height: usize,
183    mode: EncodeMode,
184    ready: VecDeque<Packet>,
185    frame_index: i64,
186    /// Packet time base (`fps_den / fps_num` from the `fps` option;
187    /// default 1/25).
188    time_base: TimeBase,
189}
190
191/// Former name of [`H265Encoder`] (from when the registry encoder was
192/// PCM-only).
193pub type H265PcmEncoder = H265Encoder;
194
195impl std::fmt::Debug for H265Encoder {
196    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197        f.debug_struct("H265Encoder")
198            .field("width", &self.width)
199            .field("height", &self.height)
200            .field("mode", &self.mode)
201            .field("ready", &self.ready.len())
202            .finish()
203    }
204}
205
206/// Direct factory endpoint: construct the software H.265 encoder.
207/// Codec options: `mode` (`"pcm"` lossless bootstrap, the default,
208/// `"intra"` real CABAC intra coding, or `"inter"` low-delay P GOPs),
209/// `qp` (`SliceQpY` 0..=51 for the intra / inter modes, default 26),
210/// `gop` (inter mode IDR period, default 0 = single leading IDR),
211/// `amp` (inter mode: asymmetric motion partitions), the in-loop
212/// filter switches `deblock` / `sao` (intra / inter modes; default
213/// off), the ABR pair `bitrate` / `fps` (intra / inter modes:
214/// per-frame QP elected against a target average bitrate), and `aq`
215/// (intra / inter modes: spatial adaptive quantization via per-CTB
216/// `cu_qp_delta`, strength 1..=3).
217///
218/// # Errors
219/// [`Error::InvalidData`] when width / height are missing or not
220/// nonzero multiples of 16, the pixel format is declared and is not
221/// 4:2:0 8-bit planar, or a codec option is malformed.
222pub fn make_encoder(params: &CodecParameters) -> Result<Box<dyn Encoder>> {
223    let width = params
224        .width
225        .ok_or_else(|| Error::InvalidData("h265 encode: width is required".into()))?
226        as usize;
227    let height = params
228        .height
229        .ok_or_else(|| Error::InvalidData("h265 encode: height is required".into()))?
230        as usize;
231    if width == 0 || height == 0 || width % 16 != 0 || height % 16 != 0 {
232        return Err(Error::InvalidData(format!(
233            "h265 encode: dimensions must be nonzero multiples of 16, got {width}x{height}"
234        )));
235    }
236    if let Some(pf) = params.pixel_format {
237        if pf != PixelFormat::Yuv420P {
238            return Err(Error::InvalidData(format!(
239                "h265 encode: only yuv420p input is supported, got {pf:?}"
240            )));
241        }
242    }
243    let parse_qp = |params: &CodecParameters| -> Result<i32> {
244        match params.options.get("qp") {
245            None => Ok(26),
246            Some(v) => v
247                .parse::<i32>()
248                .ok()
249                .filter(|q| (0..=51).contains(q))
250                .ok_or_else(|| {
251                    Error::InvalidData(format!("h265 encode: qp must be 0..=51, got {v:?}"))
252                }),
253        }
254    };
255    let parse_flag = |params: &CodecParameters, key: &str| -> Result<bool> {
256        match params.options.get(key) {
257            None | Some("0") | Some("false") => Ok(false),
258            Some("1") | Some("true") => Ok(true),
259            Some(v) => Err(Error::InvalidData(format!(
260                "h265 encode: {key} must be 0/1/true/false, got {v:?}"
261            ))),
262        }
263    };
264    let parse_lf = |params: &CodecParameters| -> Result<loopfilter::LoopFilterCfg> {
265        let sao = parse_flag(params, "sao")?;
266        Ok(loopfilter::LoopFilterCfg {
267            deblocking: parse_flag(params, "deblock")?,
268            sao_luma: sao,
269            sao_chroma: sao,
270        })
271    };
272    // `bitrate` — bits per second, optional `k` / `M` suffix.
273    let parse_bitrate = |params: &CodecParameters| -> Result<Option<u64>> {
274        let Some(v) = params.options.get("bitrate") else {
275            return Ok(None);
276        };
277        let (digits, mult) = match v.as_bytes().last() {
278            Some(b'k' | b'K') => (&v[..v.len() - 1], 1_000u64),
279            Some(b'M') => (&v[..v.len() - 1], 1_000_000),
280            _ => (v, 1),
281        };
282        digits
283            .parse::<u64>()
284            .ok()
285            .map(|n| n.saturating_mul(mult))
286            .filter(|&b| b >= 1_000)
287            .map(Some)
288            .ok_or_else(|| {
289                Error::InvalidData(format!(
290                    "h265 encode: bitrate must be an integer >= 1000 bits/s \
291                     (optional k/M suffix), got {v:?}"
292                ))
293            })
294    };
295    // `fps` — `"25"` or `"30000/1001"`, default 25.
296    let parse_fps = |params: &CodecParameters| -> Result<(u32, u32)> {
297        let Some(v) = params.options.get("fps") else {
298            return Ok((25, 1));
299        };
300        let parsed = match v.split_once('/') {
301            None => v.parse::<u32>().ok().map(|n| (n, 1)),
302            Some((n, d)) => n.parse::<u32>().ok().zip(d.parse::<u32>().ok()),
303        };
304        parsed.filter(|&(n, d)| n > 0 && d > 0).ok_or_else(|| {
305            Error::InvalidData(format!(
306                "h265 encode: fps must be a positive integer or num/den ratio, got {v:?}"
307            ))
308        })
309    };
310    // `aq` — spatial adaptive-quantization strength 0..=3.
311    let parse_aq = |params: &CodecParameters| -> Result<u8> {
312        match params.options.get("aq") {
313            None => Ok(0),
314            Some(v) => v.parse::<u8>().ok().filter(|&a| a <= 3).ok_or_else(|| {
315                Error::InvalidData(format!("h265 encode: aq must be 0..=3, got {v:?}"))
316            }),
317        }
318    };
319    // `ctb` — quadtree-coder CTB size (16 / 32 / 64): routes the
320    // intra / inter modes through the recursive coding-quadtree coder
321    // (encoder::ctu) instead of the fixed one-CU-per-CTB bootstrap.
322    let parse_ctb = |params: &CodecParameters| -> Result<Option<ctu::TreeCfg>> {
323        match params.options.get("ctb") {
324            None => Ok(None),
325            Some(v) => v
326                .parse::<usize>()
327                .ok()
328                .and_then(ctu::TreeCfg::new)
329                .map(Some)
330                .ok_or_else(|| {
331                    Error::InvalidData(format!("h265 encode: ctb must be 16, 32 or 64, got {v:?}"))
332                }),
333        }
334    };
335    let tree = parse_ctb(params)?;
336    // `cturc` — CTU-level rate feedback (requires bitrate + ctb).
337    let ctu_rc = parse_flag(params, "cturc")?;
338    if ctu_rc && (params.options.get("bitrate").is_none() || tree.is_none()) {
339        return Err(Error::InvalidData(
340            "h265 encode: cturc requires the bitrate and ctb options".into(),
341        ));
342    }
343    // `tmvp` — temporal motion-vector prediction (inter mode).
344    let tmvp = parse_flag(params, "tmvp")?;
345    // `refs` — active references per list, 1..=4 (inter mode).
346    let refs = match params.options.get("refs") {
347        None => None,
348        Some(v) => Some(
349            v.parse::<usize>()
350                .ok()
351                .filter(|n| (1..=4).contains(n))
352                .ok_or_else(|| {
353                    Error::InvalidData(format!("h265 encode: refs must be 1..=4, got {v:?}"))
354                })?,
355        ),
356    };
357    if (tmvp || refs.is_some()) && params.options.get("mode") != Some("inter") {
358        return Err(Error::InvalidData(
359            "h265 encode: the tmvp / refs options require mode \"inter\"".into(),
360        ));
361    }
362    let bitrate = parse_bitrate(params)?;
363    // `bufsize` — VBV buffer size in bits, optional `k` / `M` suffix
364    // (requires `bitrate`).
365    let bufsize = match params.options.get("bufsize") {
366        None => None,
367        Some(v) => {
368            if bitrate.is_none() {
369                return Err(Error::InvalidData(
370                    "h265 encode: bufsize requires the bitrate option".into(),
371                ));
372            }
373            let (digits, mult) = match v.as_bytes().last() {
374                Some(b'k' | b'K') => (&v[..v.len() - 1], 1_000u64),
375                Some(b'M') => (&v[..v.len() - 1], 1_000_000),
376                _ => (v, 1),
377            };
378            Some(
379                digits
380                    .parse::<u64>()
381                    .ok()
382                    .map(|n| n.saturating_mul(mult))
383                    .filter(|&b| b >= 1_000)
384                    .ok_or_else(|| {
385                        Error::InvalidData(format!(
386                            "h265 encode: bufsize must be an integer >= 1000 bits                              (optional k/M suffix), got {v:?}"
387                        ))
388                    })?,
389            )
390        }
391    };
392    let fps = parse_fps(params)?;
393    // Declare the frame rate in the SPS VUI only when the caller
394    // stated it (an explicit `fps` option).
395    let fps_declared = params.options.get("fps").is_some();
396    // `hrd` — HRD signalling + Annex C enforcement (requires
397    // bitrate + bufsize + an explicit fps).
398    let hrd_on = parse_flag(params, "hrd")?;
399    if hrd_on && (bitrate.is_none() || bufsize.is_none() || !fps_declared) {
400        return Err(Error::InvalidData(
401            "h265 encode: hrd requires the bitrate, bufsize and fps options".into(),
402        ));
403    }
404    // `cbr` — constant-bit-rate HRD delivery (requires `hrd`).
405    let cbr_on = parse_flag(params, "cbr")?;
406    if cbr_on && !hrd_on {
407        return Err(Error::InvalidData(
408            "h265 encode: cbr requires the hrd option".into(),
409        ));
410    }
411    if params.options.get("pyramidstep").is_some() && params.options.get("pyramid").is_none() {
412        return Err(Error::InvalidData(
413            "h265 encode: pyramidstep requires the pyramid option".into(),
414        ));
415    }
416    // `adaptivegop` — close mini-GOPs at scene cuts (requires `pyramid`).
417    let adaptive_gop = parse_flag(params, "adaptivegop")?;
418    if adaptive_gop && params.options.get("pyramid").is_none() {
419        return Err(Error::InvalidData(
420            "h265 encode: adaptivegop requires the pyramid option".into(),
421        ));
422    }
423    // ABR configuration when `bitrate` is set: an explicit `qp`
424    // option seeds the controller's starting QP.
425    let rc_cfg = |params: &CodecParameters, qp: i32| -> rate::RateControlCfg {
426        let mut cfg = rate::RateControlCfg::new(bitrate.unwrap_or(0), fps.0, fps.1);
427        if params.options.get("qp").is_some() {
428            cfg.initial_qp = Some(qp);
429        }
430        cfg.vbv_buffer_bits = bufsize;
431        cfg
432    };
433    let mode = match params.options.get("mode") {
434        None | Some("pcm") => {
435            if parse_lf(params)?.any() {
436                return Err(Error::InvalidData(
437                    "h265 encode: deblock/sao options require mode \"intra\" or \"inter\"".into(),
438                ));
439            }
440            if parse_flag(params, "amp")? {
441                return Err(Error::InvalidData(
442                    "h265 encode: the amp option requires mode \"inter\"".into(),
443                ));
444            }
445            if bitrate.is_some() {
446                return Err(Error::InvalidData(
447                    "h265 encode: the bitrate option requires mode \"intra\" or \"inter\" \
448                     (PCM coding is lossless fixed-rate)"
449                        .into(),
450                ));
451            }
452            if parse_aq(params)? != 0 {
453                return Err(Error::InvalidData(
454                    "h265 encode: the aq option requires mode \"intra\"".into(),
455                ));
456            }
457            if tree.is_some() {
458                return Err(Error::InvalidData(
459                    "h265 encode: the ctb option requires mode \"intra\" or \"inter\"".into(),
460                ));
461            }
462            EncodeMode::Pcm
463        }
464        Some("intra") => {
465            if parse_flag(params, "amp")? {
466                return Err(Error::InvalidData(
467                    "h265 encode: the amp option requires mode \"inter\"".into(),
468                ));
469            }
470            let qp = parse_qp(params)?;
471            EncodeMode::Intra {
472                qp,
473                tree,
474                ctu_rc,
475                lf: parse_lf(params)?,
476                rc: bitrate.map(|_| rate::RateController::new(&rc_cfg(params, qp), width, height)),
477                aq: parse_aq(params)?,
478                timing: fps_declared.then_some((fps.1, fps.0)),
479                hrd: match hrd_on {
480                    false => None,
481                    true => {
482                        let signal = hrd::HrdSignalCfg::for_rate(
483                            bitrate.unwrap_or(64),
484                            bufsize.unwrap_or(16),
485                        )
486                        .with_cbr(cbr_on);
487                        if cbr_on {
488                            let tick_bits = signal.bit_rate.saturating_mul(u64::from(fps.1))
489                                / u64::from(fps.0.max(1));
490                            if signal.cpb_size < tick_bits.saturating_mul(2) {
491                                return Err(Error::InvalidData(format!(
492                                    "h265 encode: {}",
493                                    intra::IntraEncodeError::CbrCpbTooSmall
494                                )));
495                            }
496                        }
497                        Some((signal, hrd::HrdClock::new(signal, fps.0, fps.1)))
498                    }
499                },
500            }
501        }
502        Some("inter") => {
503            let qp = parse_qp(params)?;
504            if let Some(v) = params.options.get("pyramid") {
505                if params.options.get("gop").is_some() || params.options.get("bslices").is_some() {
506                    return Err(Error::InvalidData(
507                        "h265 encode: pyramid excludes the gop / bslices options".into(),
508                    ));
509                }
510                let g = v
511                    .parse::<usize>()
512                    .ok()
513                    .filter(|g| (2..=16).contains(g))
514                    .ok_or_else(|| {
515                        Error::InvalidData(format!(
516                            "h265 encode: pyramid must be in 2..=16, got {v:?}"
517                        ))
518                    })?;
519                // `pyramidstep` — the per-layer QP step of the
520                // hierarchical rate allocation (SliceQpY = base +
521                // layer * step; default 1).
522                let step = match params.options.get("pyramidstep") {
523                    None => 1i32,
524                    Some(v) => v
525                        .parse::<i32>()
526                        .ok()
527                        .filter(|s| (0..=6).contains(s))
528                        .ok_or_else(|| {
529                            Error::InvalidData(format!(
530                                "h265 encode: pyramidstep must be 0..=6, got {v:?}"
531                            ))
532                        })?,
533                };
534                let mut enc = pyramid::PyramidEncoder::new(width, height, qp, g)
535                    .map_err(|e| Error::InvalidData(format!("h265 encode: {e}")))?
536                    .with_layer_qp_step(step)
537                    .with_amp(parse_flag(params, "amp")?)
538                    .with_loop_filters(parse_lf(params)?)
539                    .with_aq(parse_aq(params)?);
540                if let Some(t) = tree {
541                    enc = enc.with_tree(t);
542                }
543                if let Some(n) = refs {
544                    enc = enc.with_refs(n);
545                }
546                enc = enc
547                    .with_temporal_mvp(tmvp)
548                    .with_adaptive_gop(adaptive_gop)
549                    .with_ctu_rate_control(ctu_rc);
550                if fps_declared {
551                    enc = enc.with_frame_rate(fps.0, fps.1);
552                }
553                if bitrate.is_some() {
554                    enc = enc.with_rate_control(&rc_cfg(params, qp));
555                }
556                enc = enc.with_hrd(hrd_on).with_cbr(cbr_on);
557                let delay = i64::from(enc.reorder_delay());
558                EncodeMode::Pyramid {
559                    enc,
560                    delay,
561                    pts_by_display: Vec::new(),
562                    decode_count: 0,
563                }
564            } else {
565                let gop = match params.options.get("gop") {
566                    None => 0usize,
567                    Some(v) => v.parse::<usize>().map_err(|_| {
568                        Error::InvalidData(format!(
569                            "h265 encode: gop must be a non-negative integer, got {v:?}"
570                        ))
571                    })?,
572                };
573                let b_slices = parse_flag(params, "bslices")?;
574                let mut enc = inter::LowDelayPEncoder::new(width, height, qp, gop)
575                    .map_err(|e| Error::InvalidData(format!("h265 encode: {e}")))?
576                    .with_b_slices(b_slices)
577                    .with_amp(parse_flag(params, "amp")?)
578                    .with_loop_filters(parse_lf(params)?)
579                    .with_aq(parse_aq(params)?);
580                if let Some(t) = tree {
581                    enc = enc.with_tree(t);
582                }
583                if let Some(n) = refs {
584                    enc = enc.with_refs(n);
585                }
586                enc = enc.with_temporal_mvp(tmvp).with_ctu_rate_control(ctu_rc);
587                if fps_declared {
588                    enc = enc.with_frame_rate(fps.0, fps.1);
589                }
590                if bitrate.is_some() {
591                    enc = enc.with_rate_control(&rc_cfg(params, qp));
592                }
593                enc = enc.with_hrd(hrd_on).with_cbr(cbr_on);
594                EncodeMode::Inter(enc)
595            }
596        }
597        Some(other) => {
598            return Err(Error::InvalidData(format!(
599                "h265 encode: unknown mode {other:?} (expected \"pcm\", \"intra\" or \"inter\")"
600            )))
601        }
602    };
603    let mut output_params = params.clone();
604    output_params.media_type = oxideav_core::MediaType::Video;
605    output_params.pixel_format = Some(PixelFormat::Yuv420P);
606    // Parameter sets ride in band in every access unit; no extradata.
607    output_params.extradata.clear();
608    Ok(Box::new(H265Encoder {
609        codec_id: params.codec_id.clone(),
610        output_params,
611        width,
612        height,
613        mode,
614        ready: VecDeque::new(),
615        frame_index: 0,
616        time_base: TimeBase::new(i64::from(fps.1), i64::from(fps.0)),
617    }))
618}
619
620impl Encoder for H265Encoder {
621    fn codec_id(&self) -> &CodecId {
622        &self.codec_id
623    }
624
625    fn output_params(&self) -> &CodecParameters {
626        &self.output_params
627    }
628
629    fn send_frame(&mut self, frame: &Frame) -> Result<()> {
630        let v = match frame {
631            Frame::Video(v) => v,
632            _ => return Err(Error::InvalidData("h265 encode: video frames only".into())),
633        };
634        if v.planes.len() != 3 {
635            return Err(Error::InvalidData(format!(
636                "h265 encode: expected 3 planes (yuv420p), got {}",
637                v.planes.len()
638            )));
639        }
640        // Repack each plane row-by-row (strides may exceed the width).
641        let pack = |idx: usize, w: usize, h: usize| -> Result<Vec<u8>> {
642            let plane = &v.planes[idx];
643            if plane.stride < w || plane.data.len() < plane.stride * h {
644                return Err(Error::InvalidData(format!(
645                    "h265 encode: plane {idx} too small (stride {}, len {})",
646                    plane.stride,
647                    plane.data.len()
648                )));
649            }
650            let mut out = Vec::with_capacity(w * h);
651            for row in 0..h {
652                out.extend_from_slice(&plane.data[row * plane.stride..row * plane.stride + w]);
653            }
654            Ok(out)
655        };
656        let y = pack(0, self.width, self.height)?;
657        let cb = pack(1, self.width / 2, self.height / 2)?;
658        let cr = pack(2, self.width / 2, self.height / 2)?;
659
660        let (au, keyframe) = match &mut self.mode {
661            EncodeMode::Pcm => (
662                pcm::encode_idr_pcm_au(&y, &cb, &cr, self.width, self.height)
663                    .map_err(|e| Error::InvalidData(format!("h265 encode: {e}")))?,
664                true,
665            ),
666            EncodeMode::Intra {
667                qp,
668                tree,
669                ctu_rc,
670                lf,
671                rc,
672                aq,
673                timing,
674                hrd,
675            } => {
676                let mut frame_qp = match rc {
677                    Some(rc) => rc.pick_qp(rate::FrameClass::Intra),
678                    None => *qp,
679                };
680                let budget = if *ctu_rc && tree.is_some() {
681                    rc.as_ref().and_then(|r| r.last_budget_bits())
682                } else {
683                    None
684                };
685                let cfg = intra::SpsCfg {
686                    cu_qp_delta: *aq > 0 || budget.is_some(),
687                    timing: *timing,
688                    hrd: hrd.as_ref().map(|(signal, _)| *signal),
689                    min_cb_log2: if tree.is_some() { 3 } else { 4 },
690                    tree: *tree,
691                    ..intra::SpsCfg::legacy(1)
692                };
693                // The HRD SEI prefix: every frame is an IRAP access
694                // unit, so each carries a §D.2.2 buffering period
695                // beside its §D.2.3 pic timing (no output delay:
696                // decode order == display order).
697                let sei = hrd.as_mut().map(|(_, clock)| {
698                    let (delay, offset) = clock.begin_buffering_period();
699                    let payloads = [
700                        (
701                            hrd::SEI_BUFFERING_PERIOD,
702                            hrd::buffering_period_payload(delay, offset),
703                        ),
704                        (
705                            hrd::SEI_PIC_TIMING,
706                            hrd::pic_timing_payload(clock.au_cpb_removal_delay_minus1(), 0),
707                        ),
708                    ];
709                    let mut framed = vec![0, 0, 0, 1];
710                    framed.extend(hrd::sei_prefix_nal(&payloads));
711                    framed
712                });
713                let sei_bits = sei.as_ref().map_or(0, |s| s.len() as u64 * 8);
714                let code = |frame_qp: i32| -> Result<Vec<u8>> {
715                    Ok(intra::encode_idr_intra_au_full(
716                        &y,
717                        &cb,
718                        &cr,
719                        self.width,
720                        self.height,
721                        frame_qp,
722                        &cfg,
723                        lf,
724                        *aq,
725                        budget,
726                    )
727                    .map_err(|e| Error::InvalidData(format!("h265 encode: {e}")))?
728                    .au)
729                };
730                let mut au = code(frame_qp)?;
731                // VBV (`bufsize`) / HRD constraint: re-encode at a
732                // higher QP until the whole AU (SEI included) fits
733                // the modelled decoder buffer and the Annex C
734                // arrival window.
735                let cbr = hrd.as_ref().is_some_and(|(signal, _)| signal.cbr);
736                let cap = match (
737                    rc.as_ref().and_then(|r| r.vbv_frame_cap()),
738                    hrd.as_ref().map(|(_, clock)| clock.frame_cap()),
739                ) {
740                    (Some(v), Some(h)) => Some(v.min(h)),
741                    (v, h) => v.or(h),
742                }
743                // CBR: reserve the filler quantum under the cap.
744                .map(|c| if cbr { c.saturating_sub(56) } else { c });
745                if let Some(cap) = cap {
746                    let ceiling = rc.as_ref().map_or(51, |r| r.max_qp());
747                    while au.len() as u64 * 8 + sei_bits > cap && frame_qp < ceiling {
748                        frame_qp = (frame_qp + 3).min(ceiling);
749                        au = code(frame_qp)?;
750                    }
751                }
752                if let Some(sei) = &sei {
753                    hrd::splice_sei_before_vcl(&mut au, sei);
754                }
755                if let Some((_, clock)) = &*hrd {
756                    // CBR underrun padding after the VCL NAL.
757                    let pad_bits = clock.cbr_filler_bits(au.len() as u64 * 8);
758                    if pad_bits > 0 {
759                        au.extend(hrd::filler_data_nal_framed((pad_bits as usize).div_ceil(8)));
760                    }
761                }
762                if let Some(rc) = rc {
763                    rc.update(rate::FrameClass::Intra, frame_qp, au.len() as u64 * 8);
764                }
765                if let Some((_, clock)) = hrd {
766                    clock.push_au(au.len() as u64 * 8);
767                }
768                (au, true)
769            }
770            EncodeMode::Inter(enc) => {
771                let f = enc
772                    .encode_frame(&inter::YuvFrame {
773                        y: &y,
774                        cb: &cb,
775                        cr: &cr,
776                    })
777                    .map_err(|e| Error::InvalidData(format!("h265 encode: {e}")))?;
778                (f.au, f.keyframe)
779            }
780            EncodeMode::Pyramid {
781                enc,
782                delay,
783                pts_by_display,
784                decode_count,
785            } => {
786                pts_by_display.push(v.pts);
787                let aus = enc
788                    .encode_frame(&inter::YuvFrame {
789                        y: &y,
790                        cb: &cb,
791                        cr: &cr,
792                    })
793                    .map_err(|e| Error::InvalidData(format!("h265 encode: {e}")))?;
794                self.frame_index += 1;
795                let (delay, decode_count) = (*delay, decode_count);
796                let time_base = self.time_base;
797                let mut packets =
798                    Self::pyramid_packets(aus, pts_by_display, delay, decode_count, time_base);
799                for pkt in packets.drain(..) {
800                    self.ready.push_back(pkt);
801                }
802                return Ok(());
803            }
804        };
805        let mut pkt = Packet::new(0, self.time_base, au);
806        pkt.pts = v.pts.or(Some(self.frame_index));
807        pkt.dts = pkt.pts;
808        pkt.flags.keyframe = keyframe;
809        self.frame_index += 1;
810        self.ready.push_back(pkt);
811        Ok(())
812    }
813
814    fn receive_packet(&mut self) -> Result<Packet> {
815        self.ready.pop_front().ok_or(Error::NeedMore)
816    }
817
818    fn flush(&mut self) -> Result<()> {
819        // Only the pyramid mode buffers: emit its low-delay tail.
820        if let EncodeMode::Pyramid {
821            enc,
822            delay,
823            pts_by_display,
824            decode_count,
825        } = &mut self.mode
826        {
827            let aus = enc.flush();
828            let (delay, decode_count) = (*delay, decode_count);
829            let time_base = self.time_base;
830            let mut packets =
831                Self::pyramid_packets(aus, pts_by_display, delay, decode_count, time_base);
832            for pkt in packets.drain(..) {
833                self.ready.push_back(pkt);
834            }
835        }
836        Ok(())
837    }
838}
839
840impl H265Encoder {
841    /// Turn a burst of decode-ordered pyramid access units into
842    /// packets: pts = the pushed frame's pts (default: its display
843    /// index), dts = the decode counter minus the reorder delay (a
844    /// dts never after its pts on the dyadic schedule).
845    fn pyramid_packets(
846        aus: Vec<pyramid::PyramidAu>,
847        pts_by_display: &[Option<i64>],
848        delay: i64,
849        decode_count: &mut i64,
850        time_base: TimeBase,
851    ) -> Vec<Packet> {
852        aus.into_iter()
853            .map(|au| {
854                let mut pkt = Packet::new(0, time_base, au.au);
855                pkt.pts = Some(
856                    pts_by_display
857                        .get(au.display_order)
858                        .copied()
859                        .flatten()
860                        .unwrap_or(au.display_order as i64),
861                );
862                pkt.dts = Some(*decode_count - delay);
863                *decode_count += 1;
864                pkt.flags.keyframe = au.keyframe;
865                pkt
866            })
867            .collect()
868    }
869}