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_P010LE as u32 => PixelFormat::P010le,
741 x if x == ff_sys::AVPixelFormat_AV_PIX_FMT_GRAY8 as u32 => PixelFormat::Gray8,
742 _ => {
743 log::warn!(
744 "pixel_format has no mapping, using Other \
745 format={format_u32}"
746 );
747 PixelFormat::Other(format_u32)
748 }
749 }
750}
751
752fn map_color_space(color_space: ff_sys::AVColorSpace) -> ColorSpace {
754 match color_space {
755 ff_sys::AVColorSpace_AVCOL_SPC_BT709 => ColorSpace::Bt709,
756 ff_sys::AVColorSpace_AVCOL_SPC_BT470BG | ff_sys::AVColorSpace_AVCOL_SPC_SMPTE170M => {
757 ColorSpace::Bt601
758 }
759 ff_sys::AVColorSpace_AVCOL_SPC_BT2020_NCL | ff_sys::AVColorSpace_AVCOL_SPC_BT2020_CL => {
760 ColorSpace::Bt2020
761 }
762 ff_sys::AVColorSpace_AVCOL_SPC_RGB => ColorSpace::Srgb,
763 _ => {
764 log::warn!(
765 "color_space has no mapping, using Unknown \
766 color_space={color_space}"
767 );
768 ColorSpace::Unknown
769 }
770 }
771}
772
773fn map_color_range(color_range: ff_sys::AVColorRange) -> ColorRange {
775 match color_range {
776 ff_sys::AVColorRange_AVCOL_RANGE_MPEG => ColorRange::Limited,
777 ff_sys::AVColorRange_AVCOL_RANGE_JPEG => ColorRange::Full,
778 _ => {
779 log::warn!(
780 "color_range has no mapping, using Unknown \
781 color_range={color_range}"
782 );
783 ColorRange::Unknown
784 }
785 }
786}
787
788fn map_color_primaries(color_primaries: ff_sys::AVColorPrimaries) -> ColorPrimaries {
790 match color_primaries {
791 ff_sys::AVColorPrimaries_AVCOL_PRI_BT709 => ColorPrimaries::Bt709,
792 ff_sys::AVColorPrimaries_AVCOL_PRI_BT470BG
793 | ff_sys::AVColorPrimaries_AVCOL_PRI_SMPTE170M => ColorPrimaries::Bt601,
794 ff_sys::AVColorPrimaries_AVCOL_PRI_BT2020 => ColorPrimaries::Bt2020,
795 _ => {
796 log::warn!(
797 "color_primaries has no mapping, using Unknown \
798 color_primaries={color_primaries}"
799 );
800 ColorPrimaries::Unknown
801 }
802 }
803}
804
805unsafe fn extract_audio_streams(ctx: *mut ff_sys::AVFormatContext) -> Vec<AudioStreamInfo> {
818 unsafe {
820 let nb_streams = (*ctx).nb_streams;
821 let streams_ptr = (*ctx).streams;
822
823 if streams_ptr.is_null() || nb_streams == 0 {
824 return Vec::new();
825 }
826
827 let mut audio_streams = Vec::new();
828
829 for i in 0..nb_streams {
830 let stream = *streams_ptr.add(i as usize);
832 if stream.is_null() {
833 continue;
834 }
835
836 let codecpar = (*stream).codecpar;
837 if codecpar.is_null() {
838 continue;
839 }
840
841 if (*codecpar).codec_type != ff_sys::AVMediaType_AVMEDIA_TYPE_AUDIO {
843 continue;
844 }
845
846 let stream_info = extract_single_audio_stream(stream, codecpar, i);
848 audio_streams.push(stream_info);
849 }
850
851 audio_streams
852 }
853}
854
855unsafe fn extract_single_audio_stream(
861 stream: *mut ff_sys::AVStream,
862 codecpar: *mut ff_sys::AVCodecParameters,
863 index: u32,
864) -> AudioStreamInfo {
865 unsafe {
867 let codec_id = (*codecpar).codec_id;
869 let codec = map_audio_codec(codec_id);
870 let codec_name = extract_codec_name(codec_id);
871
872 #[expect(clippy::cast_sign_loss, reason = "sample_rate is always positive")]
874 let sample_rate = (*codecpar).sample_rate as u32;
875
876 let channels = extract_channel_count(codecpar);
878
879 let channel_layout = extract_channel_layout(codecpar, channels);
881
882 let sample_format = map_sample_format((*codecpar).format);
884
885 let bitrate = extract_stream_bitrate(codecpar);
887
888 let duration = extract_stream_duration(stream);
890
891 let language = extract_language(stream);
893
894 let mut builder = AudioStreamInfo::builder()
896 .index(index)
897 .codec(codec)
898 .codec_name(codec_name)
899 .sample_rate(sample_rate)
900 .channels(channels)
901 .channel_layout(channel_layout)
902 .sample_format(sample_format);
903
904 if let Some(d) = duration {
905 builder = builder.duration(d);
906 }
907
908 if let Some(b) = bitrate {
909 builder = builder.bitrate(b);
910 }
911
912 if let Some(lang) = language {
913 builder = builder.language(lang);
914 }
915
916 builder.build()
917 }
918}
919
920unsafe fn extract_channel_count(codecpar: *mut ff_sys::AVCodecParameters) -> u32 {
931 #[expect(clippy::cast_sign_loss, reason = "channel count is always positive")]
934 let channels = unsafe { (*codecpar).ch_layout.nb_channels as u32 };
935
936 if channels > 0 {
938 channels
939 } else {
940 log::warn!(
941 "channel_count is 0 (uninitialized), falling back to mono \
942 fallback=1"
943 );
944 1
945 }
946}
947
948unsafe fn extract_channel_layout(
954 codecpar: *mut ff_sys::AVCodecParameters,
955 channels: u32,
956) -> ChannelLayout {
957 let ch_layout = unsafe { &(*codecpar).ch_layout };
960
961 if ch_layout.order == ff_sys::AVChannelOrder_AV_CHANNEL_ORDER_NATIVE {
964 let mask = unsafe { ch_layout.u.mask };
968 match mask {
969 0x4 => ChannelLayout::Mono,
971 0x3 => ChannelLayout::Stereo,
973 0x103 => ChannelLayout::Stereo2_1,
975 0x7 => ChannelLayout::Surround3_0,
977 0x33 => ChannelLayout::Quad,
979 0x37 => ChannelLayout::Surround5_0,
981 0x3F => ChannelLayout::Surround5_1,
983 0x13F => ChannelLayout::Surround6_1,
985 0x63F => ChannelLayout::Surround7_1,
987 _ => {
988 log::warn!(
989 "channel_layout mask has no mapping, deriving from channel count \
990 mask={mask} channels={channels}"
991 );
992 ChannelLayout::from_channels(channels)
993 }
994 }
995 } else {
996 log::warn!(
997 "channel_layout order is not NATIVE, deriving from channel count \
998 order={order} channels={channels}",
999 order = ch_layout.order
1000 );
1001 ChannelLayout::from_channels(channels)
1002 }
1003}
1004
1005unsafe fn extract_language(stream: *mut ff_sys::AVStream) -> Option<String> {
1011 unsafe {
1013 let metadata = (*stream).metadata;
1014 if metadata.is_null() {
1015 return None;
1016 }
1017
1018 let key = c"language";
1020 let entry = ff_sys::av_dict_get(metadata, key.as_ptr(), std::ptr::null(), 0);
1021
1022 if entry.is_null() {
1023 return None;
1024 }
1025
1026 let value_ptr = (*entry).value;
1027 if value_ptr.is_null() {
1028 return None;
1029 }
1030
1031 Some(CStr::from_ptr(value_ptr).to_string_lossy().into_owned())
1032 }
1033}
1034
1035unsafe fn extract_subtitle_streams(ctx: *mut ff_sys::AVFormatContext) -> Vec<SubtitleStreamInfo> {
1052 unsafe {
1054 let nb_streams = (*ctx).nb_streams;
1055 let streams_ptr = (*ctx).streams;
1056
1057 if streams_ptr.is_null() || nb_streams == 0 {
1058 return Vec::new();
1059 }
1060
1061 let mut subtitle_streams = Vec::new();
1062
1063 for i in 0..nb_streams {
1064 let stream = *streams_ptr.add(i as usize);
1066 if stream.is_null() {
1067 continue;
1068 }
1069
1070 let codecpar = (*stream).codecpar;
1071 if codecpar.is_null() {
1072 continue;
1073 }
1074
1075 if (*codecpar).codec_type != ff_sys::AVMediaType_AVMEDIA_TYPE_SUBTITLE {
1077 continue;
1078 }
1079
1080 let stream_info = extract_single_subtitle_stream(stream, codecpar, i);
1081 subtitle_streams.push(stream_info);
1082 }
1083
1084 subtitle_streams
1085 }
1086}
1087
1088unsafe fn extract_single_subtitle_stream(
1094 stream: *mut ff_sys::AVStream,
1095 codecpar: *mut ff_sys::AVCodecParameters,
1096 index: u32,
1097) -> SubtitleStreamInfo {
1098 unsafe {
1100 let codec_id = (*codecpar).codec_id;
1101 let codec = map_subtitle_codec(codec_id);
1102 let codec_name = extract_codec_name(codec_id);
1103
1104 #[expect(
1106 clippy::cast_sign_loss,
1107 reason = "disposition is a non-negative bitmask"
1108 )]
1109 let forced = ((*stream).disposition as u32 & ff_sys::AV_DISPOSITION_FORCED) != 0;
1110
1111 let duration = extract_stream_duration(stream);
1112 let language = extract_language(stream);
1113 let title = extract_stream_title(stream);
1114
1115 let mut builder = SubtitleStreamInfo::builder()
1116 .index(index)
1117 .codec(codec)
1118 .codec_name(codec_name)
1119 .forced(forced);
1120
1121 if let Some(d) = duration {
1122 builder = builder.duration(d);
1123 }
1124 if let Some(lang) = language {
1125 builder = builder.language(lang);
1126 }
1127 if let Some(t) = title {
1128 builder = builder.title(t);
1129 }
1130
1131 builder.build()
1132 }
1133}
1134
1135unsafe fn extract_stream_title(stream: *mut ff_sys::AVStream) -> Option<String> {
1141 unsafe {
1143 let metadata = (*stream).metadata;
1144 if metadata.is_null() {
1145 return None;
1146 }
1147
1148 let key = c"title";
1149 let entry = ff_sys::av_dict_get(metadata, key.as_ptr(), std::ptr::null(), 0);
1150
1151 if entry.is_null() {
1152 return None;
1153 }
1154
1155 let value_ptr = (*entry).value;
1156 if value_ptr.is_null() {
1157 return None;
1158 }
1159
1160 Some(CStr::from_ptr(value_ptr).to_string_lossy().into_owned())
1161 }
1162}
1163
1164fn map_subtitle_codec(codec_id: ff_sys::AVCodecID) -> SubtitleCodec {
1166 match codec_id {
1167 ff_sys::AVCodecID_AV_CODEC_ID_SRT | ff_sys::AVCodecID_AV_CODEC_ID_SUBRIP => {
1168 SubtitleCodec::Srt
1169 }
1170 ff_sys::AVCodecID_AV_CODEC_ID_SSA | ff_sys::AVCodecID_AV_CODEC_ID_ASS => SubtitleCodec::Ass,
1171 ff_sys::AVCodecID_AV_CODEC_ID_DVB_SUBTITLE => SubtitleCodec::Dvb,
1172 ff_sys::AVCodecID_AV_CODEC_ID_HDMV_PGS_SUBTITLE => SubtitleCodec::Hdmv,
1173 ff_sys::AVCodecID_AV_CODEC_ID_WEBVTT => SubtitleCodec::Webvtt,
1174 _ => {
1175 let name = unsafe { extract_codec_name(codec_id) };
1177 log::warn!("unknown subtitle codec codec_id={codec_id}");
1178 SubtitleCodec::Other(name)
1179 }
1180 }
1181}
1182
1183fn map_audio_codec(codec_id: ff_sys::AVCodecID) -> AudioCodec {
1185 match codec_id {
1186 ff_sys::AVCodecID_AV_CODEC_ID_AAC => AudioCodec::Aac,
1187 ff_sys::AVCodecID_AV_CODEC_ID_MP3 => AudioCodec::Mp3,
1188 ff_sys::AVCodecID_AV_CODEC_ID_OPUS => AudioCodec::Opus,
1189 ff_sys::AVCodecID_AV_CODEC_ID_FLAC => AudioCodec::Flac,
1190 ff_sys::AVCodecID_AV_CODEC_ID_VORBIS => AudioCodec::Vorbis,
1191 ff_sys::AVCodecID_AV_CODEC_ID_AC3 => AudioCodec::Ac3,
1192 ff_sys::AVCodecID_AV_CODEC_ID_EAC3 => AudioCodec::Eac3,
1193 ff_sys::AVCodecID_AV_CODEC_ID_DTS => AudioCodec::Dts,
1194 ff_sys::AVCodecID_AV_CODEC_ID_ALAC => AudioCodec::Alac,
1195 ff_sys::AVCodecID_AV_CODEC_ID_PCM_S16LE
1197 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_S16BE
1198 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_S24LE
1199 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_S24BE
1200 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_S32LE
1201 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_S32BE
1202 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_F32LE
1203 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_F32BE
1204 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_F64LE
1205 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_F64BE
1206 | ff_sys::AVCodecID_AV_CODEC_ID_PCM_U8 => AudioCodec::Pcm,
1207 _ => {
1208 log::warn!(
1209 "audio_codec has no mapping, using Unknown \
1210 codec_id={codec_id}"
1211 );
1212 AudioCodec::Unknown
1213 }
1214 }
1215}
1216
1217fn map_sample_format(format: i32) -> SampleFormat {
1219 #[expect(clippy::cast_sign_loss, reason = "AVSampleFormat values are positive")]
1220 let format_u32 = format as u32;
1221
1222 match format_u32 {
1223 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8 as u32 => SampleFormat::U8,
1225 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16 as u32 => SampleFormat::I16,
1226 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32 as u32 => SampleFormat::I32,
1227 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLT as u32 => SampleFormat::F32,
1228 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBL as u32 => SampleFormat::F64,
1229 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8P as u32 => SampleFormat::U8p,
1231 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16P as u32 => SampleFormat::I16p,
1232 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32P as u32 => SampleFormat::I32p,
1233 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLTP as u32 => SampleFormat::F32p,
1234 x if x == ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBLP as u32 => SampleFormat::F64p,
1235 _ => {
1237 log::warn!(
1238 "sample_format has no mapping, using Other \
1239 format={format_u32}"
1240 );
1241 SampleFormat::Other(format_u32)
1242 }
1243 }
1244}
1245
1246unsafe fn extract_chapters(ctx: *mut ff_sys::AVFormatContext) -> Vec<ChapterInfo> {
1256 unsafe {
1258 let nb_chapters = (*ctx).nb_chapters;
1259 let chapters_ptr = (*ctx).chapters;
1260
1261 if chapters_ptr.is_null() || nb_chapters == 0 {
1262 return Vec::new();
1263 }
1264
1265 let mut chapters = Vec::with_capacity(nb_chapters as usize);
1266
1267 for i in 0..nb_chapters {
1268 let chapter = *chapters_ptr.add(i as usize);
1270 if chapter.is_null() {
1271 continue;
1272 }
1273
1274 chapters.push(extract_single_chapter(chapter));
1275 }
1276
1277 chapters
1278 }
1279}
1280
1281unsafe fn extract_single_chapter(chapter: *mut ff_sys::AVChapter) -> ChapterInfo {
1287 unsafe {
1289 let id = (*chapter).id;
1290
1291 let av_tb = (*chapter).time_base;
1292 let time_base = if av_tb.den != 0 {
1293 Some(Rational::new(av_tb.num, av_tb.den))
1294 } else {
1295 log::warn!(
1296 "chapter time_base has zero denominator, treating as unknown \
1297 chapter_id={id} time_base_num={num} time_base_den=0",
1298 num = av_tb.num
1299 );
1300 None
1301 };
1302
1303 let (start, end) = if let Some(tb) = time_base {
1304 (
1305 pts_to_duration((*chapter).start, tb),
1306 pts_to_duration((*chapter).end, tb),
1307 )
1308 } else {
1309 (std::time::Duration::ZERO, std::time::Duration::ZERO)
1310 };
1311
1312 let title = extract_chapter_title((*chapter).metadata);
1313 let metadata = extract_chapter_metadata((*chapter).metadata);
1314
1315 let mut builder = ChapterInfo::builder().id(id).start(start).end(end);
1316
1317 if let Some(t) = title {
1318 builder = builder.title(t);
1319 }
1320 if let Some(tb) = time_base {
1321 builder = builder.time_base(tb);
1322 }
1323 if let Some(m) = metadata {
1324 builder = builder.metadata(m);
1325 }
1326
1327 builder.build()
1328 }
1329}
1330
1331fn pts_to_duration(pts: i64, time_base: Rational) -> std::time::Duration {
1335 if pts <= 0 {
1336 return std::time::Duration::ZERO;
1337 }
1338 #[expect(clippy::cast_precision_loss, reason = "media timestamps are bounded")]
1341 let secs = (pts as f64) * f64::from(time_base.num()) / f64::from(time_base.den());
1342 if secs > 0.0 {
1343 std::time::Duration::from_secs_f64(secs)
1344 } else {
1345 std::time::Duration::ZERO
1346 }
1347}
1348
1349unsafe fn extract_chapter_title(dict: *mut ff_sys::AVDictionary) -> Option<String> {
1357 unsafe {
1359 if dict.is_null() {
1360 return None;
1361 }
1362 let entry = ff_sys::av_dict_get(dict, c"title".as_ptr(), std::ptr::null(), 0);
1363 if entry.is_null() {
1364 return None;
1365 }
1366 let value_ptr = (*entry).value;
1367 if value_ptr.is_null() {
1368 return None;
1369 }
1370 Some(CStr::from_ptr(value_ptr).to_string_lossy().into_owned())
1371 }
1372}
1373
1374unsafe fn extract_chapter_metadata(
1382 dict: *mut ff_sys::AVDictionary,
1383) -> Option<HashMap<String, String>> {
1384 unsafe {
1386 if dict.is_null() {
1387 return None;
1388 }
1389
1390 let mut map = HashMap::new();
1391 let mut entry: *const ff_sys::AVDictionaryEntry = std::ptr::null();
1392 let flags = ff_sys::AV_DICT_IGNORE_SUFFIX.cast_signed();
1393
1394 loop {
1395 entry = ff_sys::av_dict_get(dict, c"".as_ptr(), entry, flags);
1396 if entry.is_null() {
1397 break;
1398 }
1399
1400 let key_ptr = (*entry).key;
1401 let value_ptr = (*entry).value;
1402
1403 if key_ptr.is_null() || value_ptr.is_null() {
1404 continue;
1405 }
1406
1407 let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
1408 if key == "title" {
1409 continue;
1410 }
1411 let value = CStr::from_ptr(value_ptr).to_string_lossy().into_owned();
1412 map.insert(key, value);
1413 }
1414
1415 if map.is_empty() { None } else { Some(map) }
1416 }
1417}
1418
1419#[cfg(test)]
1420mod tests {
1421 use super::*;
1422
1423 #[test]
1424 fn test_open_nonexistent_file() {
1425 let result = open("/nonexistent/path/to/video.mp4");
1426 assert!(result.is_err());
1427 match result {
1428 Err(ProbeError::FileNotFound { path }) => {
1429 assert!(path.to_string_lossy().contains("video.mp4"));
1430 }
1431 _ => panic!("Expected FileNotFound error"),
1432 }
1433 }
1434
1435 #[test]
1436 fn test_open_invalid_file() {
1437 let temp_dir = std::env::temp_dir();
1439 let temp_file = temp_dir.join("ff_probe_test_invalid.mp4");
1440 std::fs::write(&temp_file, b"not a valid video file").ok();
1441
1442 let result = open(&temp_file);
1443
1444 std::fs::remove_file(&temp_file).ok();
1446
1447 assert!(result.is_err());
1449 match result {
1450 Err(ProbeError::CannotOpen { .. }) | Err(ProbeError::InvalidMedia { .. }) => {}
1451 _ => panic!("Expected CannotOpen or InvalidMedia error"),
1452 }
1453 }
1454
1455 #[test]
1456 fn test_av_time_base_constant() {
1457 assert_eq!(AV_TIME_BASE, 1_000_000);
1459 }
1460
1461 #[test]
1466 fn pts_to_duration_should_convert_millisecond_timebase_correctly() {
1467 let tb = Rational::new(1, 1000);
1469 let dur = pts_to_duration(5000, tb);
1470 assert_eq!(dur, Duration::from_secs(5));
1471 }
1472
1473 #[test]
1474 fn pts_to_duration_should_convert_mpeg_ts_timebase_correctly() {
1475 let tb = Rational::new(1, 90000);
1477 let dur = pts_to_duration(90000, tb);
1478 assert!((dur.as_secs_f64() - 1.0).abs() < 1e-6);
1479 }
1480
1481 #[test]
1482 fn pts_to_duration_should_return_zero_for_zero_pts() {
1483 let tb = Rational::new(1, 1000);
1484 assert_eq!(pts_to_duration(0, tb), Duration::ZERO);
1485 }
1486
1487 #[test]
1488 fn pts_to_duration_should_return_zero_for_negative_pts() {
1489 let tb = Rational::new(1, 1000);
1490 assert_eq!(pts_to_duration(-1, tb), Duration::ZERO);
1491 }
1492
1493 #[test]
1494 fn test_duration_conversion() {
1495 let duration_us: i64 = 5_500_000; let secs = (duration_us / AV_TIME_BASE) as u64;
1498 let micros = (duration_us % AV_TIME_BASE) as u32;
1499 let duration = Duration::new(secs, micros * 1000);
1500
1501 assert_eq!(duration.as_secs(), 5);
1502 assert_eq!(duration.subsec_micros(), 500_000);
1503 }
1504
1505 #[test]
1510 fn test_map_video_codec_h264() {
1511 let codec = map_video_codec(ff_sys::AVCodecID_AV_CODEC_ID_H264);
1512 assert_eq!(codec, VideoCodec::H264);
1513 }
1514
1515 #[test]
1516 fn test_map_video_codec_hevc() {
1517 let codec = map_video_codec(ff_sys::AVCodecID_AV_CODEC_ID_HEVC);
1518 assert_eq!(codec, VideoCodec::H265);
1519 }
1520
1521 #[test]
1522 fn test_map_video_codec_vp9() {
1523 let codec = map_video_codec(ff_sys::AVCodecID_AV_CODEC_ID_VP9);
1524 assert_eq!(codec, VideoCodec::Vp9);
1525 }
1526
1527 #[test]
1528 fn test_map_video_codec_av1() {
1529 let codec = map_video_codec(ff_sys::AVCodecID_AV_CODEC_ID_AV1);
1530 assert_eq!(codec, VideoCodec::Av1);
1531 }
1532
1533 #[test]
1534 fn test_map_video_codec_unknown() {
1535 let codec = map_video_codec(ff_sys::AVCodecID_AV_CODEC_ID_THEORA);
1537 assert_eq!(codec, VideoCodec::Unknown);
1538 }
1539
1540 #[test]
1545 fn test_map_pixel_format_yuv420p() {
1546 let format = map_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P as i32);
1547 assert_eq!(format, PixelFormat::Yuv420p);
1548 }
1549
1550 #[test]
1551 fn test_map_pixel_format_rgba() {
1552 let format = map_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_RGBA as i32);
1553 assert_eq!(format, PixelFormat::Rgba);
1554 }
1555
1556 #[test]
1557 fn test_map_pixel_format_nv12() {
1558 let format = map_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_NV12 as i32);
1559 assert_eq!(format, PixelFormat::Nv12);
1560 }
1561
1562 #[test]
1563 fn test_map_pixel_format_yuv420p10le() {
1564 let format = map_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_YUV420P10LE as i32);
1565 assert_eq!(format, PixelFormat::Yuv420p10le);
1566 }
1567
1568 #[test]
1569 fn test_map_pixel_format_unknown() {
1570 let format = map_pixel_format(ff_sys::AVPixelFormat_AV_PIX_FMT_PAL8 as i32);
1572 assert!(matches!(format, PixelFormat::Other(_)));
1573 }
1574
1575 #[test]
1580 fn test_map_color_space_bt709() {
1581 let space = map_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT709);
1582 assert_eq!(space, ColorSpace::Bt709);
1583 }
1584
1585 #[test]
1586 fn test_map_color_space_bt601() {
1587 let space = map_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT470BG);
1588 assert_eq!(space, ColorSpace::Bt601);
1589
1590 let space = map_color_space(ff_sys::AVColorSpace_AVCOL_SPC_SMPTE170M);
1591 assert_eq!(space, ColorSpace::Bt601);
1592 }
1593
1594 #[test]
1595 fn test_map_color_space_bt2020() {
1596 let space = map_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT2020_NCL);
1597 assert_eq!(space, ColorSpace::Bt2020);
1598
1599 let space = map_color_space(ff_sys::AVColorSpace_AVCOL_SPC_BT2020_CL);
1600 assert_eq!(space, ColorSpace::Bt2020);
1601 }
1602
1603 #[test]
1604 fn test_map_color_space_srgb() {
1605 let space = map_color_space(ff_sys::AVColorSpace_AVCOL_SPC_RGB);
1606 assert_eq!(space, ColorSpace::Srgb);
1607 }
1608
1609 #[test]
1610 fn test_map_color_space_unknown() {
1611 let space = map_color_space(ff_sys::AVColorSpace_AVCOL_SPC_UNSPECIFIED);
1612 assert_eq!(space, ColorSpace::Unknown);
1613 }
1614
1615 #[test]
1620 fn test_map_color_range_limited() {
1621 let range = map_color_range(ff_sys::AVColorRange_AVCOL_RANGE_MPEG);
1622 assert_eq!(range, ColorRange::Limited);
1623 }
1624
1625 #[test]
1626 fn test_map_color_range_full() {
1627 let range = map_color_range(ff_sys::AVColorRange_AVCOL_RANGE_JPEG);
1628 assert_eq!(range, ColorRange::Full);
1629 }
1630
1631 #[test]
1632 fn test_map_color_range_unknown() {
1633 let range = map_color_range(ff_sys::AVColorRange_AVCOL_RANGE_UNSPECIFIED);
1634 assert_eq!(range, ColorRange::Unknown);
1635 }
1636
1637 #[test]
1642 fn test_map_color_primaries_bt709() {
1643 let primaries = map_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT709);
1644 assert_eq!(primaries, ColorPrimaries::Bt709);
1645 }
1646
1647 #[test]
1648 fn test_map_color_primaries_bt601() {
1649 let primaries = map_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT470BG);
1650 assert_eq!(primaries, ColorPrimaries::Bt601);
1651
1652 let primaries = map_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_SMPTE170M);
1653 assert_eq!(primaries, ColorPrimaries::Bt601);
1654 }
1655
1656 #[test]
1657 fn test_map_color_primaries_bt2020() {
1658 let primaries = map_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_BT2020);
1659 assert_eq!(primaries, ColorPrimaries::Bt2020);
1660 }
1661
1662 #[test]
1663 fn test_map_color_primaries_unknown() {
1664 let primaries = map_color_primaries(ff_sys::AVColorPrimaries_AVCOL_PRI_UNSPECIFIED);
1665 assert_eq!(primaries, ColorPrimaries::Unknown);
1666 }
1667
1668 #[test]
1673 fn test_map_audio_codec_aac() {
1674 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_AAC);
1675 assert_eq!(codec, AudioCodec::Aac);
1676 }
1677
1678 #[test]
1679 fn test_map_audio_codec_mp3() {
1680 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_MP3);
1681 assert_eq!(codec, AudioCodec::Mp3);
1682 }
1683
1684 #[test]
1685 fn test_map_audio_codec_opus() {
1686 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_OPUS);
1687 assert_eq!(codec, AudioCodec::Opus);
1688 }
1689
1690 #[test]
1691 fn test_map_audio_codec_flac() {
1692 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_FLAC);
1693 assert_eq!(codec, AudioCodec::Flac);
1694 }
1695
1696 #[test]
1697 fn test_map_audio_codec_vorbis() {
1698 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_VORBIS);
1699 assert_eq!(codec, AudioCodec::Vorbis);
1700 }
1701
1702 #[test]
1703 fn test_map_audio_codec_ac3() {
1704 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_AC3);
1705 assert_eq!(codec, AudioCodec::Ac3);
1706 }
1707
1708 #[test]
1709 fn test_map_audio_codec_eac3() {
1710 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_EAC3);
1711 assert_eq!(codec, AudioCodec::Eac3);
1712 }
1713
1714 #[test]
1715 fn test_map_audio_codec_dts() {
1716 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_DTS);
1717 assert_eq!(codec, AudioCodec::Dts);
1718 }
1719
1720 #[test]
1721 fn test_map_audio_codec_alac() {
1722 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_ALAC);
1723 assert_eq!(codec, AudioCodec::Alac);
1724 }
1725
1726 #[test]
1727 fn test_map_audio_codec_pcm() {
1728 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_PCM_S16LE);
1730 assert_eq!(codec, AudioCodec::Pcm);
1731
1732 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_PCM_F32LE);
1733 assert_eq!(codec, AudioCodec::Pcm);
1734
1735 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_PCM_U8);
1736 assert_eq!(codec, AudioCodec::Pcm);
1737 }
1738
1739 #[test]
1740 fn test_map_audio_codec_unknown() {
1741 let codec = map_audio_codec(ff_sys::AVCodecID_AV_CODEC_ID_WMAV2);
1743 assert_eq!(codec, AudioCodec::Unknown);
1744 }
1745
1746 #[test]
1751 fn test_map_sample_format_u8() {
1752 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8 as i32);
1753 assert_eq!(format, SampleFormat::U8);
1754 }
1755
1756 #[test]
1757 fn test_map_sample_format_i16() {
1758 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16 as i32);
1759 assert_eq!(format, SampleFormat::I16);
1760 }
1761
1762 #[test]
1763 fn test_map_sample_format_i32() {
1764 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32 as i32);
1765 assert_eq!(format, SampleFormat::I32);
1766 }
1767
1768 #[test]
1769 fn test_map_sample_format_f32() {
1770 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLT as i32);
1771 assert_eq!(format, SampleFormat::F32);
1772 }
1773
1774 #[test]
1775 fn test_map_sample_format_f64() {
1776 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBL as i32);
1777 assert_eq!(format, SampleFormat::F64);
1778 }
1779
1780 #[test]
1781 fn test_map_sample_format_planar() {
1782 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_U8P as i32);
1783 assert_eq!(format, SampleFormat::U8p);
1784
1785 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S16P as i32);
1786 assert_eq!(format, SampleFormat::I16p);
1787
1788 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_S32P as i32);
1789 assert_eq!(format, SampleFormat::I32p);
1790
1791 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_FLTP as i32);
1792 assert_eq!(format, SampleFormat::F32p);
1793
1794 let format = map_sample_format(ff_sys::AVSampleFormat_AV_SAMPLE_FMT_DBLP as i32);
1795 assert_eq!(format, SampleFormat::F64p);
1796 }
1797
1798 #[test]
1799 fn test_map_sample_format_unknown() {
1800 let format = map_sample_format(999);
1802 assert!(matches!(format, SampleFormat::Other(_)));
1803 }
1804
1805 #[test]
1810 fn test_bitrate_fallback_calculation() {
1811 let file_size: u64 = 10_000_000;
1817 let duration = Duration::from_secs(10);
1818 let duration_secs = duration.as_secs_f64();
1819
1820 let calculated_bitrate = (file_size as f64 * 8.0 / duration_secs) as u64;
1821 assert_eq!(calculated_bitrate, 8_000_000);
1822 }
1823
1824 #[test]
1825 fn test_bitrate_fallback_with_subsecond_duration() {
1826 let file_size: u64 = 1_000_000;
1830 let duration = Duration::from_millis(500);
1831 let duration_secs = duration.as_secs_f64();
1832
1833 let calculated_bitrate = (file_size as f64 * 8.0 / duration_secs) as u64;
1834 assert_eq!(calculated_bitrate, 16_000_000);
1835 }
1836
1837 #[test]
1838 fn test_bitrate_zero_duration() {
1839 let duration = Duration::ZERO;
1841 let duration_secs = duration.as_secs_f64();
1842
1843 assert!(duration_secs == 0.0);
1845 }
1846
1847 #[test]
1848 fn test_bitrate_zero_file_size() {
1849 let file_size: u64 = 0;
1851 let duration = Duration::from_secs(10);
1852 let duration_secs = duration.as_secs_f64();
1853
1854 if duration_secs > 0.0 && file_size > 0 {
1855 let calculated_bitrate = (file_size as f64 * 8.0 / duration_secs) as u64;
1856 assert_eq!(calculated_bitrate, 0);
1857 } else {
1858 assert_eq!(file_size, 0);
1860 }
1861 }
1862
1863 #[test]
1864 fn test_bitrate_typical_video_file() {
1865 let file_size: u64 = 100_000_000;
1869 let duration = Duration::from_secs(300); let duration_secs = duration.as_secs_f64();
1871
1872 let calculated_bitrate = (file_size as f64 * 8.0 / duration_secs) as u64;
1873 assert_eq!(calculated_bitrate, 2_666_666);
1874 }
1875
1876 #[test]
1877 fn test_bitrate_high_quality_video() {
1878 let file_size: u64 = 5_000_000_000;
1882 let duration = Duration::from_secs(7200); let duration_secs = duration.as_secs_f64();
1884
1885 let calculated_bitrate = (file_size as f64 * 8.0 / duration_secs) as u64;
1886 assert_eq!(calculated_bitrate, 5_555_555);
1887 }
1888}