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 {
114 !color.is_hdr() && color.primaries == ColorPrimaries::Bt709
115}
116
117fn rgba8_plane(
120 frame: &AVFrame,
121 color: ColorMetadata,
122 scaler: &mut FrameScaler,
123) -> Result<Plane, DecodeError> {
124 let width = u32::try_from(frame.width).map_err(|_| DecodeError::OutOfRange)?;
125 let height = u32::try_from(frame.height).map_err(|_| DecodeError::OutOfRange)?;
126 let active = scaler.for_frame(frame, ffi::AV_PIX_FMT_RGBA)?;
127 configure_yuv_to_rgba(active, color).map_err(adapter_error)?;
128 let mut bytes = vec![0; packed_len(width, height, 4)?];
129 alpha2_ffmpeg_bridge::scale_frame_packed(active, frame, ffi::AV_PIX_FMT_RGBA, &mut bytes)
130 .map_err(adapter_error)?;
131 Ok(Plane {
132 bytes: bytes.into(),
133 stride: width.saturating_mul(4),
134 width,
135 height,
136 })
137}
138
139fn full_resolution_planes(
146 frame: &AVFrame,
147 color: ColorMetadata,
148 scaler: &mut FrameScaler,
149) -> Result<(PixelFormat, Vec<Plane>, ColorMetadata), DecodeError> {
150 let width = u32::try_from(frame.width).map_err(|_| DecodeError::OutOfRange)?;
151 let height = u32::try_from(frame.height).map_err(|_| DecodeError::OutOfRange)?;
152 let profile = alpha2_ffmpeg_bridge::sample_profile(frame.format)
153 .ok_or_else(|| DecodeError::Unsupported("unsupported FFmpeg pixel format".into()))?;
154 let depth = match profile.bit_depth {
155 0..=8 => 8,
156 9..=10 => 10,
157 11..=12 => 12,
158 _ => 16,
159 };
160 let target = planar_444_format(profile.is_rgb, profile.has_alpha, depth);
161 let active = scaler.for_frame(frame, target)?;
162 if !profile.is_rgb {
163 let coefficients = sws_coefficients(color.matrix);
166 let full = color.range == ColorRange::Full;
167 alpha2_ffmpeg_bridge::configure_colorspace(active, coefficients, full, coefficients, full)
168 .map_err(adapter_error)?;
169 }
170 let plane_count = if profile.has_alpha { 4 } else { 3 };
171 let sample_bytes = if depth > 8 { 2 } else { 1 };
172 let plane_len = packed_len(width, height, sample_bytes)?;
173 let mut bytes = vec![0; plane_len * plane_count];
174 alpha2_ffmpeg_bridge::scale_frame_packed(active, frame, target, &mut bytes)
175 .map_err(adapter_error)?;
176 let bytes = bytes::Bytes::from(bytes);
177 let stride = width.saturating_mul(u32::try_from(sample_bytes).unwrap_or(2));
178 let plane = |index: usize| Plane {
179 bytes: bytes.slice(index * plane_len..(index + 1) * plane_len),
180 stride,
181 width,
182 height,
183 };
184 let order: &[usize] = if profile.is_rgb {
186 &[2, 0, 1]
187 } else {
188 &[0, 1, 2]
189 };
190 let mut planes = order.iter().map(|&index| plane(index)).collect::<Vec<_>>();
191 if profile.has_alpha {
192 planes.push(plane(3));
193 }
194 let format = YuvFormat::planar(0, 0, depth);
195 let format = if profile.has_alpha {
196 format.with_alpha()
197 } else {
198 format
199 };
200 let color = if profile.is_rgb {
201 ColorMetadata {
202 matrix: ColorMatrix::Rgb,
203 range: ColorRange::Full,
204 ..color
205 }
206 } else {
207 color
208 };
209 Ok((PixelFormat::Yuv(format), planes, color))
210}
211
212const fn planar_444_format(rgb: bool, alpha: bool, depth: u8) -> ffi::AVPixelFormat {
215 match (rgb, alpha, depth) {
216 (true, false, 8) => ffi::AV_PIX_FMT_GBRP,
217 (true, true, 8) => ffi::AV_PIX_FMT_GBRAP,
218 (true, false, 10) => ffi::AV_PIX_FMT_GBRP10LE,
219 (true, true, 10) => ffi::AV_PIX_FMT_GBRAP10LE,
220 (true, false, 12) => ffi::AV_PIX_FMT_GBRP12LE,
221 (true, true, 12) => ffi::AV_PIX_FMT_GBRAP12LE,
222 (true, false, _) => ffi::AV_PIX_FMT_GBRP16LE,
223 (true, true, _) => ffi::AV_PIX_FMT_GBRAP16LE,
224 (false, false, 8) => ffi::AV_PIX_FMT_YUV444P,
225 (false, true, 8) => ffi::AV_PIX_FMT_YUVA444P,
226 (false, false, 10) => ffi::AV_PIX_FMT_YUV444P10LE,
227 (false, true, 10) => ffi::AV_PIX_FMT_YUVA444P10LE,
228 (false, false, 12) => ffi::AV_PIX_FMT_YUV444P12LE,
229 (false, true, 12) => ffi::AV_PIX_FMT_YUVA444P12LE,
230 (false, false, _) => ffi::AV_PIX_FMT_YUV444P16LE,
231 (false, true, _) => ffi::AV_PIX_FMT_YUVA444P16LE,
232 }
233}
234
235#[derive(Default)]
245pub(crate) struct FrameScaler {
246 context: Option<(ScalerShape, SwsContext)>,
247 made: u64,
248}
249
250#[derive(Clone, Copy, Debug, Eq, PartialEq)]
252struct ScalerShape {
253 width: i32,
254 height: i32,
255 source: ffi::AVPixelFormat,
256 target: ffi::AVPixelFormat,
257}
258
259impl FrameScaler {
260 fn for_frame(
263 &mut self,
264 frame: &AVFrame,
265 target: ffi::AVPixelFormat,
266 ) -> Result<&mut SwsContext, DecodeError> {
267 let shape = ScalerShape {
268 width: frame.width,
269 height: frame.height,
270 source: frame.format,
271 target,
272 };
273 if self
274 .context
275 .as_ref()
276 .is_none_or(|(made_for, _)| *made_for != shape)
277 {
278 self.context = None;
279 let context = SwsContext::get_context(
280 frame.width,
281 frame.height,
282 frame.format,
283 frame.width,
284 frame.height,
285 target,
286 ffi::SWS_BILINEAR,
287 None,
288 None,
289 None,
290 )
291 .ok_or_else(|| DecodeError::Unsupported("unsupported FFmpeg pixel format".into()))?;
292 self.made += 1;
293 self.context = Some((shape, context));
294 }
295 self.context
296 .as_mut()
297 .map(|(_, context)| context)
298 .ok_or_else(|| DecodeError::Unsupported("unsupported FFmpeg pixel format".into()))
299 }
300
301 #[cfg(test)]
303 pub(crate) const fn made(&self) -> u64 {
304 self.made
305 }
306
307 #[cfg(test)]
309 pub(crate) const fn is_empty(&self) -> bool {
310 self.context.is_none()
311 }
312}
313
314fn packed_len(width: u32, height: u32, sample_bytes: usize) -> Result<usize, DecodeError> {
316 usize::try_from(width)
317 .ok()
318 .and_then(|width| {
319 usize::try_from(height)
320 .ok()
321 .and_then(|height| width.checked_mul(height))
322 })
323 .and_then(|pixels| pixels.checked_mul(sample_bytes))
324 .ok_or(DecodeError::OutOfRange)
325}
326
327pub(crate) fn decoded_chroma_location(
336 codec: ffi::AVCodecID,
337 location: ffi::AVChromaLocation,
338) -> ChromaLocation {
339 if codec == ffi::AV_CODEC_ID_H264 && location == ffi::AVCHROMA_LOC_UNSPECIFIED {
340 ChromaLocation::Left
341 } else {
342 chroma_location(location)
343 }
344}
345
346pub(crate) fn chroma_location(location: ffi::AVChromaLocation) -> ChromaLocation {
347 match location {
348 ffi::AVCHROMA_LOC_LEFT => ChromaLocation::Left,
349 ffi::AVCHROMA_LOC_CENTER => ChromaLocation::Center,
350 ffi::AVCHROMA_LOC_TOPLEFT => ChromaLocation::TopLeft,
351 _ => ChromaLocation::Unspecified,
352 }
353}
354
355pub(crate) fn configure_rgba_to_yuv(
357 scaler: &mut SwsContext,
358 color: ColorMetadata,
359) -> Result<(), alpha2_ffmpeg_bridge::BridgeError> {
360 alpha2_ffmpeg_bridge::configure_colorspace(
361 scaler,
362 i32::try_from(ffi::SWS_CS_ITU709).expect("color coefficient fits i32"),
363 true,
364 sws_coefficients(color.matrix),
365 color.range == ColorRange::Full,
366 )
367}
368
369pub(crate) fn configure_yuv_to_rgba(
371 scaler: &mut SwsContext,
372 color: ColorMetadata,
373) -> Result<(), alpha2_ffmpeg_bridge::BridgeError> {
374 alpha2_ffmpeg_bridge::configure_colorspace(
375 scaler,
376 sws_coefficients(color.matrix),
377 color.range == ColorRange::Full,
378 i32::try_from(ffi::SWS_CS_ITU709).expect("color coefficient fits i32"),
379 true,
380 )
381}
382
383pub(crate) fn sws_coefficients(matrix: ColorMatrix) -> i32 {
384 let value = match matrix {
385 ColorMatrix::Bt601 => ffi::SWS_CS_ITU601,
386 ColorMatrix::Bt709 | ColorMatrix::Rgb => ffi::SWS_CS_ITU709,
387 ColorMatrix::Bt2020Ncl => ffi::SWS_CS_BT2020,
388 };
389 i32::try_from(value).expect("color coefficient fits i32")
390}
391
392pub(crate) fn tick_base() -> AVRational {
394 ra(
395 1,
396 i32::try_from(TICKS_PER_SECOND).expect("tick rate fits i32"),
397 )
398}
399
400pub(crate) fn direct_yuv420p_frame(
406 frame: &DecodedVideoFrame,
407 width: i32,
408 height: i32,
409) -> Result<Option<AVFrame>, EncodeError> {
410 if frame.pixel_format != PixelFormat::Yuv(YuvFormat::I420) {
411 return Ok(None);
412 }
413 if frame.planes.len() != 3
414 || frame.visible_rect.x != 0
415 || frame.visible_rect.y != 0
416 || frame.planes[0].width != u32::try_from(width).unwrap_or(0)
417 || frame.planes[0].height != u32::try_from(height).unwrap_or(0)
418 {
419 return Err(EncodeError::Adapter(
420 "direct YUV encoder input must contain one uncropped coded frame".into(),
421 ));
422 }
423 alpha2_ffmpeg_bridge::yuv420p_frame(
424 &frame.planes[0].bytes,
425 frame.planes[0].stride,
426 &frame.planes[1].bytes,
427 frame.planes[1].stride,
428 &frame.planes[2].bytes,
429 frame.planes[2].stride,
430 width,
431 height,
432 )
433 .map(Some)
434 .map_err(encode_adapter_error)
435}
436
437pub(crate) struct Yuv420pConverter {
445 output: ColorMetadata,
446 scaler: Option<(ffi::AVPixelFormat, u32, u32, SwsContext)>,
447}
448
449impl Yuv420pConverter {
450 pub(crate) const fn new(output: ColorMetadata) -> Self {
452 Self {
453 output,
454 scaler: None,
455 }
456 }
457
458 pub(crate) fn convert(
464 &mut self,
465 frame: &DecodedVideoFrame,
466 width: i32,
467 height: i32,
468 ) -> Result<Option<AVFrame>, EncodeError> {
469 let PixelFormat::Yuv(format) = frame.pixel_format else {
470 return Ok(None);
471 };
472 let rgb = frame
473 .color
474 .is_some_and(|color| color.matrix == ColorMatrix::Rgb);
475 if format == YuvFormat::I420 && !rgb {
476 return direct_yuv420p_frame(frame, width, height);
477 }
478 let (width, height) = (
479 u32::try_from(width).unwrap_or(0),
480 u32::try_from(height).unwrap_or(0),
481 );
482 let stored = if format.interleaved_chroma { 2 } else { 3 };
483 if frame.visible_rect.x != 0
484 || frame.visible_rect.y != 0
485 || frame.planes.len() < stored
486 || frame.planes[0].width != width
487 || frame.planes[0].height != height
488 {
489 return Err(EncodeError::Adapter(
490 "YUV encoder input must contain one uncropped frame of the encoder's size".into(),
491 ));
492 }
493 let (source_format, planes) = if rgb {
494 if format.chroma_shift_x != 0
495 || format.chroma_shift_y != 0
496 || format.interleaved_chroma
497 || format.msb_aligned
498 || !matches!(format.bit_depth, 8 | 10 | 12 | 16)
499 {
500 return Err(EncodeError::Adapter(format!(
501 "no FFmpeg pixel format stores red, green, and blue as {format:?}"
502 )));
503 }
504 let planes = [1, 2, 0]
506 .iter()
507 .map(|&index: &usize| (&frame.planes[index].bytes[..], frame.planes[index].stride))
508 .collect::<Vec<_>>();
509 (planar_444_format(true, false, format.bit_depth), planes)
510 } else {
511 let layout = alpha2_ffmpeg_bridge::YuvPlaneLayout {
512 chroma_shift_x: format.chroma_shift_x,
513 chroma_shift_y: format.chroma_shift_y,
514 bit_depth: format.bit_depth,
515 interleaved_chroma: format.interleaved_chroma,
516 msb_aligned: format.msb_aligned,
517 };
518 let source_format =
519 alpha2_ffmpeg_bridge::yuv_pixel_format(layout).ok_or_else(|| {
520 EncodeError::Adapter(format!("no FFmpeg pixel format stores {format:?}"))
521 })?;
522 let planes = frame.planes[..stored]
523 .iter()
524 .map(|plane| (&plane.bytes[..], plane.stride))
525 .collect::<Vec<_>>();
526 (source_format, planes)
527 };
528 let source = alpha2_ffmpeg_bridge::planar_frame(source_format, &planes, width, height)
529 .map_err(encode_adapter_error)?;
530 if self
531 .scaler
532 .as_ref()
533 .is_none_or(|(cached, w, h, _)| (*cached, *w, *h) != (source_format, width, height))
534 {
535 let (signed_width, signed_height) = (
536 i32::try_from(width).map_err(|_| EncodeError::Adapter("width overflow".into()))?,
537 i32::try_from(height)
538 .map_err(|_| EncodeError::Adapter("height overflow".into()))?,
539 );
540 let mut scaler = SwsContext::get_context(
541 signed_width,
542 signed_height,
543 source_format,
544 signed_width,
545 signed_height,
546 ffi::AV_PIX_FMT_YUV420P,
547 ffi::SWS_BILINEAR,
548 None,
549 None,
550 None,
551 )
552 .ok_or_else(|| EncodeError::Adapter("cannot create a YUV to YUV420P scaler".into()))?;
553 if rgb {
554 configure_rgba_to_yuv(&mut scaler, self.output).map_err(encode_adapter_error)?;
555 }
556 self.scaler = Some((source_format, width, height, scaler));
557 }
558 let Some((_, _, _, scaler)) = self.scaler.as_mut() else {
559 unreachable!("the scaler was just made");
560 };
561 let mut output = AVFrame::new();
562 output.set_format(ffi::AV_PIX_FMT_YUV420P);
563 output.set_width(source.width);
564 output.set_height(source.height);
565 output.get_buffer(1).map_err(encode_adapter_error)?;
566 scaler
567 .scale_frame(&source, 0, source.height, &mut output)
568 .map_err(encode_adapter_error)?;
569 Ok(Some(output))
570 }
571}
572
573pub(crate) fn timestamp(value: i64, base: AVRational) -> Option<MediaTime> {
575 (value != ffi::AV_NOPTS_VALUE && base.den > 0)
576 .then(|| MediaTime::from_ticks(av_rescale_q(value, base, tick_base())))
577}
578
579pub(crate) fn declared_sar(value: AVRational) -> Option<SampleAspectRatio> {
583 SampleAspectRatio::new(
584 u64::try_from(value.num).ok()?,
585 u64::try_from(value.den).ok()?,
586 )
587}
588
589pub(crate) fn guess_sar(container: AVRational, pictures: AVRational) -> SampleAspectRatio {
595 declared_sar(container)
596 .or_else(|| declared_sar(pictures))
597 .unwrap_or_default()
598}
599
600pub(crate) fn stream_rotation(metadata: Option<rsmpeg::avutil::AVDictionaryRef<'_>>) -> Rotation {
602 let Some(metadata) = metadata else {
603 return Rotation::Deg0;
604 };
605 let Some(entry) = metadata.get(c"rotate", None, 0) else {
606 return Rotation::Deg0;
607 };
608 match entry
609 .value()
610 .to_string_lossy()
611 .parse::<i32>()
612 .unwrap_or(0)
613 .rem_euclid(360)
614 {
615 90 => Rotation::Deg90,
616 180 => Rotation::Deg180,
617 270 => Rotation::Deg270,
618 _ => Rotation::Deg0,
619 }
620}
621
622pub(crate) fn rotation_from_display(counter_clockwise: f64) -> Option<Rotation> {
629 let normalized = (-counter_clockwise).rem_euclid(360.0);
630 if !(0.5..359.5).contains(&normalized) {
631 Some(Rotation::Deg0)
632 } else if (normalized - 90.0).abs() < 0.5 {
633 Some(Rotation::Deg90)
634 } else if (normalized - 180.0).abs() < 0.5 {
635 Some(Rotation::Deg180)
636 } else if (normalized - 270.0).abs() < 0.5 {
637 Some(Rotation::Deg270)
638 } else {
639 None
640 }
641}
642
643pub(crate) fn orientation_from_display(display: DisplayRotation) -> Option<Orientation> {
652 if display.flipped {
653 rotation_from_display(-display.counter_clockwise_degrees).map(|rotation| Orientation {
654 rotation,
655 mirror: true,
656 })
657 } else {
658 rotation_from_display(display.counter_clockwise_degrees).map(Orientation::rotated)
659 }
660}
661
662pub(crate) fn display_of(orientation: Orientation) -> Option<DisplayRotation> {
666 if orientation == Orientation::default() {
667 return None;
668 }
669 let clockwise = match orientation.rotation {
670 Rotation::Deg0 => 0.0,
671 Rotation::Deg90 => 90.0,
672 Rotation::Deg180 => 180.0,
673 Rotation::Deg270 => 270.0,
674 };
675 Some(DisplayRotation {
676 counter_clockwise_degrees: if orientation.mirror {
677 clockwise
678 } else {
679 -clockwise
680 },
681 flipped: orientation.mirror,
682 })
683}
684
685const HVC1: u32 = u32::from_le_bytes(*b"hvc1");
687const HEV1: u32 = u32::from_le_bytes(*b"hev1");
689
690pub(crate) const fn muxer_name(container: Container) -> &'static CStr {
692 match container {
693 Container::Mp4 => c"mp4",
694 Container::Mov => c"mov",
695 Container::Mkv => c"matroska",
696 Container::Webm => c"webm",
697 }
698}
699
700pub(crate) fn muxer_header_options(container: Container) -> Option<AVDictionary> {
709 matches!(container, Container::Mkv | Container::Webm)
710 .then(|| AVDictionary::new(c"fflags", c"+bitexact", 0))
711}
712
713pub(crate) fn container_codec_tag(
727 container: Container,
728 codec_id: ffi::AVCodecID,
729 tag: u32,
730 copied: bool,
731) -> u32 {
732 if matches!(container, Container::Mkv | Container::Webm) {
733 0
734 } else if codec_id == ffi::AV_CODEC_ID_HEVC {
735 if tag == HVC1 || tag == HEV1 {
736 tag
737 } else {
738 HVC1
739 }
740 } else if copied {
741 0
742 } else {
743 tag
744 }
745}
746
747pub(crate) fn color_metadata(
748 parameters: &rsmpeg::avcodec::AVCodecParameters,
749) -> Option<ColorMetadata> {
750 map_color(
751 parameters.color_primaries,
752 parameters.color_trc,
753 parameters.color_space,
754 parameters.color_range,
755 )
756}
757
758pub(crate) fn frame_color_metadata(frame: &AVFrame) -> Option<ColorMetadata> {
759 map_color(
760 frame.color_primaries,
761 frame.color_trc,
762 frame.colorspace,
763 frame.color_range,
764 )
765}
766
767pub(crate) fn map_color(
773 primaries: ffi::AVColorPrimaries,
774 transfer: ffi::AVColorTransferCharacteristic,
775 matrix: ffi::AVColorSpace,
776 range: ffi::AVColorRange,
777) -> Option<ColorMetadata> {
778 if primaries == ffi::AVCOL_PRI_UNSPECIFIED
779 && transfer == ffi::AVCOL_TRC_UNSPECIFIED
780 && matrix == ffi::AVCOL_SPC_UNSPECIFIED
781 && range == ffi::AVCOL_RANGE_UNSPECIFIED
782 {
783 return None;
784 }
785 let primaries = match primaries {
786 ffi::AVCOL_PRI_BT2020 => ColorPrimaries::Bt2020,
787 ffi::AVCOL_PRI_SMPTE170M | ffi::AVCOL_PRI_BT470BG => ColorPrimaries::Bt601,
788 _ => ColorPrimaries::Bt709,
789 };
790 let transfer = match transfer {
791 ffi::AVCOL_TRC_SMPTE2084 => TransferFunction::Pq,
792 ffi::AVCOL_TRC_ARIB_STD_B67 => TransferFunction::Hlg,
793 ffi::AVCOL_TRC_IEC61966_2_1 => TransferFunction::Srgb,
794 _ => TransferFunction::Bt709,
795 };
796 let matrix = match matrix {
797 ffi::AVCOL_SPC_BT2020_NCL => ColorMatrix::Bt2020Ncl,
798 ffi::AVCOL_SPC_SMPTE170M | ffi::AVCOL_SPC_BT470BG => ColorMatrix::Bt601,
799 ffi::AVCOL_SPC_RGB => ColorMatrix::Rgb,
800 _ => ColorMatrix::Bt709,
801 };
802 Some(ColorMetadata {
803 primaries,
804 transfer,
805 matrix,
806 range: if range == ffi::AVCOL_RANGE_JPEG {
807 ColorRange::Full
808 } else {
809 ColorRange::Limited
810 },
811 dynamic_range: if matches!(transfer, TransferFunction::Pq | TransferFunction::Hlg) {
812 DynamicRange::Hdr
813 } else {
814 DynamicRange::Sdr
815 },
816 })
817}
818
819pub(crate) fn video_decoder_options(threads: i64) -> AVDictionary {
828 AVDictionary::new_int(c"threads", threads, 0).set(c"flags", c"+unaligned", 0)
829}
830
831pub(crate) fn video_encoder_options(
834 color: ColorMetadata,
835 codec: &AVCodec,
836 on_hardware: bool,
837 threads: i64,
838 width: u32,
839) -> AVDictionary {
840 let mut options = color_options(color);
841 if encoder_class(codec) == NativeEncoderClass::Hybrid {
845 options = options.set_int(c"hw_encoding", i64::from(on_hardware), 0);
846 }
847 if on_hardware {
848 return options;
849 }
850 let name = codec.name().to_string_lossy();
851 for (key, value) in software_encoder_profile(&name, width, threads) {
852 options = set_encoder_option(options, key, &value);
853 }
854 options.set_int(c"threads", threads, 0)
855}
856
857fn software_encoder_profile(name: &str, width: u32, threads: i64) -> Vec<(&'static CStr, String)> {
874 match name {
875 "libvpx-vp9" => vec![
876 (c"deadline", "good".into()),
877 (c"cpu-used", "4".into()),
878 (c"row-mt", "1".into()),
879 (c"tile-columns", vp9_tile_columns_log2(width).to_string()),
880 (c"lag-in-frames", "25".into()),
881 (c"auto-alt-ref", "1".into()),
882 ],
883 "libsvtav1" => vec![
884 (c"preset", "8".into()),
885 (
886 c"svtav1-params",
887 format!(
888 "lp={}",
889 if deterministic_encode() {
890 1
891 } else {
892 svt_av1_parallelism(threads)
893 }
894 ),
895 ),
896 ],
897 _ => Vec::new(),
898 }
899}
900
901pub(crate) fn deterministic_encode() -> bool {
905 std::env::var_os("ALPHA2_DETERMINISTIC_ENCODE").is_some_and(|value| value == "1")
906}
907
908pub(crate) fn vp9_tile_columns_log2(width: u32) -> u32 {
911 (width / 256).max(1).ilog2().min(6)
912}
913
914pub(crate) fn svt_av1_parallelism(threads: i64) -> u32 {
918 match threads {
919 ..=1 => 1,
920 2 => 2,
921 3..=5 => 3,
922 6..=11 => 4,
923 12..=23 => 5,
924 _ => 6,
925 }
926}
927
928pub(crate) fn set_encoder_option(options: AVDictionary, key: &CStr, value: &str) -> AVDictionary {
932 let value = CString::new(value).unwrap_or_default();
933 if key == c"svtav1-params" && options.get(key, None, 0).is_some() {
934 let appended = CString::new(format!(":{}", value.to_string_lossy())).unwrap_or_default();
935 options.set(key, &appended, ffi::AV_DICT_APPEND)
936 } else {
937 options.set(key, &value, 0)
938 }
939}
940
941#[must_use]
948pub fn codec_thread_budget() -> i64 {
949 i64::try_from(configured_codec_thread_budget().min(16)).unwrap_or(1)
950}
951
952pub fn configured_codec_thread_override() -> Result<Option<usize>, String> {
960 codec_thread_override(std::env::var_os("ALPHA2_FFMPEG_THREADS").as_deref())
961}
962
963pub fn codec_thread_override(setting: Option<&std::ffi::OsStr>) -> Result<Option<usize>, String> {
970 let Some(setting) = setting else {
971 return Ok(None);
972 };
973 setting
974 .to_str()
975 .and_then(|value| value.parse::<usize>().ok())
976 .filter(|value| *value > 0)
977 .map(|value| Some(value.min(64)))
978 .ok_or_else(|| setting.to_string_lossy().into_owned())
979}
980
981fn configured_codec_thread_budget() -> usize {
982 configured_codec_thread_override()
983 .ok()
984 .flatten()
985 .unwrap_or_else(|| {
986 std::thread::available_parallelism()
987 .map_or(1, std::num::NonZeroUsize::get)
988 .saturating_sub(2)
989 .max(1)
990 })
991 .clamp(1, 64)
992}
993
994#[derive(Clone, Copy, Debug, PartialEq, Eq)]
996pub struct CodecThreadRequest {
997 pub decode_concurrency: usize,
999 pub hardware_decode: bool,
1001 pub source_frame_pixels: u64,
1003 pub software_video_encoder: bool,
1005}
1006
1007const DECODE_PIXELS_PER_THREAD: u64 = 2_000_000;
1009const MIN_SOFTWARE_DECODER_THREADS: usize = 2;
1011const MAX_SOFTWARE_DECODER_THREADS: usize = 6;
1015const ENCODER_THREAD_FLOOR: usize = 4;
1017
1018#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1021pub struct CodecThreadAllocation {
1022 pub total_threads: usize,
1024 pub decode_concurrency: usize,
1026 pub decoder_threads: i64,
1028 pub encoder_threads: i64,
1030 pub allocated_threads: usize,
1032}
1033
1034#[must_use]
1056pub fn codec_thread_allocation(request: CodecThreadRequest) -> CodecThreadAllocation {
1057 let configured = configured_codec_thread_budget();
1058 let encoder_slots = usize::from(request.software_video_encoder);
1059 let decode_concurrency = request
1060 .decode_concurrency
1061 .max(1)
1062 .min(configured.saturating_sub(encoder_slots).max(1));
1063 let total_threads = configured.max(decode_concurrency.saturating_add(encoder_slots));
1064 let encoder_floor = if request.software_video_encoder {
1067 ENCODER_THREAD_FLOOR.min(total_threads.saturating_sub(decode_concurrency).max(1))
1068 } else {
1069 0
1070 };
1071 let decode_budget = total_threads
1072 .saturating_sub(encoder_floor)
1073 .max(decode_concurrency);
1074 let even_share = {
1075 let contexts = decode_concurrency.saturating_add(encoder_slots).max(1);
1076 (total_threads / contexts).max(1)
1077 };
1078 let decoder_threads = if request.hardware_decode || !request.software_video_encoder {
1079 even_share
1082 } else {
1083 software_decoder_threads(request.source_frame_pixels)
1084 .clamp(1, (decode_budget / decode_concurrency).max(1))
1085 };
1086 let decode_assigned = decoder_threads.saturating_mul(decode_concurrency);
1087 let encoder_threads = if request.software_video_encoder {
1090 total_threads.saturating_sub(decode_assigned).max(1)
1091 } else {
1092 0
1093 };
1094 CodecThreadAllocation {
1095 total_threads,
1096 decode_concurrency,
1097 decoder_threads: i64::try_from(decoder_threads).unwrap_or(1).clamp(1, 64),
1098 encoder_threads: i64::try_from(encoder_threads).unwrap_or(0).clamp(0, 64),
1099 allocated_threads: decode_assigned.saturating_add(encoder_threads),
1100 }
1101}
1102
1103fn software_decoder_threads(source_frame_pixels: u64) -> usize {
1108 usize::try_from(source_frame_pixels.div_ceil(DECODE_PIXELS_PER_THREAD))
1109 .unwrap_or(MAX_SOFTWARE_DECODER_THREADS)
1110 .clamp(MIN_SOFTWARE_DECODER_THREADS, MAX_SOFTWARE_DECODER_THREADS)
1111}
1112
1113pub(crate) fn unused_option_names(options: Option<AVDictionary>) -> Option<String> {
1115 let options = options?;
1116 let names = options
1117 .iter()
1118 .map(|entry| entry.key().to_string_lossy().into_owned())
1119 .collect::<Vec<_>>();
1120 (!names.is_empty()).then(|| names.join(", "))
1121}
1122
1123pub(crate) fn reject_unused_decode_options(
1124 options: Option<AVDictionary>,
1125) -> Result<(), DecodeError> {
1126 if let Some(names) = unused_option_names(options) {
1127 Err(DecodeError::Adapter(format!(
1128 "FFmpeg decoder did not consume options: {names}"
1129 )))
1130 } else {
1131 Ok(())
1132 }
1133}
1134
1135pub(crate) fn reject_unused_encode_options(
1136 options: Option<AVDictionary>,
1137) -> Result<(), EncodeError> {
1138 if let Some(names) = unused_option_names(options) {
1139 Err(EncodeError::Adapter(format!(
1140 "FFmpeg encoder did not consume options: {names}"
1141 )))
1142 } else {
1143 Ok(())
1144 }
1145}
1146
1147pub(crate) fn color_tags(
1154 color: ColorMetadata,
1155) -> (
1156 ffi::AVColorPrimaries,
1157 ffi::AVColorTransferCharacteristic,
1158 ffi::AVColorSpace,
1159 ffi::AVColorRange,
1160) {
1161 (
1162 match color.primaries {
1163 ColorPrimaries::Bt601 => ffi::AVCOL_PRI_SMPTE170M,
1164 ColorPrimaries::Bt709 => ffi::AVCOL_PRI_BT709,
1165 ColorPrimaries::Bt2020 => ffi::AVCOL_PRI_BT2020,
1166 },
1167 match color.transfer {
1168 TransferFunction::Bt709 => ffi::AVCOL_TRC_BT709,
1169 TransferFunction::Srgb => ffi::AVCOL_TRC_IEC61966_2_1,
1170 TransferFunction::Pq => ffi::AVCOL_TRC_SMPTE2084,
1171 TransferFunction::Hlg => ffi::AVCOL_TRC_ARIB_STD_B67,
1172 },
1173 match color.matrix {
1174 ColorMatrix::Bt601 => ffi::AVCOL_SPC_SMPTE170M,
1175 ColorMatrix::Bt709 => ffi::AVCOL_SPC_BT709,
1176 ColorMatrix::Bt2020Ncl => ffi::AVCOL_SPC_BT2020_NCL,
1177 ColorMatrix::Rgb => ffi::AVCOL_SPC_RGB,
1178 },
1179 match color.range {
1180 ColorRange::Limited => ffi::AVCOL_RANGE_MPEG,
1181 ColorRange::Full => ffi::AVCOL_RANGE_JPEG,
1182 },
1183 )
1184}
1185
1186pub(crate) fn color_options(color: ColorMetadata) -> AVDictionary {
1191 let (primaries, transfer, matrix, range) = color_tags(color);
1192 AVDictionary::new_int(c"color_primaries", i64::from(primaries), 0)
1193 .set_int(c"color_trc", i64::from(transfer), 0)
1194 .set_int(c"colorspace", i64::from(matrix), 0)
1195 .set_int(c"color_range", i64::from(range), 0)
1196}
1197
1198fn gpu_fault(error: &dyn std::any::Any) -> Option<alpha2_media::GpuFault> {
1201 error
1202 .downcast_ref::<alpha2_ffmpeg_bridge::BridgeError>()
1203 .and_then(alpha2_ffmpeg_bridge::BridgeError::gpu_fault)
1204}
1205
1206pub(crate) fn adapter_error(error: impl std::fmt::Display + 'static) -> DecodeError {
1209 match gpu_fault(&error) {
1210 Some(fault) => DecodeError::Gpu {
1211 fault,
1212 message: error.to_string(),
1213 },
1214 None => DecodeError::Adapter(error.to_string()),
1215 }
1216}
1217
1218pub(crate) fn encode_adapter_error(error: impl std::fmt::Display + 'static) -> EncodeError {
1221 match gpu_fault(&error) {
1222 Some(fault) => EncodeError::Gpu {
1223 fault,
1224 message: error.to_string(),
1225 },
1226 None => EncodeError::Adapter(error.to_string()),
1227 }
1228}
1229
1230pub(crate) fn encode_adapter_error_at(
1231 context: &str,
1232 error: impl std::fmt::Display + 'static,
1233) -> EncodeError {
1234 match gpu_fault(&error) {
1235 Some(fault) => EncodeError::Gpu {
1236 fault,
1237 message: format!("{context}: {error}"),
1238 },
1239 None => EncodeError::Adapter(format!("{context}: {error}")),
1240 }
1241}