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 framerate: AVRational,
36
37 sample_aspect_ratio: AVRational,
39
40 metadata: HashMap<String, String>,
42
43 avg_framerate: 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: f32,
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_framerate: 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 framerate = (*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_framerate = (*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 = framerate.num as f32 / framerate.den as f32;
288
289 let rotate = metadata
291 .get("rotate")
292 .and_then(|rotate| rotate.parse::<i32>().ok())
293 .unwrap_or(0); let video_stream_info = StreamInfo::Video {
296 index,
297 time_base,
298 start_time,
299 duration,
300 nb_frames,
301 framerate,
302 sample_aspect_ratio,
303 metadata,
304 avg_framerate,
305 codec_id,
306 codec_name: codec_name.to_string(),
307 width,
308 height,
309 bit_rate,
310 pixel_format,
311 video_delay,
312 fps,
313 rotate,
314 };
315
316 avformat_close_input(&mut in_fmt_ctx);
317
318 Ok(Some(video_stream_info))
319 }
320}
321
322pub fn find_audio_stream_info(url: &str) -> Result<Option<StreamInfo>> {
337 let mut in_fmt_ctx = null_mut();
338
339 let url_cstr = CString::new(url)?;
340 unsafe {
341 #[cfg(not(feature = "docs-rs"))]
342 let mut ret = {
343 avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), null_mut())
344 };
345 #[cfg(feature = "docs-rs")]
346 let mut ret = 0;
347 if ret < 0 {
348 avformat_close_input(&mut in_fmt_ctx);
349 return Err(OpenInputError::from(ret).into());
350 }
351
352 ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
353 if ret < 0 {
354 avformat_close_input(&mut in_fmt_ctx);
355 return Err(FindStreamError::from(ret).into());
356 }
357
358 let audio_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, null_mut(), 0);
359 if audio_index < 0 {
360 avformat_close_input(&mut in_fmt_ctx);
361 return Ok(None);
362 }
363 let streams = (*in_fmt_ctx).streams;
364 let audio_stream = *streams.offset(audio_index as isize);
365
366 let index = (*audio_stream).index;
367 let time_base = (*audio_stream).time_base;
368 let start_time = (*audio_stream).start_time;
369 let duration = (*audio_stream).duration;
370 let nb_frames = (*audio_stream).nb_frames;
371 let metadata = (*audio_stream).metadata;
372 let metadata = av_dict_to_hashmap(metadata);
373 let avg_framerate = (*audio_stream).avg_frame_rate;
374
375 let codec_parameters = (*audio_stream).codecpar;
376 let codec_id = (*codec_parameters).codec_id;
377 let codec_name = CStr::from_ptr(avcodec_get_name(codec_id));
378 let codec_name = codec_name.to_str().unwrap_or("Unknown codec");
379 let sample_rate = (*codec_parameters).sample_rate;
380 #[cfg(not(feature = "docs-rs"))]
381 let ch_layout = (*codec_parameters).ch_layout;
382 let bit_rate = (*codec_parameters).bit_rate;
383 let sample_format = (*codec_parameters).format;
384 let frame_size = (*codec_parameters).frame_size;
385
386 let audio_stream_info = StreamInfo::Audio {
387 index,
388 time_base,
389 start_time,
390 duration,
391 nb_frames,
392 metadata,
393 avg_framerate,
394 codec_id,
395 codec_name: codec_name.to_string(),
396 sample_rate,
397 #[cfg(not(feature = "docs-rs"))]
398 order: ch_layout.order,
399 #[cfg(feature = "docs-rs")]
400 nb_channels: 0,
401 #[cfg(not(feature = "docs-rs"))]
402 nb_channels: ch_layout.nb_channels,
403 bit_rate,
404 sample_format,
405 frame_size,
406 };
407
408 avformat_close_input(&mut in_fmt_ctx);
409
410 Ok(Some(audio_stream_info))
411 }
412}
413
414pub fn find_subtitle_stream_info(url: &str) -> Result<Option<StreamInfo>> {
430 let mut in_fmt_ctx = null_mut();
431
432 let url_cstr = CString::new(url)?;
433 unsafe {
434 #[cfg(not(feature = "docs-rs"))]
435 let mut ret = {
436 avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), null_mut())
437 };
438 #[cfg(feature = "docs-rs")]
439 let mut ret = 0;
440 if ret < 0 {
441 avformat_close_input(&mut in_fmt_ctx);
442 return Err(OpenInputError::from(ret).into());
443 }
444
445 ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
446 if ret < 0 {
447 avformat_close_input(&mut in_fmt_ctx);
448 return Err(FindStreamError::from(ret).into());
449 }
450
451 let subtitle_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_SUBTITLE, -1, -1, null_mut(), 0);
452 if subtitle_index < 0 {
453 avformat_close_input(&mut in_fmt_ctx);
454 return Ok(None);
455 }
456
457 let streams = (*in_fmt_ctx).streams;
458 let subtitle_stream = *streams.offset(subtitle_index as isize);
459
460 let index = (*subtitle_stream).index;
461 let time_base = (*subtitle_stream).time_base;
462 let start_time = (*subtitle_stream).start_time;
463 let duration = (*subtitle_stream).duration;
464 let nb_frames = (*subtitle_stream).nb_frames;
465 let metadata = (*subtitle_stream).metadata;
466 let metadata = av_dict_to_hashmap(metadata);
467
468 let codec_parameters = (*subtitle_stream).codecpar;
469 let codec_id = (*codec_parameters).codec_id;
470 let codec_name = CStr::from_ptr(avcodec_get_name(codec_id));
471 let codec_name = codec_name.to_str().unwrap_or("Unknown codec");
472
473 let subtitle_stream_info = StreamInfo::Subtitle {
474 index,
475 time_base,
476 start_time,
477 duration,
478 nb_frames,
479 metadata,
480 codec_id,
481 codec_name: codec_name.to_string(),
482 };
483
484 avformat_close_input(&mut in_fmt_ctx);
485
486 Ok(Some(subtitle_stream_info))
487 }
488}
489
490pub fn find_data_stream_info(url: &str) -> Result<Option<StreamInfo>> {
504 let mut in_fmt_ctx = null_mut();
505 let url_cstr = CString::new(url)?;
506 unsafe {
507 #[cfg(not(feature = "docs-rs"))]
508 let mut ret = {
509 avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), null_mut())
510 };
511 #[cfg(feature = "docs-rs")]
512 let mut ret = 0;
513 if ret < 0 {
514 avformat_close_input(&mut in_fmt_ctx);
515 return Err(OpenInputError::from(ret).into());
516 }
517
518 ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
519 if ret < 0 {
520 avformat_close_input(&mut in_fmt_ctx);
521 return Err(FindStreamError::from(ret).into());
522 }
523
524 let data_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_DATA, -1, -1, null_mut(), 0);
525 if data_index < 0 {
526 avformat_close_input(&mut in_fmt_ctx);
527 return Ok(None);
528 }
529
530 let streams = (*in_fmt_ctx).streams;
531 let data_stream = *streams.offset(data_index as isize);
532
533 let index = (*data_stream).index;
534 let time_base = (*data_stream).time_base;
535 let start_time = (*data_stream).start_time;
536 let duration = (*data_stream).duration;
537 let metadata = av_dict_to_hashmap((*data_stream).metadata);
538
539 avformat_close_input(&mut in_fmt_ctx);
540
541 Ok(Some(StreamInfo::Data {
542 index,
543 time_base,
544 start_time,
545 duration,
546 metadata,
547 }))
548 }
549}
550
551pub fn find_attachment_stream_info(url: &str) -> Result<Option<StreamInfo>> {
566 let mut in_fmt_ctx = null_mut();
567 let url_cstr = CString::new(url)?;
568 unsafe {
569 #[cfg(not(feature = "docs-rs"))]
570 let mut ret = {
571 avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), null_mut())
572 };
573 #[cfg(feature = "docs-rs")]
574 let mut ret = 0;
575 if ret < 0 {
576 avformat_close_input(&mut in_fmt_ctx);
577 return Err(OpenInputError::from(ret).into());
578 }
579
580 ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
581 if ret < 0 {
582 avformat_close_input(&mut in_fmt_ctx);
583 return Err(FindStreamError::from(ret).into());
584 }
585
586 let attachment_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_ATTACHMENT, -1, -1, null_mut(), 0);
587 if attachment_index < 0 {
588 avformat_close_input(&mut in_fmt_ctx);
589 return Ok(None);
590 }
591
592 let streams = (*in_fmt_ctx).streams;
593 let attachment_stream = *streams.offset(attachment_index as isize);
594
595 let index = (*attachment_stream).index;
596 let metadata = av_dict_to_hashmap((*attachment_stream).metadata);
597
598 let codec_parameters = (*attachment_stream).codecpar;
599 let codec_id = (*codec_parameters).codec_id;
600 let codec_name = CStr::from_ptr(avcodec_get_name(codec_id))
601 .to_str()
602 .unwrap_or("Unknown codec")
603 .to_string();
604
605 avformat_close_input(&mut in_fmt_ctx);
606
607 Ok(Some(StreamInfo::Attachment {
608 index,
609 metadata,
610 codec_id,
611 codec_name,
612 }))
613 }
614}
615
616pub fn find_unknown_stream_info(url: &str) -> Result<Option<StreamInfo>> {
630 let mut in_fmt_ctx = null_mut();
631 let url_cstr = CString::new(url)?;
632 unsafe {
633 #[cfg(not(feature = "docs-rs"))]
634 let mut ret = {
635 avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), null_mut())
636 };
637 #[cfg(feature = "docs-rs")]
638 let mut ret = 0;
639 if ret < 0 {
640 avformat_close_input(&mut in_fmt_ctx);
641 return Err(OpenInputError::from(ret).into());
642 }
643
644 ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
645 if ret < 0 {
646 avformat_close_input(&mut in_fmt_ctx);
647 return Err(FindStreamError::from(ret).into());
648 }
649
650 let unknown_index = av_find_best_stream(in_fmt_ctx, AVMEDIA_TYPE_UNKNOWN, -1, -1, null_mut(), 0);
651 if unknown_index < 0 {
652 avformat_close_input(&mut in_fmt_ctx);
653 return Ok(None);
654 }
655
656 let streams = (*in_fmt_ctx).streams;
657 let unknown_stream = *streams.offset(unknown_index as isize);
658
659 let index = (*unknown_stream).index;
660 let metadata = av_dict_to_hashmap((*unknown_stream).metadata);
661
662 avformat_close_input(&mut in_fmt_ctx);
663
664 Ok(Some(StreamInfo::Unknown {
665 index,
666 metadata,
667 }))
668 }
669}
670
671pub fn find_all_stream_infos(url: &str) -> Result<Vec<StreamInfo>> {
685 let mut in_fmt_ctx = null_mut();
686 let url_cstr = CString::new(url)?;
687 unsafe {
688 #[cfg(not(feature = "docs-rs"))]
689 let mut ret = {
690 avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), null_mut())
691 };
692 #[cfg(feature = "docs-rs")]
693 let mut ret = 0;
694 if ret < 0 {
695 avformat_close_input(&mut in_fmt_ctx);
696 return Err(OpenInputError::from(ret).into());
697 }
698
699 ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
700 if ret < 0 {
701 avformat_close_input(&mut in_fmt_ctx);
702 return Err(FindStreamError::from(ret).into());
703 }
704
705 let mut stream_infos = Vec::new();
706
707 let stream_count = (*in_fmt_ctx).nb_streams;
708
709 for i in 0..stream_count {
710 let stream = *(*in_fmt_ctx).streams.add(i as usize);
711 let codec_parameters = (*stream).codecpar;
712 let codec_id = (*codec_parameters).codec_id;
713 let codec_name = CStr::from_ptr(avcodec_get_name(codec_id)).to_str().unwrap_or("Unknown codec").to_string();
714
715 let index = (*stream).index;
716 let time_base = (*stream).time_base;
717 let start_time = (*stream).start_time;
718 let duration = (*stream).duration;
719 let nb_frames = (*stream).nb_frames;
720 let avg_framerate = (*stream).avg_frame_rate;
721 let metadata = av_dict_to_hashmap((*stream).metadata);
722
723 match (*codec_parameters).codec_type {
724 AVMEDIA_TYPE_VIDEO => {
725 let width = (*codec_parameters).width;
726 let height = (*codec_parameters).height;
727 let bit_rate = (*codec_parameters).bit_rate;
728 let pixel_format = (*codec_parameters).format;
729 let video_delay = (*codec_parameters).video_delay;
730 let framerate = (*stream).r_frame_rate;
731 let sample_aspect_ratio = (*stream).sample_aspect_ratio;
732 let fps = framerate.num as f32 / framerate.den as f32;
733
734 let rotate = metadata
736 .get("rotate")
737 .and_then(|rotate| rotate.parse::<i32>().ok())
738 .unwrap_or(0); stream_infos.push(StreamInfo::Video {
741 index,
742 time_base,
743 start_time,
744 duration,
745 nb_frames,
746 framerate,
747 sample_aspect_ratio,
748 metadata,
749 avg_framerate,
750 codec_id,
751 codec_name,
752 width,
753 height,
754 bit_rate,
755 pixel_format,
756 video_delay,
757 fps,
758 rotate
759 });
760 }
761 AVMEDIA_TYPE_AUDIO => {
762 let sample_rate = (*codec_parameters).sample_rate;
763 #[cfg(not(feature = "docs-rs"))]
764 let ch_layout = (*codec_parameters).ch_layout;
765 let sample_format = (*codec_parameters).format;
766 let frame_size = (*codec_parameters).frame_size;
767 let bit_rate = (*codec_parameters).bit_rate;
768
769 stream_infos.push(StreamInfo::Audio {
770 index,
771 time_base,
772 start_time,
773 duration,
774 nb_frames,
775 metadata,
776 avg_framerate,
777 codec_id,
778 codec_name,
779 sample_rate,
780 #[cfg(not(feature = "docs-rs"))]
781 order: ch_layout.order,
782 #[cfg(feature = "docs-rs")]
783 nb_channels: 0,
784 #[cfg(not(feature = "docs-rs"))]
785 nb_channels: ch_layout.nb_channels,
786 bit_rate,
787 sample_format,
788 frame_size,
789 });
790 }
791 AVMEDIA_TYPE_SUBTITLE => {
792 stream_infos.push(StreamInfo::Subtitle {
793 index,
794 time_base,
795 start_time,
796 duration,
797 nb_frames,
798 metadata,
799 codec_id,
800 codec_name,
801 });
802 }
803 AVMEDIA_TYPE_DATA => {
804 stream_infos.push(StreamInfo::Data {
805 index,
806 time_base,
807 start_time,
808 duration,
809 metadata,
810 });
811 }
812 AVMEDIA_TYPE_ATTACHMENT => {
813 stream_infos.push(StreamInfo::Attachment {
814 index,
815 metadata,
816 codec_id,
817 codec_name,
818 });
819 }
820 AVMEDIA_TYPE_UNKNOWN => {
821 stream_infos.push(StreamInfo::Unknown {
822 index,
823 metadata,
824 });
825 }
826 _ => {}
827 }
828 }
829
830 avformat_close_input(&mut in_fmt_ctx);
831
832 Ok(stream_infos)
833 }
834}
835
836unsafe fn av_dict_to_hashmap(dict: *mut AVDictionary) -> HashMap<String, String> {
837 let mut map = HashMap::new();
838 let mut entry: *mut AVDictionaryEntry = null_mut();
839
840 while {
841 entry = av_dict_get(dict, null(), entry, AV_DICT_IGNORE_SUFFIX);
842 !entry.is_null()
843 } {
844 let key = CStr::from_ptr((*entry).key).to_string_lossy().into_owned();
845 let value = CStr::from_ptr((*entry).value).to_string_lossy().into_owned();
846
847 map.insert(key, value);
848 }
849
850 map
851}
852
853
854#[cfg(test)]
855mod tests {
856 use super::*;
857
858 #[test]
859 fn test_not_found() {
860 let result = find_all_stream_infos("not_found.mp4");
861 assert!(result.is_err());
862
863 let error = result.err().unwrap();
864 println!("{error}");
865 assert!(matches!(error, crate::error::Error::OpenInputStream(OpenInputError::NotFound)))
866 }
867
868 #[test]
869 fn test_find_all_stream_infos() {
870 let stream_infos = find_all_stream_infos("test.mp4").unwrap();
871 assert_eq!(2, stream_infos.len());
872 for stream_info in stream_infos {
873 println!("{:?}", stream_info);
874 }
875 }
876
877 #[test]
878 fn test_find_video_stream_info() {
879 let option = find_video_stream_info("test.mp4").unwrap();
880 assert!(option.is_some());
881 let video_stream_info = option.unwrap();
882 println!("video_stream_info:{:?}", video_stream_info);
883 }
884
885 #[test]
886 fn test_find_audio_stream_info() {
887 let option = find_audio_stream_info("test.mp4").unwrap();
888 assert!(option.is_some());
889 let audio_stream_info = option.unwrap();
890 println!("audio_stream_info:{:?}", audio_stream_info);
891 }
892
893 #[test]
894 fn test_find_subtitle_stream_info() {
895 let option = find_subtitle_stream_info("test.mp4").unwrap();
896 assert!(option.is_none())
897 }
898
899 #[test]
900 fn test_find_data_stream_info() {
901 let option = find_data_stream_info("test.mp4").unwrap();
902 assert!(option.is_none());
903 }
904
905 #[test]
906 fn test_find_attachment_stream_info() {
907 let option = find_attachment_stream_info("test.mp4").unwrap();
908 assert!(option.is_none())
909 }
910
911 #[test]
912 fn test_find_unknown_stream_info() {
913 let option = find_unknown_stream_info("test.mp4").unwrap();
914 assert!(option.is_none())
915 }
916}