1use std::collections::HashMap;
2use std::ffi::{CStr, CString};
3use std::ptr::{null, null_mut};
4
5use ffmpeg_sys_next::{avformat_close_input, avformat_open_input};
6use ffmpeg_sys_next::AVMediaType::{AVMEDIA_TYPE_ATTACHMENT, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_DATA, AVMEDIA_TYPE_SUBTITLE, AVMEDIA_TYPE_UNKNOWN, AVMEDIA_TYPE_VIDEO};
7#[cfg(not(feature = "docs-rs"))]
8use ffmpeg_sys_next::AVChannelOrder;
9use ffmpeg_sys_next::{av_dict_get, av_find_best_stream, avcodec_get_name, avformat_find_stream_info, AVCodecID, AVDictionary, AVDictionaryEntry, AVRational, AV_DICT_IGNORE_SUFFIX};
10
11use crate::error::{FindStreamError, OpenInputError, Result};
12
13#[derive(Debug, Clone)]
14pub enum StreamInfo {
15 Video {
17 index: i32,
21
22 time_base: AVRational,
24
25 start_time: i64,
27
28 duration: i64,
30
31 nb_frames: i64,
33
34 r_frame_rate: AVRational,
36
37 sample_aspect_ratio: AVRational,
39
40 metadata: HashMap<String, String>,
42
43 avg_frame_rate: AVRational,
45
46 codec_id: AVCodecID,
50
51 codec_name: String,
53
54 width: i32,
56
57 height: i32,
59
60 bit_rate: i64,
62
63 pixel_format: i32,
65
66 video_delay: i32,
68
69 fps: f64,
72
73 rotate: i32,
76 },
77 Audio {
79 index: i32,
83
84 time_base: AVRational,
86
87 start_time: i64,
89
90 duration: i64,
92
93 nb_frames: i64,
95
96 metadata: HashMap<String, String>,
98
99 avg_frame_rate: AVRational,
101
102 codec_id: AVCodecID,
106
107 codec_name: String,
109
110 sample_rate: i32,
112
113 #[cfg(not(feature = "docs-rs"))]
115 order: AVChannelOrder,
116
117 nb_channels: i32,
119
120 bit_rate: i64,
122
123 sample_format: i32,
125
126 frame_size: i32,
128 },
129 Subtitle {
131 index: i32,
135
136 time_base: AVRational,
138
139 start_time: i64,
141
142 duration: i64,
144
145 nb_frames: i64,
147
148 metadata: HashMap<String, String>,
150
151 codec_id: AVCodecID,
155
156 codec_name: String,
158 },
159 Data {
161 index: i32,
165
166 time_base: AVRational,
168
169 start_time: i64,
171
172 duration: i64,
174
175 metadata: HashMap<String, String>,
177 },
178 Attachment {
180 index: i32,
184
185 metadata: HashMap<String, String>,
187
188 codec_id: AVCodecID,
192
193 codec_name: String,
195 },
196 Unknown {
198 index: i32,
202
203 metadata: HashMap<String, String>,
205 },
206}
207
208
209impl StreamInfo {
210 pub fn stream_type(&self) -> &'static str {
211 match self {
212 StreamInfo::Video { .. } => "Video",
213 StreamInfo::Audio { .. } => "Audio",
214 StreamInfo::Subtitle { .. } => "Subtitle",
215 StreamInfo::Data { .. } => "Data",
216 StreamInfo::Attachment { .. } => "Attachment",
217 StreamInfo::Unknown { .. } => "Unknown"
218 }
219 }
220}
221
222
223pub fn find_video_stream_info(url: &str) -> Result<Option<StreamInfo>> {
238 let mut in_fmt_ctx = null_mut();
239
240 let url_cstr = CString::new(url)?;
241 unsafe {
242 #[cfg(not(feature = "docs-rs"))]
243 let mut ret = {
244 avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), null_mut())
245 };
246 #[cfg(feature = "docs-rs")]
247 let mut ret = 0;
248 if ret < 0 {
249 avformat_close_input(&mut in_fmt_ctx);
250 return Err(OpenInputError::from(ret).into());
251 }
252
253 ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
254 if ret < 0 {
255 avformat_close_input(&mut in_fmt_ctx);
256 return Err(FindStreamError::from(ret).into());
257 }
258
259 let video_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, null_mut(), 0);
260 if video_index < 0 {
261 avformat_close_input(&mut in_fmt_ctx);
262 return Ok(None);
263 }
264 let streams = (*in_fmt_ctx).streams;
265 let video_stream = *streams.offset(video_index as isize);
266
267 let index = (*video_stream).index;
268 let time_base = (*video_stream).time_base;
269 let start_time = (*video_stream).start_time;
270 let duration = (*video_stream).duration;
271 let nb_frames = (*video_stream).nb_frames;
272 let r_frame_rate = (*video_stream).r_frame_rate;
273 let sample_aspect_ratio = (*video_stream).sample_aspect_ratio;
274 let metadata = (*video_stream).metadata;
275 let metadata = av_dict_to_hashmap(metadata);
276 let avg_frame_rate = (*video_stream).avg_frame_rate;
277
278 let codec_parameters = (*video_stream).codecpar;
279 let codec_id = (*codec_parameters).codec_id;
280 let codec_name = CStr::from_ptr(avcodec_get_name(codec_id));
281 let codec_name = codec_name.to_str().unwrap_or("Unknown codec");
282 let width = (*codec_parameters).width;
283 let height = (*codec_parameters).height;
284 let bit_rate = (*codec_parameters).bit_rate;
285 let pixel_format = (*codec_parameters).format;
286 let video_delay = (*codec_parameters).video_delay;
287 let fps = if avg_frame_rate.den == 0 {
288 0.0
289 } else {
290 avg_frame_rate.num as f64 / avg_frame_rate.den as f64
291 };
292
293 let rotate = metadata
295 .get("rotate")
296 .and_then(|rotate| rotate.parse::<i32>().ok())
297 .unwrap_or(0); let video_stream_info = StreamInfo::Video {
300 index,
301 time_base,
302 start_time,
303 duration,
304 nb_frames,
305 r_frame_rate,
306 sample_aspect_ratio,
307 metadata,
308 avg_frame_rate,
309 codec_id,
310 codec_name: codec_name.to_string(),
311 width,
312 height,
313 bit_rate,
314 pixel_format,
315 video_delay,
316 fps,
317 rotate,
318 };
319
320 avformat_close_input(&mut in_fmt_ctx);
321
322 Ok(Some(video_stream_info))
323 }
324}
325
326pub fn find_audio_stream_info(url: &str) -> Result<Option<StreamInfo>> {
341 let mut in_fmt_ctx = null_mut();
342
343 let url_cstr = CString::new(url)?;
344 unsafe {
345 #[cfg(not(feature = "docs-rs"))]
346 let mut ret = {
347 avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), null_mut())
348 };
349 #[cfg(feature = "docs-rs")]
350 let mut ret = 0;
351 if ret < 0 {
352 avformat_close_input(&mut in_fmt_ctx);
353 return Err(OpenInputError::from(ret).into());
354 }
355
356 ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
357 if ret < 0 {
358 avformat_close_input(&mut in_fmt_ctx);
359 return Err(FindStreamError::from(ret).into());
360 }
361
362 let audio_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, null_mut(), 0);
363 if audio_index < 0 {
364 avformat_close_input(&mut in_fmt_ctx);
365 return Ok(None);
366 }
367 let streams = (*in_fmt_ctx).streams;
368 let audio_stream = *streams.offset(audio_index as isize);
369
370 let index = (*audio_stream).index;
371 let time_base = (*audio_stream).time_base;
372 let start_time = (*audio_stream).start_time;
373 let duration = (*audio_stream).duration;
374 let nb_frames = (*audio_stream).nb_frames;
375 let metadata = (*audio_stream).metadata;
376 let metadata = av_dict_to_hashmap(metadata);
377 let avg_frame_rate = (*audio_stream).avg_frame_rate;
378
379 let codec_parameters = (*audio_stream).codecpar;
380 let codec_id = (*codec_parameters).codec_id;
381 let codec_name = CStr::from_ptr(avcodec_get_name(codec_id));
382 let codec_name = codec_name.to_str().unwrap_or("Unknown codec");
383 let sample_rate = (*codec_parameters).sample_rate;
384 #[cfg(not(feature = "docs-rs"))]
385 let ch_layout = (*codec_parameters).ch_layout;
386 let bit_rate = (*codec_parameters).bit_rate;
387 let sample_format = (*codec_parameters).format;
388 let frame_size = (*codec_parameters).frame_size;
389
390 let audio_stream_info = StreamInfo::Audio {
391 index,
392 time_base,
393 start_time,
394 duration,
395 nb_frames,
396 metadata,
397 avg_frame_rate,
398 codec_id,
399 codec_name: codec_name.to_string(),
400 sample_rate,
401 #[cfg(not(feature = "docs-rs"))]
402 order: ch_layout.order,
403 #[cfg(feature = "docs-rs")]
404 nb_channels: 0,
405 #[cfg(not(feature = "docs-rs"))]
406 nb_channels: ch_layout.nb_channels,
407 bit_rate,
408 sample_format,
409 frame_size,
410 };
411
412 avformat_close_input(&mut in_fmt_ctx);
413
414 Ok(Some(audio_stream_info))
415 }
416}
417
418pub fn find_subtitle_stream_info(url: &str) -> Result<Option<StreamInfo>> {
434 let mut in_fmt_ctx = null_mut();
435
436 let url_cstr = CString::new(url)?;
437 unsafe {
438 #[cfg(not(feature = "docs-rs"))]
439 let mut ret = {
440 avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), null_mut())
441 };
442 #[cfg(feature = "docs-rs")]
443 let mut ret = 0;
444 if ret < 0 {
445 avformat_close_input(&mut in_fmt_ctx);
446 return Err(OpenInputError::from(ret).into());
447 }
448
449 ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
450 if ret < 0 {
451 avformat_close_input(&mut in_fmt_ctx);
452 return Err(FindStreamError::from(ret).into());
453 }
454
455 let subtitle_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_SUBTITLE, -1, -1, null_mut(), 0);
456 if subtitle_index < 0 {
457 avformat_close_input(&mut in_fmt_ctx);
458 return Ok(None);
459 }
460
461 let streams = (*in_fmt_ctx).streams;
462 let subtitle_stream = *streams.offset(subtitle_index as isize);
463
464 let index = (*subtitle_stream).index;
465 let time_base = (*subtitle_stream).time_base;
466 let start_time = (*subtitle_stream).start_time;
467 let duration = (*subtitle_stream).duration;
468 let nb_frames = (*subtitle_stream).nb_frames;
469 let metadata = (*subtitle_stream).metadata;
470 let metadata = av_dict_to_hashmap(metadata);
471
472 let codec_parameters = (*subtitle_stream).codecpar;
473 let codec_id = (*codec_parameters).codec_id;
474 let codec_name = CStr::from_ptr(avcodec_get_name(codec_id));
475 let codec_name = codec_name.to_str().unwrap_or("Unknown codec");
476
477 let subtitle_stream_info = StreamInfo::Subtitle {
478 index,
479 time_base,
480 start_time,
481 duration,
482 nb_frames,
483 metadata,
484 codec_id,
485 codec_name: codec_name.to_string(),
486 };
487
488 avformat_close_input(&mut in_fmt_ctx);
489
490 Ok(Some(subtitle_stream_info))
491 }
492}
493
494pub fn find_data_stream_info(url: &str) -> Result<Option<StreamInfo>> {
508 let mut in_fmt_ctx = null_mut();
509 let url_cstr = CString::new(url)?;
510 unsafe {
511 #[cfg(not(feature = "docs-rs"))]
512 let mut ret = {
513 avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), null_mut())
514 };
515 #[cfg(feature = "docs-rs")]
516 let mut ret = 0;
517 if ret < 0 {
518 avformat_close_input(&mut in_fmt_ctx);
519 return Err(OpenInputError::from(ret).into());
520 }
521
522 ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
523 if ret < 0 {
524 avformat_close_input(&mut in_fmt_ctx);
525 return Err(FindStreamError::from(ret).into());
526 }
527
528 let data_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_DATA, -1, -1, null_mut(), 0);
529 if data_index < 0 {
530 avformat_close_input(&mut in_fmt_ctx);
531 return Ok(None);
532 }
533
534 let streams = (*in_fmt_ctx).streams;
535 let data_stream = *streams.offset(data_index as isize);
536
537 let index = (*data_stream).index;
538 let time_base = (*data_stream).time_base;
539 let start_time = (*data_stream).start_time;
540 let duration = (*data_stream).duration;
541 let metadata = av_dict_to_hashmap((*data_stream).metadata);
542
543 avformat_close_input(&mut in_fmt_ctx);
544
545 Ok(Some(StreamInfo::Data {
546 index,
547 time_base,
548 start_time,
549 duration,
550 metadata,
551 }))
552 }
553}
554
555pub fn find_attachment_stream_info(url: &str) -> Result<Option<StreamInfo>> {
570 let mut in_fmt_ctx = null_mut();
571 let url_cstr = CString::new(url)?;
572 unsafe {
573 #[cfg(not(feature = "docs-rs"))]
574 let mut ret = {
575 avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), null_mut())
576 };
577 #[cfg(feature = "docs-rs")]
578 let mut ret = 0;
579 if ret < 0 {
580 avformat_close_input(&mut in_fmt_ctx);
581 return Err(OpenInputError::from(ret).into());
582 }
583
584 ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
585 if ret < 0 {
586 avformat_close_input(&mut in_fmt_ctx);
587 return Err(FindStreamError::from(ret).into());
588 }
589
590 let attachment_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_ATTACHMENT, -1, -1, null_mut(), 0);
591 if attachment_index < 0 {
592 avformat_close_input(&mut in_fmt_ctx);
593 return Ok(None);
594 }
595
596 let streams = (*in_fmt_ctx).streams;
597 let attachment_stream = *streams.offset(attachment_index as isize);
598
599 let index = (*attachment_stream).index;
600 let metadata = av_dict_to_hashmap((*attachment_stream).metadata);
601
602 let codec_parameters = (*attachment_stream).codecpar;
603 let codec_id = (*codec_parameters).codec_id;
604 let codec_name = CStr::from_ptr(avcodec_get_name(codec_id))
605 .to_str()
606 .unwrap_or("Unknown codec")
607 .to_string();
608
609 avformat_close_input(&mut in_fmt_ctx);
610
611 Ok(Some(StreamInfo::Attachment {
612 index,
613 metadata,
614 codec_id,
615 codec_name,
616 }))
617 }
618}
619
620pub fn find_unknown_stream_info(url: &str) -> Result<Option<StreamInfo>> {
634 let mut in_fmt_ctx = null_mut();
635 let url_cstr = CString::new(url)?;
636 unsafe {
637 #[cfg(not(feature = "docs-rs"))]
638 let mut ret = {
639 avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), null_mut())
640 };
641 #[cfg(feature = "docs-rs")]
642 let mut ret = 0;
643 if ret < 0 {
644 avformat_close_input(&mut in_fmt_ctx);
645 return Err(OpenInputError::from(ret).into());
646 }
647
648 ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
649 if ret < 0 {
650 avformat_close_input(&mut in_fmt_ctx);
651 return Err(FindStreamError::from(ret).into());
652 }
653
654 let unknown_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_UNKNOWN, -1, -1, null_mut(), 0);
655 if unknown_index < 0 {
656 avformat_close_input(&mut in_fmt_ctx);
657 return Ok(None);
658 }
659
660 let streams = (*in_fmt_ctx).streams;
661 let unknown_stream = *streams.offset(unknown_index as isize);
662
663 let index = (*unknown_stream).index;
664 let metadata = av_dict_to_hashmap((*unknown_stream).metadata);
665
666 avformat_close_input(&mut in_fmt_ctx);
667
668 Ok(Some(StreamInfo::Unknown {
669 index,
670 metadata,
671 }))
672 }
673}
674
675pub fn find_all_stream_infos(url: &str) -> Result<Vec<StreamInfo>> {
689 let mut in_fmt_ctx = null_mut();
690 let url_cstr = CString::new(url)?;
691 unsafe {
692 #[cfg(not(feature = "docs-rs"))]
693 let mut ret = {
694 avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), null_mut())
695 };
696 #[cfg(feature = "docs-rs")]
697 let mut ret = 0;
698 if ret < 0 {
699 avformat_close_input(&mut in_fmt_ctx);
700 return Err(OpenInputError::from(ret).into());
701 }
702
703 ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
704 if ret < 0 {
705 avformat_close_input(&mut in_fmt_ctx);
706 return Err(FindStreamError::from(ret).into());
707 }
708
709 let mut stream_infos = Vec::new();
710
711 let stream_count = (*in_fmt_ctx).nb_streams;
712
713 for i in 0..stream_count {
714 let stream = *(*in_fmt_ctx).streams.add(i as usize);
715 let codec_parameters = (*stream).codecpar;
716 let codec_id = (*codec_parameters).codec_id;
717 let codec_name = CStr::from_ptr(avcodec_get_name(codec_id)).to_str().unwrap_or("Unknown codec").to_string();
718
719 let index = (*stream).index;
720 let time_base = (*stream).time_base;
721 let start_time = (*stream).start_time;
722 let duration = (*stream).duration;
723 let nb_frames = (*stream).nb_frames;
724 let avg_frame_rate = (*stream).avg_frame_rate;
725 let metadata = av_dict_to_hashmap((*stream).metadata);
726
727 match (*codec_parameters).codec_type {
728 AVMEDIA_TYPE_VIDEO => {
729 let width = (*codec_parameters).width;
730 let height = (*codec_parameters).height;
731 let bit_rate = (*codec_parameters).bit_rate;
732 let pixel_format = (*codec_parameters).format;
733 let video_delay = (*codec_parameters).video_delay;
734 let r_frame_rate = (*stream).r_frame_rate;
735 let sample_aspect_ratio = (*stream).sample_aspect_ratio;
736 let fps = if avg_frame_rate.den == 0 {
737 0.0
738 } else {
739 avg_frame_rate.num as f64 / avg_frame_rate.den as f64
740 };
741
742 let rotate = metadata
744 .get("rotate")
745 .and_then(|rotate| rotate.parse::<i32>().ok())
746 .unwrap_or(0); stream_infos.push(StreamInfo::Video {
749 index,
750 time_base,
751 start_time,
752 duration,
753 nb_frames,
754 r_frame_rate,
755 sample_aspect_ratio,
756 metadata,
757 avg_frame_rate,
758 codec_id,
759 codec_name,
760 width,
761 height,
762 bit_rate,
763 pixel_format,
764 video_delay,
765 fps,
766 rotate
767 });
768 }
769 AVMEDIA_TYPE_AUDIO => {
770 let sample_rate = (*codec_parameters).sample_rate;
771 #[cfg(not(feature = "docs-rs"))]
772 let ch_layout = (*codec_parameters).ch_layout;
773 let sample_format = (*codec_parameters).format;
774 let frame_size = (*codec_parameters).frame_size;
775 let bit_rate = (*codec_parameters).bit_rate;
776
777 stream_infos.push(StreamInfo::Audio {
778 index,
779 time_base,
780 start_time,
781 duration,
782 nb_frames,
783 metadata,
784 avg_frame_rate,
785 codec_id,
786 codec_name,
787 sample_rate,
788 #[cfg(not(feature = "docs-rs"))]
789 order: ch_layout.order,
790 #[cfg(feature = "docs-rs")]
791 nb_channels: 0,
792 #[cfg(not(feature = "docs-rs"))]
793 nb_channels: ch_layout.nb_channels,
794 bit_rate,
795 sample_format,
796 frame_size,
797 });
798 }
799 AVMEDIA_TYPE_SUBTITLE => {
800 stream_infos.push(StreamInfo::Subtitle {
801 index,
802 time_base,
803 start_time,
804 duration,
805 nb_frames,
806 metadata,
807 codec_id,
808 codec_name,
809 });
810 }
811 AVMEDIA_TYPE_DATA => {
812 stream_infos.push(StreamInfo::Data {
813 index,
814 time_base,
815 start_time,
816 duration,
817 metadata,
818 });
819 }
820 AVMEDIA_TYPE_ATTACHMENT => {
821 stream_infos.push(StreamInfo::Attachment {
822 index,
823 metadata,
824 codec_id,
825 codec_name,
826 });
827 }
828 AVMEDIA_TYPE_UNKNOWN => {
829 stream_infos.push(StreamInfo::Unknown {
830 index,
831 metadata,
832 });
833 }
834 _ => {}
835 }
836 }
837
838 avformat_close_input(&mut in_fmt_ctx);
839
840 Ok(stream_infos)
841 }
842}
843
844unsafe fn av_dict_to_hashmap(dict: *mut AVDictionary) -> HashMap<String, String> {
845 let mut map = HashMap::new();
846 let mut entry: *mut AVDictionaryEntry = null_mut();
847
848 while {
849 entry = av_dict_get(dict, null(), entry, AV_DICT_IGNORE_SUFFIX);
850 !entry.is_null()
851 } {
852 let key = CStr::from_ptr((*entry).key).to_string_lossy().into_owned();
853 let value = CStr::from_ptr((*entry).value).to_string_lossy().into_owned();
854
855 map.insert(key, value);
856 }
857
858 map
859}
860
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865
866 #[test]
867 fn test_not_found() {
868 let result = find_all_stream_infos("not_found.mp4");
869 assert!(result.is_err());
870
871 let error = result.err().unwrap();
872 println!("{error}");
873 assert!(matches!(error, crate::error::Error::OpenInputStream(OpenInputError::NotFound)))
874 }
875
876 #[test]
877 fn test_find_all_stream_infos() {
878 let stream_infos = find_all_stream_infos("test.mp4").unwrap();
879 assert_eq!(2, stream_infos.len());
880 for stream_info in stream_infos {
881 println!("{:?}", stream_info);
882 }
883 }
884
885 #[test]
886 fn test_find_video_stream_info() {
887 let option = find_video_stream_info("test.mp4").unwrap();
888 assert!(option.is_some());
889 let video_stream_info = option.unwrap();
890 println!("video_stream_info:{:?}", video_stream_info);
891 }
892
893 #[test]
894 fn test_find_audio_stream_info() {
895 let option = find_audio_stream_info("test.mp4").unwrap();
896 assert!(option.is_some());
897 let audio_stream_info = option.unwrap();
898 println!("audio_stream_info:{:?}", audio_stream_info);
899 }
900
901 #[test]
902 fn test_find_subtitle_stream_info() {
903 let option = find_subtitle_stream_info("test.mp4").unwrap();
904 assert!(option.is_none())
905 }
906
907 #[test]
908 fn test_find_data_stream_info() {
909 let option = find_data_stream_info("test.mp4").unwrap();
910 assert!(option.is_none());
911 }
912
913 #[test]
914 fn test_find_attachment_stream_info() {
915 let option = find_attachment_stream_info("test.mp4").unwrap();
916 assert!(option.is_none())
917 }
918
919 #[test]
920 fn test_find_unknown_stream_info() {
921 let option = find_unknown_stream_info("test.mp4").unwrap();
922 assert!(option.is_none())
923 }
924}