1use core::ptr::{addr_of, read_unaligned};
11
12use derive_more::{IsVariant, TryUnwrap, Unwrap};
13use ffmpeg_next::ffi::{
14 AV_NOPTS_VALUE, AVChromaLocation, AVColorPrimaries, AVColorRange, AVColorSpace,
15 AVColorTransferCharacteristic, AVFrame, AVPictureType, AVSubtitleType, av_buffer_alloc,
16};
17use mediadecode::{
18 PixelFormat, Timebase, Timestamp,
19 color::{ChromaLocation, ColorInfo, ColorMatrix, ColorPrimaries, ColorRange, ColorTransfer},
20 frame::{AudioFrame, Dimensions, Plane, Rect, SubtitleFrame, VideoFrame},
21 subtitle::{Bitmap as SubtitleBitmap, SubtitlePayload, Text as SubtitleText},
22};
23use mediaframe::audio::ChannelLayoutDescription;
24use smol_str::SmolStr;
25
26use crate::{
27 FfmpegBuffer, boundary,
28 extras::{AudioFrameExtra, PictureType, SideDataEntry, SubtitleFrameExtra, VideoFrameExtra},
29 pixdesc,
30 sample_format::SampleFormat,
31};
32
33#[derive(Debug, Clone)]
38pub struct UnsupportedPixelFormat {
39 format: PixelFormat,
40 raw: i32,
41 name: Option<SmolStr>,
42}
43
44impl UnsupportedPixelFormat {
45 #[inline]
47 pub const fn new(format: PixelFormat, raw: i32, name: Option<SmolStr>) -> Self {
48 Self { format, raw, name }
49 }
50
51 #[inline]
59 pub const fn format(&self) -> &PixelFormat {
60 &self.format
61 }
62 #[inline]
69 pub const fn raw(&self) -> i32 {
70 self.raw
71 }
72 #[inline]
78 pub fn name(&self) -> Option<&str> {
79 self.name.as_deref()
80 }
81}
82
83impl core::fmt::Display for UnsupportedPixelFormat {
84 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
85 match &self.name {
86 Some(name) => write!(
87 f,
88 "convert: unsupported pixel format {:?} (AVPixelFormat {} = {name:?})",
89 self.format, self.raw
90 ),
91 None => write!(
92 f,
93 "convert: unsupported pixel format {:?} (AVPixelFormat {}, unnamed by libavutil)",
94 self.format, self.raw
95 ),
96 }
97 }
98}
99
100#[derive(Debug, Clone, Copy)]
104pub struct InvalidPlaneLayout {
105 plane: usize,
106}
107
108impl InvalidPlaneLayout {
109 #[inline]
111 pub const fn new(plane: usize) -> Self {
112 Self { plane }
113 }
114 #[inline]
116 pub const fn plane(&self) -> usize {
117 self.plane
118 }
119}
120
121impl core::fmt::Display for InvalidPlaneLayout {
122 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
123 write!(f, "convert: invalid layout on plane {}", self.plane)
124 }
125}
126
127#[derive(Debug, Clone, Copy)]
132pub struct BufferAcquireFailed {
133 plane: usize,
134}
135
136impl BufferAcquireFailed {
137 #[inline]
139 pub const fn new(plane: usize) -> Self {
140 Self { plane }
141 }
142 #[inline]
144 pub const fn plane(&self) -> usize {
145 self.plane
146 }
147}
148
149impl core::fmt::Display for BufferAcquireFailed {
150 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
151 write!(
152 f,
153 "convert: could not acquire buffer ref for plane {}",
154 self.plane
155 )
156 }
157}
158
159#[derive(Debug, Clone, IsVariant, Unwrap, TryUnwrap)]
161#[non_exhaustive]
162#[unwrap(ref, ref_mut)]
163#[try_unwrap(ref, ref_mut)]
164pub enum ConvertError {
165 NullFrame,
167 UnsupportedPixelFormat(UnsupportedPixelFormat),
170 InvalidPlaneLayout(InvalidPlaneLayout),
172 BufferAcquireFailed(BufferAcquireFailed),
175}
176
177impl core::fmt::Display for ConvertError {
178 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
179 match self {
180 Self::NullFrame => write!(f, "convert: AVFrame pointer was null"),
181 Self::UnsupportedPixelFormat(p) => core::fmt::Display::fmt(p, f),
182 Self::InvalidPlaneLayout(p) => core::fmt::Display::fmt(p, f),
183 Self::BufferAcquireFailed(p) => core::fmt::Display::fmt(p, f),
184 }
185 }
186}
187
188impl core::error::Error for ConvertError {}
189
190fn unsupported_pixel_format(format: PixelFormat, raw: i32) -> ConvertError {
196 ConvertError::UnsupportedPixelFormat(UnsupportedPixelFormat::new(
197 format,
198 raw,
199 crate::ffi::pix_fmt_name(raw),
200 ))
201}
202
203pub fn video_frame_from(
210 frame: &ffmpeg_next::Frame,
211 time_base: Timebase,
212) -> Result<VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, FfmpegBuffer>, ConvertError> {
213 unsafe { av_frame_to_video_frame(frame.as_ptr(), time_base) }
216}
217
218pub fn audio_frame_from(
221 frame: &ffmpeg_next::frame::Audio,
222 time_base: Timebase,
223) -> Result<
224 AudioFrame<SampleFormat, ChannelLayoutDescription, AudioFrameExtra, FfmpegBuffer>,
225 ConvertError,
226> {
227 unsafe { av_frame_to_audio_frame(frame.as_ptr(), time_base) }
230}
231
232pub fn subtitle_frame_from(
235 subtitle: &ffmpeg_next::Subtitle,
236 time_base: Timebase,
237) -> Result<SubtitleFrame<SubtitleFrameExtra, FfmpegBuffer>, ConvertError> {
238 unsafe { av_subtitle_to_subtitle_frame(subtitle.as_ptr(), time_base) }
241}
242
243pub unsafe fn av_frame_to_video_frame(
256 av_frame: *const AVFrame,
257 time_base: Timebase,
258) -> Result<VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, FfmpegBuffer>, ConvertError> {
259 if av_frame.is_null() {
260 return Err(ConvertError::NullFrame);
261 }
262 let format_raw = unsafe { (*av_frame).format };
277 let width_raw = unsafe { (*av_frame).width };
278 let height_raw = unsafe { (*av_frame).height };
279 let pts_raw = unsafe { (*av_frame).pts };
280 let duration_raw = unsafe { (*av_frame).duration };
281 let pix_fmt = boundary::from_av_pixel_format(format_raw);
282 let width = width_raw.max(0) as u32;
283 let height = height_raw.max(0) as u32;
284
285 if !pixdesc::is_deliverable(&pix_fmt) {
291 return Err(unsupported_pixel_format(pix_fmt, format_raw));
292 }
293 let geom = match pixdesc::plane_geometry(&pix_fmt, width as usize, height as usize) {
301 Some(g) => g,
302 None => return Err(unsupported_pixel_format(pix_fmt, format_raw)),
303 };
304
305 let mut planes_out: [Plane<FfmpegBuffer>; 4] = [
306 plane_placeholder()?,
307 plane_placeholder()?,
308 plane_placeholder()?,
309 plane_placeholder()?,
310 ];
311 let mut plane_count: u8 = 0;
312
313 #[allow(clippy::needless_range_loop)]
322 for plane_idx in 0..geom.count {
323 let linesize = unsafe { (*av_frame).linesize[plane_idx] };
326 if linesize <= 0 {
327 return Err(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(
333 plane_idx,
334 )));
335 }
336 let data_ptr = unsafe { (*av_frame).data[plane_idx] };
337 if data_ptr.is_null() {
338 return Err(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(
339 plane_idx,
340 )));
341 }
342 let plane_h = geom.height[plane_idx];
343 let row_bytes = geom.row_bytes[plane_idx];
344 if row_bytes > linesize as usize {
345 return Err(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(
346 plane_idx,
347 )));
348 }
349 let (view, exported_stride) = if (linesize as usize) == row_bytes {
365 let plane_bytes =
366 (plane_h)
367 .checked_mul(linesize as usize)
368 .ok_or(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(
369 plane_idx,
370 )))?;
371 let buf = unsafe { find_backing_buffer(av_frame, data_ptr, plane_bytes) }.ok_or(
372 ConvertError::BufferAcquireFailed(BufferAcquireFailed::new(plane_idx)),
373 )?;
374 let offset = unsafe { (data_ptr as usize).wrapping_sub((*buf).data as usize) };
378 let view = unsafe { FfmpegBuffer::from_ref_view(buf, offset, plane_bytes) }.ok_or(
381 ConvertError::BufferAcquireFailed(BufferAcquireFailed::new(plane_idx)),
382 )?;
383 (view, linesize as u32)
384 } else {
385 let total_bytes = row_bytes
386 .checked_mul(plane_h)
387 .ok_or(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(
388 plane_idx,
389 )))?;
390 let last_row_offset = (plane_h.saturating_sub(1))
400 .checked_mul(linesize as usize)
401 .ok_or(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(
402 plane_idx,
403 )))?;
404 let readable_extent =
405 last_row_offset
406 .checked_add(row_bytes)
407 .ok_or(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(
408 plane_idx,
409 )))?;
410 unsafe { find_backing_buffer(av_frame, data_ptr, readable_extent) }.ok_or(
415 ConvertError::BufferAcquireFailed(BufferAcquireFailed::new(plane_idx)),
416 )?;
417 let mut packed: std::vec::Vec<u8> = std::vec::Vec::new();
418 packed
419 .try_reserve_exact(total_bytes)
420 .map_err(|_| ConvertError::BufferAcquireFailed(BufferAcquireFailed::new(plane_idx)))?;
421 for row_idx in 0..plane_h {
422 let row_offset =
423 (row_idx)
424 .checked_mul(linesize as usize)
425 .ok_or(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(
426 plane_idx,
427 )))?;
428 let row_slice =
433 unsafe { core::slice::from_raw_parts(data_ptr.add(row_offset) as *const u8, row_bytes) };
434 packed.extend_from_slice(row_slice);
435 }
436 let buf = FfmpegBuffer::copy_from_slice(&packed).ok_or(ConvertError::BufferAcquireFailed(
437 BufferAcquireFailed::new(plane_idx),
438 ))?;
439 (buf, row_bytes as u32)
440 };
441
442 planes_out[plane_idx] = Plane::new(view, exported_stride);
443 plane_count = (plane_idx + 1) as u8;
444 }
445
446 let pts = if pts_raw != AV_NOPTS_VALUE {
448 Some(Timestamp::new(pts_raw, time_base))
449 } else {
450 None
451 };
452 let duration = if duration_raw > 0 {
453 Some(Timestamp::new(duration_raw, time_base))
454 } else {
455 None
456 };
457
458 let visible_rect = unsafe { build_visible_rect(av_frame, width, height) };
460
461 let color_primaries_raw =
473 unsafe { read_unaligned(addr_of!((*av_frame).color_primaries) as *const i32) };
474 let color_trc_raw = unsafe { read_unaligned(addr_of!((*av_frame).color_trc) as *const i32) };
475 let colorspace_raw = unsafe { read_unaligned(addr_of!((*av_frame).colorspace) as *const i32) };
476 let color_range_raw = unsafe { read_unaligned(addr_of!((*av_frame).color_range) as *const i32) };
477 let chroma_location_raw =
478 unsafe { read_unaligned(addr_of!((*av_frame).chroma_location) as *const i32) };
479 let color = ColorInfo::UNSPECIFIED
480 .with_primaries(map_primaries(color_primaries_raw))
481 .with_transfer(map_transfer(color_trc_raw))
482 .with_matrix(map_matrix(colorspace_raw))
483 .with_range(map_range_for(&pix_fmt, color_range_raw))
484 .with_chroma_location(map_chroma_loc(chroma_location_raw));
485
486 let extra = unsafe { build_video_frame_extra(av_frame) };
488
489 let mut out = VideoFrame::new(
492 Dimensions::new(width, height),
493 pix_fmt,
494 planes_out,
495 plane_count,
496 extra,
497 )
498 .with_pts(pts)
499 .with_duration(duration)
500 .with_color(color);
501 if let Some(r) = visible_rect {
502 out = out.with_visible_rect(Some(r));
503 }
504 Ok(out)
505}
506
507fn plane_placeholder() -> Result<Plane<FfmpegBuffer>, ConvertError> {
508 let raw = unsafe { av_buffer_alloc(0) };
512 let raw = if raw.is_null() {
515 unsafe { av_buffer_alloc(1) }
516 } else {
517 raw
518 };
519 if raw.is_null() {
520 return Err(ConvertError::BufferAcquireFailed(BufferAcquireFailed::new(
522 4,
523 )));
524 }
525 let buf = unsafe { FfmpegBuffer::take(raw) }.ok_or(ConvertError::BufferAcquireFailed(
526 BufferAcquireFailed::new(4),
527 ))?;
528 Ok(Plane::new(buf, 0))
529}
530
531unsafe fn build_visible_rect(av_frame: *const AVFrame, width: u32, height: u32) -> Option<Rect> {
537 let crop_left = unsafe { (*av_frame).crop_left } as u32;
538 let crop_top = unsafe { (*av_frame).crop_top } as u32;
539 let crop_right = unsafe { (*av_frame).crop_right } as u32;
540 let crop_bottom = unsafe { (*av_frame).crop_bottom } as u32;
541 if crop_left == 0 && crop_top == 0 && crop_right == 0 && crop_bottom == 0 {
542 return None;
543 }
544 let x = crop_left;
545 let y = crop_top;
546 let w = width.saturating_sub(crop_left).saturating_sub(crop_right);
547 let h = height.saturating_sub(crop_top).saturating_sub(crop_bottom);
548 Some(Rect::new(x, y, w, h))
549}
550
551unsafe fn build_video_frame_extra(av_frame: *const AVFrame) -> VideoFrameExtra {
556 let mut out = VideoFrameExtra::default();
557 let sar_num = unsafe { (*av_frame).sample_aspect_ratio.num };
559 let sar_den = unsafe { (*av_frame).sample_aspect_ratio.den };
560 if sar_num > 0 && sar_den > 0 && (sar_num != 1 || sar_den != 1) {
561 out.set_sample_aspect_ratio(Some((sar_num as u32, sar_den as u32)));
562 }
563 let pict_type_raw = unsafe { read_unaligned(addr_of!((*av_frame).pict_type) as *const i32) };
569 out.set_picture_type(map_picture_type_raw(pict_type_raw));
570 let flags = unsafe { (*av_frame).flags };
574 out.set_key_frame(flags & ffmpeg_next::ffi::AV_FRAME_FLAG_KEY != 0);
575 out.set_interlaced(flags & ffmpeg_next::ffi::AV_FRAME_FLAG_INTERLACED != 0);
576 out.set_top_field_first(flags & ffmpeg_next::ffi::AV_FRAME_FLAG_TOP_FIELD_FIRST != 0);
577 let bet = unsafe { (*av_frame).best_effort_timestamp };
579 if bet != AV_NOPTS_VALUE {
580 out.set_best_effort_timestamp(Some(bet));
581 }
582 out.set_side_data(unsafe { collect_side_data(av_frame) });
584 out
585}
586
587pub(crate) const SIDE_DATA_MAX_ENTRIES: usize = 64;
594pub(crate) const SIDE_DATA_MAX_TOTAL_BYTES: usize = 256 * 1024;
601
602const SUBTITLE_MAX_RECTS: usize = 64;
606const SUBTITLE_MAX_TEXT_BYTES_PER_RECT: usize = 64 * 1024;
610const SUBTITLE_MAX_TEXT_TOTAL_BYTES: usize = 256 * 1024;
613const SUBTITLE_MAX_BITMAP_BYTES_PER_RECT: usize = 16 * 1024 * 1024;
617const SUBTITLE_MAX_BITMAP_TOTAL_BYTES: usize = 32 * 1024 * 1024;
619
620unsafe fn bounded_cstr_bytes<'a>(ptr: *const core::ffi::c_char, cap: usize) -> Option<&'a [u8]> {
638 let max = cap.saturating_add(1);
641 for i in 0..max {
642 let byte = unsafe { *(ptr.add(i) as *const u8) };
646 if byte == 0 {
647 return Some(unsafe { core::slice::from_raw_parts(ptr as *const u8, i) });
650 }
651 }
652 None
655}
656
657unsafe fn collect_side_data(av_frame: *const AVFrame) -> std::vec::Vec<SideDataEntry> {
671 let nb_side_data_raw = unsafe { (*av_frame).nb_side_data };
677 let side_data = unsafe { (*av_frame).side_data };
678 if nb_side_data_raw <= 0 || side_data.is_null() {
679 return Vec::new();
680 }
681 let count_raw = nb_side_data_raw as usize;
682 let count = count_raw.min(SIDE_DATA_MAX_ENTRIES);
683 if count_raw > SIDE_DATA_MAX_ENTRIES {
684 tracing::warn!(
685 cap = SIDE_DATA_MAX_ENTRIES,
686 requested = count_raw,
687 "mediadecode-ffmpeg: AVFrame.nb_side_data exceeds entry cap; truncating",
688 );
689 }
690 let mut out: Vec<SideDataEntry> = Vec::new();
691 if out.try_reserve_exact(count).is_err() {
692 return Vec::new();
693 }
694 let mut total_bytes: usize = 0;
695 for i in 0..count {
696 let sd = unsafe { *side_data.add(i) };
697 if sd.is_null() {
698 continue;
699 }
700 let kind = unsafe { read_unaligned(addr_of!((*sd).type_) as *const i32) };
704 let size = unsafe { (*sd).size };
705 let data_ptr = unsafe { (*sd).data };
706 let data_slice = if size == 0 || data_ptr.is_null() {
707 Vec::new()
708 } else {
709 let projected = total_bytes.saturating_add(size);
713 if projected > SIDE_DATA_MAX_TOTAL_BYTES {
714 tracing::warn!(
715 cap = SIDE_DATA_MAX_TOTAL_BYTES,
716 projected,
717 "mediadecode-ffmpeg: AVFrame side-data byte cap reached; dropping remaining entries",
718 );
719 break;
720 }
721 total_bytes = projected;
722 let mut buf: Vec<u8> = Vec::new();
725 if buf.try_reserve_exact(size).is_err() {
726 continue;
727 }
728 let src = unsafe { core::slice::from_raw_parts(data_ptr, size) };
731 buf.extend_from_slice(src);
732 buf
733 };
734 out.push(SideDataEntry::new(kind, data_slice));
735 }
736 out
737}
738
739unsafe fn find_backing_buffer(
748 av_frame: *const AVFrame,
749 data_ptr: *const u8,
750 bytes: usize,
751) -> Option<*mut ffmpeg_next::ffi::AVBufferRef> {
752 let buf_array_len = unsafe { (*av_frame).buf.len() };
753 for i in 0..buf_array_len {
754 let buf = unsafe { (*av_frame).buf[i] };
755 if buf.is_null() {
756 continue;
757 }
758 let buf_data = unsafe { (*buf).data as *const u8 };
759 let buf_size = unsafe { (*buf).size };
760 if buf_data.is_null() {
761 continue;
762 }
763 let start = buf_data as usize;
764 let Some(end) = start.checked_add(buf_size) else {
765 continue;
766 };
767 let dp = data_ptr as usize;
768 let Some(dp_end) = dp.checked_add(bytes) else {
769 continue;
770 };
771 if dp >= start && dp_end <= end {
772 return Some(buf);
773 }
774 }
775 None
776}
777
778fn map_primaries(raw: i32) -> ColorPrimaries {
779 match raw {
780 x if x == AVColorPrimaries::AVCOL_PRI_BT709 as i32 => ColorPrimaries::Bt709,
781 x if x == AVColorPrimaries::AVCOL_PRI_UNSPECIFIED as i32 => ColorPrimaries::Unspecified,
782 x if x == AVColorPrimaries::AVCOL_PRI_BT470M as i32 => ColorPrimaries::Bt470M,
783 x if x == AVColorPrimaries::AVCOL_PRI_BT470BG as i32 => ColorPrimaries::Bt470Bg,
784 x if x == AVColorPrimaries::AVCOL_PRI_SMPTE170M as i32 => ColorPrimaries::Smpte170M,
785 x if x == AVColorPrimaries::AVCOL_PRI_SMPTE240M as i32 => ColorPrimaries::Smpte240M,
786 x if x == AVColorPrimaries::AVCOL_PRI_FILM as i32 => ColorPrimaries::Film,
787 x if x == AVColorPrimaries::AVCOL_PRI_BT2020 as i32 => ColorPrimaries::Bt2020,
788 x if x == AVColorPrimaries::AVCOL_PRI_SMPTE428 as i32 => ColorPrimaries::SmpteSt428,
789 x if x == AVColorPrimaries::AVCOL_PRI_SMPTE431 as i32 => ColorPrimaries::SmpteRp431,
790 x if x == AVColorPrimaries::AVCOL_PRI_SMPTE432 as i32 => ColorPrimaries::SmpteEg432,
791 x if x == AVColorPrimaries::AVCOL_PRI_EBU3213 as i32 => ColorPrimaries::Ebu3213E,
792 _ => ColorPrimaries::Unspecified,
793 }
794}
795
796fn map_transfer(raw: i32) -> ColorTransfer {
797 match raw {
798 x if x == AVColorTransferCharacteristic::AVCOL_TRC_BT709 as i32 => ColorTransfer::Bt709,
799 x if x == AVColorTransferCharacteristic::AVCOL_TRC_UNSPECIFIED as i32 => {
800 ColorTransfer::Unspecified
801 }
802 x if x == AVColorTransferCharacteristic::AVCOL_TRC_GAMMA22 as i32 => ColorTransfer::Gamma22,
803 x if x == AVColorTransferCharacteristic::AVCOL_TRC_GAMMA28 as i32 => ColorTransfer::Gamma28,
804 x if x == AVColorTransferCharacteristic::AVCOL_TRC_SMPTE170M as i32 => ColorTransfer::Smpte170M,
805 x if x == AVColorTransferCharacteristic::AVCOL_TRC_SMPTE240M as i32 => ColorTransfer::Smpte240M,
806 x if x == AVColorTransferCharacteristic::AVCOL_TRC_LINEAR as i32 => ColorTransfer::Linear,
807 x if x == AVColorTransferCharacteristic::AVCOL_TRC_LOG as i32 => ColorTransfer::Log100,
808 x if x == AVColorTransferCharacteristic::AVCOL_TRC_LOG_SQRT as i32 => ColorTransfer::Log316,
809 x if x == AVColorTransferCharacteristic::AVCOL_TRC_IEC61966_2_4 as i32 => {
810 ColorTransfer::Iec6196624
811 }
812 x if x == AVColorTransferCharacteristic::AVCOL_TRC_BT1361_ECG as i32 => {
813 ColorTransfer::Bt1361Ecg
814 }
815 x if x == AVColorTransferCharacteristic::AVCOL_TRC_IEC61966_2_1 as i32 => {
816 ColorTransfer::Iec6196621
817 }
818 x if x == AVColorTransferCharacteristic::AVCOL_TRC_BT2020_10 as i32 => {
819 ColorTransfer::Bt2020_10Bit
820 }
821 x if x == AVColorTransferCharacteristic::AVCOL_TRC_BT2020_12 as i32 => {
822 ColorTransfer::Bt2020_12Bit
823 }
824 x if x == AVColorTransferCharacteristic::AVCOL_TRC_SMPTE2084 as i32 => {
825 ColorTransfer::SmpteSt2084Pq
826 }
827 x if x == AVColorTransferCharacteristic::AVCOL_TRC_SMPTE428 as i32 => ColorTransfer::SmpteSt428,
828 x if x == AVColorTransferCharacteristic::AVCOL_TRC_ARIB_STD_B67 as i32 => {
829 ColorTransfer::AribStdB67Hlg
830 }
831 _ => ColorTransfer::Unspecified,
832 }
833}
834
835fn map_matrix(raw: i32) -> ColorMatrix {
836 match raw {
837 x if x == AVColorSpace::AVCOL_SPC_BT709 as i32 => ColorMatrix::Bt709,
838 x if x == AVColorSpace::AVCOL_SPC_BT2020_NCL as i32 => ColorMatrix::Bt2020Ncl,
839 x if x == AVColorSpace::AVCOL_SPC_SMPTE170M as i32 => ColorMatrix::Bt601,
840 x if x == AVColorSpace::AVCOL_SPC_BT470BG as i32 => ColorMatrix::Bt601,
841 x if x == AVColorSpace::AVCOL_SPC_SMPTE240M as i32 => ColorMatrix::Smpte240m,
842 x if x == AVColorSpace::AVCOL_SPC_FCC as i32 => ColorMatrix::Fcc,
843 x if x == AVColorSpace::AVCOL_SPC_YCGCO as i32 => ColorMatrix::YCgCo,
844 _ => ColorMatrix::Bt709, }
846}
847
848fn map_range(raw: i32) -> ColorRange {
849 match raw {
850 x if x == AVColorRange::AVCOL_RANGE_JPEG as i32 => ColorRange::Full,
851 x if x == AVColorRange::AVCOL_RANGE_MPEG as i32 => ColorRange::Limited,
852 _ => ColorRange::Unspecified,
853 }
854}
855
856fn is_yuvj(pix_fmt: &PixelFormat) -> bool {
862 matches!(
863 pix_fmt,
864 PixelFormat::Yuvj411p
865 | PixelFormat::Yuvj420p
866 | PixelFormat::Yuvj422p
867 | PixelFormat::Yuvj440p
868 | PixelFormat::Yuvj444p
869 )
870}
871
872fn map_range_for(pix_fmt: &PixelFormat, color_range_raw: i32) -> ColorRange {
884 if is_yuvj(pix_fmt) {
885 return ColorRange::Full;
886 }
887 map_range(color_range_raw)
888}
889
890fn map_chroma_loc(raw: i32) -> ChromaLocation {
891 match raw {
892 x if x == AVChromaLocation::AVCHROMA_LOC_LEFT as i32 => ChromaLocation::Left,
893 x if x == AVChromaLocation::AVCHROMA_LOC_CENTER as i32 => ChromaLocation::Center,
894 x if x == AVChromaLocation::AVCHROMA_LOC_TOPLEFT as i32 => ChromaLocation::TopLeft,
895 x if x == AVChromaLocation::AVCHROMA_LOC_TOP as i32 => ChromaLocation::Top,
896 x if x == AVChromaLocation::AVCHROMA_LOC_BOTTOMLEFT as i32 => ChromaLocation::BottomLeft,
897 x if x == AVChromaLocation::AVCHROMA_LOC_BOTTOM as i32 => ChromaLocation::Bottom,
898 _ => ChromaLocation::Unspecified,
899 }
900}
901
902pub unsafe fn av_frame_to_audio_frame(
917 av_frame: *const AVFrame,
918 time_base: Timebase,
919) -> Result<
920 AudioFrame<SampleFormat, ChannelLayoutDescription, AudioFrameExtra, FfmpegBuffer>,
921 ConvertError,
922> {
923 if av_frame.is_null() {
924 return Err(ConvertError::NullFrame);
925 }
926 let format_raw = unsafe { (*av_frame).format };
934 let sample_rate_raw = unsafe { (*av_frame).sample_rate };
935 let nb_samples_raw = unsafe { (*av_frame).nb_samples };
936 let pts_raw = unsafe { (*av_frame).pts };
937 let duration_raw = unsafe { (*av_frame).duration };
938 let bet_raw = unsafe { (*av_frame).best_effort_timestamp };
939
940 let sample_format = SampleFormat::from_raw(format_raw);
941 let sample_rate = sample_rate_raw.max(0) as u32;
942 let nb_samples = nb_samples_raw.max(0) as u32;
943
944 let ch_layout_ptr = unsafe { addr_of!((*av_frame).ch_layout) };
948 let channel_layout =
949 unsafe { crate::channel_layout::channel_layout_description_from_raw_ptr(ch_layout_ptr) };
950 let channel_count_full = channel_layout.channels();
951 let channel_count = channel_count_full.min(255) as u8;
952
953 let is_planar = sample_format.is_planar();
955 let plane_count_full = if is_planar { channel_count as usize } else { 1 };
956 if plane_count_full > 8 {
964 return Err(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(8)));
965 }
966 let plane_count = plane_count_full as u8;
967
968 let linesize0 = unsafe { (*av_frame).linesize[0] };
975 if nb_samples > 0 && linesize0 <= 0 {
976 return Err(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(0)));
977 }
978 let plane_bytes = linesize0.max(0) as usize;
979 if nb_samples > 0 {
980 let bytes_per_sample = sample_format
981 .bytes_per_sample()
982 .ok_or(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(0)))?
983 as usize;
984 let expected_per_plane = if is_planar {
985 (nb_samples as usize)
987 .checked_mul(bytes_per_sample)
988 .ok_or(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(0)))?
989 } else {
990 (nb_samples as usize)
992 .checked_mul(bytes_per_sample)
993 .and_then(|x| x.checked_mul(channel_count.max(1) as usize))
994 .ok_or(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(0)))?
995 };
996 if plane_bytes < expected_per_plane {
997 return Err(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(0)));
998 }
999 }
1000
1001 let mut planes_out: [Plane<FfmpegBuffer>; 8] = [
1002 audio_plane_placeholder()?,
1003 audio_plane_placeholder()?,
1004 audio_plane_placeholder()?,
1005 audio_plane_placeholder()?,
1006 audio_plane_placeholder()?,
1007 audio_plane_placeholder()?,
1008 audio_plane_placeholder()?,
1009 audio_plane_placeholder()?,
1010 ];
1011
1012 #[allow(clippy::needless_range_loop)]
1016 for plane_idx in 0..plane_count as usize {
1017 let data_ptr = unsafe { (*av_frame).data[plane_idx] };
1018 if data_ptr.is_null() {
1019 return Err(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(
1025 plane_idx,
1026 )));
1027 }
1028 let buf = unsafe { find_audio_backing_buffer(av_frame, data_ptr, plane_bytes) }.ok_or(
1029 ConvertError::BufferAcquireFailed(BufferAcquireFailed::new(plane_idx)),
1030 )?;
1031 let offset = unsafe { (data_ptr as usize).wrapping_sub((*buf).data as usize) };
1034 let view = unsafe { FfmpegBuffer::from_ref_view(buf, offset, plane_bytes) }.ok_or(
1037 ConvertError::BufferAcquireFailed(BufferAcquireFailed::new(plane_idx)),
1038 )?;
1039 planes_out[plane_idx] = Plane::new(view, plane_bytes as u32);
1040 }
1041
1042 let pts = if pts_raw != AV_NOPTS_VALUE {
1043 Some(Timestamp::new(pts_raw, time_base))
1044 } else {
1045 None
1046 };
1047 let duration = if duration_raw > 0 {
1048 Some(Timestamp::new(duration_raw, time_base))
1049 } else {
1050 None
1051 };
1052
1053 let mut extra = AudioFrameExtra::default();
1054 if bet_raw != AV_NOPTS_VALUE {
1055 extra.set_best_effort_timestamp(Some(bet_raw));
1056 }
1057 extra.set_side_data(unsafe { collect_side_data(av_frame) });
1061
1062 Ok(
1063 AudioFrame::new(
1064 sample_rate,
1065 nb_samples,
1066 channel_count,
1067 sample_format,
1068 channel_layout,
1069 planes_out,
1070 plane_count,
1071 extra,
1072 )
1073 .with_pts(pts)
1074 .with_duration(duration),
1075 )
1076}
1077
1078fn audio_plane_placeholder() -> Result<Plane<FfmpegBuffer>, ConvertError> {
1079 let raw = unsafe { av_buffer_alloc(1) };
1080 if raw.is_null() {
1081 return Err(ConvertError::BufferAcquireFailed(BufferAcquireFailed::new(
1082 8,
1083 )));
1084 }
1085 let buf = unsafe { FfmpegBuffer::take(raw) }.ok_or(ConvertError::BufferAcquireFailed(
1086 BufferAcquireFailed::new(8),
1087 ))?;
1088 Ok(Plane::new(buf, 0))
1089}
1090
1091pub(crate) unsafe fn find_audio_backing_buffer(
1097 av_frame: *const AVFrame,
1098 data_ptr: *const u8,
1099 bytes: usize,
1100) -> Option<*mut ffmpeg_next::ffi::AVBufferRef> {
1101 let buf_array_len = unsafe { (*av_frame).buf.len() };
1106 for i in 0..buf_array_len {
1107 let buf = unsafe { (*av_frame).buf[i] };
1108 if buf.is_null() {
1109 continue;
1110 }
1111 let buf_data = unsafe { (*buf).data as *const u8 };
1112 let buf_size = unsafe { (*buf).size };
1113 if buf_data.is_null() {
1114 continue;
1115 }
1116 let start = buf_data as usize;
1117 let Some(end) = start.checked_add(buf_size) else {
1118 continue;
1119 };
1120 let dp = data_ptr as usize;
1121 let Some(dp_end) = dp.checked_add(bytes) else {
1122 continue;
1123 };
1124 if dp >= start && dp_end <= end {
1125 return Some(buf);
1126 }
1127 }
1128 None
1129}
1130
1131pub unsafe fn av_subtitle_to_subtitle_frame(
1155 av_subtitle: *const ffmpeg_next::ffi::AVSubtitle,
1156 time_base: Timebase,
1157) -> Result<SubtitleFrame<SubtitleFrameExtra, FfmpegBuffer>, ConvertError> {
1158 if av_subtitle.is_null() {
1159 return Err(ConvertError::NullFrame);
1160 }
1161 let mut text_chunks: std::vec::Vec<u8> = std::vec::Vec::new();
1166 let mut bitmap_regions: std::vec::Vec<mediadecode::subtitle::BitmapRegion<FfmpegBuffer>> =
1167 std::vec::Vec::new();
1168
1169 let count_raw = unsafe { (*av_subtitle).num_rects } as usize;
1170 let rects_ptr = unsafe { (*av_subtitle).rects };
1171 if count_raw > 0 && rects_ptr.is_null() {
1175 return Err(ConvertError::NullFrame);
1176 }
1177 let count = count_raw.min(SUBTITLE_MAX_RECTS);
1186 if count_raw > SUBTITLE_MAX_RECTS {
1187 tracing::warn!(
1188 cap = SUBTITLE_MAX_RECTS,
1189 requested = count_raw,
1190 "mediadecode-ffmpeg: AVSubtitle.num_rects exceeds rect cap; truncating",
1191 );
1192 }
1193 let mut text_total_bytes: usize = 0;
1194 let mut bitmap_total_bytes: usize = 0;
1195
1196 let text_kind = AVSubtitleType::SUBTITLE_TEXT as i32;
1197 let ass_kind = AVSubtitleType::SUBTITLE_ASS as i32;
1198 let bitmap_kind = AVSubtitleType::SUBTITLE_BITMAP as i32;
1199 for i in 0..count {
1200 let rect_ptr = unsafe { *rects_ptr.add(i) };
1204 if rect_ptr.is_null() {
1205 continue;
1206 }
1207 let rect_type_raw = unsafe { read_unaligned(addr_of!((*rect_ptr).type_) as *const i32) };
1213 let rect_text_ptr = unsafe { (*rect_ptr).text };
1216 let rect_ass_ptr = unsafe { (*rect_ptr).ass };
1217 let rect_data0_ptr = unsafe { (*rect_ptr).data[0] };
1218 let rect_data1_ptr = unsafe { (*rect_ptr).data[1] };
1219 let rect_linesize0 = unsafe { (*rect_ptr).linesize[0] };
1220 let rect_w = unsafe { (*rect_ptr).w };
1221 let rect_h = unsafe { (*rect_ptr).h };
1222 let rect_x = unsafe { (*rect_ptr).x };
1223 let rect_y = unsafe { (*rect_ptr).y };
1224
1225 match rect_type_raw {
1226 x if x == text_kind && !rect_text_ptr.is_null() => {
1227 let bytes = unsafe { bounded_cstr_bytes(rect_text_ptr, SUBTITLE_MAX_TEXT_BYTES_PER_RECT) }
1237 .ok_or(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(0)))?;
1238 if bytes.len() > SUBTITLE_MAX_TEXT_BYTES_PER_RECT {
1242 return Err(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(0)));
1243 }
1244 let separator = if text_chunks.is_empty() { 0 } else { 1 };
1245 let projected = text_total_bytes
1246 .saturating_add(bytes.len())
1247 .saturating_add(separator);
1248 if projected > SUBTITLE_MAX_TEXT_TOTAL_BYTES {
1249 return Err(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(0)));
1250 }
1251 if separator == 1 {
1252 text_chunks.push(b'\n');
1253 }
1254 text_chunks.extend_from_slice(bytes);
1255 text_total_bytes = projected;
1256 }
1257 x if x == ass_kind && !rect_ass_ptr.is_null() => {
1258 let bytes = unsafe { bounded_cstr_bytes(rect_ass_ptr, SUBTITLE_MAX_TEXT_BYTES_PER_RECT) }
1261 .ok_or(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(0)))?;
1262 if bytes.len() > SUBTITLE_MAX_TEXT_BYTES_PER_RECT {
1263 return Err(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(0)));
1264 }
1265 let separator = if text_chunks.is_empty() { 0 } else { 1 };
1266 let projected = text_total_bytes
1267 .saturating_add(bytes.len())
1268 .saturating_add(separator);
1269 if projected > SUBTITLE_MAX_TEXT_TOTAL_BYTES {
1270 return Err(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(0)));
1271 }
1272 if separator == 1 {
1273 text_chunks.push(b'\n');
1274 }
1275 text_chunks.extend_from_slice(bytes);
1276 text_total_bytes = projected;
1277 }
1278 x if x == bitmap_kind => {
1279 let w = rect_w.max(0) as u32;
1283 let h = rect_h.max(0) as u32;
1284 let stride = rect_linesize0.max(0) as u32;
1285 if rect_data0_ptr.is_null() || stride == 0 || h == 0 {
1286 continue;
1287 }
1288 let data_len = (stride as usize)
1292 .checked_mul(h as usize)
1293 .ok_or(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(0)))?;
1294 if data_len > SUBTITLE_MAX_BITMAP_BYTES_PER_RECT {
1298 return Err(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(0)));
1299 }
1300 let projected_total = bitmap_total_bytes.saturating_add(data_len);
1301 if projected_total > SUBTITLE_MAX_BITMAP_TOTAL_BYTES {
1302 return Err(ConvertError::InvalidPlaneLayout(InvalidPlaneLayout::new(0)));
1303 }
1304 let data_slice = unsafe { core::slice::from_raw_parts(rect_data0_ptr, data_len) };
1307 let data_buf = FfmpegBuffer::copy_from_slice(data_slice).ok_or(
1308 ConvertError::BufferAcquireFailed(BufferAcquireFailed::new(0)),
1309 )?;
1310 let palette_len = 256 * 4;
1311 let palette_buf = if rect_data1_ptr.is_null() {
1312 FfmpegBuffer::copy_from_slice(&[]).ok_or(ConvertError::BufferAcquireFailed(
1313 BufferAcquireFailed::new(1),
1314 ))?
1315 } else {
1316 let p = unsafe { core::slice::from_raw_parts(rect_data1_ptr, palette_len) };
1318 FfmpegBuffer::copy_from_slice(p).ok_or(ConvertError::BufferAcquireFailed(
1319 BufferAcquireFailed::new(1),
1320 ))?
1321 };
1322 bitmap_regions.push(mediadecode::subtitle::BitmapRegion::new(
1323 rect_x.max(0) as u32,
1324 rect_y.max(0) as u32,
1325 w,
1326 h,
1327 stride,
1328 data_buf,
1329 palette_buf,
1330 ));
1331 bitmap_total_bytes = projected_total;
1332 }
1333 _ => {}
1334 }
1335 }
1336
1337 let payload = if !text_chunks.is_empty() {
1338 let buf = FfmpegBuffer::copy_from_slice(&text_chunks).ok_or(
1339 ConvertError::BufferAcquireFailed(BufferAcquireFailed::new(0)),
1340 )?;
1341 SubtitlePayload::Text(SubtitleText::new(buf, None))
1342 } else if !bitmap_regions.is_empty() {
1343 SubtitlePayload::Bitmap(SubtitleBitmap::new(bitmap_regions))
1344 } else {
1345 let buf = FfmpegBuffer::copy_from_slice(&[]).ok_or(ConvertError::BufferAcquireFailed(
1347 BufferAcquireFailed::new(0),
1348 ))?;
1349 SubtitlePayload::Text(SubtitleText::new(buf, None))
1350 };
1351
1352 let sub_pts = unsafe { (*av_subtitle).pts };
1353 let pts = if sub_pts != AV_NOPTS_VALUE {
1354 Some(Timestamp::new(sub_pts, time_base))
1355 } else {
1356 None
1357 };
1358
1359 let extra = SubtitleFrameExtra::new(unsafe { (*av_subtitle).start_display_time }, unsafe {
1360 (*av_subtitle).end_display_time
1361 });
1362
1363 Ok(SubtitleFrame::new(payload, extra).with_pts(pts))
1364}
1365
1366fn map_picture_type_raw(raw: i32) -> PictureType {
1367 match raw {
1368 x if x == AVPictureType::AV_PICTURE_TYPE_I as i32 => PictureType::I,
1369 x if x == AVPictureType::AV_PICTURE_TYPE_P as i32 => PictureType::P,
1370 x if x == AVPictureType::AV_PICTURE_TYPE_B as i32 => PictureType::B,
1371 x if x == AVPictureType::AV_PICTURE_TYPE_S as i32 => PictureType::S,
1372 x if x == AVPictureType::AV_PICTURE_TYPE_SI as i32 => PictureType::Si,
1373 x if x == AVPictureType::AV_PICTURE_TYPE_SP as i32 => PictureType::Sp,
1374 x if x == AVPictureType::AV_PICTURE_TYPE_BI as i32 => PictureType::Bi,
1375 _ => PictureType::Unspecified,
1376 }
1377}
1378
1379#[cfg(test)]
1380mod tests;