Skip to main content

oxideav_avif/
sequence.rs

1//! AVIF **image sequence** (`avis`) encode — av1-avif §3 / §6.3 / §8.2,
2//! the AV1 ISOBMFF binding §2 (AV1 sample entry, sample format, sync
3//! samples) and ISO/IEC 14496-12 §8 (movie / track / sample tables).
4//!
5//! [`encode_sequence`] turns a list of same-layout [`StillImage`]s into
6//! one file:
7//!
8//! * `ftyp` — major `avis`, compatible `avis` / `avif` / `mif1` /
9//!   `msf1` / `miaf` / `av01` (the binding's §2.1 `SHALL`), the AVIF
10//!   profile brand elected from the coded `seq_profile` (§8), and
11//!   `avio` when every sample is a KEY frame (§6.3).
12//! * `meta` — a still primary `av01` item aliasing the first sample's
13//!   bytes in `mdat` (§6.3 NOTE: a file that is primarily an image
14//!   sequence still has at least an image item; the first sample is a
15//!   sync sample, i.e. valid AV1 Image Item Data per §2.1).
16//! * `moov` / `trak` — `'pict'` handler (§3), one `av01` sample entry
17//!   (§3: exactly one) carrying `av1C` (+ `colr`, + `clap` when the
18//!   coded extents pad the requested ones), `stts` / `stsc` / `stsz` /
19//!   `stco` and — when not every sample is a sync sample — `stss`
20//!   (14496-12 §8.6.2: absence means every sample is sync).
21//! * `mdat` — the AV1 temporal units, one per sample (binding §2.4).
22//!
23//! Frames are coded either all-intra (every sample a KEY frame, one
24//! Sequence Header per sample — §3 requires repeated headers to be
25//! identical, which holds by construction) or as KEY + P groups of up
26//! to [`SequenceEncodeOptions::gop_length`] frames through the AV1
27//! crate's inter encoder; the first sample of each group is the sync
28//! sample.
29
30use crate::api::EncodeSpeed;
31use crate::error::{AvifError as Error, Result};
32use crate::mux::{
33    prop_av1c, prop_clap, prop_colr, prop_ispe, prop_pixi, sequence_brands, ProfileBrand,
34};
35use crate::still::{
36    av1_err, av1c_from_seq, coded_extent, colr_is_full_range, encode_coded_item,
37    full_range_temporal_unit, pad_plane, pixi_bits, top_left_clap, CodedTuning, StillImage,
38    STILL_MAX_CODED_DIM,
39};
40use oxideav_av1::encoder::inter_frame::{encode_gop_yuv_with_q, GOP_MAX_FRAMES};
41use oxideav_av1::encoder::yuv_frame::YuvFrame;
42use oxideav_heif::{HeifWriter, SequenceWriter};
43
44/// Tuning for [`encode_sequence`].
45#[non_exhaustive]
46#[derive(Clone, Copy, Debug)]
47pub struct SequenceEncodeOptions {
48    /// Movie / media timescale in ticks per second (`mvhd` / `mdhd`).
49    pub timescale: u32,
50    /// Duration of every sample in timescale ticks (`stts`).
51    pub frame_duration: u32,
52    /// AV1 `base_q_idx`; `0` = lossless.
53    pub base_q_idx: u8,
54    /// Code every frame as a KEY frame (every sample a sync sample,
55    /// `avio` brand). Otherwise KEY + P groups.
56    pub all_intra: bool,
57    /// Frames per KEY + P group, 1..=`GOP_MAX_FRAMES` (64). Ignored
58    /// when `all_intra`.
59    pub gop_length: usize,
60}
61
62impl SequenceEncodeOptions {
63    /// Every field as a positional argument, in declaration order
64    /// (the record is `#[non_exhaustive]`: build it here, or from
65    /// `Default` where one exists, then read / assign the public
66    /// fields).
67    #[allow(clippy::too_many_arguments)]
68    pub fn new(
69        timescale: u32,
70        frame_duration: u32,
71        base_q_idx: u8,
72        all_intra: bool,
73        gop_length: usize,
74    ) -> Self {
75        Self {
76            timescale,
77            frame_duration,
78            base_q_idx,
79            all_intra,
80            gop_length,
81        }
82    }
83    /// Setter: replace `timescale`.
84    pub fn with_timescale(mut self, timescale: u32) -> Self {
85        self.timescale = timescale;
86        self
87    }
88    /// Setter: replace `frame_duration`.
89    pub fn with_frame_duration(mut self, frame_duration: u32) -> Self {
90        self.frame_duration = frame_duration;
91        self
92    }
93    /// Setter: replace `base_q_idx`.
94    pub fn with_base_q_idx(mut self, base_q_idx: u8) -> Self {
95        self.base_q_idx = base_q_idx;
96        self
97    }
98    /// Setter: replace `all_intra`.
99    pub fn with_all_intra(mut self, all_intra: bool) -> Self {
100        self.all_intra = all_intra;
101        self
102    }
103    /// Setter: replace `gop_length`.
104    pub fn with_gop_length(mut self, gop_length: usize) -> Self {
105        self.gop_length = gop_length;
106        self
107    }
108}
109
110impl Default for SequenceEncodeOptions {
111    fn default() -> Self {
112        Self {
113            timescale: 30,
114            frame_duration: 1,
115            base_q_idx: 0,
116            all_intra: false,
117            gop_length: GOP_MAX_FRAMES,
118        }
119    }
120}
121
122/// Encode `frames` as an `avis` image sequence. Every frame must share
123/// one width, height, bit depth and chroma layout; alpha / depth
124/// auxiliaries are not carried (an auxiliary image sequence track is
125/// out of scope here).
126pub fn encode_sequence(frames: &[StillImage], opts: &SequenceEncodeOptions) -> Result<Vec<u8>> {
127    encode_sequence_timed(frames, opts, None)
128}
129
130/// [`encode_sequence`] with one display duration per frame (in
131/// `opts.timescale` units) instead of the uniform `frame_duration`;
132/// `None` is the uniform case.
133pub(crate) fn encode_sequence_timed(
134    frames: &[StillImage],
135    opts: &SequenceEncodeOptions,
136    durations: Option<&[u32]>,
137) -> Result<Vec<u8>> {
138    let first = frames
139        .first()
140        .ok_or_else(|| Error::invalid("avif sequence: no frames"))?;
141    if let Some(d) = durations {
142        if d.len() != frames.len() {
143            return Err(Error::invalid(format!(
144                "avif sequence: {} durations for {} frames",
145                d.len(),
146                frames.len()
147            )));
148        }
149    }
150    if opts.timescale == 0 || opts.frame_duration == 0 {
151        return Err(Error::invalid(
152            "avif sequence: timescale and frame_duration must be non-zero",
153        ));
154    }
155    if !(1..=GOP_MAX_FRAMES).contains(&opts.gop_length) && !opts.all_intra {
156        return Err(Error::invalid(format!(
157            "avif sequence: gop_length {} outside 1..={GOP_MAX_FRAMES}",
158            opts.gop_length
159        )));
160    }
161    let (pw, ph) = (coded_extent(first.width), coded_extent(first.height));
162    if pw > STILL_MAX_CODED_DIM || ph > STILL_MAX_CODED_DIM {
163        return Err(Error::unsupported(format!(
164            "avif sequence: coded extents {pw}x{ph} exceed {STILL_MAX_CODED_DIM}"
165        )));
166    }
167    let mut yuv = Vec::with_capacity(frames.len());
168    for (i, f) in frames.iter().enumerate() {
169        f.validate()?;
170        if (f.width, f.height, f.bit_depth, f.chroma)
171            != (first.width, first.height, first.bit_depth, first.chroma)
172        {
173            return Err(Error::invalid(format!(
174                "avif sequence: frame {i} ({}x{} {}-bit {:?}) differs from frame 0 ({}x{} {}-bit {:?})",
175                f.width, f.height, f.bit_depth, f.chroma, first.width, first.height,
176                first.bit_depth, first.chroma
177            )));
178        }
179        if f.alpha.is_some() || f.depth_map.is_some() {
180            return Err(Error::unsupported(
181                "avif sequence: auxiliary (alpha / depth) image sequences are not supported",
182            ));
183        }
184        let (w, h) = (f.width as usize, f.height as usize);
185        let y = pad_plane(&f.y, w, h, pw as usize, ph as usize);
186        let (u, v) = if f.chroma.has_chroma() {
187            let (sx, sy) = f.chroma.subsampling();
188            let (cw, ch) = f.chroma_dims();
189            let (pcw, pch) = ((pw >> sx) as usize, (ph >> sy) as usize);
190            (
191                pad_plane(&f.u, cw as usize, ch as usize, pcw, pch),
192                pad_plane(&f.v, cw as usize, ch as usize, pcw, pch),
193            )
194        } else {
195            (Vec::new(), Vec::new())
196        };
197        yuv.push(YuvFrame {
198            width: pw,
199            height: ph,
200            bit_depth: f.bit_depth,
201            format: f.chroma.to_av1(),
202            y,
203            u,
204            v,
205        });
206    }
207    let full_range = colr_is_full_range(first);
208
209    // Code the samples.
210    let mut samples: Vec<(Vec<u8>, bool)> = Vec::with_capacity(frames.len());
211    let mut av1c: Option<Vec<u8>> = None;
212    let mut seq_profile = 0u8;
213    if opts.all_intra {
214        for f in &yuv {
215            let coded = encode_coded_item(
216                pw,
217                ph,
218                f.bit_depth,
219                f.format,
220                f.y.clone(),
221                f.u.clone(),
222                f.v.clone(),
223                opts.base_q_idx,
224                full_range,
225                CodedTuning::sequence(EncodeSpeed::Fast, 1),
226                "sequence frame",
227            )?;
228            if av1c.is_none() {
229                av1c = Some(coded.av1c);
230                seq_profile = coded.seq_profile;
231            }
232            samples.push((coded.payload, true));
233        }
234    } else {
235        for chunk in yuv.chunks(opts.gop_length) {
236            let gop = encode_gop_yuv_with_q(chunk, opts.base_q_idx)
237                .map_err(|e| av1_err("sequence GOP", e))?;
238            if av1c.is_none() {
239                av1c = Some(av1c_from_seq(&gop.seq));
240                seq_profile = gop.seq.seq_profile;
241            }
242            for (i, unit) in gop.temporal_units.into_iter().enumerate() {
243                let unit = if i == 0 && full_range {
244                    full_range_temporal_unit(&unit, &gop.seq)?
245                } else {
246                    unit
247                };
248                samples.push((unit, i == 0));
249            }
250        }
251    }
252    let av1c = av1c.expect("at least one sample");
253    if samples.len() != frames.len() {
254        return Err(Error::invalid(format!(
255            "avif sequence: coded {} samples for {} frames",
256            samples.len(),
257            frames.len()
258        )));
259    }
260    let profile_brand = match seq_profile {
261        0 => ProfileBrand::Baseline,
262        1 => ProfileBrand::Advanced,
263        _ => ProfileBrand::Bare,
264    };
265    let clap = ((pw, ph) != (first.width, first.height))
266        .then(|| top_left_clap(first.width, first.height, pw, ph));
267    let all_sync = samples.iter().all(|(_, s)| *s);
268    if pw > u32::from(u16::MAX) || ph > u32::from(u16::MAX) {
269        return Err(Error::unsupported(
270            "avif sequence: coded extents exceed the 16-bit sample-entry fields",
271        ));
272    }
273    // The file is the container's `SequenceWriter`: `ftyp` with this
274    // crate's brands, the cover still (one `av01` item with the
275    // track's properties, its `iloc` aliasing sample 0 in the track
276    // `mdat` — HEIF §7.1 recommends the cover, av1-avif §6.3 NOTE
277    // requires the item), `moov` / `trak` with the `pict` handler,
278    // the `av01` sample entry carrying `av1C` (+ `colr`, + `clap`)
279    // and the mandatory `ccst` (HEIF §7.2.3.1), `stts` / `stsc` /
280    // `stsz` / `stco`, `stss` only when a sample is not sync.
281    let pixi = pixi_bits(first);
282    let mut props = vec![
283        prop_av1c(&av1c, "sequence ")?,
284        prop_ispe(pw, ph),
285        prop_pixi(&pixi),
286    ];
287    let mut entry_properties = Vec::new();
288    if let Some(c) = first.colr.as_ref() {
289        let (colr, _) = prop_colr(c)?;
290        entry_properties.push(colr.clone());
291        props.push((colr, false));
292    }
293    if let Some(c) = clap.as_ref() {
294        let (clap, essential) = prop_clap(c);
295        entry_properties.push(clap.clone());
296        props.push((clap, essential));
297    }
298    let (av1c_prop, _) = prop_av1c(&av1c, "sequence ")?;
299    let (major, compat) = sequence_brands(profile_brand, all_sync);
300    let mut writer = SequenceWriter::new(*b"av01", av1c_prop, pw as u16, ph as u16, opts.timescale)
301        .with_brands(major, compat);
302    writer.entry_properties = entry_properties;
303    // `ccst` (HEIF §7.2.3.4): all-intra tracks have no inter-predicted
304    // image (every reference is vacuously a sync sample, no reference
305    // pictures); the KEY + P groups predict from the previous frame,
306    // intra prediction may be used, any number of references the
307    // sample entry permits (15).
308    writer.coding_constraints = if all_sync {
309        (true, true, 0)
310    } else {
311        (false, true, 15)
312    };
313    let mut still = HeifWriter::new();
314    // The body is ignored: `cover_sample` points the item at sample 0.
315    let cover = still.add_coded_item(*b"av01", Vec::new(), props);
316    still.set_primary(cover);
317    writer.still = Some(still);
318    writer.cover_sample = Some(0);
319    for (i, (data, sync)) in samples.into_iter().enumerate() {
320        let duration = durations
321            .and_then(|d| d.get(i).copied())
322            .unwrap_or(opts.frame_duration);
323        writer.push_sample(data, duration, sync);
324    }
325    Ok(writer.write_to_vec()?)
326}