1use std::collections::HashMap;
2use std::ffi::{CStr, CString};
3use std::ptr::{null, null_mut};
4
5#[cfg(not(feature = "docs-rs"))]
6use ffmpeg_sys_next::AVChannelOrder;
7use ffmpeg_sys_next::AVMediaType::{
8 AVMEDIA_TYPE_ATTACHMENT, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_DATA, AVMEDIA_TYPE_SUBTITLE,
9 AVMEDIA_TYPE_UNKNOWN, AVMEDIA_TYPE_VIDEO,
10};
11use ffmpeg_sys_next::{
12 av_dict_get, av_find_best_stream, avcodec_get_name, avformat_find_stream_info, AVCodecID,
13 AVDictionary, AVDictionaryEntry, AVRational, AV_DICT_IGNORE_SUFFIX,
14};
15use ffmpeg_sys_next::{avformat_alloc_context, avformat_close_input, avformat_open_input};
16use crate::core::context::AVFormatContextBox;
17use crate::error::{FindStreamError, OpenInputError, Result};
18
19#[derive(Debug, Clone)]
20pub enum StreamInfo {
21 Video {
23 index: i32,
26
27 time_base: AVRational,
29
30 start_time: i64,
32
33 duration: i64,
35
36 nb_frames: i64,
38
39 r_frame_rate: AVRational,
41
42 sample_aspect_ratio: AVRational,
44
45 metadata: HashMap<String, String>,
47
48 avg_frame_rate: AVRational,
50
51 codec_id: AVCodecID,
54
55 codec_name: String,
57
58 width: i32,
60
61 height: i32,
63
64 bit_rate: i64,
66
67 pixel_format: i32,
69
70 video_delay: i32,
72
73 fps: f64,
76
77 rotate: i32,
80 },
81 Audio {
83 index: i32,
86
87 time_base: AVRational,
89
90 start_time: i64,
92
93 duration: i64,
95
96 nb_frames: i64,
98
99 metadata: HashMap<String, String>,
101
102 avg_frame_rate: AVRational,
104
105 codec_id: AVCodecID,
108
109 codec_name: String,
111
112 sample_rate: i32,
114
115 #[cfg(not(feature = "docs-rs"))]
117 order: AVChannelOrder,
118
119 nb_channels: i32,
121
122 bit_rate: i64,
124
125 sample_format: i32,
127
128 frame_size: i32,
130 },
131 Subtitle {
133 index: i32,
136
137 time_base: AVRational,
139
140 start_time: i64,
142
143 duration: i64,
145
146 nb_frames: i64,
148
149 metadata: HashMap<String, String>,
151
152 codec_id: AVCodecID,
155
156 codec_name: String,
158 },
159 Data {
161 index: i32,
164
165 time_base: AVRational,
167
168 start_time: i64,
170
171 duration: i64,
173
174 metadata: HashMap<String, String>,
176 },
177 Attachment {
179 index: i32,
182
183 metadata: HashMap<String, String>,
185
186 codec_id: AVCodecID,
189
190 codec_name: String,
192 },
193 Unknown {
195 index: i32,
198
199 metadata: HashMap<String, String>,
201 },
202}
203
204impl StreamInfo {
205 pub fn stream_type(&self) -> &'static str {
206 match self {
207 StreamInfo::Video { .. } => "Video",
208 StreamInfo::Audio { .. } => "Audio",
209 StreamInfo::Subtitle { .. } => "Subtitle",
210 StreamInfo::Data { .. } => "Data",
211 StreamInfo::Attachment { .. } => "Attachment",
212 StreamInfo::Unknown { .. } => "Unknown",
213 }
214 }
215}
216
217pub fn find_video_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
232 let in_fmt_ctx_box = init_format_context(url)?;
233
234 unsafe {
235 let video_index =
236 av_find_best_stream(in_fmt_ctx_box.fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, null_mut(), 0);
237 if video_index < 0 {
238 return Ok(None);
239 }
240 let streams = (*in_fmt_ctx_box.fmt_ctx).streams;
241 let video_stream = *streams.offset(video_index as isize);
242 if video_stream.is_null() {
243 return Err(FindStreamError::NoStreamFound.into());
244 }
245
246 let index = (*video_stream).index;
247 let time_base = (*video_stream).time_base;
248 let start_time = (*video_stream).start_time;
249 let duration = (*video_stream).duration;
250 let nb_frames = (*video_stream).nb_frames;
251 let r_frame_rate = (*video_stream).r_frame_rate;
252 let sample_aspect_ratio = (*video_stream).sample_aspect_ratio;
253 let metadata = (*video_stream).metadata;
254 let metadata = dict_to_hashmap(metadata);
255 let avg_frame_rate = (*video_stream).avg_frame_rate;
256
257 let codec_parameters = (*video_stream).codecpar;
258 if codec_parameters.is_null() {
259 return Err(FindStreamError::NoCodecparFound.into());
260 }
261 let codec_id = (*codec_parameters).codec_id;
262 let codec_name = codec_name(codec_id);
263 let width = (*codec_parameters).width;
264 let height = (*codec_parameters).height;
265 let bit_rate = (*codec_parameters).bit_rate;
266 let pixel_format = (*codec_parameters).format;
267 let video_delay = (*codec_parameters).video_delay;
268 let fps = if avg_frame_rate.den == 0 {
269 0.0
270 } else {
271 avg_frame_rate.num as f64 / avg_frame_rate.den as f64
272 };
273
274 let rotate = metadata
276 .get("rotate")
277 .and_then(|rotate| rotate.parse::<i32>().ok())
278 .unwrap_or(0); let video_stream_info = StreamInfo::Video {
281 index,
282 time_base,
283 start_time,
284 duration,
285 nb_frames,
286 r_frame_rate,
287 sample_aspect_ratio,
288 metadata,
289 avg_frame_rate,
290 codec_id,
291 codec_name,
292 width,
293 height,
294 bit_rate,
295 pixel_format,
296 video_delay,
297 fps,
298 rotate,
299 };
300
301 Ok(Some(video_stream_info))
302 }
303}
304
305pub fn find_audio_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
320 let in_fmt_ctx_box = init_format_context(url)?;
321
322 unsafe {
323 let audio_index =
324 av_find_best_stream(in_fmt_ctx_box.fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, null_mut(), 0);
325 if audio_index < 0 {
326 return Ok(None);
327 }
328 let streams = (*in_fmt_ctx_box.fmt_ctx).streams;
329 let audio_stream = *streams.offset(audio_index as isize);
330 if audio_stream.is_null() {
331 return Err(FindStreamError::NoStreamFound.into());
332 }
333
334 let index = (*audio_stream).index;
335 let time_base = (*audio_stream).time_base;
336 let start_time = (*audio_stream).start_time;
337 let duration = (*audio_stream).duration;
338 let nb_frames = (*audio_stream).nb_frames;
339 let metadata = (*audio_stream).metadata;
340 let metadata = dict_to_hashmap(metadata);
341 let avg_frame_rate = (*audio_stream).avg_frame_rate;
342
343 let codec_parameters = (*audio_stream).codecpar;
344 if codec_parameters.is_null() {
345 return Err(FindStreamError::NoCodecparFound.into());
346 }
347 let codec_id = (*codec_parameters).codec_id;
348 let codec_name = codec_name(codec_id);
349 let sample_rate = (*codec_parameters).sample_rate;
350 #[cfg(not(feature = "docs-rs"))]
351 let ch_layout = (*codec_parameters).ch_layout;
352 let bit_rate = (*codec_parameters).bit_rate;
353 let sample_format = (*codec_parameters).format;
354 let frame_size = (*codec_parameters).frame_size;
355
356 let audio_stream_info = StreamInfo::Audio {
357 index,
358 time_base,
359 start_time,
360 duration,
361 nb_frames,
362 metadata,
363 avg_frame_rate,
364 codec_id,
365 codec_name,
366 sample_rate,
367 #[cfg(not(feature = "docs-rs"))]
368 order: ch_layout.order,
369 #[cfg(feature = "docs-rs")]
370 nb_channels: 0,
371 #[cfg(not(feature = "docs-rs"))]
372 nb_channels: ch_layout.nb_channels,
373 bit_rate,
374 sample_format,
375 frame_size,
376 };
377
378 Ok(Some(audio_stream_info))
379 }
380}
381
382pub fn find_subtitle_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
398 let in_fmt_ctx_box = init_format_context(url)?;
399
400 unsafe {
401 let subtitle_index =
402 av_find_best_stream(in_fmt_ctx_box.fmt_ctx, AVMEDIA_TYPE_SUBTITLE, -1, -1, null_mut(), 0);
403 if subtitle_index < 0 {
404 return Ok(None);
405 }
406
407 let streams = (*in_fmt_ctx_box.fmt_ctx).streams;
408 let subtitle_stream = *streams.offset(subtitle_index as isize);
409 if subtitle_stream.is_null() {
410 return Err(FindStreamError::NoStreamFound.into());
411 }
412
413 let index = (*subtitle_stream).index;
414 let time_base = (*subtitle_stream).time_base;
415 let start_time = (*subtitle_stream).start_time;
416 let duration = (*subtitle_stream).duration;
417 let nb_frames = (*subtitle_stream).nb_frames;
418 let metadata = (*subtitle_stream).metadata;
419 let metadata = dict_to_hashmap(metadata);
420
421 let codec_parameters = (*subtitle_stream).codecpar;
422 if codec_parameters.is_null() {
423 return Err(FindStreamError::NoCodecparFound.into());
424 }
425 let codec_id = (*codec_parameters).codec_id;
426 let codec_name = codec_name(codec_id);
427
428 let subtitle_stream_info = StreamInfo::Subtitle {
429 index,
430 time_base,
431 start_time,
432 duration,
433 nb_frames,
434 metadata,
435 codec_id,
436 codec_name,
437 };
438
439 Ok(Some(subtitle_stream_info))
440 }
441}
442
443pub fn find_data_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
457 let in_fmt_ctx_box = init_format_context(url)?;
458
459 unsafe {
460 let data_index = av_find_best_stream(in_fmt_ctx_box.fmt_ctx, AVMEDIA_TYPE_DATA, -1, -1, null_mut(), 0);
461 if data_index < 0 {
462 return Ok(None);
463 }
464
465 let streams = (*in_fmt_ctx_box.fmt_ctx).streams;
466 let data_stream = *streams.offset(data_index as isize);
467 if data_stream.is_null() {
468 return Err(FindStreamError::NoStreamFound.into());
469 }
470
471 let index = (*data_stream).index;
472 let time_base = (*data_stream).time_base;
473 let start_time = (*data_stream).start_time;
474 let duration = (*data_stream).duration;
475 let metadata = dict_to_hashmap((*data_stream).metadata);
476
477 Ok(Some(StreamInfo::Data {
478 index,
479 time_base,
480 start_time,
481 duration,
482 metadata,
483 }))
484 }
485}
486
487pub fn find_attachment_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
502 let in_fmt_ctx_box = init_format_context(url)?;
503
504 unsafe {
505 let attachment_index =
506 av_find_best_stream(in_fmt_ctx_box.fmt_ctx, AVMEDIA_TYPE_ATTACHMENT, -1, -1, null_mut(), 0);
507 if attachment_index < 0 {
508 return Ok(None);
509 }
510
511 let streams = (*in_fmt_ctx_box.fmt_ctx).streams;
512 let attachment_stream = *streams.offset(attachment_index as isize);
513 if attachment_stream.is_null() {
514 return Err(FindStreamError::NoStreamFound.into());
515 }
516
517 let index = (*attachment_stream).index;
518 let metadata = dict_to_hashmap((*attachment_stream).metadata);
519
520 let codec_parameters = (*attachment_stream).codecpar;
521 if codec_parameters.is_null() {
522 return Err(FindStreamError::NoCodecparFound.into());
523 }
524 let codec_id = (*codec_parameters).codec_id;
525 let codec_name = codec_name(codec_id);
526
527 Ok(Some(StreamInfo::Attachment {
528 index,
529 metadata,
530 codec_id,
531 codec_name,
532 }))
533 }
534}
535
536pub fn find_unknown_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
550 let in_fmt_ctx_box = init_format_context(url)?;
551
552 unsafe {
553 let unknown_index =
554 av_find_best_stream(in_fmt_ctx_box.fmt_ctx, AVMEDIA_TYPE_UNKNOWN, -1, -1, null_mut(), 0);
555 if unknown_index < 0 {
556 return Ok(None);
557 }
558
559 let streams = (*in_fmt_ctx_box.fmt_ctx).streams;
560 let unknown_stream = *streams.offset(unknown_index as isize);
561 if unknown_stream.is_null() {
562 return Err(FindStreamError::NoStreamFound.into());
563 }
564
565 let index = (*unknown_stream).index;
566 let metadata = dict_to_hashmap((*unknown_stream).metadata);
567
568 Ok(Some(StreamInfo::Unknown { index, metadata }))
569 }
570}
571
572pub fn find_all_stream_infos(url: impl Into<String>) -> Result<Vec<StreamInfo>> {
586 let in_fmt_ctx_box = init_format_context(url)?;
587
588 unsafe {
589 let fmt_ctx_ptr = in_fmt_ctx_box.fmt_ctx;
590 if fmt_ctx_ptr.is_null() {
591 return Err(OpenInputError::OutOfMemory.into());
592 }
593 let fmt_ctx = &*fmt_ctx_ptr;
594 let nb_streams = fmt_ctx.nb_streams as usize;
595
596 let mut infos = Vec::with_capacity(nb_streams);
597
598 for i in 0..nb_streams {
599 let raw_stream = *fmt_ctx.streams.add(i);
600 if raw_stream.is_null() {
601 infos.push(StreamInfo::Unknown {
602 index: i as i32,
603 metadata: HashMap::new(),
604 });
605 continue;
606 }
607 let stream = &*raw_stream;
608
609 let metadata = dict_to_hashmap(stream.metadata);
610
611 if stream.codecpar.is_null() {
612 infos.push(StreamInfo::Unknown {
613 index: stream.index,
614 metadata,
615 });
616 continue;
617 }
618
619 let codecpar = &*stream.codecpar;
620 let codec_id = codecpar.codec_id;
621 let codec_name = codec_name(codec_id);
622
623 let index = stream.index;
624 let time_base = stream.time_base;
625 let start_time = stream.start_time;
626 let duration = stream.duration;
627 let nb_frames = stream.nb_frames;
628 let avg_frame_rate = stream.avg_frame_rate;
629
630 match codecpar.codec_type {
631 AVMEDIA_TYPE_VIDEO => {
632 let width = codecpar.width;
633 let height = codecpar.height;
634 let bit_rate = codecpar.bit_rate;
635 let pixel_format = codecpar.format;
636 let video_delay = codecpar.video_delay;
637 let r_frame_rate = (*stream).r_frame_rate;
638 let sample_aspect_ratio = (*stream).sample_aspect_ratio;
639 let fps = if avg_frame_rate.den == 0 {
640 0.0
641 } else {
642 avg_frame_rate.num as f64 / avg_frame_rate.den as f64
643 };
644
645 let rotate = metadata
647 .get("rotate")
648 .and_then(|rotate| rotate.parse::<i32>().ok())
649 .unwrap_or(0); infos.push(StreamInfo::Video {
652 index,
653 time_base,
654 start_time,
655 duration,
656 nb_frames,
657 r_frame_rate,
658 sample_aspect_ratio,
659 metadata,
660 avg_frame_rate,
661 codec_id,
662 codec_name,
663 width,
664 height,
665 bit_rate,
666 pixel_format,
667 video_delay,
668 fps,
669 rotate,
670 });
671 }
672 AVMEDIA_TYPE_AUDIO => {
673 let sample_rate = codecpar.sample_rate;
674 #[cfg(not(feature = "docs-rs"))]
675 let ch_layout = codecpar.ch_layout;
676 let sample_format = codecpar.format;
677 let frame_size = codecpar.frame_size;
678 let bit_rate = codecpar.bit_rate;
679
680 infos.push(StreamInfo::Audio {
681 index,
682 time_base,
683 start_time,
684 duration,
685 nb_frames,
686 metadata,
687 avg_frame_rate,
688 codec_id,
689 codec_name,
690 sample_rate,
691 #[cfg(not(feature = "docs-rs"))]
692 order: ch_layout.order,
693 #[cfg(feature = "docs-rs")]
694 nb_channels: 0,
695 #[cfg(not(feature = "docs-rs"))]
696 nb_channels: ch_layout.nb_channels,
697 bit_rate,
698 sample_format,
699 frame_size,
700 });
701 }
702 AVMEDIA_TYPE_SUBTITLE => {
703 infos.push(StreamInfo::Subtitle {
704 index,
705 time_base,
706 start_time,
707 duration,
708 nb_frames,
709 metadata,
710 codec_id,
711 codec_name,
712 });
713 }
714 AVMEDIA_TYPE_DATA => {
715 infos.push(StreamInfo::Data {
716 index,
717 time_base,
718 start_time,
719 duration,
720 metadata,
721 });
722 }
723 AVMEDIA_TYPE_ATTACHMENT => {
724 infos.push(StreamInfo::Attachment {
725 index,
726 metadata,
727 codec_id,
728 codec_name,
729 });
730 }
731 AVMEDIA_TYPE_UNKNOWN => {
732 infos.push(StreamInfo::Unknown { index, metadata });
733 }
734 _ => {}
735 }
736 }
737
738 if infos.iter().all(|i| matches!(i, StreamInfo::Unknown { .. })) {
739 return Err(FindStreamError::NoStreamFound.into());
740 }
741
742 Ok(infos)
743 }
744}
745
746#[inline]
747fn codec_name(id: AVCodecID) -> String {
748 unsafe {
749 let ptr = avcodec_get_name(id);
750 if ptr.is_null() {
751 "Unknown codec".into()
752 } else {
753 CStr::from_ptr(ptr).to_string_lossy().into_owned()
754 }
755 }
756}
757
758fn init_format_context(url: impl Into<String>) -> Result<AVFormatContextBox> {
759 crate::core::initialize_ffmpeg();
760
761 unsafe {
762 let mut in_fmt_ctx = avformat_alloc_context();
763 if in_fmt_ctx.is_null() {
764 return Err(OpenInputError::OutOfMemory.into());
765 }
766
767 let url_cstr = CString::new(url.into())?;
768
769 let mut format_opts = null_mut();
770 let scan_all_pmts_key = CString::new("scan_all_pmts")?;
771 if av_dict_get(
772 format_opts,
773 scan_all_pmts_key.as_ptr(),
774 null(),
775 ffmpeg_sys_next::AV_DICT_MATCH_CASE,
776 )
777 .is_null()
778 {
779 let scan_all_pmts_value = CString::new("1")?;
780 ffmpeg_sys_next::av_dict_set(
781 &mut format_opts,
782 scan_all_pmts_key.as_ptr(),
783 scan_all_pmts_value.as_ptr(),
784 ffmpeg_sys_next::AV_DICT_DONT_OVERWRITE,
785 );
786 };
787
788 #[cfg(not(feature = "docs-rs"))]
789 let mut ret =
790 { avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), &mut format_opts) };
791 #[cfg(feature = "docs-rs")]
792 let mut ret = 0;
793
794 if ret < 0 {
795 avformat_close_input(&mut in_fmt_ctx);
796 return Err(OpenInputError::from(ret).into());
797 }
798
799 ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
800 if ret < 0 {
801 avformat_close_input(&mut in_fmt_ctx);
802 return Err(FindStreamError::from(ret).into());
803 }
804
805 Ok(AVFormatContextBox::new(in_fmt_ctx, true, false))
806 }
807}
808
809fn dict_to_hashmap(dict: *mut AVDictionary) -> HashMap<String, String> {
810 if dict.is_null() {
811 return HashMap::new();
812 }
813 let mut map = HashMap::new();
814 unsafe {
815 let mut e: *mut AVDictionaryEntry = null_mut();
816 while {
817 e = av_dict_get(dict, null(), e, AV_DICT_IGNORE_SUFFIX);
818 !e.is_null()
819 } {
820 let k = CStr::from_ptr((*e).key).to_string_lossy().into_owned();
821 let v = CStr::from_ptr((*e).value).to_string_lossy().into_owned();
822 map.insert(k, v);
823 }
824 }
825 map
826}
827
828#[cfg(test)]
829mod tests {
830 use super::*;
831
832 #[test]
833 fn test_not_found() {
834 let result = find_all_stream_infos("not_found.mp4");
835 assert!(result.is_err());
836
837 let error = result.err().unwrap();
838 println!("{error}");
839 assert!(matches!(
840 error,
841 crate::error::Error::OpenInputStream(OpenInputError::NotFound)
842 ))
843 }
844
845 #[test]
846 fn test_find_all_stream_infos() {
847 let stream_infos = find_all_stream_infos("test.mp4").unwrap();
848 assert_eq!(2, stream_infos.len());
849 for stream_info in stream_infos {
850 println!("{:?}", stream_info);
851 }
852 }
853
854 #[test]
855 fn test_find_video_stream_info() {
856 let option = find_video_stream_info("test.mp4").unwrap();
857 assert!(option.is_some());
858 let video_stream_info = option.unwrap();
859 println!("video_stream_info:{:?}", video_stream_info);
860 }
861
862 #[test]
863 fn test_find_audio_stream_info() {
864 let option = find_audio_stream_info("test.mp4").unwrap();
865 assert!(option.is_some());
866 let audio_stream_info = option.unwrap();
867 println!("audio_stream_info:{:?}", audio_stream_info);
868 }
869
870 #[test]
871 fn test_find_subtitle_stream_info() {
872 let option = find_subtitle_stream_info("test.mp4").unwrap();
873 assert!(option.is_none())
874 }
875
876 #[test]
877 fn test_find_data_stream_info() {
878 let option = find_data_stream_info("test.mp4").unwrap();
879 assert!(option.is_none());
880 }
881
882 #[test]
883 fn test_find_attachment_stream_info() {
884 let option = find_attachment_stream_info("test.mp4").unwrap();
885 assert!(option.is_none())
886 }
887
888 #[test]
889 fn test_find_unknown_stream_info() {
890 let option = find_unknown_stream_info("test.mp4").unwrap();
891 assert!(option.is_none())
892 }
893}