1use crate::capabilities::{NativeEncoderClass, encoder_class};
4use alpha2_ffmpeg_bridge::{DecodedFrame, DisplayRotation};
5use alpha2_media::{
6 ChromaLocation, ColorMatrix, ColorMetadata, ColorPrimaries, ColorRange, DecodeError,
7 DecodedVideoFrame, DynamicRange, EncodeError, Orientation, PixelFormat, Plane, Rect, Rotation,
8 SampleAspectRatio, TransferFunction, YuvFormat,
9};
10use alpha2_time::{MediaTime, TICKS_PER_SECOND};
11use rsmpeg::avcodec::AVCodec;
12use rsmpeg::avutil::{AVDictionary, AVFrame, AVRational, av_rescale_q, ra};
13use rsmpeg::ffi;
14use rsmpeg::swscale::SwsContext;
15
16#[allow(clippy::too_many_lines)]
17pub(crate) fn convert_video_frame(
18 frame: &DecodedFrame,
19 codec: ffi::AVCodecID,
20 time_base: AVRational,
21 color: ColorMetadata,
22 scaler: &mut FrameScaler,
23) -> Result<DecodedVideoFrame, DecodeError> {
24 let width = u32::try_from(frame.width).map_err(|_| DecodeError::OutOfRange)?;
25 let height = u32::try_from(frame.height).map_err(|_| DecodeError::OutOfRange)?;
26 let pts = if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
27 frame.pts
28 } else {
29 frame.best_effort_timestamp
30 };
31 let visible_rect = Rect {
32 x: u32::try_from(frame.crop_left).unwrap_or(0),
33 y: u32::try_from(frame.crop_top).unwrap_or(0),
34 width: width.saturating_sub(
35 u32::try_from(frame.crop_left.saturating_add(frame.crop_right)).unwrap_or(0),
36 ),
37 height: height.saturating_sub(
38 u32::try_from(frame.crop_top.saturating_add(frame.crop_bottom)).unwrap_or(0),
39 ),
40 };
41 if let Some(planes) = frame.yuv_planes().map_err(adapter_error)? {
44 let layout = planes.layout;
45 return Ok(DecodedVideoFrame {
46 pts: timestamp(pts, time_base).ok_or(DecodeError::OutOfRange)?,
47 dts: None,
48 duration: timestamp(frame.duration, time_base),
49 visible_rect,
50 sample_aspect_ratio: declared_sar(frame.sample_aspect_ratio).unwrap_or_default(),
51 orientation: Orientation::UPRIGHT,
52 pixel_format: PixelFormat::Yuv(YuvFormat {
53 chroma_shift_x: layout.chroma_shift_x,
54 chroma_shift_y: layout.chroma_shift_y,
55 bit_depth: layout.bit_depth,
56 interleaved_chroma: layout.interleaved_chroma,
57 msb_aligned: layout.msb_aligned,
58 alpha: false,
59 }),
60 chroma_location: decoded_chroma_location(codec, frame.chroma_location),
61 color: Some(color),
62 planes: planes
63 .planes
64 .into_iter()
65 .map(|plane| Plane {
66 bytes: plane.bytes,
67 stride: plane.stride,
68 width: plane.width,
69 height: plane.height,
70 })
71 .collect(),
72 });
73 }
74 if !alpha2_media::frame_size_supported(width, height) {
76 return Err(DecodeError::Unsupported(format!(
77 "a {width}x{height} frame is larger than {} pixels or {} on a side",
78 alpha2_media::MAX_FRAME_PIXELS,
79 alpha2_media::MAX_FRAME_DIMENSION
80 )));
81 }
82 let (pixel_format, planes, color) = if converts_like_rgba8(color) {
83 (
84 PixelFormat::Rgba8,
85 vec![rgba8_plane(frame, color, scaler)?],
86 color,
87 )
88 } else {
89 full_resolution_planes(frame, color, scaler)?
90 };
91 Ok(DecodedVideoFrame {
92 pts: timestamp(pts, time_base).ok_or(DecodeError::OutOfRange)?,
93 dts: None,
94 duration: timestamp(frame.duration, time_base),
95 visible_rect,
96 sample_aspect_ratio: declared_sar(frame.sample_aspect_ratio).unwrap_or_default(),
97 orientation: Orientation::UPRIGHT,
98 pixel_format,
99 chroma_location: ChromaLocation::Unspecified,
101 color: Some(color),
102 planes,
103 })
104}
105
106fn converts_like_rgba8(color: ColorMetadata) -> bool {
112 !color.is_hdr() && color.primaries == ColorPrimaries::Bt709
113}
114
115fn rgba8_plane(
118 frame: &AVFrame,
119 color: ColorMetadata,
120 scaler: &mut FrameScaler,
121) -> Result<Plane, DecodeError> {
122 let width = u32::try_from(frame.width).map_err(|_| DecodeError::OutOfRange)?;
123 let height = u32::try_from(frame.height).map_err(|_| DecodeError::OutOfRange)?;
124 let active = scaler.for_frame(frame, ffi::AV_PIX_FMT_RGBA)?;
125 configure_yuv_to_rgba(active, color).map_err(adapter_error)?;
126 let mut bytes = vec![0; packed_len(width, height, 4)?];
127 alpha2_ffmpeg_bridge::scale_frame_packed(active, frame, ffi::AV_PIX_FMT_RGBA, &mut bytes)
128 .map_err(adapter_error)?;
129 Ok(Plane {
130 bytes: bytes.into(),
131 stride: width.saturating_mul(4),
132 width,
133 height,
134 })
135}
136
137fn full_resolution_planes(
144 frame: &AVFrame,
145 color: ColorMetadata,
146 scaler: &mut FrameScaler,
147) -> Result<(PixelFormat, Vec<Plane>, ColorMetadata), DecodeError> {
148 let width = u32::try_from(frame.width).map_err(|_| DecodeError::OutOfRange)?;
149 let height = u32::try_from(frame.height).map_err(|_| DecodeError::OutOfRange)?;
150 let profile = alpha2_ffmpeg_bridge::sample_profile(frame.format)
151 .ok_or_else(|| DecodeError::Unsupported("unsupported FFmpeg pixel format".into()))?;
152 let depth = match profile.bit_depth {
153 0..=8 => 8,
154 9..=10 => 10,
155 11..=12 => 12,
156 _ => 16,
157 };
158 let target = planar_444_format(profile.is_rgb, profile.has_alpha, depth);
159 let active = scaler.for_frame(frame, target)?;
160 if !profile.is_rgb {
161 let coefficients = sws_coefficients(color.matrix);
164 let full = color.range == ColorRange::Full;
165 alpha2_ffmpeg_bridge::configure_colorspace(active, coefficients, full, coefficients, full)
166 .map_err(adapter_error)?;
167 }
168 let plane_count = if profile.has_alpha { 4 } else { 3 };
169 let sample_bytes = if depth > 8 { 2 } else { 1 };
170 let plane_len = packed_len(width, height, sample_bytes)?;
171 let mut bytes = vec![0; plane_len * plane_count];
172 alpha2_ffmpeg_bridge::scale_frame_packed(active, frame, target, &mut bytes)
173 .map_err(adapter_error)?;
174 let bytes = bytes::Bytes::from(bytes);
175 let stride = width.saturating_mul(u32::try_from(sample_bytes).unwrap_or(2));
176 let plane = |index: usize| Plane {
177 bytes: bytes.slice(index * plane_len..(index + 1) * plane_len),
178 stride,
179 width,
180 height,
181 };
182 let order: &[usize] = if profile.is_rgb {
184 &[2, 0, 1]
185 } else {
186 &[0, 1, 2]
187 };
188 let mut planes = order.iter().map(|&index| plane(index)).collect::<Vec<_>>();
189 if profile.has_alpha {
190 planes.push(plane(3));
191 }
192 let format = YuvFormat::planar(0, 0, depth);
193 let format = if profile.has_alpha {
194 format.with_alpha()
195 } else {
196 format
197 };
198 let color = if profile.is_rgb {
199 ColorMetadata {
200 matrix: ColorMatrix::Rgb,
201 range: ColorRange::Full,
202 ..color
203 }
204 } else {
205 color
206 };
207 Ok((PixelFormat::Yuv(format), planes, color))
208}
209
210const fn planar_444_format(rgb: bool, alpha: bool, depth: u8) -> ffi::AVPixelFormat {
213 match (rgb, alpha, depth) {
214 (true, false, 8) => ffi::AV_PIX_FMT_GBRP,
215 (true, true, 8) => ffi::AV_PIX_FMT_GBRAP,
216 (true, false, 10) => ffi::AV_PIX_FMT_GBRP10LE,
217 (true, true, 10) => ffi::AV_PIX_FMT_GBRAP10LE,
218 (true, false, 12) => ffi::AV_PIX_FMT_GBRP12LE,
219 (true, true, 12) => ffi::AV_PIX_FMT_GBRAP12LE,
220 (true, false, _) => ffi::AV_PIX_FMT_GBRP16LE,
221 (true, true, _) => ffi::AV_PIX_FMT_GBRAP16LE,
222 (false, false, 8) => ffi::AV_PIX_FMT_YUV444P,
223 (false, true, 8) => ffi::AV_PIX_FMT_YUVA444P,
224 (false, false, 10) => ffi::AV_PIX_FMT_YUV444P10LE,
225 (false, true, 10) => ffi::AV_PIX_FMT_YUVA444P10LE,
226 (false, false, 12) => ffi::AV_PIX_FMT_YUV444P12LE,
227 (false, true, 12) => ffi::AV_PIX_FMT_YUVA444P12LE,
228 (false, false, _) => ffi::AV_PIX_FMT_YUV444P16LE,
229 (false, true, _) => ffi::AV_PIX_FMT_YUVA444P16LE,
230 }
231}
232
233#[derive(Default)]
243pub(crate) struct FrameScaler {
244 context: Option<(ScalerShape, SwsContext)>,
245 made: u64,
246}
247
248#[derive(Clone, Copy, Debug, Eq, PartialEq)]
250struct ScalerShape {
251 width: i32,
252 height: i32,
253 source: ffi::AVPixelFormat,
254 target: ffi::AVPixelFormat,
255}
256
257impl FrameScaler {
258 fn for_frame(
261 &mut self,
262 frame: &AVFrame,
263 target: ffi::AVPixelFormat,
264 ) -> Result<&mut SwsContext, DecodeError> {
265 let shape = ScalerShape {
266 width: frame.width,
267 height: frame.height,
268 source: frame.format,
269 target,
270 };
271 if self
272 .context
273 .as_ref()
274 .is_none_or(|(made_for, _)| *made_for != shape)
275 {
276 self.context = None;
277 let context = SwsContext::get_context(
278 frame.width,
279 frame.height,
280 frame.format,
281 frame.width,
282 frame.height,
283 target,
284 ffi::SWS_BILINEAR,
285 None,
286 None,
287 None,
288 )
289 .ok_or_else(|| DecodeError::Unsupported("unsupported FFmpeg pixel format".into()))?;
290 self.made += 1;
291 self.context = Some((shape, context));
292 }
293 self.context
294 .as_mut()
295 .map(|(_, context)| context)
296 .ok_or_else(|| DecodeError::Unsupported("unsupported FFmpeg pixel format".into()))
297 }
298
299 #[cfg(test)]
301 pub(crate) const fn made(&self) -> u64 {
302 self.made
303 }
304
305 #[cfg(test)]
307 pub(crate) const fn is_empty(&self) -> bool {
308 self.context.is_none()
309 }
310}
311
312fn packed_len(width: u32, height: u32, sample_bytes: usize) -> Result<usize, DecodeError> {
314 usize::try_from(width)
315 .ok()
316 .and_then(|width| {
317 usize::try_from(height)
318 .ok()
319 .and_then(|height| width.checked_mul(height))
320 })
321 .and_then(|pixels| pixels.checked_mul(sample_bytes))
322 .ok_or(DecodeError::OutOfRange)
323}
324
325pub(crate) fn decoded_chroma_location(
334 codec: ffi::AVCodecID,
335 location: ffi::AVChromaLocation,
336) -> ChromaLocation {
337 if codec == ffi::AV_CODEC_ID_H264 && location == ffi::AVCHROMA_LOC_UNSPECIFIED {
338 ChromaLocation::Left
339 } else {
340 chroma_location(location)
341 }
342}
343
344pub(crate) fn chroma_location(location: ffi::AVChromaLocation) -> ChromaLocation {
345 match location {
346 ffi::AVCHROMA_LOC_LEFT => ChromaLocation::Left,
347 ffi::AVCHROMA_LOC_CENTER => ChromaLocation::Center,
348 ffi::AVCHROMA_LOC_TOPLEFT => ChromaLocation::TopLeft,
349 _ => ChromaLocation::Unspecified,
350 }
351}
352
353pub(crate) fn configure_rgba_to_yuv(
355 scaler: &mut SwsContext,
356 color: ColorMetadata,
357) -> Result<(), alpha2_ffmpeg_bridge::BridgeError> {
358 alpha2_ffmpeg_bridge::configure_colorspace(
359 scaler,
360 i32::try_from(ffi::SWS_CS_ITU709).expect("color coefficient fits i32"),
361 true,
362 sws_coefficients(color.matrix),
363 color.range == ColorRange::Full,
364 )
365}
366
367pub(crate) fn configure_yuv_to_rgba(
369 scaler: &mut SwsContext,
370 color: ColorMetadata,
371) -> Result<(), alpha2_ffmpeg_bridge::BridgeError> {
372 alpha2_ffmpeg_bridge::configure_colorspace(
373 scaler,
374 sws_coefficients(color.matrix),
375 color.range == ColorRange::Full,
376 i32::try_from(ffi::SWS_CS_ITU709).expect("color coefficient fits i32"),
377 true,
378 )
379}
380
381pub(crate) fn sws_coefficients(matrix: ColorMatrix) -> i32 {
382 let value = match matrix {
383 ColorMatrix::Bt601 => ffi::SWS_CS_ITU601,
384 ColorMatrix::Bt709 | ColorMatrix::Rgb => ffi::SWS_CS_ITU709,
385 ColorMatrix::Bt2020Ncl => ffi::SWS_CS_BT2020,
386 };
387 i32::try_from(value).expect("color coefficient fits i32")
388}
389
390pub(crate) fn tick_base() -> AVRational {
392 ra(
393 1,
394 i32::try_from(TICKS_PER_SECOND).expect("tick rate fits i32"),
395 )
396}
397
398pub(crate) fn direct_yuv420p_frame(
404 frame: &DecodedVideoFrame,
405 width: i32,
406 height: i32,
407) -> Result<Option<AVFrame>, EncodeError> {
408 if frame.pixel_format != PixelFormat::Yuv(YuvFormat::I420) {
409 return Ok(None);
410 }
411 if frame.planes.len() != 3
412 || frame.visible_rect.x != 0
413 || frame.visible_rect.y != 0
414 || frame.planes[0].width != u32::try_from(width).unwrap_or(0)
415 || frame.planes[0].height != u32::try_from(height).unwrap_or(0)
416 {
417 return Err(EncodeError::Adapter(
418 "direct YUV encoder input must contain one uncropped coded frame".into(),
419 ));
420 }
421 alpha2_ffmpeg_bridge::yuv420p_frame(
422 &frame.planes[0].bytes,
423 frame.planes[0].stride,
424 &frame.planes[1].bytes,
425 frame.planes[1].stride,
426 &frame.planes[2].bytes,
427 frame.planes[2].stride,
428 width,
429 height,
430 )
431 .map(Some)
432 .map_err(encode_adapter_error)
433}
434
435pub(crate) struct Yuv420pConverter {
443 output: ColorMetadata,
444 scaler: Option<(ffi::AVPixelFormat, u32, u32, SwsContext)>,
445}
446
447impl Yuv420pConverter {
448 pub(crate) const fn new(output: ColorMetadata) -> Self {
450 Self {
451 output,
452 scaler: None,
453 }
454 }
455
456 pub(crate) fn convert(
462 &mut self,
463 frame: &DecodedVideoFrame,
464 width: i32,
465 height: i32,
466 ) -> Result<Option<AVFrame>, EncodeError> {
467 let PixelFormat::Yuv(format) = frame.pixel_format else {
468 return Ok(None);
469 };
470 let rgb = frame
471 .color
472 .is_some_and(|color| color.matrix == ColorMatrix::Rgb);
473 if format == YuvFormat::I420 && !rgb {
474 return direct_yuv420p_frame(frame, width, height);
475 }
476 let (width, height) = (
477 u32::try_from(width).unwrap_or(0),
478 u32::try_from(height).unwrap_or(0),
479 );
480 let stored = if format.interleaved_chroma { 2 } else { 3 };
481 if frame.visible_rect.x != 0
482 || frame.visible_rect.y != 0
483 || frame.planes.len() < stored
484 || frame.planes[0].width != width
485 || frame.planes[0].height != height
486 {
487 return Err(EncodeError::Adapter(
488 "YUV encoder input must contain one uncropped frame of the encoder's size".into(),
489 ));
490 }
491 let (source_format, planes) = if rgb {
492 if format.chroma_shift_x != 0
493 || format.chroma_shift_y != 0
494 || format.interleaved_chroma
495 || format.msb_aligned
496 || !matches!(format.bit_depth, 8 | 10 | 12 | 16)
497 {
498 return Err(EncodeError::Adapter(format!(
499 "no FFmpeg pixel format stores red, green, and blue as {format:?}"
500 )));
501 }
502 let planes = [1, 2, 0]
504 .iter()
505 .map(|&index: &usize| (&frame.planes[index].bytes[..], frame.planes[index].stride))
506 .collect::<Vec<_>>();
507 (planar_444_format(true, false, format.bit_depth), planes)
508 } else {
509 let layout = alpha2_ffmpeg_bridge::YuvPlaneLayout {
510 chroma_shift_x: format.chroma_shift_x,
511 chroma_shift_y: format.chroma_shift_y,
512 bit_depth: format.bit_depth,
513 interleaved_chroma: format.interleaved_chroma,
514 msb_aligned: format.msb_aligned,
515 };
516 let source_format =
517 alpha2_ffmpeg_bridge::yuv_pixel_format(layout).ok_or_else(|| {
518 EncodeError::Adapter(format!("no FFmpeg pixel format stores {format:?}"))
519 })?;
520 let planes = frame.planes[..stored]
521 .iter()
522 .map(|plane| (&plane.bytes[..], plane.stride))
523 .collect::<Vec<_>>();
524 (source_format, planes)
525 };
526 let source = alpha2_ffmpeg_bridge::planar_frame(source_format, &planes, width, height)
527 .map_err(encode_adapter_error)?;
528 if self
529 .scaler
530 .as_ref()
531 .is_none_or(|(cached, w, h, _)| (*cached, *w, *h) != (source_format, width, height))
532 {
533 let (signed_width, signed_height) = (
534 i32::try_from(width).map_err(|_| EncodeError::Adapter("width overflow".into()))?,
535 i32::try_from(height)
536 .map_err(|_| EncodeError::Adapter("height overflow".into()))?,
537 );
538 let mut scaler = SwsContext::get_context(
539 signed_width,
540 signed_height,
541 source_format,
542 signed_width,
543 signed_height,
544 ffi::AV_PIX_FMT_YUV420P,
545 ffi::SWS_BILINEAR,
546 None,
547 None,
548 None,
549 )
550 .ok_or_else(|| EncodeError::Adapter("cannot create a YUV to YUV420P scaler".into()))?;
551 if rgb {
552 configure_rgba_to_yuv(&mut scaler, self.output).map_err(encode_adapter_error)?;
553 }
554 self.scaler = Some((source_format, width, height, scaler));
555 }
556 let Some((_, _, _, scaler)) = self.scaler.as_mut() else {
557 unreachable!("the scaler was just made");
558 };
559 let mut output = AVFrame::new();
560 output.set_format(ffi::AV_PIX_FMT_YUV420P);
561 output.set_width(source.width);
562 output.set_height(source.height);
563 output.get_buffer(1).map_err(encode_adapter_error)?;
564 scaler
565 .scale_frame(&source, 0, source.height, &mut output)
566 .map_err(encode_adapter_error)?;
567 Ok(Some(output))
568 }
569}
570
571pub(crate) fn timestamp(value: i64, base: AVRational) -> Option<MediaTime> {
573 (value != ffi::AV_NOPTS_VALUE && base.den > 0)
574 .then(|| MediaTime::from_ticks(av_rescale_q(value, base, tick_base())))
575}
576
577pub(crate) fn declared_sar(value: AVRational) -> Option<SampleAspectRatio> {
581 SampleAspectRatio::new(
582 u64::try_from(value.num).ok()?,
583 u64::try_from(value.den).ok()?,
584 )
585}
586
587pub(crate) fn guess_sar(container: AVRational, pictures: AVRational) -> SampleAspectRatio {
593 declared_sar(container)
594 .or_else(|| declared_sar(pictures))
595 .unwrap_or_default()
596}
597
598pub(crate) fn stream_rotation(metadata: Option<rsmpeg::avutil::AVDictionaryRef<'_>>) -> Rotation {
600 let Some(metadata) = metadata else {
601 return Rotation::Deg0;
602 };
603 let Some(entry) = metadata.get(c"rotate", None, 0) else {
604 return Rotation::Deg0;
605 };
606 match entry
607 .value()
608 .to_string_lossy()
609 .parse::<i32>()
610 .unwrap_or(0)
611 .rem_euclid(360)
612 {
613 90 => Rotation::Deg90,
614 180 => Rotation::Deg180,
615 270 => Rotation::Deg270,
616 _ => Rotation::Deg0,
617 }
618}
619
620pub(crate) fn rotation_from_display(counter_clockwise: f64) -> Option<Rotation> {
627 let normalized = (-counter_clockwise).rem_euclid(360.0);
628 if !(0.5..359.5).contains(&normalized) {
629 Some(Rotation::Deg0)
630 } else if (normalized - 90.0).abs() < 0.5 {
631 Some(Rotation::Deg90)
632 } else if (normalized - 180.0).abs() < 0.5 {
633 Some(Rotation::Deg180)
634 } else if (normalized - 270.0).abs() < 0.5 {
635 Some(Rotation::Deg270)
636 } else {
637 None
638 }
639}
640
641pub(crate) fn orientation_from_display(display: DisplayRotation) -> Option<Orientation> {
650 if display.flipped {
651 rotation_from_display(-display.counter_clockwise_degrees).map(|rotation| Orientation {
652 rotation,
653 mirror: true,
654 })
655 } else {
656 rotation_from_display(display.counter_clockwise_degrees).map(Orientation::rotated)
657 }
658}
659
660pub(crate) fn display_of(orientation: Orientation) -> Option<DisplayRotation> {
664 if orientation == Orientation::default() {
665 return None;
666 }
667 let clockwise = match orientation.rotation {
668 Rotation::Deg0 => 0.0,
669 Rotation::Deg90 => 90.0,
670 Rotation::Deg180 => 180.0,
671 Rotation::Deg270 => 270.0,
672 };
673 Some(DisplayRotation {
674 counter_clockwise_degrees: if orientation.mirror {
675 clockwise
676 } else {
677 -clockwise
678 },
679 flipped: orientation.mirror,
680 })
681}
682
683const HVC1: u32 = u32::from_le_bytes(*b"hvc1");
685const HEV1: u32 = u32::from_le_bytes(*b"hev1");
687
688pub(crate) fn mp4_codec_tag(codec_id: ffi::AVCodecID, tag: u32, copied: bool) -> u32 {
696 if codec_id == ffi::AV_CODEC_ID_HEVC {
697 if tag == HVC1 || tag == HEV1 {
698 tag
699 } else {
700 HVC1
701 }
702 } else if copied {
703 0
704 } else {
705 tag
706 }
707}
708
709pub(crate) fn color_metadata(
710 parameters: &rsmpeg::avcodec::AVCodecParameters,
711) -> Option<ColorMetadata> {
712 map_color(
713 parameters.color_primaries,
714 parameters.color_trc,
715 parameters.color_space,
716 parameters.color_range,
717 )
718}
719
720pub(crate) fn frame_color_metadata(frame: &AVFrame) -> Option<ColorMetadata> {
721 map_color(
722 frame.color_primaries,
723 frame.color_trc,
724 frame.colorspace,
725 frame.color_range,
726 )
727}
728
729pub(crate) fn map_color(
735 primaries: ffi::AVColorPrimaries,
736 transfer: ffi::AVColorTransferCharacteristic,
737 matrix: ffi::AVColorSpace,
738 range: ffi::AVColorRange,
739) -> Option<ColorMetadata> {
740 if primaries == ffi::AVCOL_PRI_UNSPECIFIED
741 && transfer == ffi::AVCOL_TRC_UNSPECIFIED
742 && matrix == ffi::AVCOL_SPC_UNSPECIFIED
743 && range == ffi::AVCOL_RANGE_UNSPECIFIED
744 {
745 return None;
746 }
747 let primaries = match primaries {
748 ffi::AVCOL_PRI_BT2020 => ColorPrimaries::Bt2020,
749 ffi::AVCOL_PRI_SMPTE170M | ffi::AVCOL_PRI_BT470BG => ColorPrimaries::Bt601,
750 _ => ColorPrimaries::Bt709,
751 };
752 let transfer = match transfer {
753 ffi::AVCOL_TRC_SMPTE2084 => TransferFunction::Pq,
754 ffi::AVCOL_TRC_ARIB_STD_B67 => TransferFunction::Hlg,
755 ffi::AVCOL_TRC_IEC61966_2_1 => TransferFunction::Srgb,
756 _ => TransferFunction::Bt709,
757 };
758 let matrix = match matrix {
759 ffi::AVCOL_SPC_BT2020_NCL => ColorMatrix::Bt2020Ncl,
760 ffi::AVCOL_SPC_SMPTE170M | ffi::AVCOL_SPC_BT470BG => ColorMatrix::Bt601,
761 ffi::AVCOL_SPC_RGB => ColorMatrix::Rgb,
762 _ => ColorMatrix::Bt709,
763 };
764 Some(ColorMetadata {
765 primaries,
766 transfer,
767 matrix,
768 range: if range == ffi::AVCOL_RANGE_JPEG {
769 ColorRange::Full
770 } else {
771 ColorRange::Limited
772 },
773 dynamic_range: if matches!(transfer, TransferFunction::Pq | TransferFunction::Hlg) {
774 DynamicRange::Hdr
775 } else {
776 DynamicRange::Sdr
777 },
778 })
779}
780
781pub(crate) fn video_decoder_options(threads: i64) -> AVDictionary {
790 AVDictionary::new_int(c"threads", threads, 0).set(c"flags", c"+unaligned", 0)
791}
792
793pub(crate) fn video_encoder_options(
795 color: ColorMetadata,
796 codec: &AVCodec,
797 on_hardware: bool,
798 threads: i64,
799) -> AVDictionary {
800 let mut options = color_options(color);
801 if encoder_class(codec) == NativeEncoderClass::Hybrid {
805 options = options.set_int(c"hw_encoding", i64::from(on_hardware), 0);
806 }
807 if on_hardware {
808 options
809 } else {
810 options.set_int(c"threads", threads, 0)
811 }
812}
813
814#[must_use]
821pub fn codec_thread_budget() -> i64 {
822 i64::try_from(configured_codec_thread_budget().min(16)).unwrap_or(1)
823}
824
825pub fn configured_codec_thread_override() -> Result<Option<usize>, String> {
833 codec_thread_override(std::env::var_os("ALPHA2_FFMPEG_THREADS").as_deref())
834}
835
836pub fn codec_thread_override(setting: Option<&std::ffi::OsStr>) -> Result<Option<usize>, String> {
843 let Some(setting) = setting else {
844 return Ok(None);
845 };
846 setting
847 .to_str()
848 .and_then(|value| value.parse::<usize>().ok())
849 .filter(|value| *value > 0)
850 .map(|value| Some(value.min(64)))
851 .ok_or_else(|| setting.to_string_lossy().into_owned())
852}
853
854fn configured_codec_thread_budget() -> usize {
855 configured_codec_thread_override()
856 .ok()
857 .flatten()
858 .unwrap_or_else(|| {
859 std::thread::available_parallelism()
860 .map_or(1, std::num::NonZeroUsize::get)
861 .saturating_sub(2)
862 .max(1)
863 })
864 .clamp(1, 64)
865}
866
867#[derive(Clone, Copy, Debug, PartialEq, Eq)]
869pub struct CodecThreadRequest {
870 pub decode_concurrency: usize,
872 pub hardware_decode: bool,
874 pub source_frame_pixels: u64,
876 pub software_video_encoder: bool,
878}
879
880const DECODE_PIXELS_PER_THREAD: u64 = 2_000_000;
882const MIN_SOFTWARE_DECODER_THREADS: usize = 2;
884const MAX_SOFTWARE_DECODER_THREADS: usize = 6;
888const ENCODER_THREAD_FLOOR: usize = 4;
890
891#[derive(Clone, Copy, Debug, PartialEq, Eq)]
894pub struct CodecThreadAllocation {
895 pub total_threads: usize,
897 pub decode_concurrency: usize,
899 pub decoder_threads: i64,
901 pub encoder_threads: i64,
903 pub allocated_threads: usize,
905}
906
907#[must_use]
929pub fn codec_thread_allocation(request: CodecThreadRequest) -> CodecThreadAllocation {
930 let configured = configured_codec_thread_budget();
931 let encoder_slots = usize::from(request.software_video_encoder);
932 let decode_concurrency = request
933 .decode_concurrency
934 .max(1)
935 .min(configured.saturating_sub(encoder_slots).max(1));
936 let total_threads = configured.max(decode_concurrency.saturating_add(encoder_slots));
937 let encoder_floor = if request.software_video_encoder {
940 ENCODER_THREAD_FLOOR.min(total_threads.saturating_sub(decode_concurrency).max(1))
941 } else {
942 0
943 };
944 let decode_budget = total_threads
945 .saturating_sub(encoder_floor)
946 .max(decode_concurrency);
947 let even_share = {
948 let contexts = decode_concurrency.saturating_add(encoder_slots).max(1);
949 (total_threads / contexts).max(1)
950 };
951 let decoder_threads = if request.hardware_decode || !request.software_video_encoder {
952 even_share
955 } else {
956 software_decoder_threads(request.source_frame_pixels)
957 .clamp(1, (decode_budget / decode_concurrency).max(1))
958 };
959 let decode_assigned = decoder_threads.saturating_mul(decode_concurrency);
960 let encoder_threads = if request.software_video_encoder {
963 total_threads.saturating_sub(decode_assigned).max(1)
964 } else {
965 0
966 };
967 CodecThreadAllocation {
968 total_threads,
969 decode_concurrency,
970 decoder_threads: i64::try_from(decoder_threads).unwrap_or(1).clamp(1, 64),
971 encoder_threads: i64::try_from(encoder_threads).unwrap_or(0).clamp(0, 64),
972 allocated_threads: decode_assigned.saturating_add(encoder_threads),
973 }
974}
975
976fn software_decoder_threads(source_frame_pixels: u64) -> usize {
981 usize::try_from(source_frame_pixels.div_ceil(DECODE_PIXELS_PER_THREAD))
982 .unwrap_or(MAX_SOFTWARE_DECODER_THREADS)
983 .clamp(MIN_SOFTWARE_DECODER_THREADS, MAX_SOFTWARE_DECODER_THREADS)
984}
985
986pub(crate) fn unused_option_names(options: Option<AVDictionary>) -> Option<String> {
988 let options = options?;
989 let names = options
990 .iter()
991 .map(|entry| entry.key().to_string_lossy().into_owned())
992 .collect::<Vec<_>>();
993 (!names.is_empty()).then(|| names.join(", "))
994}
995
996pub(crate) fn reject_unused_decode_options(
997 options: Option<AVDictionary>,
998) -> Result<(), DecodeError> {
999 if let Some(names) = unused_option_names(options) {
1000 Err(DecodeError::Adapter(format!(
1001 "FFmpeg decoder did not consume options: {names}"
1002 )))
1003 } else {
1004 Ok(())
1005 }
1006}
1007
1008pub(crate) fn reject_unused_encode_options(
1009 options: Option<AVDictionary>,
1010) -> Result<(), EncodeError> {
1011 if let Some(names) = unused_option_names(options) {
1012 Err(EncodeError::Adapter(format!(
1013 "FFmpeg encoder did not consume options: {names}"
1014 )))
1015 } else {
1016 Ok(())
1017 }
1018}
1019
1020pub(crate) fn color_tags(
1027 color: ColorMetadata,
1028) -> (
1029 ffi::AVColorPrimaries,
1030 ffi::AVColorTransferCharacteristic,
1031 ffi::AVColorSpace,
1032 ffi::AVColorRange,
1033) {
1034 (
1035 match color.primaries {
1036 ColorPrimaries::Bt601 => ffi::AVCOL_PRI_SMPTE170M,
1037 ColorPrimaries::Bt709 => ffi::AVCOL_PRI_BT709,
1038 ColorPrimaries::Bt2020 => ffi::AVCOL_PRI_BT2020,
1039 },
1040 match color.transfer {
1041 TransferFunction::Bt709 => ffi::AVCOL_TRC_BT709,
1042 TransferFunction::Srgb => ffi::AVCOL_TRC_IEC61966_2_1,
1043 TransferFunction::Pq => ffi::AVCOL_TRC_SMPTE2084,
1044 TransferFunction::Hlg => ffi::AVCOL_TRC_ARIB_STD_B67,
1045 },
1046 match color.matrix {
1047 ColorMatrix::Bt601 => ffi::AVCOL_SPC_SMPTE170M,
1048 ColorMatrix::Bt709 => ffi::AVCOL_SPC_BT709,
1049 ColorMatrix::Bt2020Ncl => ffi::AVCOL_SPC_BT2020_NCL,
1050 ColorMatrix::Rgb => ffi::AVCOL_SPC_RGB,
1051 },
1052 match color.range {
1053 ColorRange::Limited => ffi::AVCOL_RANGE_MPEG,
1054 ColorRange::Full => ffi::AVCOL_RANGE_JPEG,
1055 },
1056 )
1057}
1058
1059pub(crate) fn color_options(color: ColorMetadata) -> AVDictionary {
1064 let (primaries, transfer, matrix, range) = color_tags(color);
1065 AVDictionary::new_int(c"color_primaries", i64::from(primaries), 0)
1066 .set_int(c"color_trc", i64::from(transfer), 0)
1067 .set_int(c"colorspace", i64::from(matrix), 0)
1068 .set_int(c"color_range", i64::from(range), 0)
1069}
1070
1071fn gpu_fault(error: &dyn std::any::Any) -> Option<alpha2_media::GpuFault> {
1074 error
1075 .downcast_ref::<alpha2_ffmpeg_bridge::BridgeError>()
1076 .and_then(alpha2_ffmpeg_bridge::BridgeError::gpu_fault)
1077}
1078
1079pub(crate) fn adapter_error(error: impl std::fmt::Display + 'static) -> DecodeError {
1082 match gpu_fault(&error) {
1083 Some(fault) => DecodeError::Gpu {
1084 fault,
1085 message: error.to_string(),
1086 },
1087 None => DecodeError::Adapter(error.to_string()),
1088 }
1089}
1090
1091pub(crate) fn encode_adapter_error(error: impl std::fmt::Display + 'static) -> EncodeError {
1094 match gpu_fault(&error) {
1095 Some(fault) => EncodeError::Gpu {
1096 fault,
1097 message: error.to_string(),
1098 },
1099 None => EncodeError::Adapter(error.to_string()),
1100 }
1101}
1102
1103pub(crate) fn encode_adapter_error_at(
1104 context: &str,
1105 error: impl std::fmt::Display + 'static,
1106) -> EncodeError {
1107 match gpu_fault(&error) {
1108 Some(fault) => EncodeError::Gpu {
1109 fault,
1110 message: format!("{context}: {error}"),
1111 },
1112 None => EncodeError::Adapter(format!("{context}: {error}")),
1113 }
1114}