1use crate::capabilities::{NativeEncoderClass, encoder_class};
4use alpha2_ffmpeg_bridge::{DecodedFrame, DisplayRotation};
5use std::ffi::{CStr, CString};
6
7use alpha2_media::{
8 ChromaLocation, ColorMatrix, ColorMetadata, ColorPrimaries, ColorRange, Container, DecodeError,
9 DecodedVideoFrame, DynamicRange, EncodeError, Orientation, PixelFormat, Plane, Rect, Rotation,
10 SampleAspectRatio, TransferFunction, YuvFormat,
11};
12use alpha2_time::{MediaTime, TICKS_PER_SECOND};
13use rsmpeg::avcodec::AVCodec;
14use rsmpeg::avutil::{AVDictionary, AVFrame, AVRational, av_rescale_q, ra};
15use rsmpeg::ffi;
16use rsmpeg::swscale::SwsContext;
17
18#[allow(clippy::too_many_lines)]
19pub(crate) fn convert_video_frame(
20 frame: &DecodedFrame,
21 codec: ffi::AVCodecID,
22 time_base: AVRational,
23 color: ColorMetadata,
24 scaler: &mut FrameScaler,
25) -> Result<DecodedVideoFrame, DecodeError> {
26 let width = u32::try_from(frame.width).map_err(|_| DecodeError::OutOfRange)?;
27 let height = u32::try_from(frame.height).map_err(|_| DecodeError::OutOfRange)?;
28 let pts = if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
29 frame.pts
30 } else {
31 frame.best_effort_timestamp
32 };
33 let visible_rect = Rect {
34 x: u32::try_from(frame.crop_left).unwrap_or(0),
35 y: u32::try_from(frame.crop_top).unwrap_or(0),
36 width: width.saturating_sub(
37 u32::try_from(frame.crop_left.saturating_add(frame.crop_right)).unwrap_or(0),
38 ),
39 height: height.saturating_sub(
40 u32::try_from(frame.crop_top.saturating_add(frame.crop_bottom)).unwrap_or(0),
41 ),
42 };
43 if let Some(planes) = frame.yuv_planes().map_err(adapter_error)? {
46 let layout = planes.layout;
47 return Ok(DecodedVideoFrame {
48 pts: timestamp(pts, time_base).ok_or(DecodeError::OutOfRange)?,
49 dts: None,
50 duration: timestamp(frame.duration, time_base),
51 visible_rect,
52 sample_aspect_ratio: declared_sar(frame.sample_aspect_ratio).unwrap_or_default(),
53 orientation: Orientation::UPRIGHT,
54 pixel_format: PixelFormat::Yuv(YuvFormat {
55 chroma_shift_x: layout.chroma_shift_x,
56 chroma_shift_y: layout.chroma_shift_y,
57 bit_depth: layout.bit_depth,
58 interleaved_chroma: layout.interleaved_chroma,
59 msb_aligned: layout.msb_aligned,
60 alpha: false,
61 }),
62 chroma_location: decoded_chroma_location(codec, frame.chroma_location),
63 color: Some(color),
64 planes: planes
65 .planes
66 .into_iter()
67 .map(|plane| Plane {
68 bytes: plane.bytes,
69 stride: plane.stride,
70 width: plane.width,
71 height: plane.height,
72 })
73 .collect(),
74 });
75 }
76 if !alpha2_media::frame_size_supported(width, height) {
78 return Err(DecodeError::Unsupported(format!(
79 "a {width}x{height} frame is larger than {} pixels or {} on a side",
80 alpha2_media::MAX_FRAME_PIXELS,
81 alpha2_media::MAX_FRAME_DIMENSION
82 )));
83 }
84 let (pixel_format, planes, color) = if converts_like_rgba8(color) {
85 (
86 PixelFormat::Rgba8,
87 vec![rgba8_plane(frame, color, scaler)?],
88 color,
89 )
90 } else {
91 full_resolution_planes(frame, color, scaler)?
92 };
93 Ok(DecodedVideoFrame {
94 pts: timestamp(pts, time_base).ok_or(DecodeError::OutOfRange)?,
95 dts: None,
96 duration: timestamp(frame.duration, time_base),
97 visible_rect,
98 sample_aspect_ratio: declared_sar(frame.sample_aspect_ratio).unwrap_or_default(),
99 orientation: Orientation::UPRIGHT,
100 pixel_format,
101 chroma_location: ChromaLocation::Unspecified,
103 color: Some(color),
104 planes,
105 })
106}
107
108fn converts_like_rgba8(color: ColorMetadata) -> bool {
112 !color.is_hdr() && color.primaries == ColorPrimaries::Bt709
113}
114
115fn rgba8_plane(
117 frame: &AVFrame,
118 color: ColorMetadata,
119 scaler: &mut FrameScaler,
120) -> Result<Plane, DecodeError> {
121 let width = u32::try_from(frame.width).map_err(|_| DecodeError::OutOfRange)?;
122 let height = u32::try_from(frame.height).map_err(|_| DecodeError::OutOfRange)?;
123 let active = scaler.for_frame(frame, ffi::AV_PIX_FMT_RGBA)?;
124 configure_yuv_to_rgba(active, color).map_err(adapter_error)?;
125 let mut bytes = vec![0; packed_len(width, height, 4)?];
126 alpha2_ffmpeg_bridge::scale_frame_packed(active, frame, ffi::AV_PIX_FMT_RGBA, &mut bytes)
127 .map_err(adapter_error)?;
128 Ok(Plane {
129 bytes: bytes.into(),
130 stride: width.saturating_mul(4),
131 width,
132 height,
133 })
134}
135
136fn full_resolution_planes(
139 frame: &AVFrame,
140 color: ColorMetadata,
141 scaler: &mut FrameScaler,
142) -> Result<(PixelFormat, Vec<Plane>, ColorMetadata), DecodeError> {
143 let width = u32::try_from(frame.width).map_err(|_| DecodeError::OutOfRange)?;
144 let height = u32::try_from(frame.height).map_err(|_| DecodeError::OutOfRange)?;
145 let profile = alpha2_ffmpeg_bridge::sample_profile(frame.format)
146 .ok_or_else(|| DecodeError::Unsupported("unsupported FFmpeg pixel format".into()))?;
147 let depth = match profile.bit_depth {
148 0..=8 => 8,
149 9..=10 => 10,
150 11..=12 => 12,
151 _ => 16,
152 };
153 let target = planar_444_format(profile.is_rgb, profile.has_alpha, depth);
154 let active = scaler.for_frame(frame, target)?;
155 if !profile.is_rgb {
156 let coefficients = sws_coefficients(color.matrix);
158 let full = color.range == ColorRange::Full;
159 alpha2_ffmpeg_bridge::configure_colorspace(active, coefficients, full, coefficients, full)
160 .map_err(adapter_error)?;
161 }
162 let plane_count = if profile.has_alpha { 4 } else { 3 };
163 let sample_bytes = if depth > 8 { 2 } else { 1 };
164 let plane_len = packed_len(width, height, sample_bytes)?;
165 let mut bytes = vec![0; plane_len * plane_count];
166 alpha2_ffmpeg_bridge::scale_frame_packed(active, frame, target, &mut bytes)
167 .map_err(adapter_error)?;
168 let bytes = bytes::Bytes::from(bytes);
169 let stride = width.saturating_mul(u32::try_from(sample_bytes).unwrap_or(2));
170 let plane = |index: usize| Plane {
171 bytes: bytes.slice(index * plane_len..(index + 1) * plane_len),
172 stride,
173 width,
174 height,
175 };
176 let order: &[usize] = if profile.is_rgb {
178 &[2, 0, 1]
179 } else {
180 &[0, 1, 2]
181 };
182 let mut planes = order.iter().map(|&index| plane(index)).collect::<Vec<_>>();
183 if profile.has_alpha {
184 planes.push(plane(3));
185 }
186 let format = YuvFormat::planar(0, 0, depth);
187 let format = if profile.has_alpha {
188 format.with_alpha()
189 } else {
190 format
191 };
192 let color = if profile.is_rgb {
193 ColorMetadata {
194 matrix: ColorMatrix::Rgb,
195 range: ColorRange::Full,
196 ..color
197 }
198 } else {
199 color
200 };
201 Ok((PixelFormat::Yuv(format), planes, color))
202}
203
204const fn planar_444_format(rgb: bool, alpha: bool, depth: u8) -> ffi::AVPixelFormat {
207 match (rgb, alpha, depth) {
208 (true, false, 8) => ffi::AV_PIX_FMT_GBRP,
209 (true, true, 8) => ffi::AV_PIX_FMT_GBRAP,
210 (true, false, 10) => ffi::AV_PIX_FMT_GBRP10LE,
211 (true, true, 10) => ffi::AV_PIX_FMT_GBRAP10LE,
212 (true, false, 12) => ffi::AV_PIX_FMT_GBRP12LE,
213 (true, true, 12) => ffi::AV_PIX_FMT_GBRAP12LE,
214 (true, false, _) => ffi::AV_PIX_FMT_GBRP16LE,
215 (true, true, _) => ffi::AV_PIX_FMT_GBRAP16LE,
216 (false, false, 8) => ffi::AV_PIX_FMT_YUV444P,
217 (false, true, 8) => ffi::AV_PIX_FMT_YUVA444P,
218 (false, false, 10) => ffi::AV_PIX_FMT_YUV444P10LE,
219 (false, true, 10) => ffi::AV_PIX_FMT_YUVA444P10LE,
220 (false, false, 12) => ffi::AV_PIX_FMT_YUV444P12LE,
221 (false, true, 12) => ffi::AV_PIX_FMT_YUVA444P12LE,
222 (false, false, _) => ffi::AV_PIX_FMT_YUV444P16LE,
223 (false, true, _) => ffi::AV_PIX_FMT_YUVA444P16LE,
224 }
225}
226
227#[derive(Default)]
232pub(crate) struct FrameScaler {
233 context: Option<(ScalerShape, SwsContext)>,
234 made: u64,
235}
236
237#[derive(Clone, Copy, Debug, Eq, PartialEq)]
238struct ScalerShape {
239 width: i32,
240 height: i32,
241 source: ffi::AVPixelFormat,
242 target: ffi::AVPixelFormat,
243}
244
245impl FrameScaler {
246 fn for_frame(
248 &mut self,
249 frame: &AVFrame,
250 target: ffi::AVPixelFormat,
251 ) -> Result<&mut SwsContext, DecodeError> {
252 let shape = ScalerShape {
253 width: frame.width,
254 height: frame.height,
255 source: frame.format,
256 target,
257 };
258 if self
259 .context
260 .as_ref()
261 .is_none_or(|(made_for, _)| *made_for != shape)
262 {
263 self.context = None;
264 let context = SwsContext::get_context(
265 frame.width,
266 frame.height,
267 frame.format,
268 frame.width,
269 frame.height,
270 target,
271 ffi::SWS_BILINEAR,
272 None,
273 None,
274 None,
275 )
276 .ok_or_else(|| DecodeError::Unsupported("unsupported FFmpeg pixel format".into()))?;
277 self.made += 1;
278 self.context = Some((shape, context));
279 }
280 self.context
281 .as_mut()
282 .map(|(_, context)| context)
283 .ok_or_else(|| DecodeError::Unsupported("unsupported FFmpeg pixel format".into()))
284 }
285
286 #[cfg(test)]
287 pub(crate) const fn made(&self) -> u64 {
288 self.made
289 }
290
291 #[cfg(test)]
292 pub(crate) const fn is_empty(&self) -> bool {
293 self.context.is_none()
294 }
295}
296
297fn packed_len(width: u32, height: u32, sample_bytes: usize) -> Result<usize, DecodeError> {
298 usize::try_from(width)
299 .ok()
300 .and_then(|width| {
301 usize::try_from(height)
302 .ok()
303 .and_then(|height| width.checked_mul(height))
304 })
305 .and_then(|pixels| pixels.checked_mul(sample_bytes))
306 .ok_or(DecodeError::OutOfRange)
307}
308
309pub(crate) fn decoded_chroma_location(
314 codec: ffi::AVCodecID,
315 location: ffi::AVChromaLocation,
316) -> ChromaLocation {
317 if codec == ffi::AV_CODEC_ID_H264 && location == ffi::AVCHROMA_LOC_UNSPECIFIED {
318 ChromaLocation::Left
319 } else {
320 chroma_location(location)
321 }
322}
323
324pub(crate) fn chroma_location(location: ffi::AVChromaLocation) -> ChromaLocation {
325 match location {
326 ffi::AVCHROMA_LOC_LEFT => ChromaLocation::Left,
327 ffi::AVCHROMA_LOC_CENTER => ChromaLocation::Center,
328 ffi::AVCHROMA_LOC_TOPLEFT => ChromaLocation::TopLeft,
329 _ => ChromaLocation::Unspecified,
330 }
331}
332
333pub(crate) fn configure_rgba_to_yuv(
335 scaler: &mut SwsContext,
336 color: ColorMetadata,
337) -> Result<(), alpha2_ffmpeg_bridge::BridgeError> {
338 alpha2_ffmpeg_bridge::configure_colorspace(
339 scaler,
340 i32::try_from(ffi::SWS_CS_ITU709).expect("color coefficient fits i32"),
341 true,
342 sws_coefficients(color.matrix),
343 color.range == ColorRange::Full,
344 )
345}
346
347pub(crate) fn configure_yuv_to_rgba(
349 scaler: &mut SwsContext,
350 color: ColorMetadata,
351) -> Result<(), alpha2_ffmpeg_bridge::BridgeError> {
352 alpha2_ffmpeg_bridge::configure_colorspace(
353 scaler,
354 sws_coefficients(color.matrix),
355 color.range == ColorRange::Full,
356 i32::try_from(ffi::SWS_CS_ITU709).expect("color coefficient fits i32"),
357 true,
358 )
359}
360
361pub(crate) fn sws_coefficients(matrix: ColorMatrix) -> i32 {
362 let value = match matrix {
363 ColorMatrix::Bt601 => ffi::SWS_CS_ITU601,
364 ColorMatrix::Bt709 | ColorMatrix::Rgb => ffi::SWS_CS_ITU709,
365 ColorMatrix::Bt2020Ncl => ffi::SWS_CS_BT2020,
366 };
367 i32::try_from(value).expect("color coefficient fits i32")
368}
369
370pub(crate) fn tick_base() -> AVRational {
372 ra(
373 1,
374 i32::try_from(TICKS_PER_SECOND).expect("tick rate fits i32"),
375 )
376}
377
378pub(crate) fn direct_yuv420p_frame(
383 frame: &DecodedVideoFrame,
384 width: i32,
385 height: i32,
386) -> Result<Option<AVFrame>, EncodeError> {
387 if frame.pixel_format != PixelFormat::Yuv(YuvFormat::I420) {
388 return Ok(None);
389 }
390 if frame.planes.len() != 3
391 || frame.visible_rect.x != 0
392 || frame.visible_rect.y != 0
393 || frame.planes[0].width != u32::try_from(width).unwrap_or(0)
394 || frame.planes[0].height != u32::try_from(height).unwrap_or(0)
395 {
396 return Err(EncodeError::Adapter(
397 "direct YUV encoder input must contain one uncropped coded frame".into(),
398 ));
399 }
400 alpha2_ffmpeg_bridge::yuv420p_frame(
401 &frame.planes[0].bytes,
402 frame.planes[0].stride,
403 &frame.planes[1].bytes,
404 frame.planes[1].stride,
405 &frame.planes[2].bytes,
406 frame.planes[2].stride,
407 width,
408 height,
409 )
410 .map(Some)
411 .map_err(encode_adapter_error)
412}
413
414pub(crate) struct Yuv420pConverter {
420 output: ColorMetadata,
421 scaler: Option<(ffi::AVPixelFormat, u32, u32, SwsContext)>,
422}
423
424impl Yuv420pConverter {
425 pub(crate) const fn new(output: ColorMetadata) -> Self {
427 Self {
428 output,
429 scaler: None,
430 }
431 }
432
433 pub(crate) fn convert(
438 &mut self,
439 frame: &DecodedVideoFrame,
440 width: i32,
441 height: i32,
442 ) -> Result<Option<AVFrame>, EncodeError> {
443 let PixelFormat::Yuv(format) = frame.pixel_format else {
444 return Ok(None);
445 };
446 let rgb = frame
447 .color
448 .is_some_and(|color| color.matrix == ColorMatrix::Rgb);
449 if format == YuvFormat::I420 && !rgb {
450 return direct_yuv420p_frame(frame, width, height);
451 }
452 let (width, height) = (
453 u32::try_from(width).unwrap_or(0),
454 u32::try_from(height).unwrap_or(0),
455 );
456 let stored = if format.interleaved_chroma { 2 } else { 3 };
457 if frame.visible_rect.x != 0
458 || frame.visible_rect.y != 0
459 || frame.planes.len() < stored
460 || frame.planes[0].width != width
461 || frame.planes[0].height != height
462 {
463 return Err(EncodeError::Adapter(
464 "YUV encoder input must contain one uncropped frame of the encoder's size".into(),
465 ));
466 }
467 let (source_format, planes) = if rgb {
468 if format.chroma_shift_x != 0
469 || format.chroma_shift_y != 0
470 || format.interleaved_chroma
471 || format.msb_aligned
472 || !matches!(format.bit_depth, 8 | 10 | 12 | 16)
473 {
474 return Err(EncodeError::Adapter(format!(
475 "no FFmpeg pixel format stores red, green, and blue as {format:?}"
476 )));
477 }
478 let planes = [1, 2, 0]
480 .iter()
481 .map(|&index: &usize| (&frame.planes[index].bytes[..], frame.planes[index].stride))
482 .collect::<Vec<_>>();
483 (planar_444_format(true, false, format.bit_depth), planes)
484 } else {
485 let layout = alpha2_ffmpeg_bridge::YuvPlaneLayout {
486 chroma_shift_x: format.chroma_shift_x,
487 chroma_shift_y: format.chroma_shift_y,
488 bit_depth: format.bit_depth,
489 interleaved_chroma: format.interleaved_chroma,
490 msb_aligned: format.msb_aligned,
491 };
492 let source_format =
493 alpha2_ffmpeg_bridge::yuv_pixel_format(layout).ok_or_else(|| {
494 EncodeError::Adapter(format!("no FFmpeg pixel format stores {format:?}"))
495 })?;
496 let planes = frame.planes[..stored]
497 .iter()
498 .map(|plane| (&plane.bytes[..], plane.stride))
499 .collect::<Vec<_>>();
500 (source_format, planes)
501 };
502 let source = alpha2_ffmpeg_bridge::planar_frame(source_format, &planes, width, height)
503 .map_err(encode_adapter_error)?;
504 if self
505 .scaler
506 .as_ref()
507 .is_none_or(|(cached, w, h, _)| (*cached, *w, *h) != (source_format, width, height))
508 {
509 let (signed_width, signed_height) = (
510 i32::try_from(width).map_err(|_| EncodeError::Adapter("width overflow".into()))?,
511 i32::try_from(height)
512 .map_err(|_| EncodeError::Adapter("height overflow".into()))?,
513 );
514 let mut scaler = SwsContext::get_context(
515 signed_width,
516 signed_height,
517 source_format,
518 signed_width,
519 signed_height,
520 ffi::AV_PIX_FMT_YUV420P,
521 ffi::SWS_BILINEAR,
522 None,
523 None,
524 None,
525 )
526 .ok_or_else(|| EncodeError::Adapter("cannot create a YUV to YUV420P scaler".into()))?;
527 if rgb {
528 configure_rgba_to_yuv(&mut scaler, self.output).map_err(encode_adapter_error)?;
529 }
530 self.scaler = Some((source_format, width, height, scaler));
531 }
532 let Some((_, _, _, scaler)) = self.scaler.as_mut() else {
533 unreachable!("the scaler was just made");
534 };
535 let mut output = AVFrame::new();
536 output.set_format(ffi::AV_PIX_FMT_YUV420P);
537 output.set_width(source.width);
538 output.set_height(source.height);
539 output.get_buffer(1).map_err(encode_adapter_error)?;
540 scaler
541 .scale_frame(&source, 0, source.height, &mut output)
542 .map_err(encode_adapter_error)?;
543 Ok(Some(output))
544 }
545}
546
547pub(crate) fn timestamp(value: i64, base: AVRational) -> Option<MediaTime> {
549 (value != ffi::AV_NOPTS_VALUE && base.den > 0)
550 .then(|| MediaTime::from_ticks(av_rescale_q(value, base, tick_base())))
551}
552
553pub(crate) fn declared_sar(value: AVRational) -> Option<SampleAspectRatio> {
556 SampleAspectRatio::new(
557 u64::try_from(value.num).ok()?,
558 u64::try_from(value.den).ok()?,
559 )
560}
561
562pub(crate) fn guess_sar(container: AVRational, pictures: AVRational) -> SampleAspectRatio {
565 declared_sar(container)
566 .or_else(|| declared_sar(pictures))
567 .unwrap_or_default()
568}
569
570pub(crate) fn stream_rotation(metadata: Option<rsmpeg::avutil::AVDictionaryRef<'_>>) -> Rotation {
572 let Some(metadata) = metadata else {
573 return Rotation::Deg0;
574 };
575 let Some(entry) = metadata.get(c"rotate", None, 0) else {
576 return Rotation::Deg0;
577 };
578 match entry
579 .value()
580 .to_string_lossy()
581 .parse::<i32>()
582 .unwrap_or(0)
583 .rem_euclid(360)
584 {
585 90 => Rotation::Deg90,
586 180 => Rotation::Deg180,
587 270 => Rotation::Deg270,
588 _ => Rotation::Deg0,
589 }
590}
591
592pub(crate) fn rotation_from_display(counter_clockwise: f64) -> Option<Rotation> {
596 let normalized = (-counter_clockwise).rem_euclid(360.0);
597 if !(0.5..359.5).contains(&normalized) {
598 Some(Rotation::Deg0)
599 } else if (normalized - 90.0).abs() < 0.5 {
600 Some(Rotation::Deg90)
601 } else if (normalized - 180.0).abs() < 0.5 {
602 Some(Rotation::Deg180)
603 } else if (normalized - 270.0).abs() < 0.5 {
604 Some(Rotation::Deg270)
605 } else {
606 None
607 }
608}
609
610pub(crate) fn orientation_from_display(display: DisplayRotation) -> Option<Orientation> {
615 if display.flipped {
616 rotation_from_display(-display.counter_clockwise_degrees).map(|rotation| Orientation {
617 rotation,
618 mirror: true,
619 })
620 } else {
621 rotation_from_display(display.counter_clockwise_degrees).map(Orientation::rotated)
622 }
623}
624
625pub(crate) fn display_of(orientation: Orientation) -> Option<DisplayRotation> {
628 if orientation == Orientation::default() {
629 return None;
630 }
631 let clockwise = match orientation.rotation {
632 Rotation::Deg0 => 0.0,
633 Rotation::Deg90 => 90.0,
634 Rotation::Deg180 => 180.0,
635 Rotation::Deg270 => 270.0,
636 };
637 Some(DisplayRotation {
638 counter_clockwise_degrees: if orientation.mirror {
639 clockwise
640 } else {
641 -clockwise
642 },
643 flipped: orientation.mirror,
644 })
645}
646
647const HVC1: u32 = u32::from_le_bytes(*b"hvc1");
649const HEV1: u32 = u32::from_le_bytes(*b"hev1");
651
652pub(crate) const fn muxer_name(container: Container) -> &'static CStr {
654 match container {
655 Container::Mp4 => c"mp4",
656 Container::Mov => c"mov",
657 Container::Mkv => c"matroska",
658 Container::Webm => c"webm",
659 }
660}
661
662pub(crate) fn muxer_header_options(container: Container) -> Option<AVDictionary> {
667 matches!(container, Container::Mkv | Container::Webm)
668 .then(|| AVDictionary::new(c"fflags", c"+bitexact", 0))
669}
670
671pub(crate) fn container_codec_tag(
678 container: Container,
679 codec_id: ffi::AVCodecID,
680 tag: u32,
681 copied: bool,
682) -> u32 {
683 if matches!(container, Container::Mkv | Container::Webm) {
684 0
685 } else if codec_id == ffi::AV_CODEC_ID_HEVC {
686 if tag == HVC1 || tag == HEV1 {
687 tag
688 } else {
689 HVC1
690 }
691 } else if copied {
692 0
693 } else {
694 tag
695 }
696}
697
698pub(crate) fn color_metadata(
699 parameters: &rsmpeg::avcodec::AVCodecParameters,
700) -> Option<ColorMetadata> {
701 map_color(
702 parameters.color_primaries,
703 parameters.color_trc,
704 parameters.color_space,
705 parameters.color_range,
706 )
707}
708
709pub(crate) fn frame_color_metadata(frame: &AVFrame) -> Option<ColorMetadata> {
710 map_color(
711 frame.color_primaries,
712 frame.color_trc,
713 frame.colorspace,
714 frame.color_range,
715 )
716}
717
718pub(crate) fn map_color(
723 primaries: ffi::AVColorPrimaries,
724 transfer: ffi::AVColorTransferCharacteristic,
725 matrix: ffi::AVColorSpace,
726 range: ffi::AVColorRange,
727) -> Option<ColorMetadata> {
728 if primaries == ffi::AVCOL_PRI_UNSPECIFIED
729 && transfer == ffi::AVCOL_TRC_UNSPECIFIED
730 && matrix == ffi::AVCOL_SPC_UNSPECIFIED
731 && range == ffi::AVCOL_RANGE_UNSPECIFIED
732 {
733 return None;
734 }
735 let primaries = match primaries {
736 ffi::AVCOL_PRI_BT2020 => ColorPrimaries::Bt2020,
737 ffi::AVCOL_PRI_SMPTE170M | ffi::AVCOL_PRI_BT470BG => ColorPrimaries::Bt601,
738 _ => ColorPrimaries::Bt709,
739 };
740 let transfer = match transfer {
741 ffi::AVCOL_TRC_SMPTE2084 => TransferFunction::Pq,
742 ffi::AVCOL_TRC_ARIB_STD_B67 => TransferFunction::Hlg,
743 ffi::AVCOL_TRC_IEC61966_2_1 => TransferFunction::Srgb,
744 _ => TransferFunction::Bt709,
745 };
746 let matrix = match matrix {
747 ffi::AVCOL_SPC_BT2020_NCL => ColorMatrix::Bt2020Ncl,
748 ffi::AVCOL_SPC_SMPTE170M | ffi::AVCOL_SPC_BT470BG => ColorMatrix::Bt601,
749 ffi::AVCOL_SPC_RGB => ColorMatrix::Rgb,
750 _ => ColorMatrix::Bt709,
751 };
752 Some(ColorMetadata {
753 primaries,
754 transfer,
755 matrix,
756 range: if range == ffi::AVCOL_RANGE_JPEG {
757 ColorRange::Full
758 } else {
759 ColorRange::Limited
760 },
761 dynamic_range: if matches!(transfer, TransferFunction::Pq | TransferFunction::Hlg) {
762 DynamicRange::Hdr
763 } else {
764 DynamicRange::Sdr
765 },
766 })
767}
768
769pub(crate) fn video_decoder_options(threads: i64) -> AVDictionary {
775 AVDictionary::new_int(c"threads", threads, 0).set(c"flags", c"+unaligned", 0)
776}
777
778pub(crate) fn pixel_format(layout: alpha2_media::PixelLayout) -> ffi::AVPixelFormat {
780 match layout.ffmpeg_name() {
781 "yuva420p" => ffi::AV_PIX_FMT_YUVA420P,
782 "yuv420p10le" => ffi::AV_PIX_FMT_YUV420P10LE,
783 "yuva420p10le" => ffi::AV_PIX_FMT_YUVA420P10LE,
784 "yuv422p" => ffi::AV_PIX_FMT_YUV422P,
785 "yuva422p" => ffi::AV_PIX_FMT_YUVA422P,
786 "yuv422p10le" => ffi::AV_PIX_FMT_YUV422P10LE,
787 "yuva422p10le" => ffi::AV_PIX_FMT_YUVA422P10LE,
788 "yuv444p" => ffi::AV_PIX_FMT_YUV444P,
789 "yuva444p" => ffi::AV_PIX_FMT_YUVA444P,
790 "yuv444p10le" => ffi::AV_PIX_FMT_YUV444P10LE,
791 "yuva444p10le" => ffi::AV_PIX_FMT_YUVA444P10LE,
792 "p010le" => ffi::AV_PIX_FMT_P010LE,
793 _ => ffi::AV_PIX_FMT_YUV420P,
794 }
795}
796
797#[must_use]
799pub(crate) fn frame_planes(
800 layout: alpha2_media::PixelLayout,
801 width: u32,
802 height: u32,
803) -> Vec<(usize, usize)> {
804 layout
805 .planes(width, height)
806 .into_iter()
807 .map(|(plane_width, rows, bytes)| ((plane_width * bytes) as usize, rows as usize))
808 .collect()
809}
810
811pub fn packed_frame(
817 width: u32,
818 height: u32,
819 layout: alpha2_media::PixelLayout,
820) -> Result<alpha2_ffmpeg_bridge::Yuv420pFrameBuffer, alpha2_ffmpeg_bridge::BridgeError> {
821 let (coded_width, coded_height) = (
822 i32::try_from(width)
823 .map_err(|_| alpha2_ffmpeg_bridge::BridgeError::InvalidInput("width"))?,
824 i32::try_from(height)
825 .map_err(|_| alpha2_ffmpeg_bridge::BridgeError::InvalidInput("height"))?,
826 );
827 if layout.is_yuv420p() {
828 return alpha2_ffmpeg_bridge::Yuv420pFrameBuffer::new(coded_width, coded_height);
829 }
830 alpha2_ffmpeg_bridge::Yuv420pFrameBuffer::with_format(
831 coded_width,
832 coded_height,
833 pixel_format(layout),
834 &frame_planes(layout, width, height),
835 )
836}
837
838pub(crate) fn prores_options(layout: alpha2_media::PixelLayout) -> Vec<(&'static CStr, String)> {
839 let mut options = vec![
840 (
841 c"profile",
842 if layout.alpha { "4444" } else { "hq" }.to_owned(),
843 ),
844 (c"vendor", "apl0".to_owned()),
845 ];
846 if layout.alpha {
847 options.push((c"alpha_bits", "16".to_owned()));
848 }
849 options
850}
851
852pub(crate) fn video_encoder_options(
853 color: ColorMetadata,
854 codec: &AVCodec,
855 on_hardware: bool,
856 threads: i64,
857 width: u32,
858) -> AVDictionary {
859 let mut options = color_options(color);
860 if encoder_class(codec) == NativeEncoderClass::Hybrid {
863 options = options.set_int(c"hw_encoding", i64::from(on_hardware), 0);
864 }
865 if on_hardware {
866 return options;
867 }
868 let name = codec.name().to_string_lossy();
869 for (key, value) in software_encoder_profile(&name, width, threads) {
870 options = set_encoder_option(options, key, &value);
871 }
872 options.set_int(c"threads", threads, 0)
873}
874
875fn software_encoder_profile(name: &str, width: u32, threads: i64) -> Vec<(&'static CStr, String)> {
883 match name {
884 "libvpx-vp9" => vec![
885 (c"deadline", "good".into()),
886 (c"cpu-used", "4".into()),
887 (c"row-mt", "1".into()),
888 (c"tile-columns", vp9_tile_columns_log2(width).to_string()),
889 (c"lag-in-frames", "25".into()),
890 (c"auto-alt-ref", "1".into()),
891 ],
892 "libsvtav1" => vec![
893 (c"preset", "8".into()),
894 (
895 c"svtav1-params",
896 format!(
897 "lp={}",
898 if deterministic_encode() {
899 1
900 } else {
901 svt_av1_parallelism(threads)
902 }
903 ),
904 ),
905 ],
906 _ => Vec::new(),
907 }
908}
909
910pub(crate) fn deterministic_encode() -> bool {
913 std::env::var_os("ALPHA2_DETERMINISTIC_ENCODE").is_some_and(|value| value == "1")
914}
915
916pub(crate) fn vp9_tile_columns_log2(width: u32) -> u32 {
918 (width / 256).max(1).ilog2().min(6)
919}
920
921pub(crate) fn svt_av1_parallelism(threads: i64) -> u32 {
924 match threads {
925 ..=1 => 1,
926 2 => 2,
927 3..=5 => 3,
928 6..=11 => 4,
929 12..=23 => 5,
930 _ => 6,
931 }
932}
933
934pub(crate) fn set_encoder_option(options: AVDictionary, key: &CStr, value: &str) -> AVDictionary {
937 let value = CString::new(value).unwrap_or_default();
938 if key == c"svtav1-params" && options.get(key, None, 0).is_some() {
939 let appended = CString::new(format!(":{}", value.to_string_lossy())).unwrap_or_default();
940 options.set(key, &appended, ffi::AV_DICT_APPEND)
941 } else {
942 options.set(key, &value, 0)
943 }
944}
945
946#[must_use]
951pub fn codec_thread_budget() -> i64 {
952 i64::try_from(configured_codec_thread_budget().min(16)).unwrap_or(1)
953}
954
955pub fn configured_codec_thread_override() -> Result<Option<usize>, String> {
961 codec_thread_override(std::env::var_os("ALPHA2_FFMPEG_THREADS").as_deref())
962}
963
964pub fn codec_thread_override(setting: Option<&std::ffi::OsStr>) -> Result<Option<usize>, String> {
969 let Some(setting) = setting else {
970 return Ok(None);
971 };
972 setting
973 .to_str()
974 .and_then(|value| value.parse::<usize>().ok())
975 .filter(|value| *value > 0)
976 .map(|value| Some(value.min(64)))
977 .ok_or_else(|| setting.to_string_lossy().into_owned())
978}
979
980fn configured_codec_thread_budget() -> usize {
981 configured_codec_thread_override()
982 .ok()
983 .flatten()
984 .unwrap_or_else(|| {
985 codec_thread_budget_for(
986 std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get),
987 )
988 })
989 .clamp(1, 64)
990}
991
992pub(crate) fn codec_thread_budget_for(hardware_threads: usize) -> usize {
995 hardware_threads
996 .saturating_sub(codec_thread_reserve(hardware_threads))
997 .max(1)
998}
999
1000pub(crate) const fn codec_thread_reserve(hardware_threads: usize) -> usize {
1006 match hardware_threads {
1007 16.. => 2,
1008 6..=15 => 1,
1009 _ => 0,
1010 }
1011}
1012
1013#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1015pub struct CodecThreadRequest {
1016 pub decode_concurrency: usize,
1018 pub hardware_decode: bool,
1020 pub source_frame_pixels: u64,
1022 pub software_video_encoder: bool,
1024 pub encoder_fixed_threads: Option<usize>,
1028}
1029
1030const DECODE_PIXELS_PER_THREAD: u64 = 2_000_000;
1032const MIN_SOFTWARE_DECODER_THREADS: usize = 2;
1034const MAX_SOFTWARE_DECODER_THREADS: usize = 6;
1037const ENCODER_THREAD_FLOOR: usize = 4;
1040
1041const fn encoder_thread_floor(total_threads: usize) -> usize {
1044 let third = total_threads / 3;
1045 if third < ENCODER_THREAD_FLOOR {
1046 if third == 0 { 1 } else { third }
1047 } else {
1048 ENCODER_THREAD_FLOOR
1049 }
1050}
1051pub const HARDWARE_DECODER_THREADS: usize = 2;
1059
1060#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1063pub struct CodecThreadAllocation {
1064 pub total_threads: usize,
1066 pub decode_concurrency: usize,
1068 pub decoder_threads: i64,
1070 pub hardware_decoder_threads: i64,
1072 pub encoder_threads: i64,
1074 pub allocated_threads: usize,
1076}
1077
1078#[must_use]
1091pub fn codec_thread_allocation(request: CodecThreadRequest) -> CodecThreadAllocation {
1092 codec_thread_allocation_within(request, configured_codec_thread_budget())
1093}
1094
1095pub(crate) fn codec_thread_allocation_within(
1097 request: CodecThreadRequest,
1098 configured: usize,
1099) -> CodecThreadAllocation {
1100 let encoder_slots = usize::from(request.software_video_encoder);
1101 let decode_concurrency = request
1102 .decode_concurrency
1103 .max(1)
1104 .min(configured.saturating_sub(encoder_slots).max(1));
1105 let total_threads = configured.max(decode_concurrency.saturating_add(encoder_slots));
1106 let fixed_encoder = request
1109 .encoder_fixed_threads
1110 .filter(|_| request.software_video_encoder)
1111 .map(|threads| threads.max(1));
1112 let encoder_floor = match fixed_encoder {
1113 Some(threads) => threads.min(total_threads.saturating_sub(decode_concurrency).max(1)),
1114 None if request.software_video_encoder => encoder_thread_floor(total_threads)
1115 .min(total_threads.saturating_sub(decode_concurrency).max(1)),
1116 None => 0,
1117 };
1118 let decode_budget = total_threads
1119 .saturating_sub(encoder_floor)
1120 .max(decode_concurrency);
1121 let even_share = if fixed_encoder.is_some() {
1122 (decode_budget / decode_concurrency).max(1)
1124 } else {
1125 let contexts = decode_concurrency.saturating_add(encoder_slots).max(1);
1126 (total_threads / contexts).max(1)
1127 };
1128 let decoder_threads = if request.hardware_decode || !request.software_video_encoder {
1129 even_share
1132 } else {
1133 software_decoder_threads(request.source_frame_pixels)
1136 .clamp(1, (decode_budget / decode_concurrency).max(1))
1137 .min(even_share)
1138 };
1139 let decode_assigned = decoder_threads.saturating_mul(decode_concurrency);
1140 let encoder_threads = match fixed_encoder {
1143 Some(threads) => threads,
1144 None if request.software_video_encoder => {
1145 total_threads.saturating_sub(decode_assigned).max(1)
1146 }
1147 None => 0,
1148 };
1149 CodecThreadAllocation {
1150 total_threads,
1151 decode_concurrency,
1152 decoder_threads: i64::try_from(decoder_threads).unwrap_or(1).clamp(1, 64),
1153 hardware_decoder_threads: i64::try_from(HARDWARE_DECODER_THREADS.min(total_threads))
1154 .unwrap_or(1)
1155 .max(1),
1156 encoder_threads: i64::try_from(encoder_threads).unwrap_or(0).clamp(0, 64),
1157 allocated_threads: decode_assigned.saturating_add(encoder_threads),
1158 }
1159}
1160
1161fn software_decoder_threads(source_frame_pixels: u64) -> usize {
1164 usize::try_from(source_frame_pixels.div_ceil(DECODE_PIXELS_PER_THREAD))
1165 .unwrap_or(MAX_SOFTWARE_DECODER_THREADS)
1166 .clamp(MIN_SOFTWARE_DECODER_THREADS, MAX_SOFTWARE_DECODER_THREADS)
1167}
1168
1169pub(crate) fn unused_option_names(options: Option<AVDictionary>) -> Option<String> {
1171 let options = options?;
1172 let names = options
1173 .iter()
1174 .map(|entry| entry.key().to_string_lossy().into_owned())
1175 .collect::<Vec<_>>();
1176 (!names.is_empty()).then(|| names.join(", "))
1177}
1178
1179pub(crate) fn reject_unused_decode_options(
1180 options: Option<AVDictionary>,
1181) -> Result<(), DecodeError> {
1182 if let Some(names) = unused_option_names(options) {
1183 Err(DecodeError::Adapter(format!(
1184 "FFmpeg decoder did not consume options: {names}"
1185 )))
1186 } else {
1187 Ok(())
1188 }
1189}
1190
1191pub(crate) fn reject_unused_encode_options(
1192 options: Option<AVDictionary>,
1193) -> Result<(), EncodeError> {
1194 if let Some(names) = unused_option_names(options) {
1195 Err(EncodeError::Adapter(format!(
1196 "FFmpeg encoder did not consume options: {names}"
1197 )))
1198 } else {
1199 Ok(())
1200 }
1201}
1202
1203pub(crate) fn color_tags(
1208 color: ColorMetadata,
1209) -> (
1210 ffi::AVColorPrimaries,
1211 ffi::AVColorTransferCharacteristic,
1212 ffi::AVColorSpace,
1213 ffi::AVColorRange,
1214) {
1215 (
1216 match color.primaries {
1217 ColorPrimaries::Bt601 => ffi::AVCOL_PRI_SMPTE170M,
1218 ColorPrimaries::Bt709 => ffi::AVCOL_PRI_BT709,
1219 ColorPrimaries::Bt2020 => ffi::AVCOL_PRI_BT2020,
1220 },
1221 match color.transfer {
1222 TransferFunction::Bt709 => ffi::AVCOL_TRC_BT709,
1223 TransferFunction::Srgb => ffi::AVCOL_TRC_IEC61966_2_1,
1224 TransferFunction::Pq => ffi::AVCOL_TRC_SMPTE2084,
1225 TransferFunction::Hlg => ffi::AVCOL_TRC_ARIB_STD_B67,
1226 },
1227 match color.matrix {
1228 ColorMatrix::Bt601 => ffi::AVCOL_SPC_SMPTE170M,
1229 ColorMatrix::Bt709 => ffi::AVCOL_SPC_BT709,
1230 ColorMatrix::Bt2020Ncl => ffi::AVCOL_SPC_BT2020_NCL,
1231 ColorMatrix::Rgb => ffi::AVCOL_SPC_RGB,
1232 },
1233 match color.range {
1234 ColorRange::Limited => ffi::AVCOL_RANGE_MPEG,
1235 ColorRange::Full => ffi::AVCOL_RANGE_JPEG,
1236 },
1237 )
1238}
1239
1240pub(crate) fn color_options(color: ColorMetadata) -> AVDictionary {
1243 let (primaries, transfer, matrix, range) = color_tags(color);
1244 AVDictionary::new_int(c"color_primaries", i64::from(primaries), 0)
1245 .set_int(c"color_trc", i64::from(transfer), 0)
1246 .set_int(c"colorspace", i64::from(matrix), 0)
1247 .set_int(c"color_range", i64::from(range), 0)
1248}
1249
1250fn gpu_fault(error: &dyn std::any::Any) -> Option<alpha2_media::GpuFault> {
1252 error
1253 .downcast_ref::<alpha2_ffmpeg_bridge::BridgeError>()
1254 .and_then(alpha2_ffmpeg_bridge::BridgeError::gpu_fault)
1255}
1256
1257pub(crate) fn adapter_error(error: impl std::fmt::Display + 'static) -> DecodeError {
1259 match gpu_fault(&error) {
1260 Some(fault) => DecodeError::Gpu {
1261 fault,
1262 message: error.to_string(),
1263 },
1264 None => DecodeError::Adapter(error.to_string()),
1265 }
1266}
1267
1268pub(crate) fn encode_adapter_error(error: impl std::fmt::Display + 'static) -> EncodeError {
1270 match gpu_fault(&error) {
1271 Some(fault) => EncodeError::Gpu {
1272 fault,
1273 message: error.to_string(),
1274 },
1275 None => EncodeError::Adapter(error.to_string()),
1276 }
1277}
1278
1279pub(crate) fn encode_adapter_error_at(
1280 context: &str,
1281 error: impl std::fmt::Display + 'static,
1282) -> EncodeError {
1283 match gpu_fault(&error) {
1284 Some(fault) => EncodeError::Gpu {
1285 fault,
1286 message: format!("{context}: {error}"),
1287 },
1288 None => EncodeError::Adapter(format!("{context}: {error}")),
1289 }
1290}