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 channel::AudioChannelLayout,
19 color::{ChromaLocation, ColorInfo, ColorMatrix, ColorPrimaries, ColorRange, ColorTransfer},
20 frame::{AudioFrame, Dimensions, Plane, Rect, SubtitleFrame, VideoFrame},
21 subtitle::SubtitlePayload,
22};
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<AudioFrame<SampleFormat, AudioChannelLayout, AudioFrameExtra, FfmpegBuffer>, ConvertError>
136{
137 unsafe { av_frame_to_audio_frame(frame.as_ptr(), time_base) }
140}
141
142pub fn subtitle_frame_from(
145 subtitle: &ffmpeg_next::Subtitle,
146 time_base: Timebase,
147) -> Result<SubtitleFrame<SubtitleFrameExtra, FfmpegBuffer>, ConvertError> {
148 unsafe { av_subtitle_to_subtitle_frame(subtitle.as_ptr(), time_base) }
151}
152
153pub unsafe fn av_frame_to_video_frame(
166 av_frame: *const AVFrame,
167 time_base: Timebase,
168) -> Result<VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, FfmpegBuffer>, ConvertError> {
169 if av_frame.is_null() {
170 return Err(ConvertError::NullFrame);
171 }
172 let format_raw = unsafe { (*av_frame).format };
187 let width_raw = unsafe { (*av_frame).width };
188 let height_raw = unsafe { (*av_frame).height };
189 let pts_raw = unsafe { (*av_frame).pts };
190 let duration_raw = unsafe { (*av_frame).duration };
191 let pix_fmt = boundary::from_av_pixel_format(format_raw);
192 let width = width_raw.max(0) as u32;
193 let height = height_raw.max(0) as u32;
194
195 if !pixdesc::is_deliverable(&pix_fmt) {
201 return Err(unsupported_pixel_format(pix_fmt, format_raw));
202 }
203 let geom = match pixdesc::plane_geometry(&pix_fmt, width as usize, height as usize) {
211 Some(g) => g,
212 None => return Err(unsupported_pixel_format(pix_fmt, format_raw)),
213 };
214
215 let mut planes_out: [Plane<FfmpegBuffer>; 4] = [
216 plane_placeholder()?,
217 plane_placeholder()?,
218 plane_placeholder()?,
219 plane_placeholder()?,
220 ];
221 let mut plane_count: u8 = 0;
222
223 #[allow(clippy::needless_range_loop)]
232 for plane_idx in 0..geom.count {
233 let linesize = unsafe { (*av_frame).linesize[plane_idx] };
236 if linesize <= 0 {
237 return Err(ConvertError::InvalidPlaneLayout { plane: plane_idx });
243 }
244 let data_ptr = unsafe { (*av_frame).data[plane_idx] };
245 if data_ptr.is_null() {
246 return Err(ConvertError::InvalidPlaneLayout { plane: plane_idx });
247 }
248 let plane_h = geom.height[plane_idx];
249 let row_bytes = geom.row_bytes[plane_idx];
250 if row_bytes > linesize as usize {
251 return Err(ConvertError::InvalidPlaneLayout { plane: plane_idx });
252 }
253 let (view, exported_stride) = if (linesize as usize) == row_bytes {
269 let plane_bytes = (plane_h)
270 .checked_mul(linesize as usize)
271 .ok_or(ConvertError::InvalidPlaneLayout { plane: plane_idx })?;
272 let buf = unsafe { find_backing_buffer(av_frame, data_ptr, plane_bytes) }
273 .ok_or(ConvertError::BufferAcquireFailed { plane: plane_idx })?;
274 let offset = unsafe { (data_ptr as usize).wrapping_sub((*buf).data as usize) };
278 let view = unsafe { FfmpegBuffer::from_ref_view(buf, offset, plane_bytes) }
281 .ok_or(ConvertError::BufferAcquireFailed { plane: plane_idx })?;
282 (view, linesize as u32)
283 } else {
284 let total_bytes = row_bytes
285 .checked_mul(plane_h)
286 .ok_or(ConvertError::InvalidPlaneLayout { plane: plane_idx })?;
287 let last_row_offset = (plane_h.saturating_sub(1))
297 .checked_mul(linesize as usize)
298 .ok_or(ConvertError::InvalidPlaneLayout { plane: plane_idx })?;
299 let readable_extent = last_row_offset
300 .checked_add(row_bytes)
301 .ok_or(ConvertError::InvalidPlaneLayout { plane: plane_idx })?;
302 unsafe { find_backing_buffer(av_frame, data_ptr, readable_extent) }
307 .ok_or(ConvertError::BufferAcquireFailed { plane: plane_idx })?;
308 let mut packed: std::vec::Vec<u8> = std::vec::Vec::new();
309 packed
310 .try_reserve_exact(total_bytes)
311 .map_err(|_| ConvertError::BufferAcquireFailed { plane: plane_idx })?;
312 for row_idx in 0..plane_h {
313 let row_offset = (row_idx)
314 .checked_mul(linesize as usize)
315 .ok_or(ConvertError::InvalidPlaneLayout { plane: plane_idx })?;
316 let row_slice =
321 unsafe { core::slice::from_raw_parts(data_ptr.add(row_offset) as *const u8, row_bytes) };
322 packed.extend_from_slice(row_slice);
323 }
324 let buf = FfmpegBuffer::copy_from_slice(&packed)
325 .ok_or(ConvertError::BufferAcquireFailed { plane: plane_idx })?;
326 (buf, row_bytes as u32)
327 };
328
329 planes_out[plane_idx] = Plane::new(view, exported_stride);
330 plane_count = (plane_idx + 1) as u8;
331 }
332
333 let pts = if pts_raw != AV_NOPTS_VALUE {
335 Some(Timestamp::new(pts_raw, time_base))
336 } else {
337 None
338 };
339 let duration = if duration_raw > 0 {
340 Some(Timestamp::new(duration_raw, time_base))
341 } else {
342 None
343 };
344
345 let visible_rect = unsafe { build_visible_rect(av_frame, width, height) };
347
348 let color_primaries_raw =
360 unsafe { read_unaligned(addr_of!((*av_frame).color_primaries) as *const i32) };
361 let color_trc_raw = unsafe { read_unaligned(addr_of!((*av_frame).color_trc) as *const i32) };
362 let colorspace_raw = unsafe { read_unaligned(addr_of!((*av_frame).colorspace) as *const i32) };
363 let color_range_raw = unsafe { read_unaligned(addr_of!((*av_frame).color_range) as *const i32) };
364 let chroma_location_raw =
365 unsafe { read_unaligned(addr_of!((*av_frame).chroma_location) as *const i32) };
366 let color = ColorInfo::UNSPECIFIED
367 .with_primaries(map_primaries(color_primaries_raw))
368 .with_transfer(map_transfer(color_trc_raw))
369 .with_matrix(map_matrix(colorspace_raw))
370 .with_range(map_range_for(&pix_fmt, color_range_raw))
371 .with_chroma_location(map_chroma_loc(chroma_location_raw));
372
373 let extra = unsafe { build_video_frame_extra(av_frame) };
375
376 let mut out = VideoFrame::new(
379 Dimensions::new(width, height),
380 pix_fmt,
381 planes_out,
382 plane_count,
383 extra,
384 )
385 .with_pts(pts)
386 .with_duration(duration)
387 .with_color(color);
388 if let Some(r) = visible_rect {
389 out = out.with_visible_rect(Some(r));
390 }
391 Ok(out)
392}
393
394fn plane_placeholder() -> Result<Plane<FfmpegBuffer>, ConvertError> {
395 let raw = unsafe { av_buffer_alloc(0) };
399 let raw = if raw.is_null() {
402 unsafe { av_buffer_alloc(1) }
403 } else {
404 raw
405 };
406 if raw.is_null() {
407 return Err(ConvertError::BufferAcquireFailed { plane: 4 });
409 }
410 let buf =
411 unsafe { FfmpegBuffer::take(raw) }.ok_or(ConvertError::BufferAcquireFailed { plane: 4 })?;
412 Ok(Plane::new(buf, 0))
413}
414
415unsafe fn build_visible_rect(av_frame: *const AVFrame, width: u32, height: u32) -> Option<Rect> {
421 let crop_left = unsafe { (*av_frame).crop_left } as u32;
422 let crop_top = unsafe { (*av_frame).crop_top } as u32;
423 let crop_right = unsafe { (*av_frame).crop_right } as u32;
424 let crop_bottom = unsafe { (*av_frame).crop_bottom } as u32;
425 if crop_left == 0 && crop_top == 0 && crop_right == 0 && crop_bottom == 0 {
426 return None;
427 }
428 let x = crop_left;
429 let y = crop_top;
430 let w = width.saturating_sub(crop_left).saturating_sub(crop_right);
431 let h = height.saturating_sub(crop_top).saturating_sub(crop_bottom);
432 Some(Rect::new(x, y, w, h))
433}
434
435unsafe fn build_video_frame_extra(av_frame: *const AVFrame) -> VideoFrameExtra {
440 let mut out = VideoFrameExtra::default();
441 let sar_num = unsafe { (*av_frame).sample_aspect_ratio.num };
443 let sar_den = unsafe { (*av_frame).sample_aspect_ratio.den };
444 if sar_num > 0 && sar_den > 0 && (sar_num != 1 || sar_den != 1) {
445 out.set_sample_aspect_ratio(Some((sar_num as u32, sar_den as u32)));
446 }
447 let pict_type_raw = unsafe { read_unaligned(addr_of!((*av_frame).pict_type) as *const i32) };
453 out.set_picture_type(map_picture_type_raw(pict_type_raw));
454 let flags = unsafe { (*av_frame).flags };
458 out.set_key_frame(flags & ffmpeg_next::ffi::AV_FRAME_FLAG_KEY != 0);
459 out.set_interlaced(flags & ffmpeg_next::ffi::AV_FRAME_FLAG_INTERLACED != 0);
460 out.set_top_field_first(flags & ffmpeg_next::ffi::AV_FRAME_FLAG_TOP_FIELD_FIRST != 0);
461 let bet = unsafe { (*av_frame).best_effort_timestamp };
463 if bet != AV_NOPTS_VALUE {
464 out.set_best_effort_timestamp(Some(bet));
465 }
466 out.set_side_data(unsafe { collect_side_data(av_frame) });
468 out
469}
470
471const SIDE_DATA_MAX_ENTRIES: usize = 64;
478const SIDE_DATA_MAX_TOTAL_BYTES: usize = 256 * 1024;
485
486const SUBTITLE_MAX_RECTS: usize = 64;
490const SUBTITLE_MAX_TEXT_BYTES_PER_RECT: usize = 64 * 1024;
494const SUBTITLE_MAX_TEXT_TOTAL_BYTES: usize = 256 * 1024;
497const SUBTITLE_MAX_BITMAP_BYTES_PER_RECT: usize = 16 * 1024 * 1024;
501const SUBTITLE_MAX_BITMAP_TOTAL_BYTES: usize = 32 * 1024 * 1024;
503
504unsafe fn bounded_cstr_bytes<'a>(ptr: *const core::ffi::c_char, cap: usize) -> Option<&'a [u8]> {
522 let max = cap.saturating_add(1);
525 for i in 0..max {
526 let byte = unsafe { *(ptr.add(i) as *const u8) };
530 if byte == 0 {
531 return Some(unsafe { core::slice::from_raw_parts(ptr as *const u8, i) });
534 }
535 }
536 None
539}
540
541unsafe fn collect_side_data(av_frame: *const AVFrame) -> std::vec::Vec<SideDataEntry> {
555 let nb_side_data_raw = unsafe { (*av_frame).nb_side_data };
561 let side_data = unsafe { (*av_frame).side_data };
562 if nb_side_data_raw <= 0 || side_data.is_null() {
563 return Vec::new();
564 }
565 let count_raw = nb_side_data_raw as usize;
566 let count = count_raw.min(SIDE_DATA_MAX_ENTRIES);
567 if count_raw > SIDE_DATA_MAX_ENTRIES {
568 tracing::warn!(
569 cap = SIDE_DATA_MAX_ENTRIES,
570 requested = count_raw,
571 "mediadecode-ffmpeg: AVFrame.nb_side_data exceeds entry cap; truncating",
572 );
573 }
574 let mut out: Vec<SideDataEntry> = Vec::new();
575 if out.try_reserve_exact(count).is_err() {
576 return Vec::new();
577 }
578 let mut total_bytes: usize = 0;
579 for i in 0..count {
580 let sd = unsafe { *side_data.add(i) };
581 if sd.is_null() {
582 continue;
583 }
584 let kind = unsafe { read_unaligned(addr_of!((*sd).type_) as *const i32) };
588 let size = unsafe { (*sd).size };
589 let data_ptr = unsafe { (*sd).data };
590 let data_slice = if size == 0 || data_ptr.is_null() {
591 Vec::new()
592 } else {
593 let projected = total_bytes.saturating_add(size);
597 if projected > SIDE_DATA_MAX_TOTAL_BYTES {
598 tracing::warn!(
599 cap = SIDE_DATA_MAX_TOTAL_BYTES,
600 projected,
601 "mediadecode-ffmpeg: AVFrame side-data byte cap reached; dropping remaining entries",
602 );
603 break;
604 }
605 total_bytes = projected;
606 let mut buf: Vec<u8> = Vec::new();
609 if buf.try_reserve_exact(size).is_err() {
610 continue;
611 }
612 let src = unsafe { core::slice::from_raw_parts(data_ptr, size) };
615 buf.extend_from_slice(src);
616 buf
617 };
618 out.push(SideDataEntry::new(kind, data_slice));
619 }
620 out
621}
622
623unsafe fn find_backing_buffer(
632 av_frame: *const AVFrame,
633 data_ptr: *const u8,
634 bytes: usize,
635) -> Option<*mut ffmpeg_next::ffi::AVBufferRef> {
636 let buf_array_len = unsafe { (*av_frame).buf.len() };
637 for i in 0..buf_array_len {
638 let buf = unsafe { (*av_frame).buf[i] };
639 if buf.is_null() {
640 continue;
641 }
642 let buf_data = unsafe { (*buf).data as *const u8 };
643 let buf_size = unsafe { (*buf).size };
644 if buf_data.is_null() {
645 continue;
646 }
647 let start = buf_data as usize;
648 let Some(end) = start.checked_add(buf_size) else {
649 continue;
650 };
651 let dp = data_ptr as usize;
652 let Some(dp_end) = dp.checked_add(bytes) else {
653 continue;
654 };
655 if dp >= start && dp_end <= end {
656 return Some(buf);
657 }
658 }
659 None
660}
661
662fn map_primaries(raw: i32) -> ColorPrimaries {
663 match raw {
664 x if x == AVColorPrimaries::AVCOL_PRI_BT709 as i32 => ColorPrimaries::Bt709,
665 x if x == AVColorPrimaries::AVCOL_PRI_UNSPECIFIED as i32 => ColorPrimaries::Unspecified,
666 x if x == AVColorPrimaries::AVCOL_PRI_BT470M as i32 => ColorPrimaries::Bt470M,
667 x if x == AVColorPrimaries::AVCOL_PRI_BT470BG as i32 => ColorPrimaries::Bt470Bg,
668 x if x == AVColorPrimaries::AVCOL_PRI_SMPTE170M as i32 => ColorPrimaries::Smpte170M,
669 x if x == AVColorPrimaries::AVCOL_PRI_SMPTE240M as i32 => ColorPrimaries::Smpte240M,
670 x if x == AVColorPrimaries::AVCOL_PRI_FILM as i32 => ColorPrimaries::Film,
671 x if x == AVColorPrimaries::AVCOL_PRI_BT2020 as i32 => ColorPrimaries::Bt2020,
672 x if x == AVColorPrimaries::AVCOL_PRI_SMPTE428 as i32 => ColorPrimaries::SmpteSt428,
673 x if x == AVColorPrimaries::AVCOL_PRI_SMPTE431 as i32 => ColorPrimaries::SmpteRp431,
674 x if x == AVColorPrimaries::AVCOL_PRI_SMPTE432 as i32 => ColorPrimaries::SmpteEg432,
675 x if x == AVColorPrimaries::AVCOL_PRI_EBU3213 as i32 => ColorPrimaries::Ebu3213E,
676 _ => ColorPrimaries::Unspecified,
677 }
678}
679
680fn map_transfer(raw: i32) -> ColorTransfer {
681 match raw {
682 x if x == AVColorTransferCharacteristic::AVCOL_TRC_BT709 as i32 => ColorTransfer::Bt709,
683 x if x == AVColorTransferCharacteristic::AVCOL_TRC_UNSPECIFIED as i32 => {
684 ColorTransfer::Unspecified
685 }
686 x if x == AVColorTransferCharacteristic::AVCOL_TRC_GAMMA22 as i32 => ColorTransfer::Gamma22,
687 x if x == AVColorTransferCharacteristic::AVCOL_TRC_GAMMA28 as i32 => ColorTransfer::Gamma28,
688 x if x == AVColorTransferCharacteristic::AVCOL_TRC_SMPTE170M as i32 => ColorTransfer::Smpte170M,
689 x if x == AVColorTransferCharacteristic::AVCOL_TRC_SMPTE240M as i32 => ColorTransfer::Smpte240M,
690 x if x == AVColorTransferCharacteristic::AVCOL_TRC_LINEAR as i32 => ColorTransfer::Linear,
691 x if x == AVColorTransferCharacteristic::AVCOL_TRC_LOG as i32 => ColorTransfer::Log100,
692 x if x == AVColorTransferCharacteristic::AVCOL_TRC_LOG_SQRT as i32 => ColorTransfer::Log316,
693 x if x == AVColorTransferCharacteristic::AVCOL_TRC_IEC61966_2_4 as i32 => {
694 ColorTransfer::Iec6196624
695 }
696 x if x == AVColorTransferCharacteristic::AVCOL_TRC_BT1361_ECG as i32 => {
697 ColorTransfer::Bt1361Ecg
698 }
699 x if x == AVColorTransferCharacteristic::AVCOL_TRC_IEC61966_2_1 as i32 => {
700 ColorTransfer::Iec6196621
701 }
702 x if x == AVColorTransferCharacteristic::AVCOL_TRC_BT2020_10 as i32 => {
703 ColorTransfer::Bt2020_10Bit
704 }
705 x if x == AVColorTransferCharacteristic::AVCOL_TRC_BT2020_12 as i32 => {
706 ColorTransfer::Bt2020_12Bit
707 }
708 x if x == AVColorTransferCharacteristic::AVCOL_TRC_SMPTE2084 as i32 => {
709 ColorTransfer::SmpteSt2084Pq
710 }
711 x if x == AVColorTransferCharacteristic::AVCOL_TRC_SMPTE428 as i32 => ColorTransfer::SmpteSt428,
712 x if x == AVColorTransferCharacteristic::AVCOL_TRC_ARIB_STD_B67 as i32 => {
713 ColorTransfer::AribStdB67Hlg
714 }
715 _ => ColorTransfer::Unspecified,
716 }
717}
718
719fn map_matrix(raw: i32) -> ColorMatrix {
720 match raw {
721 x if x == AVColorSpace::AVCOL_SPC_BT709 as i32 => ColorMatrix::Bt709,
722 x if x == AVColorSpace::AVCOL_SPC_BT2020_NCL as i32 => ColorMatrix::Bt2020Ncl,
723 x if x == AVColorSpace::AVCOL_SPC_SMPTE170M as i32 => ColorMatrix::Bt601,
724 x if x == AVColorSpace::AVCOL_SPC_BT470BG as i32 => ColorMatrix::Bt601,
725 x if x == AVColorSpace::AVCOL_SPC_SMPTE240M as i32 => ColorMatrix::Smpte240m,
726 x if x == AVColorSpace::AVCOL_SPC_FCC as i32 => ColorMatrix::Fcc,
727 x if x == AVColorSpace::AVCOL_SPC_YCGCO as i32 => ColorMatrix::YCgCo,
728 _ => ColorMatrix::Bt709, }
730}
731
732fn map_range(raw: i32) -> ColorRange {
733 match raw {
734 x if x == AVColorRange::AVCOL_RANGE_JPEG as i32 => ColorRange::Full,
735 x if x == AVColorRange::AVCOL_RANGE_MPEG as i32 => ColorRange::Limited,
736 _ => ColorRange::Unspecified,
737 }
738}
739
740fn is_yuvj(pix_fmt: &PixelFormat) -> bool {
746 matches!(
747 pix_fmt,
748 PixelFormat::Yuvj411p
749 | PixelFormat::Yuvj420p
750 | PixelFormat::Yuvj422p
751 | PixelFormat::Yuvj440p
752 | PixelFormat::Yuvj444p
753 )
754}
755
756fn map_range_for(pix_fmt: &PixelFormat, color_range_raw: i32) -> ColorRange {
768 if is_yuvj(pix_fmt) {
769 return ColorRange::Full;
770 }
771 map_range(color_range_raw)
772}
773
774fn map_chroma_loc(raw: i32) -> ChromaLocation {
775 match raw {
776 x if x == AVChromaLocation::AVCHROMA_LOC_LEFT as i32 => ChromaLocation::Left,
777 x if x == AVChromaLocation::AVCHROMA_LOC_CENTER as i32 => ChromaLocation::Center,
778 x if x == AVChromaLocation::AVCHROMA_LOC_TOPLEFT as i32 => ChromaLocation::TopLeft,
779 x if x == AVChromaLocation::AVCHROMA_LOC_TOP as i32 => ChromaLocation::Top,
780 x if x == AVChromaLocation::AVCHROMA_LOC_BOTTOMLEFT as i32 => ChromaLocation::BottomLeft,
781 x if x == AVChromaLocation::AVCHROMA_LOC_BOTTOM as i32 => ChromaLocation::Bottom,
782 _ => ChromaLocation::Unspecified,
783 }
784}
785
786pub unsafe fn av_frame_to_audio_frame(
801 av_frame: *const AVFrame,
802 time_base: Timebase,
803) -> Result<AudioFrame<SampleFormat, AudioChannelLayout, AudioFrameExtra, FfmpegBuffer>, ConvertError>
804{
805 if av_frame.is_null() {
806 return Err(ConvertError::NullFrame);
807 }
808 let format_raw = unsafe { (*av_frame).format };
815 let sample_rate_raw = unsafe { (*av_frame).sample_rate };
816 let nb_samples_raw = unsafe { (*av_frame).nb_samples };
817 let pts_raw = unsafe { (*av_frame).pts };
818 let duration_raw = unsafe { (*av_frame).duration };
819 let bet_raw = unsafe { (*av_frame).best_effort_timestamp };
820
821 let sample_format = SampleFormat::from_raw(format_raw);
822 let sample_rate = sample_rate_raw.max(0) as u32;
823 let nb_samples = nb_samples_raw.max(0) as u32;
824
825 let ch_layout_ptr = unsafe { addr_of!((*av_frame).ch_layout) };
829 let channel_layout =
830 unsafe { crate::channel_layout::audio_channel_layout_from_raw_ptr(ch_layout_ptr) };
831 let channel_count_full = channel_layout.channels();
832 let channel_count = channel_count_full.min(255) as u8;
833
834 let is_planar = sample_format.is_planar();
836 let plane_count_full = if is_planar { channel_count as usize } else { 1 };
837 if plane_count_full > 8 {
845 return Err(ConvertError::InvalidPlaneLayout { plane: 8 });
846 }
847 let plane_count = plane_count_full as u8;
848
849 let linesize0 = unsafe { (*av_frame).linesize[0] };
856 if nb_samples > 0 && linesize0 <= 0 {
857 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
858 }
859 let plane_bytes = linesize0.max(0) as usize;
860 if nb_samples > 0 {
861 let bytes_per_sample = sample_format
862 .bytes_per_sample()
863 .ok_or(ConvertError::InvalidPlaneLayout { plane: 0 })? as usize;
864 let expected_per_plane = if is_planar {
865 (nb_samples as usize)
867 .checked_mul(bytes_per_sample)
868 .ok_or(ConvertError::InvalidPlaneLayout { plane: 0 })?
869 } else {
870 (nb_samples as usize)
872 .checked_mul(bytes_per_sample)
873 .and_then(|x| x.checked_mul(channel_count.max(1) as usize))
874 .ok_or(ConvertError::InvalidPlaneLayout { plane: 0 })?
875 };
876 if plane_bytes < expected_per_plane {
877 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
878 }
879 }
880
881 let mut planes_out: [Plane<FfmpegBuffer>; 8] = [
882 audio_plane_placeholder()?,
883 audio_plane_placeholder()?,
884 audio_plane_placeholder()?,
885 audio_plane_placeholder()?,
886 audio_plane_placeholder()?,
887 audio_plane_placeholder()?,
888 audio_plane_placeholder()?,
889 audio_plane_placeholder()?,
890 ];
891
892 #[allow(clippy::needless_range_loop)]
896 for plane_idx in 0..plane_count as usize {
897 let data_ptr = unsafe { (*av_frame).data[plane_idx] };
898 if data_ptr.is_null() {
899 return Err(ConvertError::InvalidPlaneLayout { plane: plane_idx });
905 }
906 let buf = unsafe { find_audio_backing_buffer(av_frame, data_ptr, plane_bytes) }
907 .ok_or(ConvertError::BufferAcquireFailed { plane: plane_idx })?;
908 let offset = unsafe { (data_ptr as usize).wrapping_sub((*buf).data as usize) };
911 let view = unsafe { FfmpegBuffer::from_ref_view(buf, offset, plane_bytes) }
914 .ok_or(ConvertError::BufferAcquireFailed { plane: plane_idx })?;
915 planes_out[plane_idx] = Plane::new(view, plane_bytes as u32);
916 }
917
918 let pts = if pts_raw != AV_NOPTS_VALUE {
919 Some(Timestamp::new(pts_raw, time_base))
920 } else {
921 None
922 };
923 let duration = if duration_raw > 0 {
924 Some(Timestamp::new(duration_raw, time_base))
925 } else {
926 None
927 };
928
929 let mut extra = AudioFrameExtra::default();
930 if bet_raw != AV_NOPTS_VALUE {
931 extra.set_best_effort_timestamp(Some(bet_raw));
932 }
933 extra.set_side_data(unsafe { collect_side_data(av_frame) });
937
938 Ok(
939 AudioFrame::new(
940 sample_rate,
941 nb_samples,
942 channel_count,
943 sample_format,
944 channel_layout,
945 planes_out,
946 plane_count,
947 extra,
948 )
949 .with_pts(pts)
950 .with_duration(duration),
951 )
952}
953
954fn audio_plane_placeholder() -> Result<Plane<FfmpegBuffer>, ConvertError> {
955 let raw = unsafe { av_buffer_alloc(1) };
956 if raw.is_null() {
957 return Err(ConvertError::BufferAcquireFailed { plane: 8 });
958 }
959 let buf =
960 unsafe { FfmpegBuffer::take(raw) }.ok_or(ConvertError::BufferAcquireFailed { plane: 8 })?;
961 Ok(Plane::new(buf, 0))
962}
963
964unsafe fn find_audio_backing_buffer(
967 av_frame: *const AVFrame,
968 data_ptr: *const u8,
969 bytes: usize,
970) -> Option<*mut ffmpeg_next::ffi::AVBufferRef> {
971 let buf_array_len = unsafe { (*av_frame).buf.len() };
976 for i in 0..buf_array_len {
977 let buf = unsafe { (*av_frame).buf[i] };
978 if buf.is_null() {
979 continue;
980 }
981 let buf_data = unsafe { (*buf).data as *const u8 };
982 let buf_size = unsafe { (*buf).size };
983 if buf_data.is_null() {
984 continue;
985 }
986 let start = buf_data as usize;
987 let Some(end) = start.checked_add(buf_size) else {
988 continue;
989 };
990 let dp = data_ptr as usize;
991 let Some(dp_end) = dp.checked_add(bytes) else {
992 continue;
993 };
994 if dp >= start && dp_end <= end {
995 return Some(buf);
996 }
997 }
998 None
999}
1000
1001pub unsafe fn av_subtitle_to_subtitle_frame(
1025 av_subtitle: *const ffmpeg_next::ffi::AVSubtitle,
1026 time_base: Timebase,
1027) -> Result<SubtitleFrame<SubtitleFrameExtra, FfmpegBuffer>, ConvertError> {
1028 if av_subtitle.is_null() {
1029 return Err(ConvertError::NullFrame);
1030 }
1031 let mut text_chunks: std::vec::Vec<u8> = std::vec::Vec::new();
1036 let mut bitmap_regions: std::vec::Vec<mediadecode::subtitle::BitmapRegion<FfmpegBuffer>> =
1037 std::vec::Vec::new();
1038
1039 let count_raw = unsafe { (*av_subtitle).num_rects } as usize;
1040 let rects_ptr = unsafe { (*av_subtitle).rects };
1041 if count_raw > 0 && rects_ptr.is_null() {
1045 return Err(ConvertError::NullFrame);
1046 }
1047 let count = count_raw.min(SUBTITLE_MAX_RECTS);
1056 if count_raw > SUBTITLE_MAX_RECTS {
1057 tracing::warn!(
1058 cap = SUBTITLE_MAX_RECTS,
1059 requested = count_raw,
1060 "mediadecode-ffmpeg: AVSubtitle.num_rects exceeds rect cap; truncating",
1061 );
1062 }
1063 let mut text_total_bytes: usize = 0;
1064 let mut bitmap_total_bytes: usize = 0;
1065
1066 let text_kind = AVSubtitleType::SUBTITLE_TEXT as i32;
1067 let ass_kind = AVSubtitleType::SUBTITLE_ASS as i32;
1068 let bitmap_kind = AVSubtitleType::SUBTITLE_BITMAP as i32;
1069 for i in 0..count {
1070 let rect_ptr = unsafe { *rects_ptr.add(i) };
1074 if rect_ptr.is_null() {
1075 continue;
1076 }
1077 let rect_type_raw = unsafe { read_unaligned(addr_of!((*rect_ptr).type_) as *const i32) };
1083 let rect_text_ptr = unsafe { (*rect_ptr).text };
1086 let rect_ass_ptr = unsafe { (*rect_ptr).ass };
1087 let rect_data0_ptr = unsafe { (*rect_ptr).data[0] };
1088 let rect_data1_ptr = unsafe { (*rect_ptr).data[1] };
1089 let rect_linesize0 = unsafe { (*rect_ptr).linesize[0] };
1090 let rect_w = unsafe { (*rect_ptr).w };
1091 let rect_h = unsafe { (*rect_ptr).h };
1092 let rect_x = unsafe { (*rect_ptr).x };
1093 let rect_y = unsafe { (*rect_ptr).y };
1094
1095 match rect_type_raw {
1096 x if x == text_kind && !rect_text_ptr.is_null() => {
1097 let bytes = unsafe { bounded_cstr_bytes(rect_text_ptr, SUBTITLE_MAX_TEXT_BYTES_PER_RECT) }
1107 .ok_or(ConvertError::InvalidPlaneLayout { plane: 0 })?;
1108 if bytes.len() > SUBTITLE_MAX_TEXT_BYTES_PER_RECT {
1112 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
1113 }
1114 let separator = if text_chunks.is_empty() { 0 } else { 1 };
1115 let projected = text_total_bytes
1116 .saturating_add(bytes.len())
1117 .saturating_add(separator);
1118 if projected > SUBTITLE_MAX_TEXT_TOTAL_BYTES {
1119 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
1120 }
1121 if separator == 1 {
1122 text_chunks.push(b'\n');
1123 }
1124 text_chunks.extend_from_slice(bytes);
1125 text_total_bytes = projected;
1126 }
1127 x if x == ass_kind && !rect_ass_ptr.is_null() => {
1128 let bytes = unsafe { bounded_cstr_bytes(rect_ass_ptr, SUBTITLE_MAX_TEXT_BYTES_PER_RECT) }
1131 .ok_or(ConvertError::InvalidPlaneLayout { plane: 0 })?;
1132 if bytes.len() > SUBTITLE_MAX_TEXT_BYTES_PER_RECT {
1133 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
1134 }
1135 let separator = if text_chunks.is_empty() { 0 } else { 1 };
1136 let projected = text_total_bytes
1137 .saturating_add(bytes.len())
1138 .saturating_add(separator);
1139 if projected > SUBTITLE_MAX_TEXT_TOTAL_BYTES {
1140 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
1141 }
1142 if separator == 1 {
1143 text_chunks.push(b'\n');
1144 }
1145 text_chunks.extend_from_slice(bytes);
1146 text_total_bytes = projected;
1147 }
1148 x if x == bitmap_kind => {
1149 let w = rect_w.max(0) as u32;
1153 let h = rect_h.max(0) as u32;
1154 let stride = rect_linesize0.max(0) as u32;
1155 if rect_data0_ptr.is_null() || stride == 0 || h == 0 {
1156 continue;
1157 }
1158 let data_len = (stride as usize)
1162 .checked_mul(h as usize)
1163 .ok_or(ConvertError::InvalidPlaneLayout { plane: 0 })?;
1164 if data_len > SUBTITLE_MAX_BITMAP_BYTES_PER_RECT {
1168 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
1169 }
1170 let projected_total = bitmap_total_bytes.saturating_add(data_len);
1171 if projected_total > SUBTITLE_MAX_BITMAP_TOTAL_BYTES {
1172 return Err(ConvertError::InvalidPlaneLayout { plane: 0 });
1173 }
1174 let data_slice = unsafe { core::slice::from_raw_parts(rect_data0_ptr, data_len) };
1177 let data_buf = FfmpegBuffer::copy_from_slice(data_slice)
1178 .ok_or(ConvertError::BufferAcquireFailed { plane: 0 })?;
1179 let palette_len = 256 * 4;
1180 let palette_buf = if rect_data1_ptr.is_null() {
1181 FfmpegBuffer::copy_from_slice(&[])
1182 .ok_or(ConvertError::BufferAcquireFailed { plane: 1 })?
1183 } else {
1184 let p = unsafe { core::slice::from_raw_parts(rect_data1_ptr, palette_len) };
1186 FfmpegBuffer::copy_from_slice(p).ok_or(ConvertError::BufferAcquireFailed { plane: 1 })?
1187 };
1188 bitmap_regions.push(mediadecode::subtitle::BitmapRegion::new(
1189 rect_x.max(0) as u32,
1190 rect_y.max(0) as u32,
1191 w,
1192 h,
1193 stride,
1194 data_buf,
1195 palette_buf,
1196 ));
1197 bitmap_total_bytes = projected_total;
1198 }
1199 _ => {}
1200 }
1201 }
1202
1203 let payload = if !text_chunks.is_empty() {
1204 let buf = FfmpegBuffer::copy_from_slice(&text_chunks)
1205 .ok_or(ConvertError::BufferAcquireFailed { plane: 0 })?;
1206 SubtitlePayload::Text {
1207 text: buf,
1208 language: None,
1209 }
1210 } else if !bitmap_regions.is_empty() {
1211 SubtitlePayload::Bitmap {
1212 regions: bitmap_regions,
1213 }
1214 } else {
1215 let buf =
1217 FfmpegBuffer::copy_from_slice(&[]).ok_or(ConvertError::BufferAcquireFailed { plane: 0 })?;
1218 SubtitlePayload::Text {
1219 text: buf,
1220 language: None,
1221 }
1222 };
1223
1224 let sub_pts = unsafe { (*av_subtitle).pts };
1225 let pts = if sub_pts != AV_NOPTS_VALUE {
1226 Some(Timestamp::new(sub_pts, time_base))
1227 } else {
1228 None
1229 };
1230
1231 let extra = SubtitleFrameExtra::new(unsafe { (*av_subtitle).start_display_time }, unsafe {
1232 (*av_subtitle).end_display_time
1233 });
1234
1235 Ok(SubtitleFrame::new(payload, extra).with_pts(pts))
1236}
1237
1238fn map_picture_type_raw(raw: i32) -> PictureType {
1239 match raw {
1240 x if x == AVPictureType::AV_PICTURE_TYPE_I as i32 => PictureType::I,
1241 x if x == AVPictureType::AV_PICTURE_TYPE_P as i32 => PictureType::P,
1242 x if x == AVPictureType::AV_PICTURE_TYPE_B as i32 => PictureType::B,
1243 x if x == AVPictureType::AV_PICTURE_TYPE_S as i32 => PictureType::S,
1244 x if x == AVPictureType::AV_PICTURE_TYPE_SI as i32 => PictureType::Si,
1245 x if x == AVPictureType::AV_PICTURE_TYPE_SP as i32 => PictureType::Sp,
1246 x if x == AVPictureType::AV_PICTURE_TYPE_BI as i32 => PictureType::Bi,
1247 _ => PictureType::Unspecified,
1248 }
1249}
1250
1251#[cfg(test)]
1252mod tests;