1use core::ptr::{addr_of, read_unaligned};
11
12use ffmpeg_next::ffi::{
13 AV_NOPTS_VALUE, AVChromaLocation, AVColorPrimaries, AVColorRange, AVColorSpace,
14 AVColorTransferCharacteristic, AVFrame, AVPictureType, AVSubtitleType, av_buffer_alloc,
15};
16use mediadecode::{
17 PixelFormat, Timebase, Timestamp,
18 color::{ChromaLocation, ColorInfo, ColorMatrix, ColorPrimaries, ColorRange, ColorTransfer},
19 frame::{AudioFrame, Dimensions, Plane, Rect, SubtitleFrame, VideoFrame},
20 subtitle::SubtitlePayload,
21};
22use mediaframe::audio::ChannelLayoutDescription;
23use smol_str::SmolStr;
24
25use crate::{
26 FfmpegBuffer, boundary,
27 extras::{AudioFrameExtra, PictureType, SideDataEntry, SubtitleFrameExtra, VideoFrameExtra},
28 pixdesc,
29 sample_format::SampleFormat,
30};
31
32#[derive(Debug, Clone)]
34#[non_exhaustive]
35pub enum ConvertError {
36 NullFrame,
38 UnsupportedPixelFormat {
41 format: PixelFormat,
49 raw: i32,
56 name: Option<SmolStr>,
62 },
63 InvalidPlaneLayout {
65 plane: usize,
67 },
68 BufferAcquireFailed {
71 plane: usize,
73 },
74}
75
76impl core::fmt::Display for ConvertError {
77 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
78 match self {
79 Self::NullFrame => write!(f, "convert: AVFrame pointer was null"),
80 Self::UnsupportedPixelFormat { format, raw, name } => match name {
81 Some(name) => write!(
82 f,
83 "convert: unsupported pixel format {format:?} (AVPixelFormat {raw} = {name:?})"
84 ),
85 None => write!(
86 f,
87 "convert: unsupported pixel format {format:?} (AVPixelFormat {raw}, unnamed by libavutil)"
88 ),
89 },
90 Self::InvalidPlaneLayout { plane } => {
91 write!(f, "convert: invalid layout on plane {plane}")
92 }
93 Self::BufferAcquireFailed { plane } => {
94 write!(f, "convert: could not acquire buffer ref for plane {plane}")
95 }
96 }
97 }
98}
99
100impl core::error::Error for ConvertError {}
101
102fn unsupported_pixel_format(format: PixelFormat, raw: i32) -> ConvertError {
108 ConvertError::UnsupportedPixelFormat {
109 format,
110 raw,
111 name: crate::ffi::pix_fmt_name(raw),
112 }
113}
114
115pub fn video_frame_from(
122 frame: &ffmpeg_next::Frame,
123 time_base: Timebase,
124) -> Result<VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, FfmpegBuffer>, ConvertError> {
125 unsafe { av_frame_to_video_frame(frame.as_ptr(), time_base) }
128}
129
130pub fn audio_frame_from(
133 frame: &ffmpeg_next::frame::Audio,
134 time_base: Timebase,
135) -> Result<
136 AudioFrame<SampleFormat, ChannelLayoutDescription, AudioFrameExtra, FfmpegBuffer>,
137 ConvertError,
138> {
139 unsafe { av_frame_to_audio_frame(frame.as_ptr(), time_base) }
142}
143
144pub fn subtitle_frame_from(
147 subtitle: &ffmpeg_next::Subtitle,
148 time_base: Timebase,
149) -> Result<SubtitleFrame<SubtitleFrameExtra, FfmpegBuffer>, ConvertError> {
150 unsafe { av_subtitle_to_subtitle_frame(subtitle.as_ptr(), time_base) }
153}
154
155pub unsafe fn av_frame_to_video_frame(
168 av_frame: *const AVFrame,
169 time_base: Timebase,
170) -> Result<VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, FfmpegBuffer>, ConvertError> {
171 if av_frame.is_null() {
172 return Err(ConvertError::NullFrame);
173 }
174 let format_raw = unsafe { (*av_frame).format };
189 let width_raw = unsafe { (*av_frame).width };
190 let height_raw = unsafe { (*av_frame).height };
191 let pts_raw = unsafe { (*av_frame).pts };
192 let duration_raw = unsafe { (*av_frame).duration };
193 let pix_fmt = boundary::from_av_pixel_format(format_raw);
194 let width = width_raw.max(0) as u32;
195 let height = height_raw.max(0) as u32;
196
197 if !pixdesc::is_deliverable(&pix_fmt) {
203 return Err(unsupported_pixel_format(pix_fmt, format_raw));
204 }
205 let geom = match pixdesc::plane_geometry(&pix_fmt, width as usize, height as usize) {
213 Some(g) => g,
214 None => return Err(unsupported_pixel_format(pix_fmt, format_raw)),
215 };
216
217 let mut planes_out: [Plane<FfmpegBuffer>; 4] = [
218 plane_placeholder()?,
219 plane_placeholder()?,
220 plane_placeholder()?,
221 plane_placeholder()?,
222 ];
223 let mut plane_count: u8 = 0;
224
225 #[allow(clippy::needless_range_loop)]
234 for plane_idx in 0..geom.count {
235 let linesize = unsafe { (*av_frame).linesize[plane_idx] };
238 if linesize <= 0 {
239 return Err(ConvertError::InvalidPlaneLayout { plane: plane_idx });
245 }
246 let data_ptr = unsafe { (*av_frame).data[plane_idx] };
247 if data_ptr.is_null() {
248 return Err(ConvertError::InvalidPlaneLayout { plane: plane_idx });
249 }
250 let plane_h = geom.height[plane_idx];
251 let row_bytes = geom.row_bytes[plane_idx];
252 if row_bytes > linesize as usize {
253 return Err(ConvertError::InvalidPlaneLayout { plane: plane_idx });
254 }
255 let (view, exported_stride) = if (linesize as usize) == row_bytes {
271 let plane_bytes = (plane_h)
272 .checked_mul(linesize as usize)
273 .ok_or(ConvertError::InvalidPlaneLayout { plane: plane_idx })?;
274 let buf = unsafe { find_backing_buffer(av_frame, data_ptr, plane_bytes) }
275 .ok_or(ConvertError::BufferAcquireFailed { plane: plane_idx })?;
276 let offset = unsafe { (data_ptr as usize).wrapping_sub((*buf).data as usize) };
280 let view = unsafe { FfmpegBuffer::from_ref_view(buf, offset, plane_bytes) }
283 .ok_or(ConvertError::BufferAcquireFailed { plane: plane_idx })?;
284 (view, linesize as u32)
285 } else {
286 let total_bytes = row_bytes
287 .checked_mul(plane_h)
288 .ok_or(ConvertError::InvalidPlaneLayout { plane: plane_idx })?;
289 let last_row_offset = (plane_h.saturating_sub(1))
299 .checked_mul(linesize as usize)
300 .ok_or(ConvertError::InvalidPlaneLayout { plane: plane_idx })?;
301 let readable_extent = last_row_offset
302 .checked_add(row_bytes)
303 .ok_or(ConvertError::InvalidPlaneLayout { plane: plane_idx })?;
304 unsafe { find_backing_buffer(av_frame, data_ptr, readable_extent) }
309 .ok_or(ConvertError::BufferAcquireFailed { plane: plane_idx })?;
310 let mut packed: std::vec::Vec<u8> = std::vec::Vec::new();
311 packed
312 .try_reserve_exact(total_bytes)
313 .map_err(|_| ConvertError::BufferAcquireFailed { plane: plane_idx })?;
314 for row_idx in 0..plane_h {
315 let row_offset = (row_idx)
316 .checked_mul(linesize as usize)
317 .ok_or(ConvertError::InvalidPlaneLayout { plane: plane_idx })?;
318 let row_slice =
323 unsafe { core::slice::from_raw_parts(data_ptr.add(row_offset) as *const u8, row_bytes) };
324 packed.extend_from_slice(row_slice);
325 }
326 let buf = FfmpegBuffer::copy_from_slice(&packed)
327 .ok_or(ConvertError::BufferAcquireFailed { plane: plane_idx })?;
328 (buf, row_bytes as u32)
329 };
330
331 planes_out[plane_idx] = Plane::new(view, exported_stride);
332 plane_count = (plane_idx + 1) as u8;
333 }
334
335 let pts = if pts_raw != AV_NOPTS_VALUE {
337 Some(Timestamp::new(pts_raw, time_base))
338 } else {
339 None
340 };
341 let duration = if duration_raw > 0 {
342 Some(Timestamp::new(duration_raw, time_base))
343 } else {
344 None
345 };
346
347 let visible_rect = unsafe { build_visible_rect(av_frame, width, height) };
349
350 let color_primaries_raw =
362 unsafe { read_unaligned(addr_of!((*av_frame).color_primaries) as *const i32) };
363 let color_trc_raw = unsafe { read_unaligned(addr_of!((*av_frame).color_trc) as *const i32) };
364 let colorspace_raw = unsafe { read_unaligned(addr_of!((*av_frame).colorspace) as *const i32) };
365 let color_range_raw = unsafe { read_unaligned(addr_of!((*av_frame).color_range) as *const i32) };
366 let chroma_location_raw =
367 unsafe { read_unaligned(addr_of!((*av_frame).chroma_location) as *const i32) };
368 let color = ColorInfo::UNSPECIFIED
369 .with_primaries(map_primaries(color_primaries_raw))
370 .with_transfer(map_transfer(color_trc_raw))
371 .with_matrix(map_matrix(colorspace_raw))
372 .with_range(map_range_for(&pix_fmt, color_range_raw))
373 .with_chroma_location(map_chroma_loc(chroma_location_raw));
374
375 let extra = unsafe { build_video_frame_extra(av_frame) };
377
378 let mut out = VideoFrame::new(
381 Dimensions::new(width, height),
382 pix_fmt,
383 planes_out,
384 plane_count,
385 extra,
386 )
387 .with_pts(pts)
388 .with_duration(duration)
389 .with_color(color);
390 if let Some(r) = visible_rect {
391 out = out.with_visible_rect(Some(r));
392 }
393 Ok(out)
394}
395
396fn plane_placeholder() -> Result<Plane<FfmpegBuffer>, ConvertError> {
397 let raw = unsafe { av_buffer_alloc(0) };
401 let raw = if raw.is_null() {
404 unsafe { av_buffer_alloc(1) }
405 } else {
406 raw
407 };
408 if raw.is_null() {
409 return Err(ConvertError::BufferAcquireFailed { plane: 4 });
411 }
412 let buf =
413 unsafe { FfmpegBuffer::take(raw) }.ok_or(ConvertError::BufferAcquireFailed { plane: 4 })?;
414 Ok(Plane::new(buf, 0))
415}
416
417unsafe fn build_visible_rect(av_frame: *const AVFrame, width: u32, height: u32) -> Option<Rect> {
423 let crop_left = unsafe { (*av_frame).crop_left } as u32;
424 let crop_top = unsafe { (*av_frame).crop_top } as u32;
425 let crop_right = unsafe { (*av_frame).crop_right } as u32;
426 let crop_bottom = unsafe { (*av_frame).crop_bottom } as u32;
427 if crop_left == 0 && crop_top == 0 && crop_right == 0 && crop_bottom == 0 {
428 return None;
429 }
430 let x = crop_left;
431 let y = crop_top;
432 let w = width.saturating_sub(crop_left).saturating_sub(crop_right);
433 let h = height.saturating_sub(crop_top).saturating_sub(crop_bottom);
434 Some(Rect::new(x, y, w, h))
435}
436
437unsafe fn build_video_frame_extra(av_frame: *const AVFrame) -> VideoFrameExtra {
442 let mut out = VideoFrameExtra::default();
443 let sar_num = unsafe { (*av_frame).sample_aspect_ratio.num };
445 let sar_den = unsafe { (*av_frame).sample_aspect_ratio.den };
446 if sar_num > 0 && sar_den > 0 && (sar_num != 1 || sar_den != 1) {
447 out.set_sample_aspect_ratio(Some((sar_num as u32, sar_den as u32)));
448 }
449 let pict_type_raw = unsafe { read_unaligned(addr_of!((*av_frame).pict_type) as *const i32) };
455 out.set_picture_type(map_picture_type_raw(pict_type_raw));
456 let flags = unsafe { (*av_frame).flags };
460 out.set_key_frame(flags & ffmpeg_next::ffi::AV_FRAME_FLAG_KEY != 0);
461 out.set_interlaced(flags & ffmpeg_next::ffi::AV_FRAME_FLAG_INTERLACED != 0);
462 out.set_top_field_first(flags & ffmpeg_next::ffi::AV_FRAME_FLAG_TOP_FIELD_FIRST != 0);
463 let bet = unsafe { (*av_frame).best_effort_timestamp };
465 if bet != AV_NOPTS_VALUE {
466 out.set_best_effort_timestamp(Some(bet));
467 }
468 out.set_side_data(unsafe { collect_side_data(av_frame) });
470 out
471}
472
473const SIDE_DATA_MAX_ENTRIES: usize = 64;
480const SIDE_DATA_MAX_TOTAL_BYTES: usize = 256 * 1024;
487
488const SUBTITLE_MAX_RECTS: usize = 64;
492const SUBTITLE_MAX_TEXT_BYTES_PER_RECT: usize = 64 * 1024;
496const SUBTITLE_MAX_TEXT_TOTAL_BYTES: usize = 256 * 1024;
499const SUBTITLE_MAX_BITMAP_BYTES_PER_RECT: usize = 16 * 1024 * 1024;
503const SUBTITLE_MAX_BITMAP_TOTAL_BYTES: usize = 32 * 1024 * 1024;
505
506unsafe fn bounded_cstr_bytes<'a>(ptr: *const core::ffi::c_char, cap: usize) -> Option<&'a [u8]> {
524 let max = cap.saturating_add(1);
527 for i in 0..max {
528 let byte = unsafe { *(ptr.add(i) as *const u8) };
532 if byte == 0 {
533 return Some(unsafe { core::slice::from_raw_parts(ptr as *const u8, i) });
536 }
537 }
538 None
541}
542
543unsafe fn collect_side_data(av_frame: *const AVFrame) -> std::vec::Vec<SideDataEntry> {
557 let nb_side_data_raw = unsafe { (*av_frame).nb_side_data };
563 let side_data = unsafe { (*av_frame).side_data };
564 if nb_side_data_raw <= 0 || side_data.is_null() {
565 return Vec::new();
566 }
567 let count_raw = nb_side_data_raw as usize;
568 let count = count_raw.min(SIDE_DATA_MAX_ENTRIES);
569 if count_raw > SIDE_DATA_MAX_ENTRIES {
570 tracing::warn!(
571 cap = SIDE_DATA_MAX_ENTRIES,
572 requested = count_raw,
573 "mediadecode-ffmpeg: AVFrame.nb_side_data exceeds entry cap; truncating",
574 );
575 }
576 let mut out: Vec<SideDataEntry> = Vec::new();
577 if out.try_reserve_exact(count).is_err() {
578 return Vec::new();
579 }
580 let mut total_bytes: usize = 0;
581 for i in 0..count {
582 let sd = unsafe { *side_data.add(i) };
583 if sd.is_null() {
584 continue;
585 }
586 let kind = unsafe { read_unaligned(addr_of!((*sd).type_) as *const i32) };
590 let size = unsafe { (*sd).size };
591 let data_ptr = unsafe { (*sd).data };
592 let data_slice = if size == 0 || data_ptr.is_null() {
593 Vec::new()
594 } else {
595 let projected = total_bytes.saturating_add(size);
599 if projected > SIDE_DATA_MAX_TOTAL_BYTES {
600 tracing::warn!(
601 cap = SIDE_DATA_MAX_TOTAL_BYTES,
602 projected,
603 "mediadecode-ffmpeg: AVFrame side-data byte cap reached; dropping remaining entries",
604 );
605 break;
606 }
607 total_bytes = projected;
608 let mut buf: Vec<u8> = Vec::new();
611 if buf.try_reserve_exact(size).is_err() {
612 continue;
613 }
614 let src = unsafe { core::slice::from_raw_parts(data_ptr, size) };
617 buf.extend_from_slice(src);
618 buf
619 };
620 out.push(SideDataEntry::new(kind, data_slice));
621 }
622 out
623}
624
625unsafe fn find_backing_buffer(
634 av_frame: *const AVFrame,
635 data_ptr: *const u8,
636 bytes: usize,
637) -> Option<*mut ffmpeg_next::ffi::AVBufferRef> {
638 let buf_array_len = unsafe { (*av_frame).buf.len() };
639 for i in 0..buf_array_len {
640 let buf = unsafe { (*av_frame).buf[i] };
641 if buf.is_null() {
642 continue;
643 }
644 let buf_data = unsafe { (*buf).data as *const u8 };
645 let buf_size = unsafe { (*buf).size };
646 if buf_data.is_null() {
647 continue;
648 }
649 let start = buf_data as usize;
650 let Some(end) = start.checked_add(buf_size) else {
651 continue;
652 };
653 let dp = data_ptr as usize;
654 let Some(dp_end) = dp.checked_add(bytes) else {
655 continue;
656 };
657 if dp >= start && dp_end <= end {
658 return Some(buf);
659 }
660 }
661 None
662}
663
664fn map_primaries(raw: i32) -> ColorPrimaries {
665 match raw {
666 x if x == AVColorPrimaries::AVCOL_PRI_BT709 as i32 => ColorPrimaries::Bt709,
667 x if x == AVColorPrimaries::AVCOL_PRI_UNSPECIFIED as i32 => ColorPrimaries::Unspecified,
668 x if x == AVColorPrimaries::AVCOL_PRI_BT470M as i32 => ColorPrimaries::Bt470M,
669 x if x == AVColorPrimaries::AVCOL_PRI_BT470BG as i32 => ColorPrimaries::Bt470Bg,
670 x if x == AVColorPrimaries::AVCOL_PRI_SMPTE170M as i32 => ColorPrimaries::Smpte170M,
671 x if x == AVColorPrimaries::AVCOL_PRI_SMPTE240M as i32 => ColorPrimaries::Smpte240M,
672 x if x == AVColorPrimaries::AVCOL_PRI_FILM as i32 => ColorPrimaries::Film,
673 x if x == AVColorPrimaries::AVCOL_PRI_BT2020 as i32 => ColorPrimaries::Bt2020,
674 x if x == AVColorPrimaries::AVCOL_PRI_SMPTE428 as i32 => ColorPrimaries::SmpteSt428,
675 x if x == AVColorPrimaries::AVCOL_PRI_SMPTE431 as i32 => ColorPrimaries::SmpteRp431,
676 x if x == AVColorPrimaries::AVCOL_PRI_SMPTE432 as i32 => ColorPrimaries::SmpteEg432,
677 x if x == AVColorPrimaries::AVCOL_PRI_EBU3213 as i32 => ColorPrimaries::Ebu3213E,
678 _ => ColorPrimaries::Unspecified,
679 }
680}
681
682fn map_transfer(raw: i32) -> ColorTransfer {
683 match raw {
684 x if x == AVColorTransferCharacteristic::AVCOL_TRC_BT709 as i32 => ColorTransfer::Bt709,
685 x if x == AVColorTransferCharacteristic::AVCOL_TRC_UNSPECIFIED as i32 => {
686 ColorTransfer::Unspecified
687 }
688 x if x == AVColorTransferCharacteristic::AVCOL_TRC_GAMMA22 as i32 => ColorTransfer::Gamma22,
689 x if x == AVColorTransferCharacteristic::AVCOL_TRC_GAMMA28 as i32 => ColorTransfer::Gamma28,
690 x if x == AVColorTransferCharacteristic::AVCOL_TRC_SMPTE170M as i32 => ColorTransfer::Smpte170M,
691 x if x == AVColorTransferCharacteristic::AVCOL_TRC_SMPTE240M as i32 => ColorTransfer::Smpte240M,
692 x if x == AVColorTransferCharacteristic::AVCOL_TRC_LINEAR as i32 => ColorTransfer::Linear,
693 x if x == AVColorTransferCharacteristic::AVCOL_TRC_LOG as i32 => ColorTransfer::Log100,
694 x if x == AVColorTransferCharacteristic::AVCOL_TRC_LOG_SQRT as i32 => ColorTransfer::Log316,
695 x if x == AVColorTransferCharacteristic::AVCOL_TRC_IEC61966_2_4 as i32 => {
696 ColorTransfer::Iec6196624
697 }
698 x if x == AVColorTransferCharacteristic::AVCOL_TRC_BT1361_ECG as i32 => {
699 ColorTransfer::Bt1361Ecg
700 }
701 x if x == AVColorTransferCharacteristic::AVCOL_TRC_IEC61966_2_1 as i32 => {
702 ColorTransfer::Iec6196621
703 }
704 x if x == AVColorTransferCharacteristic::AVCOL_TRC_BT2020_10 as i32 => {
705 ColorTransfer::Bt2020_10Bit
706 }
707 x if x == AVColorTransferCharacteristic::AVCOL_TRC_BT2020_12 as i32 => {
708 ColorTransfer::Bt2020_12Bit
709 }
710 x if x == AVColorTransferCharacteristic::AVCOL_TRC_SMPTE2084 as i32 => {
711 ColorTransfer::SmpteSt2084Pq
712 }
713 x if x == AVColorTransferCharacteristic::AVCOL_TRC_SMPTE428 as i32 => ColorTransfer::SmpteSt428,
714 x if x == AVColorTransferCharacteristic::AVCOL_TRC_ARIB_STD_B67 as i32 => {
715 ColorTransfer::AribStdB67Hlg
716 }
717 _ => ColorTransfer::Unspecified,
718 }
719}
720
721fn map_matrix(raw: i32) -> ColorMatrix {
722 match raw {
723 x if x == AVColorSpace::AVCOL_SPC_BT709 as i32 => ColorMatrix::Bt709,
724 x if x == AVColorSpace::AVCOL_SPC_BT2020_NCL as i32 => ColorMatrix::Bt2020Ncl,
725 x if x == AVColorSpace::AVCOL_SPC_SMPTE170M as i32 => ColorMatrix::Bt601,
726 x if x == AVColorSpace::AVCOL_SPC_BT470BG as i32 => ColorMatrix::Bt601,
727 x if x == AVColorSpace::AVCOL_SPC_SMPTE240M as i32 => ColorMatrix::Smpte240m,
728 x if x == AVColorSpace::AVCOL_SPC_FCC as i32 => ColorMatrix::Fcc,
729 x if x == AVColorSpace::AVCOL_SPC_YCGCO as i32 => ColorMatrix::YCgCo,
730 _ => ColorMatrix::Bt709, }
732}
733
734fn map_range(raw: i32) -> ColorRange {
735 match raw {
736 x if x == AVColorRange::AVCOL_RANGE_JPEG as i32 => ColorRange::Full,
737 x if x == AVColorRange::AVCOL_RANGE_MPEG as i32 => ColorRange::Limited,
738 _ => ColorRange::Unspecified,
739 }
740}
741
742fn is_yuvj(pix_fmt: &PixelFormat) -> bool {
748 matches!(
749 pix_fmt,
750 PixelFormat::Yuvj411p
751 | PixelFormat::Yuvj420p
752 | PixelFormat::Yuvj422p
753 | PixelFormat::Yuvj440p
754 | PixelFormat::Yuvj444p
755 )
756}
757
758fn map_range_for(pix_fmt: &PixelFormat, color_range_raw: i32) -> ColorRange {
770 if is_yuvj(pix_fmt) {
771 return ColorRange::Full;
772 }
773 map_range(color_range_raw)
774}
775
776fn map_chroma_loc(raw: i32) -> ChromaLocation {
777 match raw {
778 x if x == AVChromaLocation::AVCHROMA_LOC_LEFT as i32 => ChromaLocation::Left,
779 x if x == AVChromaLocation::AVCHROMA_LOC_CENTER as i32 => ChromaLocation::Center,
780 x if x == AVChromaLocation::AVCHROMA_LOC_TOPLEFT as i32 => ChromaLocation::TopLeft,
781 x if x == AVChromaLocation::AVCHROMA_LOC_TOP as i32 => ChromaLocation::Top,
782 x if x == AVChromaLocation::AVCHROMA_LOC_BOTTOMLEFT as i32 => ChromaLocation::BottomLeft,
783 x if x == AVChromaLocation::AVCHROMA_LOC_BOTTOM as i32 => ChromaLocation::Bottom,
784 _ => ChromaLocation::Unspecified,
785 }
786}
787
788pub unsafe fn av_frame_to_audio_frame(
803 av_frame: *const AVFrame,
804 time_base: Timebase,
805) -> Result<
806 AudioFrame<SampleFormat, ChannelLayoutDescription, AudioFrameExtra, FfmpegBuffer>,
807 ConvertError,
808> {
809 if av_frame.is_null() {
810 return Err(ConvertError::NullFrame);
811 }
812 let format_raw = unsafe { (*av_frame).format };
820 let sample_rate_raw = unsafe { (*av_frame).sample_rate };
821 let nb_samples_raw = unsafe { (*av_frame).nb_samples };
822 let pts_raw = unsafe { (*av_frame).pts };
823 let duration_raw = unsafe { (*av_frame).duration };
824 let bet_raw = unsafe { (*av_frame).best_effort_timestamp };
825
826 let sample_format = SampleFormat::from_raw(format_raw);
827 let sample_rate = sample_rate_raw.max(0) as u32;
828 let nb_samples = nb_samples_raw.max(0) as u32;
829
830 let ch_layout_ptr = unsafe { addr_of!((*av_frame).ch_layout) };
834 let channel_layout =
835 unsafe { crate::channel_layout::channel_layout_description_from_raw_ptr(ch_layout_ptr) };
836 let channel_count_full = channel_layout.channels();
837 let channel_count = channel_count_full.min(255) as u8;
838
839 let is_planar = sample_format.is_planar();
841 let plane_count_full = if is_planar { channel_count as usize } else { 1 };
842 if plane_count_full > 8 {
850 return Err(ConvertError::InvalidPlaneLayout { plane: 8 });
851 }
852 let plane_count = plane_count_full as u8;
853
854 let linesize0 = unsafe { (*av_frame).linesize[0] };
861 if nb_samples > 0 && linesize0 <= 0 {
862 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
863 }
864 let plane_bytes = linesize0.max(0) as usize;
865 if nb_samples > 0 {
866 let bytes_per_sample = sample_format
867 .bytes_per_sample()
868 .ok_or(ConvertError::InvalidPlaneLayout { plane: 0 })? as usize;
869 let expected_per_plane = if is_planar {
870 (nb_samples as usize)
872 .checked_mul(bytes_per_sample)
873 .ok_or(ConvertError::InvalidPlaneLayout { plane: 0 })?
874 } else {
875 (nb_samples as usize)
877 .checked_mul(bytes_per_sample)
878 .and_then(|x| x.checked_mul(channel_count.max(1) as usize))
879 .ok_or(ConvertError::InvalidPlaneLayout { plane: 0 })?
880 };
881 if plane_bytes < expected_per_plane {
882 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
883 }
884 }
885
886 let mut planes_out: [Plane<FfmpegBuffer>; 8] = [
887 audio_plane_placeholder()?,
888 audio_plane_placeholder()?,
889 audio_plane_placeholder()?,
890 audio_plane_placeholder()?,
891 audio_plane_placeholder()?,
892 audio_plane_placeholder()?,
893 audio_plane_placeholder()?,
894 audio_plane_placeholder()?,
895 ];
896
897 #[allow(clippy::needless_range_loop)]
901 for plane_idx in 0..plane_count as usize {
902 let data_ptr = unsafe { (*av_frame).data[plane_idx] };
903 if data_ptr.is_null() {
904 return Err(ConvertError::InvalidPlaneLayout { plane: plane_idx });
910 }
911 let buf = unsafe { find_audio_backing_buffer(av_frame, data_ptr, plane_bytes) }
912 .ok_or(ConvertError::BufferAcquireFailed { plane: plane_idx })?;
913 let offset = unsafe { (data_ptr as usize).wrapping_sub((*buf).data as usize) };
916 let view = unsafe { FfmpegBuffer::from_ref_view(buf, offset, plane_bytes) }
919 .ok_or(ConvertError::BufferAcquireFailed { plane: plane_idx })?;
920 planes_out[plane_idx] = Plane::new(view, plane_bytes as u32);
921 }
922
923 let pts = if pts_raw != AV_NOPTS_VALUE {
924 Some(Timestamp::new(pts_raw, time_base))
925 } else {
926 None
927 };
928 let duration = if duration_raw > 0 {
929 Some(Timestamp::new(duration_raw, time_base))
930 } else {
931 None
932 };
933
934 let mut extra = AudioFrameExtra::default();
935 if bet_raw != AV_NOPTS_VALUE {
936 extra.set_best_effort_timestamp(Some(bet_raw));
937 }
938 extra.set_side_data(unsafe { collect_side_data(av_frame) });
942
943 Ok(
944 AudioFrame::new(
945 sample_rate,
946 nb_samples,
947 channel_count,
948 sample_format,
949 channel_layout,
950 planes_out,
951 plane_count,
952 extra,
953 )
954 .with_pts(pts)
955 .with_duration(duration),
956 )
957}
958
959fn audio_plane_placeholder() -> Result<Plane<FfmpegBuffer>, ConvertError> {
960 let raw = unsafe { av_buffer_alloc(1) };
961 if raw.is_null() {
962 return Err(ConvertError::BufferAcquireFailed { plane: 8 });
963 }
964 let buf =
965 unsafe { FfmpegBuffer::take(raw) }.ok_or(ConvertError::BufferAcquireFailed { plane: 8 })?;
966 Ok(Plane::new(buf, 0))
967}
968
969unsafe fn find_audio_backing_buffer(
972 av_frame: *const AVFrame,
973 data_ptr: *const u8,
974 bytes: usize,
975) -> Option<*mut ffmpeg_next::ffi::AVBufferRef> {
976 let buf_array_len = unsafe { (*av_frame).buf.len() };
981 for i in 0..buf_array_len {
982 let buf = unsafe { (*av_frame).buf[i] };
983 if buf.is_null() {
984 continue;
985 }
986 let buf_data = unsafe { (*buf).data as *const u8 };
987 let buf_size = unsafe { (*buf).size };
988 if buf_data.is_null() {
989 continue;
990 }
991 let start = buf_data as usize;
992 let Some(end) = start.checked_add(buf_size) else {
993 continue;
994 };
995 let dp = data_ptr as usize;
996 let Some(dp_end) = dp.checked_add(bytes) else {
997 continue;
998 };
999 if dp >= start && dp_end <= end {
1000 return Some(buf);
1001 }
1002 }
1003 None
1004}
1005
1006pub unsafe fn av_subtitle_to_subtitle_frame(
1030 av_subtitle: *const ffmpeg_next::ffi::AVSubtitle,
1031 time_base: Timebase,
1032) -> Result<SubtitleFrame<SubtitleFrameExtra, FfmpegBuffer>, ConvertError> {
1033 if av_subtitle.is_null() {
1034 return Err(ConvertError::NullFrame);
1035 }
1036 let mut text_chunks: std::vec::Vec<u8> = std::vec::Vec::new();
1041 let mut bitmap_regions: std::vec::Vec<mediadecode::subtitle::BitmapRegion<FfmpegBuffer>> =
1042 std::vec::Vec::new();
1043
1044 let count_raw = unsafe { (*av_subtitle).num_rects } as usize;
1045 let rects_ptr = unsafe { (*av_subtitle).rects };
1046 if count_raw > 0 && rects_ptr.is_null() {
1050 return Err(ConvertError::NullFrame);
1051 }
1052 let count = count_raw.min(SUBTITLE_MAX_RECTS);
1061 if count_raw > SUBTITLE_MAX_RECTS {
1062 tracing::warn!(
1063 cap = SUBTITLE_MAX_RECTS,
1064 requested = count_raw,
1065 "mediadecode-ffmpeg: AVSubtitle.num_rects exceeds rect cap; truncating",
1066 );
1067 }
1068 let mut text_total_bytes: usize = 0;
1069 let mut bitmap_total_bytes: usize = 0;
1070
1071 let text_kind = AVSubtitleType::SUBTITLE_TEXT as i32;
1072 let ass_kind = AVSubtitleType::SUBTITLE_ASS as i32;
1073 let bitmap_kind = AVSubtitleType::SUBTITLE_BITMAP as i32;
1074 for i in 0..count {
1075 let rect_ptr = unsafe { *rects_ptr.add(i) };
1079 if rect_ptr.is_null() {
1080 continue;
1081 }
1082 let rect_type_raw = unsafe { read_unaligned(addr_of!((*rect_ptr).type_) as *const i32) };
1088 let rect_text_ptr = unsafe { (*rect_ptr).text };
1091 let rect_ass_ptr = unsafe { (*rect_ptr).ass };
1092 let rect_data0_ptr = unsafe { (*rect_ptr).data[0] };
1093 let rect_data1_ptr = unsafe { (*rect_ptr).data[1] };
1094 let rect_linesize0 = unsafe { (*rect_ptr).linesize[0] };
1095 let rect_w = unsafe { (*rect_ptr).w };
1096 let rect_h = unsafe { (*rect_ptr).h };
1097 let rect_x = unsafe { (*rect_ptr).x };
1098 let rect_y = unsafe { (*rect_ptr).y };
1099
1100 match rect_type_raw {
1101 x if x == text_kind && !rect_text_ptr.is_null() => {
1102 let bytes = unsafe { bounded_cstr_bytes(rect_text_ptr, SUBTITLE_MAX_TEXT_BYTES_PER_RECT) }
1112 .ok_or(ConvertError::InvalidPlaneLayout { plane: 0 })?;
1113 if bytes.len() > SUBTITLE_MAX_TEXT_BYTES_PER_RECT {
1117 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
1118 }
1119 let separator = if text_chunks.is_empty() { 0 } else { 1 };
1120 let projected = text_total_bytes
1121 .saturating_add(bytes.len())
1122 .saturating_add(separator);
1123 if projected > SUBTITLE_MAX_TEXT_TOTAL_BYTES {
1124 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
1125 }
1126 if separator == 1 {
1127 text_chunks.push(b'\n');
1128 }
1129 text_chunks.extend_from_slice(bytes);
1130 text_total_bytes = projected;
1131 }
1132 x if x == ass_kind && !rect_ass_ptr.is_null() => {
1133 let bytes = unsafe { bounded_cstr_bytes(rect_ass_ptr, SUBTITLE_MAX_TEXT_BYTES_PER_RECT) }
1136 .ok_or(ConvertError::InvalidPlaneLayout { plane: 0 })?;
1137 if bytes.len() > SUBTITLE_MAX_TEXT_BYTES_PER_RECT {
1138 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
1139 }
1140 let separator = if text_chunks.is_empty() { 0 } else { 1 };
1141 let projected = text_total_bytes
1142 .saturating_add(bytes.len())
1143 .saturating_add(separator);
1144 if projected > SUBTITLE_MAX_TEXT_TOTAL_BYTES {
1145 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
1146 }
1147 if separator == 1 {
1148 text_chunks.push(b'\n');
1149 }
1150 text_chunks.extend_from_slice(bytes);
1151 text_total_bytes = projected;
1152 }
1153 x if x == bitmap_kind => {
1154 let w = rect_w.max(0) as u32;
1158 let h = rect_h.max(0) as u32;
1159 let stride = rect_linesize0.max(0) as u32;
1160 if rect_data0_ptr.is_null() || stride == 0 || h == 0 {
1161 continue;
1162 }
1163 let data_len = (stride as usize)
1167 .checked_mul(h as usize)
1168 .ok_or(ConvertError::InvalidPlaneLayout { plane: 0 })?;
1169 if data_len > SUBTITLE_MAX_BITMAP_BYTES_PER_RECT {
1173 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
1174 }
1175 let projected_total = bitmap_total_bytes.saturating_add(data_len);
1176 if projected_total > SUBTITLE_MAX_BITMAP_TOTAL_BYTES {
1177 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
1178 }
1179 let data_slice = unsafe { core::slice::from_raw_parts(rect_data0_ptr, data_len) };
1182 let data_buf = FfmpegBuffer::copy_from_slice(data_slice)
1183 .ok_or(ConvertError::BufferAcquireFailed { plane: 0 })?;
1184 let palette_len = 256 * 4;
1185 let palette_buf = if rect_data1_ptr.is_null() {
1186 FfmpegBuffer::copy_from_slice(&[])
1187 .ok_or(ConvertError::BufferAcquireFailed { plane: 1 })?
1188 } else {
1189 let p = unsafe { core::slice::from_raw_parts(rect_data1_ptr, palette_len) };
1191 FfmpegBuffer::copy_from_slice(p).ok_or(ConvertError::BufferAcquireFailed { plane: 1 })?
1192 };
1193 bitmap_regions.push(mediadecode::subtitle::BitmapRegion::new(
1194 rect_x.max(0) as u32,
1195 rect_y.max(0) as u32,
1196 w,
1197 h,
1198 stride,
1199 data_buf,
1200 palette_buf,
1201 ));
1202 bitmap_total_bytes = projected_total;
1203 }
1204 _ => {}
1205 }
1206 }
1207
1208 let payload = if !text_chunks.is_empty() {
1209 let buf = FfmpegBuffer::copy_from_slice(&text_chunks)
1210 .ok_or(ConvertError::BufferAcquireFailed { plane: 0 })?;
1211 SubtitlePayload::Text {
1212 text: buf,
1213 language: None,
1214 }
1215 } else if !bitmap_regions.is_empty() {
1216 SubtitlePayload::Bitmap {
1217 regions: bitmap_regions,
1218 }
1219 } else {
1220 let buf =
1222 FfmpegBuffer::copy_from_slice(&[]).ok_or(ConvertError::BufferAcquireFailed { plane: 0 })?;
1223 SubtitlePayload::Text {
1224 text: buf,
1225 language: None,
1226 }
1227 };
1228
1229 let sub_pts = unsafe { (*av_subtitle).pts };
1230 let pts = if sub_pts != AV_NOPTS_VALUE {
1231 Some(Timestamp::new(sub_pts, time_base))
1232 } else {
1233 None
1234 };
1235
1236 let extra = SubtitleFrameExtra::new(unsafe { (*av_subtitle).start_display_time }, unsafe {
1237 (*av_subtitle).end_display_time
1238 });
1239
1240 Ok(SubtitleFrame::new(payload, extra).with_pts(pts))
1241}
1242
1243fn map_picture_type_raw(raw: i32) -> PictureType {
1244 match raw {
1245 x if x == AVPictureType::AV_PICTURE_TYPE_I as i32 => PictureType::I,
1246 x if x == AVPictureType::AV_PICTURE_TYPE_P as i32 => PictureType::P,
1247 x if x == AVPictureType::AV_PICTURE_TYPE_B as i32 => PictureType::B,
1248 x if x == AVPictureType::AV_PICTURE_TYPE_S as i32 => PictureType::S,
1249 x if x == AVPictureType::AV_PICTURE_TYPE_SI as i32 => PictureType::Si,
1250 x if x == AVPictureType::AV_PICTURE_TYPE_SP as i32 => PictureType::Sp,
1251 x if x == AVPictureType::AV_PICTURE_TYPE_BI as i32 => PictureType::Bi,
1252 _ => PictureType::Unspecified,
1253 }
1254}
1255
1256#[cfg(test)]
1257mod tests;