1use oxideav_core::frame::VideoFrame;
98use oxideav_core::{
99 parse_options, CodecId, CodecOptionsStruct, CodecParameters, ColorRange, ColorSignal, Encoder,
100 Error, Frame, OptionField, OptionKind, OptionValue, Packet, PixelFormat, Result, TimeBase,
101};
102
103use crate::api::{ColorInfo, EncodeOptions, EncodeSpeed};
104use crate::meta::Colr;
105use crate::still::{encode_still, identity_full_range_colr, StillChroma, StillImage};
106
107pub const ENCODER_PIXEL_FORMATS: &[PixelFormat] = &[
111 PixelFormat::Yuv420P,
113 PixelFormat::Yuv422P,
114 PixelFormat::Yuv444P,
115 PixelFormat::YuvJ420P,
116 PixelFormat::YuvJ422P,
117 PixelFormat::YuvJ444P,
118 PixelFormat::Gray8,
119 PixelFormat::Yuv420P10Le,
121 PixelFormat::Yuv422P10Le,
122 PixelFormat::Yuv444P10Le,
123 PixelFormat::Yuv420P12Le,
124 PixelFormat::Yuv422P12Le,
125 PixelFormat::Yuv444P12Le,
126 PixelFormat::Gray10Le,
127 PixelFormat::Gray12Le,
128 PixelFormat::Rgb24,
130 PixelFormat::Rgba,
131 PixelFormat::Bgr24,
132 PixelFormat::Bgra,
133 PixelFormat::Gbrp8,
135 PixelFormat::Gbrap8,
136 PixelFormat::Gbrp10Le,
137 PixelFormat::Gbrap10Le,
138 PixelFormat::Gbrp12Le,
139 PixelFormat::Gbrap12Le,
140 PixelFormat::Yuva420P,
142 PixelFormat::Ya8,
143];
144
145pub fn quality_to_base_q_idx(quality: u8) -> u8 {
148 EncodeOptions::default().with_quality(quality).base_q_idx
149}
150
151#[non_exhaustive]
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub struct AvifEncoderOptions {
159 pub quality: Option<u8>,
161 pub q: Option<u8>,
163 pub alpha_q: Option<u8>,
165 pub chroma: StillChroma,
167 pub premultiplied: bool,
169 pub speed: EncodeSpeed,
171 pub threads: Option<usize>,
174}
175
176impl Default for AvifEncoderOptions {
177 fn default() -> Self {
178 Self {
179 quality: None,
180 q: None,
181 alpha_q: None,
182 chroma: StillChroma::Yuv420,
183 premultiplied: false,
184 speed: EncodeSpeed::Fast,
185 threads: None,
186 }
187 }
188}
189
190impl AvifEncoderOptions {
191 pub fn new() -> Self {
193 Self::default()
194 }
195 pub fn with_quality(mut self, quality: Option<u8>) -> Self {
197 self.quality = quality;
198 self
199 }
200 pub fn with_q(mut self, q: Option<u8>) -> Self {
202 self.q = q;
203 self
204 }
205 pub fn with_alpha_q(mut self, alpha_q: Option<u8>) -> Self {
207 self.alpha_q = alpha_q;
208 self
209 }
210 pub fn with_chroma(mut self, chroma: StillChroma) -> Self {
212 self.chroma = chroma;
213 self
214 }
215 pub fn with_premultiplied(mut self, premultiplied: bool) -> Self {
217 self.premultiplied = premultiplied;
218 self
219 }
220 pub fn with_speed(mut self, speed: EncodeSpeed) -> Self {
222 self.speed = speed;
223 self
224 }
225 pub fn with_threads(mut self, threads: Option<usize>) -> Self {
227 self.threads = threads;
228 self
229 }
230
231 pub fn from_params(params: &CodecParameters) -> Result<Self> {
233 parse_options::<Self>(¶ms.options)
234 }
235
236 fn from_params_lenient(params: &CodecParameters) -> Self {
239 let mut out = Self::default();
240 for (k, v) in params.options.iter() {
241 let one = oxideav_core::CodecOptions::new().set(k, v);
242 if let Ok(parsed) = parse_options::<Self>(&one) {
243 if let Err(e) = parsed.apply_into(&mut out, k) {
244 debug_assert!(false, "lenient option apply: {e}");
245 }
246 }
247 }
248 out
249 }
250
251 fn apply_into(&self, into: &mut Self, key: &str) -> Result<()> {
253 match key {
254 "quality" => into.quality = self.quality,
255 "q" => into.q = self.q,
256 "alpha_q" => into.alpha_q = self.alpha_q,
257 "chroma" => into.chroma = self.chroma,
258 "premultiplied" => into.premultiplied = self.premultiplied,
259 "speed" => into.speed = self.speed,
260 "threads" => into.threads = self.threads,
261 other => return Err(Error::invalid(format!("unknown option '{other}'"))),
262 }
263 Ok(())
264 }
265
266 pub fn resolve(&self) -> EncodeOptions {
272 let from_quality = self.quality.map(quality_to_base_q_idx).unwrap_or(0);
273 EncodeOptions::new(
274 self.q.unwrap_or(from_quality),
275 self.alpha_q.unwrap_or(from_quality),
276 self.premultiplied,
277 )
278 .with_chroma(self.chroma)
279 .with_speed(self.speed)
280 .with_threads(self.threads.unwrap_or(1))
281 }
282}
283
284fn parse_u8_option(key: &str, v: &OptionValue, max: u32) -> Result<u8> {
285 let n = v.as_u32()?;
286 if n > max {
287 return Err(Error::invalid(format!(
288 "avif encode: option '{key}' = {n} is outside 0..={max}"
289 )));
290 }
291 Ok(n as u8)
292}
293
294impl CodecOptionsStruct for AvifEncoderOptions {
295 const SCHEMA: &'static [OptionField] = &[
296 OptionField {
297 name: "quality",
298 kind: OptionKind::U32,
299 default: OptionValue::U32(100),
300 help: "Quality 0..=100 (100 = lossless): when q is not given, base_q_idx = ((100 - quality) * 255 + 50) / 100 for the colour planes and, when alpha_q is not given, for the alpha auxiliary",
301 },
302 OptionField {
303 name: "q",
304 kind: OptionKind::U32,
305 default: OptionValue::U32(0),
306 help: "AV1 base_q_idx 0..=255 for the colour planes (0 = lossless); wins over quality",
307 },
308 OptionField {
309 name: "alpha_q",
310 kind: OptionKind::U32,
311 default: OptionValue::U32(0),
312 help: "AV1 base_q_idx 0..=255 for the alpha auxiliary (0 = lossless); wins over quality",
313 },
314 OptionField {
315 name: "chroma",
316 kind: OptionKind::Enum(&["420", "422", "444"]),
317 default: OptionValue::String(String::new()),
318 help: "Y'CbCr layout a packed RGB(A) / BGR(A) frame is coded in at a lossy quality (default 420; odd extents promote to 444); lossless RGB keeps the identity-matrix 4:4:4 coding",
319 },
320 OptionField {
321 name: "premultiplied",
322 kind: OptionKind::Bool,
323 default: OptionValue::Bool(false),
324 help: "Emit the prem reference declaring the colour planes premultiplied by alpha (signalling only)",
325 },
326 OptionField {
327 name: "speed",
328 kind: OptionKind::Enum(&["fast", "balanced", "thorough"]),
329 default: OptionValue::String(String::new()),
330 help: "AV1 search effort (default fast)",
331 },
332 OptionField {
333 name: "threads",
334 kind: OptionKind::U32,
335 default: OptionValue::U32(0),
336 help: "Worker threads for the AV1 tile search (bytes never depend on it); 0 = the execution context's budget (serial unless granted)",
337 },
338 ];
339
340 fn apply(&mut self, key: &str, value: &OptionValue) -> Result<()> {
341 match key {
342 "quality" => self.quality = Some(parse_u8_option(key, value, 100)?),
343 "q" => self.q = Some(parse_u8_option(key, value, 255)?),
344 "alpha_q" => self.alpha_q = Some(parse_u8_option(key, value, 255)?),
345 "chroma" => {
346 self.chroma = match value.as_str()? {
347 "420" => StillChroma::Yuv420,
348 "422" => StillChroma::Yuv422,
349 "444" => StillChroma::Yuv444,
350 other => {
351 return Err(Error::invalid(format!(
352 "avif encode: chroma '{other}' is not 420 / 422 / 444"
353 )))
354 }
355 }
356 }
357 "premultiplied" => self.premultiplied = value.as_bool()?,
358 "speed" => {
359 self.speed = match value.as_str()? {
360 "fast" => EncodeSpeed::Fast,
361 "balanced" => EncodeSpeed::Balanced,
362 "thorough" => EncodeSpeed::Thorough,
363 other => {
364 return Err(Error::invalid(format!(
365 "avif encode: speed '{other}' is not fast / balanced / thorough"
366 )))
367 }
368 }
369 }
370 "threads" => {
371 let n = value.as_u32()?;
372 self.threads = (n > 0).then_some(n as usize);
373 }
374 other => return Err(Error::invalid(format!("unknown option '{other}'"))),
375 }
376 Ok(())
377 }
378}
379
380pub struct AvifEncoder {
384 params: CodecParameters,
385 opts: EncodeOptions,
386 threads_option: Option<usize>,
388 exec_threads: usize,
390 pending: Vec<Packet>,
391 flushed: bool,
392}
393
394impl AvifEncoder {
395 pub fn new(codec_id: CodecId) -> Self {
397 Self::with_params(CodecParameters::video(codec_id))
398 }
399
400 pub fn with_params(params: CodecParameters) -> Self {
407 let opts = AvifEncoderOptions::from_params_lenient(¶ms);
408 Self::from_options(params, opts)
409 }
410
411 pub fn try_with_params(params: CodecParameters) -> Result<Self> {
416 let opts = AvifEncoderOptions::from_params(¶ms)?;
417 Ok(Self::from_options(params, opts))
418 }
419
420 pub fn from_options(params: CodecParameters, opts: AvifEncoderOptions) -> Self {
423 Self {
424 params,
425 opts: opts.resolve(),
426 threads_option: opts.threads,
427 exec_threads: 1,
428 pending: Vec::new(),
429 flushed: false,
430 }
431 }
432
433 pub fn encode_options(&self) -> EncodeOptions {
438 self.opts
439 .with_threads(self.threads_option.unwrap_or(self.exec_threads))
440 }
441
442 fn lossy_rgb_color(&self) -> ColorInfo {
448 let signal = self.params.color_signal;
449 let mut color = ColorInfo::miaf_default();
450 if !signal.primaries.is_unspecified() {
451 color.primaries = signal.primaries.0;
452 }
453 if !signal.transfer.is_unspecified() {
454 color.transfer = signal.transfer.0;
455 }
456 let m = signal.matrix.0;
457 if !signal.matrix.is_unspecified()
458 && m != 0
459 && oxideav_heif::rgb::matrix_kr_kb(u16::from(m)).is_some()
460 {
461 color.matrix = m;
462 }
463 match self.params.resolved_color_range() {
464 ColorRange::Limited => color.range = crate::api::ColorRange::Limited,
465 ColorRange::Full => color.range = crate::api::ColorRange::Full,
466 _ => {}
467 }
468 color
469 }
470
471 fn lossy_rgb(&self) -> Option<(ColorInfo, StillChroma)> {
475 (self.opts.base_q_idx > 0).then(|| (self.lossy_rgb_color(), self.opts.chroma))
476 }
477
478 fn frame_to_still(&self, vf: &VideoFrame) -> Result<StillImage> {
481 let width = self
482 .params
483 .width
484 .ok_or_else(|| Error::invalid("avif encode: CodecParameters.width required"))?;
485 let height = self
486 .params
487 .height
488 .ok_or_else(|| Error::invalid("avif encode: CodecParameters.height required"))?;
489 let fmt = self
490 .params
491 .pixel_format
492 .ok_or_else(|| Error::invalid("avif encode: CodecParameters.pixel_format required"))?;
493
494 let img = match fmt {
495 PixelFormat::Yuv420P | PixelFormat::YuvJ420P => planar8(
496 vf,
497 width,
498 height,
499 StillChroma::Yuv420,
500 fmt == PixelFormat::YuvJ420P,
501 )?,
502 PixelFormat::Yuv422P | PixelFormat::YuvJ422P => planar8(
503 vf,
504 width,
505 height,
506 StillChroma::Yuv422,
507 fmt == PixelFormat::YuvJ422P,
508 )?,
509 PixelFormat::Yuv444P | PixelFormat::YuvJ444P => planar8(
510 vf,
511 width,
512 height,
513 StillChroma::Yuv444,
514 fmt == PixelFormat::YuvJ444P,
515 )?,
516 PixelFormat::Gray8 => planar8(vf, width, height, StillChroma::Monochrome, false)?,
517 PixelFormat::Yuv420P10Le => planar16(vf, width, height, 10, StillChroma::Yuv420)?,
518 PixelFormat::Yuv422P10Le => planar16(vf, width, height, 10, StillChroma::Yuv422)?,
519 PixelFormat::Yuv444P10Le => planar16(vf, width, height, 10, StillChroma::Yuv444)?,
520 PixelFormat::Yuv420P12Le => planar16(vf, width, height, 12, StillChroma::Yuv420)?,
521 PixelFormat::Yuv422P12Le => planar16(vf, width, height, 12, StillChroma::Yuv422)?,
522 PixelFormat::Yuv444P12Le => planar16(vf, width, height, 12, StillChroma::Yuv444)?,
523 PixelFormat::Gray10Le => planar16(vf, width, height, 10, StillChroma::Monochrome)?,
524 PixelFormat::Gray12Le => planar16(vf, width, height, 12, StillChroma::Monochrome)?,
525 PixelFormat::Rgb24 => packed_rgb(vf, width, height, 3, [0, 1, 2], self.lossy_rgb())?,
526 PixelFormat::Bgr24 => packed_rgb(vf, width, height, 3, [2, 1, 0], self.lossy_rgb())?,
527 PixelFormat::Rgba => packed_rgb(vf, width, height, 4, [0, 1, 2], self.lossy_rgb())?,
528 PixelFormat::Bgra => packed_rgb(vf, width, height, 4, [2, 1, 0], self.lossy_rgb())?,
529 PixelFormat::Yuva420P => yuva420(vf, width, height)?,
530 PixelFormat::Ya8 => ya8(vf, width, height)?,
531 PixelFormat::Gbrp8 => planar_gbr(vf, width, height, 8, false)?,
532 PixelFormat::Gbrap8 => planar_gbr(vf, width, height, 8, true)?,
533 PixelFormat::Gbrp10Le => planar_gbr(vf, width, height, 10, false)?,
534 PixelFormat::Gbrap10Le => planar_gbr(vf, width, height, 10, true)?,
535 PixelFormat::Gbrp12Le => planar_gbr(vf, width, height, 12, false)?,
536 PixelFormat::Gbrap12Le => planar_gbr(vf, width, height, 12, true)?,
537 other => {
538 return Err(Error::unsupported(format!(
539 "avif encode: pixel format {other:?} is not supported \
540 (planar YUV 8/10/12-bit, Gray, RGB(A)/BGR(A), Gbrp(a) 8/10/12-bit, \
541 Yuva420P, Ya8)"
542 )))
543 }
544 };
545 Ok(apply_color_signal(img, &self.params))
546 }
547}
548
549fn apply_color_signal(mut img: StillImage, params: &CodecParameters) -> StillImage {
554 let signal = params.color_signal;
555 let range = params.resolved_color_range();
556 if signal.is_unspecified() && range.is_unspecified() {
557 return img;
558 }
559 let (mut p, mut t, mut m, mut full) = match img.colr {
560 Some(Colr::Nclx {
561 colour_primaries,
562 transfer_characteristics,
563 matrix_coefficients,
564 full_range,
565 }) => (
566 colour_primaries,
567 transfer_characteristics,
568 matrix_coefficients,
569 full_range,
570 ),
571 _ => (2, 2, 2, false),
572 };
573 let rgb = m == 0;
574 if !signal.primaries.is_unspecified() {
575 p = u16::from(signal.primaries.0);
576 }
577 if !signal.transfer.is_unspecified() {
578 t = u16::from(signal.transfer.0);
579 }
580 if !rgb && !signal.matrix.is_unspecified() {
581 m = u16::from(signal.matrix.0);
582 }
583 match range {
584 ColorRange::Full => full = true,
585 ColorRange::Limited => full = false,
586 _ => {}
587 }
588 img.colr = Some(Colr::Nclx {
589 colour_primaries: p,
590 transfer_characteristics: t,
591 matrix_coefficients: m,
592 full_range: full,
593 });
594 img
595}
596
597fn signal_of(img: &StillImage) -> ColorSignal {
600 crate::signal::color_signal_for(img.colr.as_ref())
601}
602
603fn pack8(plane: &oxideav_core::frame::VideoPlane, w: usize, h: usize) -> Result<Vec<u16>> {
606 if plane.data.len() < plane.stride * (h - 1) + w || plane.stride < w {
607 return Err(Error::invalid(format!(
608 "avif encode: plane too short (stride {}, need {w}x{h})",
609 plane.stride
610 )));
611 }
612 let mut out = Vec::with_capacity(w * h);
613 for row in 0..h {
614 let s = row * plane.stride;
615 out.extend(plane.data[s..s + w].iter().map(|&b| b as u16));
616 }
617 Ok(out)
618}
619
620fn pack16le(plane: &oxideav_core::frame::VideoPlane, w: usize, h: usize) -> Result<Vec<u16>> {
622 let row_bytes = w * 2;
623 if plane.data.len() < plane.stride * (h - 1) + row_bytes || plane.stride < row_bytes {
624 return Err(Error::invalid(format!(
625 "avif encode: 16-bit plane too short (stride {}, need {w}x{h}x2)",
626 plane.stride
627 )));
628 }
629 let mut out = Vec::with_capacity(w * h);
630 for row in 0..h {
631 let s = row * plane.stride;
632 out.extend(
633 plane.data[s..s + row_bytes]
634 .chunks_exact(2)
635 .map(|c| u16::from_le_bytes([c[0], c[1]])),
636 );
637 }
638 Ok(out)
639}
640
641fn chroma_shift(chroma: StillChroma) -> (u32, u32) {
642 match chroma {
643 StillChroma::Yuv420 => (1, 1),
644 StillChroma::Yuv422 => (1, 0),
645 StillChroma::Yuv444 | StillChroma::Monochrome => (0, 0),
646 }
647}
648
649fn planar8(
650 vf: &VideoFrame,
651 width: u32,
652 height: u32,
653 chroma: StillChroma,
654 full_range: bool,
655) -> Result<StillImage> {
656 let planes_needed = if chroma == StillChroma::Monochrome {
657 1
658 } else {
659 3
660 };
661 if vf.image_plane_count() != planes_needed {
662 return Err(Error::invalid(format!(
663 "avif encode: {chroma:?} expects {planes_needed} planes, got {}",
664 vf.image_plane_count()
665 )));
666 }
667 let (sx, sy) = chroma_shift(chroma);
668 let y = pack8(&vf.planes[0], width as usize, height as usize)?;
669 let (u, v) = if planes_needed == 3 {
670 let (cw, ch) = ((width >> sx) as usize, (height >> sy) as usize);
671 (pack8(&vf.planes[1], cw, ch)?, pack8(&vf.planes[2], cw, ch)?)
672 } else {
673 (Vec::new(), Vec::new())
674 };
675 let mut img = StillImage::yuv(width, height, 8, chroma, y, u, v)?;
676 if full_range {
677 img = img.with_colr(crate::meta::Colr::Nclx {
681 colour_primaries: 2,
682 transfer_characteristics: 2,
683 matrix_coefficients: 2,
684 full_range: true,
685 });
686 }
687 Ok(img)
688}
689
690fn planar16(
691 vf: &VideoFrame,
692 width: u32,
693 height: u32,
694 bit_depth: u8,
695 chroma: StillChroma,
696) -> Result<StillImage> {
697 let planes_needed = if chroma == StillChroma::Monochrome {
698 1
699 } else {
700 3
701 };
702 if vf.image_plane_count() != planes_needed {
703 return Err(Error::invalid(format!(
704 "avif encode: {chroma:?} {bit_depth}-bit expects {planes_needed} planes, got {}",
705 vf.image_plane_count()
706 )));
707 }
708 let (sx, sy) = chroma_shift(chroma);
709 let y = pack16le(&vf.planes[0], width as usize, height as usize)?;
710 let (u, v) = if planes_needed == 3 {
711 let (cw, ch) = ((width >> sx) as usize, (height >> sy) as usize);
712 (
713 pack16le(&vf.planes[1], cw, ch)?,
714 pack16le(&vf.planes[2], cw, ch)?,
715 )
716 } else {
717 (Vec::new(), Vec::new())
718 };
719 Ok(StillImage::yuv(width, height, bit_depth, chroma, y, u, v)?)
720}
721
722fn packed_rgb(
728 vf: &VideoFrame,
729 width: u32,
730 height: u32,
731 bpp: usize,
732 rgb_at: [usize; 3],
733 lossy: Option<(ColorInfo, StillChroma)>,
734) -> Result<StillImage> {
735 if vf.image_plane_count() != 1 {
736 return Err(Error::invalid(format!(
737 "avif encode: packed RGB expects 1 plane, got {}",
738 vf.image_plane_count()
739 )));
740 }
741 let plane = &vf.planes[0];
742 let (w, h) = (width as usize, height as usize);
743 let row_bytes = w * bpp;
744 if plane.data.len() < plane.stride * (h - 1) + row_bytes || plane.stride < row_bytes {
745 return Err(Error::invalid(format!(
746 "avif encode: packed plane too short (stride {}, need {w}x{h}x{bpp})",
747 plane.stride
748 )));
749 }
750 let mut rgb = Vec::with_capacity(w * h * 3);
751 let mut alpha = if bpp == 4 {
752 Vec::with_capacity(w * h)
753 } else {
754 Vec::new()
755 };
756 for row in 0..h {
757 let s = row * plane.stride;
758 for px in plane.data[s..s + row_bytes].chunks_exact(bpp) {
759 rgb.push(px[rgb_at[0]]);
760 rgb.push(px[rgb_at[1]]);
761 rgb.push(px[rgb_at[2]]);
762 if bpp == 4 {
763 alpha.push(px[3] as u16);
764 }
765 }
766 }
767 let img = match lossy {
768 Some((color, chroma)) => {
769 crate::api_codec::still_of_rgb_in(width, height, &rgb, 3, color, chroma)?
770 }
771 None => StillImage::rgb8(width, height, &rgb)?,
772 };
773 if bpp == 4 {
774 Ok(img.with_alpha(alpha)?)
775 } else {
776 Ok(img)
777 }
778}
779
780fn coded_label(img: &StillImage) -> Option<PixelFormat> {
785 let (sx, sy) = img.chroma.subsampling();
786 let layout = crate::image::AvifPixelFormat::from_layout(
787 img.bit_depth,
788 sx as u8,
789 sy as u8,
790 img.chroma == StillChroma::Monochrome,
791 img.alpha.is_some(),
792 )?;
793 Some(crate::signal::labelled_pixel_format(
794 layout,
795 &signal_of(img),
796 ))
797}
798
799fn planar_gbr(
803 vf: &VideoFrame,
804 width: u32,
805 height: u32,
806 bit_depth: u8,
807 alpha: bool,
808) -> Result<StillImage> {
809 let planes_needed = if alpha { 4 } else { 3 };
810 if vf.image_plane_count() != planes_needed {
811 return Err(Error::invalid(format!(
812 "avif encode: planar RGB{} {bit_depth}-bit expects {planes_needed} planes, got {}",
813 if alpha { "A" } else { "" },
814 vf.image_plane_count()
815 )));
816 }
817 let (w, h) = (width as usize, height as usize);
818 let pack = |i: usize| -> Result<Vec<u16>> {
819 if bit_depth == 8 {
820 pack8(&vf.planes[i], w, h)
821 } else {
822 pack16le(&vf.planes[i], w, h)
823 }
824 };
825 let (g, b, r) = (pack(0)?, pack(1)?, pack(2)?);
826 let img = StillImage::yuv(width, height, bit_depth, StillChroma::Yuv444, g, b, r)?
827 .with_colr(identity_full_range_colr());
828 if alpha {
829 Ok(img.with_alpha(pack(3)?)?)
830 } else {
831 Ok(img)
832 }
833}
834
835fn yuva420(vf: &VideoFrame, width: u32, height: u32) -> Result<StillImage> {
836 if vf.image_plane_count() != 4 {
837 return Err(Error::invalid(format!(
838 "avif encode: Yuva420P expects 4 planes, got {}",
839 vf.image_plane_count()
840 )));
841 }
842 let (w, h) = (width as usize, height as usize);
843 let (cw, ch) = (width.div_ceil(2) as usize, height.div_ceil(2) as usize);
844 let img = StillImage::yuv(
845 width,
846 height,
847 8,
848 StillChroma::Yuv420,
849 pack8(&vf.planes[0], w, h)?,
850 pack8(&vf.planes[1], cw, ch)?,
851 pack8(&vf.planes[2], cw, ch)?,
852 )?;
853 Ok(img.with_alpha(pack8(&vf.planes[3], w, h)?)?)
854}
855
856fn ya8(vf: &VideoFrame, width: u32, height: u32) -> Result<StillImage> {
857 if vf.image_plane_count() != 1 {
858 return Err(Error::invalid(format!(
859 "avif encode: Ya8 expects 1 interleaved plane, got {}",
860 vf.image_plane_count()
861 )));
862 }
863 let plane = &vf.planes[0];
864 let (w, h) = (width as usize, height as usize);
865 let row_bytes = w * 2;
866 if plane.data.len() < plane.stride * (h - 1) + row_bytes || plane.stride < row_bytes {
867 return Err(Error::invalid(format!(
868 "avif encode: Ya8 plane too short (stride {}, need {w}x{h}x2)",
869 plane.stride
870 )));
871 }
872 let mut y = Vec::with_capacity(w * h);
873 let mut a = Vec::with_capacity(w * h);
874 for row in 0..h {
875 let s = row * plane.stride;
876 for px in plane.data[s..s + row_bytes].chunks_exact(2) {
877 y.push(px[0] as u16);
878 a.push(px[1] as u16);
879 }
880 }
881 let img = StillImage::yuv(
882 width,
883 height,
884 8,
885 StillChroma::Monochrome,
886 y,
887 Vec::new(),
888 Vec::new(),
889 )?;
890 Ok(img.with_alpha(a)?)
891}
892
893impl Encoder for AvifEncoder {
894 fn codec_id(&self) -> &CodecId {
895 &self.params.codec_id
896 }
897
898 fn output_params(&self) -> &CodecParameters {
899 &self.params
900 }
901
902 fn send_frame(&mut self, frame: &Frame) -> Result<()> {
903 let vf = match frame {
904 Frame::Video(v) => v,
905 other => {
906 return Err(Error::invalid(format!(
907 "avif encode: expected a video frame, got {other:?}"
908 )))
909 }
910 };
911 let img = self.frame_to_still(vf)?;
912 self.params.color_signal = signal_of(&img);
913 if let Some(label) = coded_label(&img) {
917 self.params.pixel_format = Some(label);
918 }
919 let bytes = encode_still(&img, &self.encode_options())?;
920 let mut pkt = Packet::new(0, TimeBase::new(1, 90_000), bytes);
921 if let Some(pts) = vf.pts {
922 pkt = pkt.with_pts(pts);
923 }
924 pkt = pkt.with_keyframe(true);
925 self.pending.push(pkt);
926 Ok(())
927 }
928
929 fn receive_packet(&mut self) -> Result<Packet> {
930 if self.pending.is_empty() {
931 return if self.flushed {
932 Err(Error::Eof)
933 } else {
934 Err(Error::NeedMore)
935 };
936 }
937 Ok(self.pending.remove(0))
938 }
939
940 fn flush(&mut self) -> Result<()> {
941 self.flushed = true;
942 Ok(())
943 }
944
945 fn set_execution_context(&mut self, ctx: &oxideav_core::ExecutionContext) {
949 self.exec_threads = ctx.threads.max(1);
950 }
951}
952
953pub fn make_encoder(params: &CodecParameters) -> Result<Box<dyn Encoder>> {
959 Ok(Box::new(AvifEncoder::try_with_params(params.clone())?))
960}
961
962#[cfg(test)]
963mod tests {
964 use super::*;
965 use oxideav_core::frame::{VideoFrame, VideoPlane};
966 use oxideav_core::Decoder;
967
968 fn video_params(fmt: PixelFormat, w: u32, h: u32) -> CodecParameters {
969 let mut p = CodecParameters::video(CodecId::new(crate::CODEC_ID_STR));
970 p.width = Some(w);
971 p.height = Some(h);
972 p.pixel_format = Some(fmt);
973 p
974 }
975
976 fn planar_frame(dims: &[(usize, usize)], seed: u8) -> Frame {
977 Frame::Video(VideoFrame {
978 pts: Some(7),
979 planes: dims
980 .iter()
981 .enumerate()
982 .map(|(i, &(w, h))| VideoPlane {
983 stride: w,
984 data: (0..w * h)
985 .map(|j| (j as u8).wrapping_mul(31) ^ seed.wrapping_add(i as u8))
986 .collect(),
987 })
988 .collect(),
989 })
990 }
991
992 #[test]
996 fn encoder_trait_round_trips_yuv420_frame() {
997 let params = video_params(PixelFormat::Yuv420P, 16, 16);
998 let mut enc = make_encoder(¶ms).expect("make encoder");
999 let frame = planar_frame(&[(16, 16), (8, 8), (8, 8)], 3);
1000 enc.send_frame(&frame).expect("send_frame");
1001 let pkt = enc.receive_packet().expect("receive_packet");
1002 assert_eq!(pkt.pts, Some(7), "pts carried through");
1003 assert!(pkt.flags.keyframe, "still images are sync samples");
1004
1005 assert!(matches!(enc.receive_packet(), Err(Error::NeedMore)));
1007 enc.flush().expect("flush");
1008 assert!(matches!(enc.receive_packet(), Err(Error::Eof)));
1009
1010 let mut dec = crate::decoder::AvifDecoder::new(CodecId::new(crate::CODEC_ID_STR));
1013 dec.send_packet(&Packet::new(0, TimeBase::new(1, 1), pkt.data.clone()))
1014 .expect("decode muxed file");
1015 let vf = match dec.receive_frame().expect("frame") {
1016 Frame::Video(v) => v,
1017 other => panic!("expected VideoFrame, got {other:?}"),
1018 };
1019 let src = match &frame {
1020 Frame::Video(v) => v,
1021 _ => unreachable!(),
1022 };
1023 assert_eq!(vf.image_plane_count(), 3);
1024 for i in 0..3 {
1025 assert_eq!(vf.planes[i].data, src.planes[i].data, "plane {i}");
1026 }
1027 }
1028
1029 #[test]
1033 fn encoder_trait_rgba_and_q_option() {
1034 let (w, h) = (16u32, 16u32);
1035 let rgba: Vec<u8> = (0..w * h * 4).map(|i| ((i * 13) & 0xff) as u8).collect();
1036 let mk_frame = || {
1037 Frame::Video(VideoFrame {
1038 pts: Some(0),
1039 planes: vec![VideoPlane {
1040 stride: (w * 4) as usize,
1041 data: rgba.clone(),
1042 }],
1043 })
1044 };
1045 let params = video_params(PixelFormat::Rgba, w, h);
1046 let mut enc = make_encoder(¶ms).expect("encoder");
1047 enc.send_frame(&mk_frame()).expect("send");
1048 let lossless = enc.receive_packet().expect("packet");
1049 let info = crate::inspect::inspect(&lossless.data).expect("inspect");
1050 assert!(info.has_alpha, "alpha auxiliary present");
1051
1052 let mut lossy_params = video_params(PixelFormat::Rgba, w, h);
1053 lossy_params.options = oxideav_core::CodecOptions::new().set("q", "120");
1054 let mut enc = make_encoder(&lossy_params).expect("encoder");
1055 enc.send_frame(&mk_frame()).expect("send");
1056 let lossy = enc.receive_packet().expect("packet");
1057 assert!(
1058 lossy.data.len() < lossless.data.len(),
1059 "q=120 ({}) must be smaller than lossless ({})",
1060 lossy.data.len(),
1061 lossless.data.len()
1062 );
1063 }
1064
1065 #[test]
1068 fn encoder_trait_rejects_bad_setup() {
1069 let params = CodecParameters::video(CodecId::new(crate::CODEC_ID_STR));
1071 let mut enc = make_encoder(¶ms).expect("encoder");
1072 let frame = planar_frame(&[(8, 8)], 0);
1073 match enc.send_frame(&frame) {
1074 Err(Error::InvalidData(msg)) => assert!(msg.contains("width"), "{msg}"),
1075 other => panic!("expected InvalidData, got {other:?}"),
1076 }
1077 let params = video_params(PixelFormat::Pal8, 8, 8);
1079 let mut enc = make_encoder(¶ms).expect("encoder");
1080 match enc.send_frame(&frame) {
1081 Err(Error::Unsupported(msg)) => assert!(msg.contains("Pal8"), "{msg}"),
1082 other => panic!("expected Unsupported, got {other:?}"),
1083 }
1084 }
1085}