1#![allow(unsafe_code)]
54
55use std::collections::HashMap;
56use std::ffi::CStr;
57use std::path::Path;
58use std::time::Duration;
59
60use ff_format::channel::ChannelLayout;
61use ff_format::chapter::ChapterInfo;
62use ff_format::codec::{AudioCodec, SubtitleCodec, VideoCodec};
63use ff_format::color::{ColorPrimaries, ColorRange, ColorSpace};
64use ff_format::stream::{AudioStreamInfo, SubtitleStreamInfo, VideoStreamInfo};
65use ff_format::{MediaInfo, PixelFormat, Rational, SampleFormat};
66
67use crate::error::ProbeError;
68
69const AV_TIME_BASE: i64 = 1_000_000;
71
72pub fn open(path: impl AsRef<Path>) -> Result<MediaInfo, ProbeError> {
128 let path = path.as_ref();
129
130 log::debug!("probing media file path={}", path.display());
131
132 if !path.exists() {
134 return Err(ProbeError::FileNotFound {
135 path: path.to_path_buf(),
136 });
137 }
138
139 let file_size = std::fs::metadata(path).map(|m| m.len())?;
141
142 let ctx = unsafe { ff_sys::avformat::open_input(path) }.map_err(|err_code| {
145 ProbeError::CannotOpen {
146 path: path.to_path_buf(),
147 reason: ff_sys::av_error_string(err_code),
148 }
149 })?;
150
151 if let Err(err_code) = unsafe { ff_sys::avformat::find_stream_info(ctx) } {
154 unsafe {
156 let mut ctx_ptr = ctx;
157 ff_sys::avformat::close_input(&raw mut ctx_ptr);
158 }
159 return Err(ProbeError::InvalidMedia {
160 path: path.to_path_buf(),
161 reason: ff_sys::av_error_string(err_code),
162 });
163 }
164
165 let (format, format_long_name, duration) = unsafe { extract_format_info(ctx) };
168
169 let bitrate = unsafe { calculate_container_bitrate(ctx, file_size, duration) };
172
173 let metadata = unsafe { extract_metadata(ctx) };
176
177 let video_streams = unsafe { extract_video_streams(ctx) };
180
181 let audio_streams = unsafe { extract_audio_streams(ctx) };
184
185 let subtitle_streams = unsafe { extract_subtitle_streams(ctx) };
188
189 let chapters = unsafe { extract_chapters(ctx) };
192
193 unsafe {
196 let mut ctx_ptr = ctx;
197 ff_sys::avformat::close_input(&raw mut ctx_ptr);
198 }
199
200 log::debug!(
201 "probe complete video_streams={} audio_streams={} subtitle_streams={} chapters={}",
202 video_streams.len(),
203 audio_streams.len(),
204 subtitle_streams.len(),
205 chapters.len()
206 );
207
208 let mut builder = MediaInfo::builder()
210 .path(path)
211 .format(format)
212 .duration(duration)
213 .file_size(file_size)
214 .video_streams(video_streams)
215 .audio_streams(audio_streams)
216 .subtitle_streams(subtitle_streams)
217 .chapters(chapters)
218 .metadata_map(metadata);
219
220 if let Some(name) = format_long_name {
221 builder = builder.format_long_name(name);
222 }
223
224 if let Some(bps) = bitrate {
225 builder = builder.bitrate(bps);
226 }
227
228 Ok(builder.build())
229}
230
231unsafe fn extract_format_info(
237 ctx: *mut ff_sys::AVFormatContext,
238) -> (String, Option<String>, Duration) {
239 unsafe {
241 let format = extract_format_name(ctx);
242 let format_long_name = extract_format_long_name(ctx);
243 let duration = extract_duration(ctx);
244
245 (format, format_long_name, duration)
246 }
247}
248
249unsafe fn extract_format_name(ctx: *mut ff_sys::AVFormatContext) -> String {
255 unsafe {
257 let iformat = (*ctx).iformat;
258 if iformat.is_null() {
259 return String::from("unknown");
260 }
261
262 let name_ptr = (*iformat).name;
263 if name_ptr.is_null() {
264 return String::from("unknown");
265 }
266
267 CStr::from_ptr(name_ptr).to_string_lossy().into_owned()
268 }
269}
270
271unsafe fn extract_format_long_name(ctx: *mut ff_sys::AVFormatContext) -> Option<String> {
277 unsafe {
279 let iformat = (*ctx).iformat;
280 if iformat.is_null() {
281 return None;
282 }
283
284 let long_name_ptr = (*iformat).long_name;
285 if long_name_ptr.is_null() {
286 return None;
287 }
288
289 Some(CStr::from_ptr(long_name_ptr).to_string_lossy().into_owned())
290 }
291}
292
293unsafe fn extract_duration(ctx: *mut ff_sys::AVFormatContext) -> Duration {
302 let duration_us = unsafe { (*ctx).duration };
304
305 if duration_us <= 0 {
308 return Duration::ZERO;
309 }
310
311 #[expect(clippy::cast_sign_loss, reason = "verified duration_us > 0")]
315 let secs = (duration_us / AV_TIME_BASE) as u64;
316 #[expect(clippy::cast_sign_loss, reason = "verified duration_us > 0")]
317 let micros = (duration_us % AV_TIME_BASE) as u32;
318
319 Duration::new(secs, micros * 1000)
320}
321
322unsafe fn calculate_container_bitrate(
343 ctx: *mut ff_sys::AVFormatContext,
344 file_size: u64,
345 duration: Duration,
346) -> Option<u64> {
347 let bitrate = unsafe { (*ctx).bit_rate };
349
350 if bitrate > 0 {
352 #[expect(clippy::cast_sign_loss, reason = "verified bitrate > 0")]
353 return Some(bitrate as u64);
354 }
355
356 let duration_secs = duration.as_secs_f64();
359 if duration_secs > 0.0 && file_size > 0 {
360 #[expect(
364 clippy::cast_precision_loss,
365 reason = "precision loss acceptable for file size; f64 handles up to 9 PB"
366 )]
367 let file_size_f64 = file_size as f64;
368
369 #[expect(
370 clippy::cast_possible_truncation,
371 reason = "bitrate values are bounded by practical file sizes"
372 )]
373 #[expect(
374 clippy::cast_sign_loss,
375 reason = "result is always positive since both operands are positive"
376 )]
377 let calculated_bitrate = (file_size_f64 * 8.0 / duration_secs) as u64;
378 Some(calculated_bitrate)
379 } else {
380 None
381 }
382}
383
384unsafe fn extract_metadata(ctx: *mut ff_sys::AVFormatContext) -> HashMap<String, String> {
402 let mut metadata = HashMap::new();
403
404 unsafe {
406 let dict = (*ctx).metadata;
407 if dict.is_null() {
408 return metadata;
409 }
410
411 let mut entry: *const ff_sys::AVDictionaryEntry = std::ptr::null();
414
415 let flags = ff_sys::AV_DICT_IGNORE_SUFFIX.cast_signed();
417
418 loop {
419 entry = ff_sys::av_dict_get(dict, c"".as_ptr(), entry, flags);
422
423 if entry.is_null() {
424 break;
425 }
426
427 let key_ptr = (*entry).key;
429 let value_ptr = (*entry).value;
430
431 if key_ptr.is_null() || value_ptr.is_null() {
432 continue;
433 }
434
435 let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
437 let value = CStr::from_ptr(value_ptr).to_string_lossy().into_owned();
438
439 metadata.insert(key, value);
440 }
441 }
442
443 metadata
444}
445
446unsafe fn extract_video_streams(ctx: *mut ff_sys::AVFormatContext) -> Vec<VideoStreamInfo> {
459 unsafe {
461 let nb_streams = (*ctx).nb_streams;
462 let streams_ptr = (*ctx).streams;
463
464 if streams_ptr.is_null() || nb_streams == 0 {
465 return Vec::new();
466 }
467
468 let mut video_streams = Vec::new();
469
470 for i in 0..nb_streams {
471 let stream = *streams_ptr.add(i as usize);
473 if stream.is_null() {
474 continue;
475 }
476
477 let codecpar = (*stream).codecpar;
478 if codecpar.is_null() {
479 continue;
480 }
481
482 if (*codecpar).codec_type != ff_sys::AVMediaType_AVMEDIA_TYPE_VIDEO {
484 continue;
485 }
486
487 let stream_info = extract_single_video_stream(stream, codecpar, i);
489 video_streams.push(stream_info);
490 }
491
492 video_streams
493 }
494}
495
496unsafe fn extract_single_video_stream(
502 stream: *mut ff_sys::AVStream,
503 codecpar: *mut ff_sys::AVCodecParameters,
504 index: u32,
505) -> VideoStreamInfo {
506 unsafe {
508 let codec_id = (*codecpar).codec_id;
510 let codec = map_video_codec(codec_id);
511 let codec_name = extract_codec_name(codec_id);
512
513 #[expect(clippy::cast_sign_loss, reason = "width/height are always positive")]
515 let width = (*codecpar).width as u32;
516 #[expect(clippy::cast_sign_loss, reason = "width/height are always positive")]
517 let height = (*codecpar).height as u32;
518
519 let pixel_format = map_pixel_format((*codecpar).format);
521
522 let frame_rate = extract_frame_rate(stream);
524
525 let bitrate = extract_stream_bitrate(codecpar);
527
528 let color_space = map_color_space((*codecpar).color_space);
530 let color_range = map_color_range((*codecpar).color_range);
531 let color_primaries = map_color_primaries((*codecpar).color_primaries);
532
533 let duration = extract_stream_duration(stream);
535
536 let frame_count = extract_frame_count(stream);
538
539 let mut builder = VideoStreamInfo::builder()
541 .index(index)
542 .codec(codec)
543 .codec_name(codec_name)
544 .width(width)
545 .height(height)
546 .pixel_format(pixel_format)
547 .frame_rate(frame_rate)
548 .color_space(color_space)
549 .color_range(color_range)
550 .color_primaries(color_primaries);
551
552 if let Some(d) = duration {
553 builder = builder.duration(d);
554 }
555
556 if let Some(b) = bitrate {
557 builder = builder.bitrate(b);
558 }
559
560 if let Some(c) = frame_count {
561 builder = builder.frame_count(c);
562 }
563
564 builder.build()
565 }
566}
567
568unsafe fn extract_codec_name(codec_id: ff_sys::AVCodecID) -> String {
574 let name_ptr = unsafe { ff_sys::avcodec_get_name(codec_id) };
576
577 if name_ptr.is_null() {
578 return String::from("unknown");
579 }
580
581 unsafe { CStr::from_ptr(name_ptr).to_string_lossy().into_owned() }
583}
584
585unsafe fn extract_frame_rate(stream: *mut ff_sys::AVStream) -> Rational {
594 unsafe {
596 let r_frame_rate = (*stream).r_frame_rate;
598 if r_frame_rate.den > 0 && r_frame_rate.num > 0 {
599 return Rational::new(r_frame_rate.num, r_frame_rate.den);
600 }
601
602 let avg_frame_rate = (*stream).avg_frame_rate;
604 if avg_frame_rate.den > 0 && avg_frame_rate.num > 0 {
605 return Rational::new(avg_frame_rate.num, avg_frame_rate.den);
606 }
607
608 {
610 log::warn!(
611 "frame_rate unavailable, falling back to 30fps \
612 r_frame_rate={}/{} avg_frame_rate={}/{} fallback=30/1",
613 r_frame_rate.num,
614 r_frame_rate.den,
615 avg_frame_rate.num,
616 avg_frame_rate.den
617 );
618 Rational::new(30, 1)
619 }
620 }
621}
622
623unsafe fn extract_stream_bitrate(codecpar: *mut ff_sys::AVCodecParameters) -> Option<u64> {
631 let bitrate = unsafe { (*codecpar).bit_rate };
633
634 if bitrate > 0 {
635 #[expect(clippy::cast_sign_loss, reason = "verified bitrate > 0")]
636 Some(bitrate as u64)
637 } else {
638 None
639 }
640}
641
642unsafe fn extract_stream_duration(stream: *mut ff_sys::AVStream) -> Option<Duration> {
650 unsafe {
652 let duration_pts = (*stream).duration;
653
654 if duration_pts <= 0 {
656 return None;
657 }
658
659 let time_base = (*stream).time_base;
661 if time_base.den == 0 {
662 return None;
663 }
664
665 #[expect(clippy::cast_precision_loss, reason = "media timestamps are bounded")]
669 let secs = (duration_pts as f64) * f64::from(time_base.num) / f64::from(time_base.den);
670
671 if secs > 0.0 {
672 Some(Duration::from_secs_f64(secs))
673 } else {
674 None
675 }
676 }
677}
678
679unsafe fn extract_frame_count(stream: *mut ff_sys::AVStream) -> Option<u64> {
687 let nb_frames = unsafe { (*stream).nb_frames };
689
690 if nb_frames > 0 {
691 #[expect(clippy::cast_sign_loss, reason = "verified nb_frames > 0")]
692 Some(nb_frames as u64)
693 } else {
694 None
695 }
696}
697
698fn map_video_codec(codec_id: ff_sys::AVCodecID) -> VideoCodec {
704 match codec_id {
705 ff_sys::AVCodecID_AV_CODEC_ID_H264 => VideoCodec::H264,
706 ff_sys::AVCodecID_AV_CODEC_ID_HEVC => VideoCodec::H265,
707 ff_sys::AVCodecID_AV_CODEC_ID_VP8 => VideoCodec::Vp8,
708 ff_sys::AVCodecID_AV_CODEC_ID_VP9 => VideoCodec::Vp9,
709 ff_sys::AVCodecID_AV_CODEC_ID_AV1 => VideoCodec::Av1,
710 ff_sys::AVCodecID_AV_CODEC_ID_PRORES => VideoCodec::ProRes,
711 ff_sys::AVCodecID_AV_CODEC_ID_MPEG4 => VideoCodec::Mpeg4,
712 ff_sys::AVCodecID_AV_CODEC_ID_MPEG2VIDEO => VideoCodec::Mpeg2,
713 ff_sys::AVCodecID_AV_CODEC_ID_MJPEG => VideoCodec::Mjpeg,
714 _ => {
715 log::warn!(
716 "video_codec has no mapping, using Unknown \
717 codec_id={codec_id}"
718 );
719 VideoCodec::Unknown
720 }
721 }
722}
723
724fn map_pixel_format(format: i32) -> PixelFormat {
726 #[expect(clippy::cast_sign_loss, reason = "AVPixelFormat values are positive")]
727 let format_u32 = format as u32;
728
729 match format_u32 {
730 x if x == ff_sys::AVPixelFormat_AV_PIX_FMT_RGB24 as u32 => PixelFormat::Rgb24,
731 x if x == ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA as u32 => PixelFormat::Rgba,
732 x if x == ff_sys::AVPixelFormat_AV_PIX_FMT_BGR24 as u32 => PixelFormat::Bgr24,
733 x if x == ff_sys::AVPixelFormat_AV_PIX_FMT_BGRA as u32 => PixelFormat::Bgra,
734 x if x == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P as u32 => PixelFormat::Yuv420p,
735 x if x == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P as u32 => PixelFormat::Yuv422p,
736 x if x == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV444P as u32 => PixelFormat::Yuv444p,
737 x if x == ff_sys::AVPixelFormat_AV_PIX_FMT_NV12 as u32 => PixelFormat::Nv12,
738 x if x == ff_sys::AVPixelFormat_AV_PIX_FMT_NV21 as u32 => PixelFormat::Nv21,
739 x if x == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P10LE as u32 => PixelFormat::Yuv420p10le,
740 x if x == ff_sys::AVPixelFormat_AV_PIX_FMT_YUV422P10LE as u32 => PixelFormat::Yuv422p10le,
741 x if x == ff_sys::AVPixelFormat_AV_PIX_FMT_P010LE as u32 => PixelFormat::P010le,
742 x if x == ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8 as u32 => PixelFormat::Gray8,
743 _ => {
744 log::warn!(
745 "pixel_format has no mapping, using Other \
746 format={format_u32}"
747 );
748 PixelFormat::Other(format_u32)
749 }
750 }
751}
752
753fn map_color_space(color_space: ff_sys::AVColorSpace) -> ColorSpace {
755 match color_space {
756 ff_sys::AVColorSpace_AVCOL_SPC_BT709 => ColorSpace::Bt709,
757 ff_sys::AVColorSpace_AVCOL_SPC_BT470BG | ff_sys::AVColorSpace_AVCOL_SPC_SMPTE170M => {
758 ColorSpace::Bt601
759 }
760 ff_sys::AVColorSpace_AVCOL_SPC_BT2020_NCL | ff_sys::AVColorSpace_AVCOL_SPC_BT2020_CL => {
761 ColorSpace::Bt2020
762 }
763 ff_sys::AVColorSpace_AVCOL_SPC_RGB => ColorSpace::Srgb,
764 _ => {
765 log::warn!(
766 "color_space has no mapping, using Unknown \
767 color_space={color_space}"
768 );
769 ColorSpace::Unknown
770 }
771 }
772}
773
774fn map_color_range(color_range: ff_sys::AVColorRange) -> ColorRange {
776 match color_range {
777 ff_sys::AVColorRange_AVCOL_RANGE_MPEG => ColorRange::Limited,
778 ff_sys::AVColorRange_AVCOL_RANGE_JPEG => ColorRange::Full,
779 _ => {
780 log::warn!(
781 "color_range has no mapping, using Unknown \
782 color_range={color_range}"
783 );
784 ColorRange::Unknown
785 }
786 }
787}
788
789fn map_color_primaries(color_primaries: ff_sys::AVColorPrimaries) -> ColorPrimaries {
791 match color_primaries {
792 ff_sys::AVColorPrimaries_AVCOL_PRI_BT709 => ColorPrimaries::Bt709,
793 ff_sys::AVColorPrimaries_AVCOL_PRI_BT470BG
794 | ff_sys::AVColorPrimaries_AVCOL_PRI_SMPTE170M => ColorPrimaries::Bt601,
795 ff_sys::AVColorPrimaries_AVCOL_PRI_BT2020 => ColorPrimaries::Bt2020,
796 _ => {
797 log::warn!(
798 "color_primaries has no mapping, using Unknown \
799 color_primaries={color_primaries}"
800 );
801 ColorPrimaries::Unknown
802 }
803 }
804}
805
806unsafe fn extract_audio_streams(ctx: *mut ff_sys::AVFormatContext) -> Vec<AudioStreamInfo> {
819 unsafe {
821 let nb_streams = (*ctx).nb_streams;
822 let streams_ptr = (*ctx).streams;
823
824 if streams_ptr.is_null() || nb_streams == 0 {
825 return Vec::new();
826 }
827
828 let mut audio_streams = Vec::new();
829
830 for i in 0..nb_streams {
831 let stream = *streams_ptr.add(i as usize);
833 if stream.is_null() {
834 continue;
835 }
836
837 let codecpar = (*stream).codecpar;
838 if codecpar.is_null() {
839 continue;
840 }
841
842 if (*codecpar).codec_type != ff_sys::AVMediaType_AVMEDIA_TYPE_AUDIO {
844 continue;
845 }
846
847 let stream_info = extract_single_audio_stream(stream, codecpar, i);
849 audio_streams.push(stream_info);
850 }
851
852 audio_streams
853 }
854}
855
856unsafe fn extract_single_audio_stream(
862 stream: *mut ff_sys::AVStream,
863 codecpar: *mut ff_sys::AVCodecParameters,
864 index: u32,
865) -> AudioStreamInfo {
866 unsafe {
868 let codec_id = (*codecpar).codec_id;
870 let codec = map_audio_codec(codec_id);
871 let codec_name = extract_codec_name(codec_id);
872
873 #[expect(clippy::cast_sign_loss, reason = "sample_rate is always positive")]
875 let sample_rate = (*codecpar).sample_rate as u32;
876
877 let channels = extract_channel_count(codecpar);
879
880 let channel_layout = extract_channel_layout(codecpar, channels);
882
883 let sample_format = map_sample_format((*codecpar).format);
885
886 let bitrate = extract_stream_bitrate(codecpar);
888
889 let duration = extract_stream_duration(stream);
891
892 let language = extract_language(stream);
894
895 let mut builder = AudioStreamInfo::builder()
897 .index(index)
898 .codec(codec)
899 .codec_name(codec_name)
900 .sample_rate(sample_rate)
901 .channels(channels)
902 .channel_layout(channel_layout)
903 .sample_format(sample_format);
904
905 if let Some(d) = duration {
906 builder = builder.duration(d);
907 }
908
909 if let Some(b) = bitrate {
910 builder = builder.bitrate(b);
911 }
912
913 if let Some(lang) = language {
914 builder = builder.language(lang);
915 }
916
917 builder.build()
918 }
919}
920
921unsafe fn extract_channel_count(codecpar: *mut ff_sys::AVCodecParameters) -> u32 {
932 #[expect(clippy::cast_sign_loss, reason = "channel count is always positive")]
935 let channels = unsafe { (*codecpar).ch_layout.nb_channels as u32 };
936
937 if channels > 0 {
939 channels
940 } else {
941 log::warn!(
942 "channel_count is 0 (uninitialized), falling back to mono \
943 fallback=1"
944 );
945 1
946 }
947}
948
949unsafe fn extract_channel_layout(
955 codecpar: *mut ff_sys::AVCodecParameters,
956 channels: u32,
957) -> ChannelLayout {
958 let ch_layout = unsafe { &(*codecpar).ch_layout };
961
962 if ch_layout.order == ff_sys::AVChannelOrder_AV_CHANNEL_ORDER_NATIVE {
965 let mask = unsafe { ch_layout.u.mask };
969 match mask {
970 0x4 => ChannelLayout::Mono,
972 0x3 => ChannelLayout::Stereo,
974 0x103 => ChannelLayout::Stereo2_1,
976 0x7 => ChannelLayout::Surround3_0,
978 0x33 => ChannelLayout::Quad,
980 0x37 => ChannelLayout::Surround5_0,
982 0x3F => ChannelLayout::Surround5_1,
984 0x13F => ChannelLayout::Surround6_1,
986 0x63F => ChannelLayout::Surround7_1,
988 _ => {
989 log::warn!(
990 "channel_layout mask has no mapping, deriving from channel count \
991 mask={mask} channels={channels}"
992 );
993 ChannelLayout::from_channels(channels)
994 }
995 }
996 } else {
997 log::warn!(
998 "channel_layout order is not NATIVE, deriving from channel count \
999 order={order} channels={channels}",
1000 order = ch_layout.order
1001 );
1002 ChannelLayout::from_channels(channels)
1003 }
1004}
1005
1006unsafe fn extract_language(stream: *mut ff_sys::AVStream) -> Option<String> {
1012 unsafe {
1014 let metadata = (*stream).metadata;
1015 if metadata.is_null() {
1016 return None;
1017 }
1018
1019 let key = c"language";
1021 let entry = ff_sys::av_dict_get(metadata, key.as_ptr(), std::ptr::null(), 0);
1022
1023 if entry.is_null() {
1024 return None;
1025 }
1026
1027 let value_ptr = (*entry).value;
1028 if value_ptr.is_null() {
1029 return None;
1030 }
1031
1032 Some(CStr::from_ptr(value_ptr).to_string_lossy().into_owned())
1033 }
1034}
1035
1036unsafe fn extract_subtitle_streams(ctx: *mut ff_sys::AVFormatContext) -> Vec<SubtitleStreamInfo> {
1053 unsafe {
1055 let nb_streams = (*ctx).nb_streams;
1056 let streams_ptr = (*ctx).streams;
1057
1058 if streams_ptr.is_null() || nb_streams == 0 {
1059 return Vec::new();
1060 }
1061
1062 let mut subtitle_streams = Vec::new();
1063
1064 for i in 0..nb_streams {
1065 let stream = *streams_ptr.add(i as usize);
1067 if stream.is_null() {
1068 continue;
1069 }
1070
1071 let codecpar = (*stream).codecpar;
1072 if codecpar.is_null() {
1073 continue;
1074 }
1075
1076 if (*codecpar).codec_type != ff_sys::AVMediaType_AVMEDIA_TYPE_SUBTITLE {
1078 continue;
1079 }
1080
1081 let stream_info = extract_single_subtitle_stream(stream, codecpar, i);
1082 subtitle_streams.push(stream_info);
1083 }
1084
1085 subtitle_streams
1086 }
1087}
1088
1089unsafe fn extract_single_subtitle_stream(
1095 stream: *mut ff_sys::AVStream,
1096 codecpar: *mut ff_sys::AVCodecParameters,
1097 index: u32,
1098) -> SubtitleStreamInfo {
1099 unsafe {
1101 let codec_id = (*codecpar).codec_id;
1102 let codec = map_subtitle_codec(codec_id);
1103 let codec_name = extract_codec_name(codec_id);
1104
1105 #[expect(
1107 clippy::cast_sign_loss,
1108 reason = "disposition is a non-negative bitmask"
1109 )]
1110 let forced = ((*stream).disposition as u32 & ff_sys::AV_DISPOSITION_FORCED) != 0;
1111
1112 let duration = extract_stream_duration(stream);
1113 let language = extract_language(stream);
1114 let title = extract_stream_title(stream);
1115
1116 let mut builder = SubtitleStreamInfo::builder()
1117 .index(index)
1118 .codec(codec)
1119 .codec_name(codec_name)
1120 .forced(forced);
1121
1122 if let Some(d) = duration {
1123 builder = builder.duration(d);
1124 }
1125 if let Some(lang) = language {
1126 builder = builder.language(lang);
1127 }
1128 if let Some(t) = title {
1129 builder = builder.title(t);
1130 }
1131
1132 builder.build()
1133 }
1134}
1135
1136unsafe fn extract_stream_title(stream: *mut ff_sys::AVStream) -> Option<String> {
1142 unsafe {
1144 let metadata = (*stream).metadata;
1145 if metadata.is_null() {
1146 return None;
1147 }
1148
1149 let key = c"title";
1150 let entry = ff_sys::av_dict_get(metadata, key.as_ptr(), std::ptr::null(), 0);
1151
1152 if entry.is_null() {
1153 return None;
1154 }
1155
1156 let value_ptr = (*entry).value;
1157 if value_ptr.is_null() {
1158 return None;
1159 }
1160
1161 Some(CStr::from_ptr(value_ptr).to_string_lossy().into_owned())
1162 }
1163}
1164
1165fn map_subtitle_codec(codec_id: ff_sys::AVCodecID) -> SubtitleCodec {
1167 match codec_id {
1168 ff_sys::AVCodecID_AV_CODEC_ID_SRT | ff_sys::AVCodecID_AV_CODEC_ID_SUBRIP => {
1169 SubtitleCodec::Srt
1170 }
1171 ff_sys::AVCodecID_AV_CODEC_ID_SSA | ff_sys::AVCodecID_AV_CODEC_ID_ASS => SubtitleCodec::Ass,
1172 ff_sys::AVCodecID_AV_CODEC_ID_DVB_SUBTITLE => SubtitleCodec::Dvb,
1173 ff_sys::AVCodecID_AV_CODEC_ID_HDMV_PGS_SUBTITLE => SubtitleCodec::Hdmv,
1174 ff_sys::AVCodecID_AV_CODEC_ID_WEBVTT => SubtitleCodec::Webvtt,
1175 _ => {
1176 let name = unsafe { extract_codec_name(codec_id) };
1178 log::warn!("unknown subtitle codec codec_id={codec_id}");
1179 SubtitleCodec::Other(name)
1180 }
1181 }
1182}
1183
1184fn map_audio_codec(codec_id: ff_sys::AVCodecID) -> AudioCodec {
1186 match codec_id {
1187 ff_sys::AVCodecID_AV_CODEC_ID_AAC => AudioCodec::Aac,
1188 ff_sys::AVCodecID_AV_CODEC_ID_MP3 => AudioCodec::Mp3,
1189 ff_sys::AVCodecID_AV_CODEC_ID_OPUS => AudioCodec::Opus,
1190 ff_sys::AVCodecID_AV_CODEC_ID_FLAC => AudioCodec::Flac,
1191 ff_sys::AVCodecID_AV_CODEC_ID_VORBIS => AudioCodec::Vorbis,
1192 ff_sys::AVCodecID_AV_CODEC_ID_AC3 => AudioCodec::Ac3,
1193 ff_sys::AVCodecID_AV_CODEC_ID_EAC3 => AudioCodec::Eac3,
1194 ff_sys::AVCodecID_AV_CODEC_ID_DTS => AudioCodec::Dts,
1195 ff_sys::AVCodecID_AV_CODEC_ID_ALAC => AudioCodec::Alac,
1196 ff_sys::AVCodecID_AV_CODEC_ID_PCM_S16LE
1198 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_S16BE
1199 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_S24LE
1200 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_S24BE
1201 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_S32LE
1202 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_S32BE
1203 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_F32LE
1204 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_F32BE
1205 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_F64LE
1206 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_F64BE
1207 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_U8 => AudioCodec::Pcm,
1208 _ => {
1209 log::warn!(
1210 "audio_codec has no mapping, using Unknown \
1211 codec_id={codec_id}"
1212 );
1213 AudioCodec::Unknown
1214 }
1215 }
1216}
1217
1218fn map_sample_format(format: i32) -> SampleFormat {
1220 #[expect(clippy::cast_sign_loss, reason = "AVSampleFormat values are positive")]
1221 let format_u32 = format as u32;
1222
1223 match format_u32 {
1224 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8 as u32 => SampleFormat::U8,
1226 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16 as u32 => SampleFormat::I16,
1227 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32 as u32 => SampleFormat::I32,
1228 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLT as u32 => SampleFormat::F32,
1229 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBL as u32 => SampleFormat::F64,
1230 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8P as u32 => SampleFormat::U8p,
1232 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16P as u32 => SampleFormat::I16p,
1233 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32P as u32 => SampleFormat::I32p,
1234 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLTP as u32 => SampleFormat::F32p,
1235 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBLP as u32 => SampleFormat::F64p,
1236 _ => {
1238 log::warn!(
1239 "sample_format has no mapping, using Other \
1240 format={format_u32}"
1241 );
1242 SampleFormat::Other(format_u32)
1243 }
1244 }
1245}
1246
1247unsafe fn extract_chapters(ctx: *mut ff_sys::AVFormatContext) -> Vec<ChapterInfo> {
1257 unsafe {
1259 let nb_chapters = (*ctx).nb_chapters;
1260 let chapters_ptr = (*ctx).chapters;
1261
1262 if chapters_ptr.is_null() || nb_chapters == 0 {
1263 return Vec::new();
1264 }
1265
1266 let mut chapters = Vec::with_capacity(nb_chapters as usize);
1267
1268 for i in 0..nb_chapters {
1269 let chapter = *chapters_ptr.add(i as usize);
1271 if chapter.is_null() {
1272 continue;
1273 }
1274
1275 chapters.push(extract_single_chapter(chapter));
1276 }
1277
1278 chapters
1279 }
1280}
1281
1282unsafe fn extract_single_chapter(chapter: *mut ff_sys::AVChapter) -> ChapterInfo {
1288 unsafe {
1290 let id = (*chapter).id;
1291
1292 let av_tb = (*chapter).time_base;
1293 let time_base = if av_tb.den != 0 {
1294 Some(Rational::new(av_tb.num, av_tb.den))
1295 } else {
1296 log::warn!(
1297 "chapter time_base has zero denominator, treating as unknown \
1298 chapter_id={id} time_base_num={num} time_base_den=0",
1299 num = av_tb.num
1300 );
1301 None
1302 };
1303
1304 let (start, end) = if let Some(tb) = time_base {
1305 (
1306 pts_to_duration((*chapter).start, tb),
1307 pts_to_duration((*chapter).end, tb),
1308 )
1309 } else {
1310 (std::time::Duration::ZERO, std::time::Duration::ZERO)
1311 };
1312
1313 let title = extract_chapter_title((*chapter).metadata);
1314 let metadata = extract_chapter_metadata((*chapter).metadata);
1315
1316 let mut builder = ChapterInfo::builder().id(id).start(start).end(end);
1317
1318 if let Some(t) = title {
1319 builder = builder.title(t);
1320 }
1321 if let Some(tb) = time_base {
1322 builder = builder.time_base(tb);
1323 }
1324 if let Some(m) = metadata {
1325 builder = builder.metadata(m);
1326 }
1327
1328 builder.build()
1329 }
1330}
1331
1332fn pts_to_duration(pts: i64, time_base: Rational) -> std::time::Duration {
1336 if pts <= 0 {
1337 return std::time::Duration::ZERO;
1338 }
1339 #[expect(clippy::cast_precision_loss, reason = "media timestamps are bounded")]
1342 let secs = (pts as f64) * f64::from(time_base.num()) / f64::from(time_base.den());
1343 if secs > 0.0 {
1344 std::time::Duration::from_secs_f64(secs)
1345 } else {
1346 std::time::Duration::ZERO
1347 }
1348}
1349
1350unsafe fn extract_chapter_title(dict: *mut ff_sys::AVDictionary) -> Option<String> {
1358 unsafe {
1360 if dict.is_null() {
1361 return None;
1362 }
1363 let entry = ff_sys::av_dict_get(dict, c"title".as_ptr(), std::ptr::null(), 0);
1364 if entry.is_null() {
1365 return None;
1366 }
1367 let value_ptr = (*entry).value;
1368 if value_ptr.is_null() {
1369 return None;
1370 }
1371 Some(CStr::from_ptr(value_ptr).to_string_lossy().into_owned())
1372 }
1373}
1374
1375unsafe fn extract_chapter_metadata(
1383 dict: *mut ff_sys::AVDictionary,
1384) -> Option<HashMap<String, String>> {
1385 unsafe {
1387 if dict.is_null() {
1388 return None;
1389 }
1390
1391 let mut map = HashMap::new();
1392 let mut entry: *const ff_sys::AVDictionaryEntry = std::ptr::null();
1393 let flags = ff_sys::AV_DICT_IGNORE_SUFFIX.cast_signed();
1394
1395 loop {
1396 entry = ff_sys::av_dict_get(dict, c"".as_ptr(), entry, flags);
1397 if entry.is_null() {
1398 break;
1399 }
1400
1401 let key_ptr = (*entry).key;
1402 let value_ptr = (*entry).value;
1403
1404 if key_ptr.is_null() || value_ptr.is_null() {
1405 continue;
1406 }
1407
1408 let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
1409 if key == "title" {
1410 continue;
1411 }
1412 let value = CStr::from_ptr(value_ptr).to_string_lossy().into_owned();
1413 map.insert(key, value);
1414 }
1415
1416 if map.is_empty() { None } else { Some(map) }
1417 }
1418}
1419
1420#[cfg(test)]
1421mod tests {
1422 use super::*;
1423
1424 #[test]
1425 fn test_open_nonexistent_file() {
1426 let result = open("/nonexistent/path/to/video.mp4");
1427 assert!(result.is_err());
1428 match result {
1429 Err(ProbeError::FileNotFound { path }) => {
1430 assert!(path.to_string_lossy().contains("video.mp4"));
1431 }
1432 _ => panic!("Expected FileNotFound error"),
1433 }
1434 }
1435
1436 #[test]
1437 fn test_open_invalid_file() {
1438 let temp_dir = std::env::temp_dir();
1440 let temp_file = temp_dir.join("ff_probe_test_invalid.mp4");
1441 std::fs::write(&temp_file, b"not a valid video file").ok();
1442
1443 let result = open(&temp_file);
1444
1445 std::fs::remove_file(&temp_file).ok();
1447
1448 assert!(result.is_err());
1450 match result {
1451 Err(ProbeError::CannotOpen { .. }) | Err(ProbeError::InvalidMedia { .. }) => {}
1452 _ => panic!("Expected CannotOpen or InvalidMedia error"),
1453 }
1454 }
1455
1456 #[test]
1457 fn test_av_time_base_constant() {
1458 assert_eq!(AV_TIME_BASE, 1_000_000);
1460 }
1461
1462 #[test]
1467 fn pts_to_duration_should_convert_millisecond_timebase_correctly() {
1468 let tb = Rational::new(1, 1000);
1470 let dur = pts_to_duration(5000, tb);
1471 assert_eq!(dur, Duration::from_secs(5));
1472 }
1473
1474 #[test]
1475 fn pts_to_duration_should_convert_mpeg_ts_timebase_correctly() {
1476 let tb = Rational::new(1, 90000);
1478 let dur = pts_to_duration(90000, tb);
1479 assert!((dur.as_secs_f64() - 1.0).abs() < 1e-6);
1480 }
1481
1482 #[test]
1483 fn pts_to_duration_should_return_zero_for_zero_pts() {
1484 let tb = Rational::new(1, 1000);
1485 assert_eq!(pts_to_duration(0, tb), Duration::ZERO);
1486 }
1487
1488 #[test]
1489 fn pts_to_duration_should_return_zero_for_negative_pts() {
1490 let tb = Rational::new(1, 1000);
1491 assert_eq!(pts_to_duration(-1, tb), Duration::ZERO);
1492 }
1493
1494 #[test]
1495 fn test_duration_conversion() {
1496 let duration_us: i64 = 5_500_000; let secs = (duration_us / AV_TIME_BASE) as u64;
1499 let micros = (duration_us % AV_TIME_BASE) as u32;
1500 let duration = Duration::new(secs, micros * 1000);
1501
1502 assert_eq!(duration.as_secs(), 5);
1503 assert_eq!(duration.subsec_micros(), 500_000);
1504 }
1505
1506 #[test]
1511 fn test_map_video_codec_h264() {
1512 let codec = map_video_codec(ff_sys::AVCodecID_AV_CODEC_ID_H264);
1513 assert_eq!(codec, VideoCodec::H264);
1514 }
1515
1516 #[test]
1517 fn test_map_video_codec_hevc() {
1518 let codec = map_video_codec(ff_sys::AVCodecID_AV_CODEC_ID_HEVC);
1519 assert_eq!(codec, VideoCodec::H265);
1520 }
1521
1522 #[test]
1523 fn test_map_video_codec_vp9() {
1524 let codec = map_video_codec(ff_sys::AVCodecID_AV_CODEC_ID_VP9);
1525 assert_eq!(codec, VideoCodec::Vp9);
1526 }
1527
1528 #[test]
1529 fn test_map_video_codec_av1() {
1530 let codec = map_video_codec(ff_sys::AVCodecID_AV_CODEC_ID_AV1);
1531 assert_eq!(codec, VideoCodec::Av1);
1532 }
1533
1534 #[test]
1535 fn test_map_video_codec_unknown() {
1536 let codec = map_video_codec(ff_sys::AVCodecID_AV_CODEC_ID_THEORA);
1538 assert_eq!(codec, VideoCodec::Unknown);
1539 }
1540
1541 #[test]
1546 fn test_map_pixel_format_yuv420p() {
1547 let format = map_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P as i32);
1548 assert_eq!(format, PixelFormat::Yuv420p);
1549 }
1550
1551 #[test]
1552 fn test_map_pixel_format_rgba() {
1553 let format = map_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA as i32);
1554 assert_eq!(format, PixelFormat::Rgba);
1555 }
1556
1557 #[test]
1558 fn test_map_pixel_format_nv12() {
1559 let format = map_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NV12 as i32);
1560 assert_eq!(format, PixelFormat::Nv12);
1561 }
1562
1563 #[test]
1564 fn test_map_pixel_format_yuv420p10le() {
1565 let format = map_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P10LE as i32);
1566 assert_eq!(format, PixelFormat::Yuv420p10le);
1567 }
1568
1569 #[test]
1570 fn test_map_pixel_format_unknown() {
1571 let format = map_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_PAL8 as i32);
1573 assert!(matches!(format, PixelFormat::Other(_)));
1574 }
1575
1576 #[test]
1581 fn test_map_color_space_bt709() {
1582 let space = map_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT709);
1583 assert_eq!(space, ColorSpace::Bt709);
1584 }
1585
1586 #[test]
1587 fn test_map_color_space_bt601() {
1588 let space = map_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT470BG);
1589 assert_eq!(space, ColorSpace::Bt601);
1590
1591 let space = map_color_space(ff_sys::AVColorSpace_AVCOL_SPC_SMPTE170M);
1592 assert_eq!(space, ColorSpace::Bt601);
1593 }
1594
1595 #[test]
1596 fn test_map_color_space_bt2020() {
1597 let space = map_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT2020_NCL);
1598 assert_eq!(space, ColorSpace::Bt2020);
1599
1600 let space = map_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT2020_CL);
1601 assert_eq!(space, ColorSpace::Bt2020);
1602 }
1603
1604 #[test]
1605 fn test_map_color_space_srgb() {
1606 let space = map_color_space(ff_sys::AVColorSpace_AVCOL_SPC_RGB);
1607 assert_eq!(space, ColorSpace::Srgb);
1608 }
1609
1610 #[test]
1611 fn test_map_color_space_unknown() {
1612 let space = map_color_space(ff_sys::AVColorSpace_AVCOL_SPC_UNSPECIFIED);
1613 assert_eq!(space, ColorSpace::Unknown);
1614 }
1615
1616 #[test]
1621 fn test_map_color_range_limited() {
1622 let range = map_color_range(ff_sys::AVColorRange_AVCOL_RANGE_MPEG);
1623 assert_eq!(range, ColorRange::Limited);
1624 }
1625
1626 #[test]
1627 fn test_map_color_range_full() {
1628 let range = map_color_range(ff_sys::AVColorRange_AVCOL_RANGE_JPEG);
1629 assert_eq!(range, ColorRange::Full);
1630 }
1631
1632 #[test]
1633 fn test_map_color_range_unknown() {
1634 let range = map_color_range(ff_sys::AVColorRange_AVCOL_RANGE_UNSPECIFIED);
1635 assert_eq!(range, ColorRange::Unknown);
1636 }
1637
1638 #[test]
1643 fn test_map_color_primaries_bt709() {
1644 let primaries = map_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT709);
1645 assert_eq!(primaries, ColorPrimaries::Bt709);
1646 }
1647
1648 #[test]
1649 fn test_map_color_primaries_bt601() {
1650 let primaries = map_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT470BG);
1651 assert_eq!(primaries, ColorPrimaries::Bt601);
1652
1653 let primaries = map_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_SMPTE170M);
1654 assert_eq!(primaries, ColorPrimaries::Bt601);
1655 }
1656
1657 #[test]
1658 fn test_map_color_primaries_bt2020() {
1659 let primaries = map_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT2020);
1660 assert_eq!(primaries, ColorPrimaries::Bt2020);
1661 }
1662
1663 #[test]
1664 fn test_map_color_primaries_unknown() {
1665 let primaries = map_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_UNSPECIFIED);
1666 assert_eq!(primaries, ColorPrimaries::Unknown);
1667 }
1668
1669 #[test]
1674 fn test_map_audio_codec_aac() {
1675 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_AAC);
1676 assert_eq!(codec, AudioCodec::Aac);
1677 }
1678
1679 #[test]
1680 fn test_map_audio_codec_mp3() {
1681 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_MP3);
1682 assert_eq!(codec, AudioCodec::Mp3);
1683 }
1684
1685 #[test]
1686 fn test_map_audio_codec_opus() {
1687 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_OPUS);
1688 assert_eq!(codec, AudioCodec::Opus);
1689 }
1690
1691 #[test]
1692 fn test_map_audio_codec_flac() {
1693 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_FLAC);
1694 assert_eq!(codec, AudioCodec::Flac);
1695 }
1696
1697 #[test]
1698 fn test_map_audio_codec_vorbis() {
1699 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_VORBIS);
1700 assert_eq!(codec, AudioCodec::Vorbis);
1701 }
1702
1703 #[test]
1704 fn test_map_audio_codec_ac3() {
1705 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_AC3);
1706 assert_eq!(codec, AudioCodec::Ac3);
1707 }
1708
1709 #[test]
1710 fn test_map_audio_codec_eac3() {
1711 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_EAC3);
1712 assert_eq!(codec, AudioCodec::Eac3);
1713 }
1714
1715 #[test]
1716 fn test_map_audio_codec_dts() {
1717 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_DTS);
1718 assert_eq!(codec, AudioCodec::Dts);
1719 }
1720
1721 #[test]
1722 fn test_map_audio_codec_alac() {
1723 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_ALAC);
1724 assert_eq!(codec, AudioCodec::Alac);
1725 }
1726
1727 #[test]
1728 fn test_map_audio_codec_pcm() {
1729 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_PCM_S16LE);
1731 assert_eq!(codec, AudioCodec::Pcm);
1732
1733 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_PCM_F32LE);
1734 assert_eq!(codec, AudioCodec::Pcm);
1735
1736 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_PCM_U8);
1737 assert_eq!(codec, AudioCodec::Pcm);
1738 }
1739
1740 #[test]
1741 fn test_map_audio_codec_unknown() {
1742 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_WMAV2);
1744 assert_eq!(codec, AudioCodec::Unknown);
1745 }
1746
1747 #[test]
1752 fn test_map_sample_format_u8() {
1753 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8 as i32);
1754 assert_eq!(format, SampleFormat::U8);
1755 }
1756
1757 #[test]
1758 fn test_map_sample_format_i16() {
1759 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16 as i32);
1760 assert_eq!(format, SampleFormat::I16);
1761 }
1762
1763 #[test]
1764 fn test_map_sample_format_i32() {
1765 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32 as i32);
1766 assert_eq!(format, SampleFormat::I32);
1767 }
1768
1769 #[test]
1770 fn test_map_sample_format_f32() {
1771 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLT as i32);
1772 assert_eq!(format, SampleFormat::F32);
1773 }
1774
1775 #[test]
1776 fn test_map_sample_format_f64() {
1777 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBL as i32);
1778 assert_eq!(format, SampleFormat::F64);
1779 }
1780
1781 #[test]
1782 fn test_map_sample_format_planar() {
1783 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8P as i32);
1784 assert_eq!(format, SampleFormat::U8p);
1785
1786 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16P as i32);
1787 assert_eq!(format, SampleFormat::I16p);
1788
1789 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32P as i32);
1790 assert_eq!(format, SampleFormat::I32p);
1791
1792 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLTP as i32);
1793 assert_eq!(format, SampleFormat::F32p);
1794
1795 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBLP as i32);
1796 assert_eq!(format, SampleFormat::F64p);
1797 }
1798
1799 #[test]
1800 fn test_map_sample_format_unknown() {
1801 let format = map_sample_format(999);
1803 assert!(matches!(format, SampleFormat::Other(_)));
1804 }
1805
1806 #[test]
1811 fn test_bitrate_fallback_calculation() {
1812 let file_size: u64 = 10_000_000;
1818 let duration = Duration::from_secs(10);
1819 let duration_secs = duration.as_secs_f64();
1820
1821 let calculated_bitrate = (file_size as f64 * 8.0 / duration_secs) as u64;
1822 assert_eq!(calculated_bitrate, 8_000_000);
1823 }
1824
1825 #[test]
1826 fn test_bitrate_fallback_with_subsecond_duration() {
1827 let file_size: u64 = 1_000_000;
1831 let duration = Duration::from_millis(500);
1832 let duration_secs = duration.as_secs_f64();
1833
1834 let calculated_bitrate = (file_size as f64 * 8.0 / duration_secs) as u64;
1835 assert_eq!(calculated_bitrate, 16_000_000);
1836 }
1837
1838 #[test]
1839 fn test_bitrate_zero_duration() {
1840 let duration = Duration::ZERO;
1842 let duration_secs = duration.as_secs_f64();
1843
1844 assert!(duration_secs == 0.0);
1846 }
1847
1848 #[test]
1849 fn test_bitrate_zero_file_size() {
1850 let file_size: u64 = 0;
1852 let duration = Duration::from_secs(10);
1853 let duration_secs = duration.as_secs_f64();
1854
1855 if duration_secs > 0.0 && file_size > 0 {
1856 let calculated_bitrate = (file_size as f64 * 8.0 / duration_secs) as u64;
1857 assert_eq!(calculated_bitrate, 0);
1858 } else {
1859 assert_eq!(file_size, 0);
1861 }
1862 }
1863
1864 #[test]
1865 fn test_bitrate_typical_video_file() {
1866 let file_size: u64 = 100_000_000;
1870 let duration = Duration::from_secs(300); let duration_secs = duration.as_secs_f64();
1872
1873 let calculated_bitrate = (file_size as f64 * 8.0 / duration_secs) as u64;
1874 assert_eq!(calculated_bitrate, 2_666_666);
1875 }
1876
1877 #[test]
1878 fn test_bitrate_high_quality_video() {
1879 let file_size: u64 = 5_000_000_000;
1883 let duration = Duration::from_secs(7200); let duration_secs = duration.as_secs_f64();
1885
1886 let calculated_bitrate = (file_size as f64 * 8.0 / duration_secs) as u64;
1887 assert_eq!(calculated_bitrate, 5_555_555);
1888 }
1889}