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
243 let index = (*video_stream).index;
244 let time_base = (*video_stream).time_base;
245 let start_time = (*video_stream).start_time;
246 let duration = (*video_stream).duration;
247 let nb_frames = (*video_stream).nb_frames;
248 let r_frame_rate = (*video_stream).r_frame_rate;
249 let sample_aspect_ratio = (*video_stream).sample_aspect_ratio;
250 let metadata = (*video_stream).metadata;
251 let metadata = av_dict_to_hashmap(metadata);
252 let avg_frame_rate = (*video_stream).avg_frame_rate;
253
254 let codec_parameters = (*video_stream).codecpar;
255 let codec_id = (*codec_parameters).codec_id;
256 let codec_name = CStr::from_ptr(avcodec_get_name(codec_id));
257 let codec_name = codec_name.to_str().unwrap_or("Unknown codec");
258 let width = (*codec_parameters).width;
259 let height = (*codec_parameters).height;
260 let bit_rate = (*codec_parameters).bit_rate;
261 let pixel_format = (*codec_parameters).format;
262 let video_delay = (*codec_parameters).video_delay;
263 let fps = if avg_frame_rate.den == 0 {
264 0.0
265 } else {
266 avg_frame_rate.num as f64 / avg_frame_rate.den as f64
267 };
268
269 let rotate = metadata
271 .get("rotate")
272 .and_then(|rotate| rotate.parse::<i32>().ok())
273 .unwrap_or(0); let video_stream_info = StreamInfo::Video {
276 index,
277 time_base,
278 start_time,
279 duration,
280 nb_frames,
281 r_frame_rate,
282 sample_aspect_ratio,
283 metadata,
284 avg_frame_rate,
285 codec_id,
286 codec_name: codec_name.to_string(),
287 width,
288 height,
289 bit_rate,
290 pixel_format,
291 video_delay,
292 fps,
293 rotate,
294 };
295
296 Ok(Some(video_stream_info))
297 }
298}
299
300pub fn find_audio_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
315 let in_fmt_ctx_box = init_format_context(url)?;
316
317 unsafe {
318 let audio_index =
319 av_find_best_stream(in_fmt_ctx_box.fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, null_mut(), 0);
320 if audio_index < 0 {
321 return Ok(None);
322 }
323 let streams = (*in_fmt_ctx_box.fmt_ctx).streams;
324 let audio_stream = *streams.offset(audio_index as isize);
325
326 let index = (*audio_stream).index;
327 let time_base = (*audio_stream).time_base;
328 let start_time = (*audio_stream).start_time;
329 let duration = (*audio_stream).duration;
330 let nb_frames = (*audio_stream).nb_frames;
331 let metadata = (*audio_stream).metadata;
332 let metadata = av_dict_to_hashmap(metadata);
333 let avg_frame_rate = (*audio_stream).avg_frame_rate;
334
335 let codec_parameters = (*audio_stream).codecpar;
336 let codec_id = (*codec_parameters).codec_id;
337 let codec_name = CStr::from_ptr(avcodec_get_name(codec_id));
338 let codec_name = codec_name.to_str().unwrap_or("Unknown codec");
339 let sample_rate = (*codec_parameters).sample_rate;
340 #[cfg(not(feature = "docs-rs"))]
341 let ch_layout = (*codec_parameters).ch_layout;
342 let bit_rate = (*codec_parameters).bit_rate;
343 let sample_format = (*codec_parameters).format;
344 let frame_size = (*codec_parameters).frame_size;
345
346 let audio_stream_info = StreamInfo::Audio {
347 index,
348 time_base,
349 start_time,
350 duration,
351 nb_frames,
352 metadata,
353 avg_frame_rate,
354 codec_id,
355 codec_name: codec_name.to_string(),
356 sample_rate,
357 #[cfg(not(feature = "docs-rs"))]
358 order: ch_layout.order,
359 #[cfg(feature = "docs-rs")]
360 nb_channels: 0,
361 #[cfg(not(feature = "docs-rs"))]
362 nb_channels: ch_layout.nb_channels,
363 bit_rate,
364 sample_format,
365 frame_size,
366 };
367
368 Ok(Some(audio_stream_info))
369 }
370}
371
372pub fn find_subtitle_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
388 let in_fmt_ctx_box = init_format_context(url)?;
389
390 unsafe {
391 let subtitle_index =
392 av_find_best_stream(in_fmt_ctx_box.fmt_ctx, AVMEDIA_TYPE_SUBTITLE, -1, -1, null_mut(), 0);
393 if subtitle_index < 0 {
394 return Ok(None);
395 }
396
397 let streams = (*in_fmt_ctx_box.fmt_ctx).streams;
398 let subtitle_stream = *streams.offset(subtitle_index as isize);
399
400 let index = (*subtitle_stream).index;
401 let time_base = (*subtitle_stream).time_base;
402 let start_time = (*subtitle_stream).start_time;
403 let duration = (*subtitle_stream).duration;
404 let nb_frames = (*subtitle_stream).nb_frames;
405 let metadata = (*subtitle_stream).metadata;
406 let metadata = av_dict_to_hashmap(metadata);
407
408 let codec_parameters = (*subtitle_stream).codecpar;
409 let codec_id = (*codec_parameters).codec_id;
410 let codec_name = CStr::from_ptr(avcodec_get_name(codec_id));
411 let codec_name = codec_name.to_str().unwrap_or("Unknown codec");
412
413 let subtitle_stream_info = StreamInfo::Subtitle {
414 index,
415 time_base,
416 start_time,
417 duration,
418 nb_frames,
419 metadata,
420 codec_id,
421 codec_name: codec_name.to_string(),
422 };
423
424 Ok(Some(subtitle_stream_info))
425 }
426}
427
428pub fn find_data_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
442 let in_fmt_ctx_box = init_format_context(url)?;
443
444 unsafe {
445 let data_index = av_find_best_stream(in_fmt_ctx_box.fmt_ctx, AVMEDIA_TYPE_DATA, -1, -1, null_mut(), 0);
446 if data_index < 0 {
447 return Ok(None);
448 }
449
450 let streams = (*in_fmt_ctx_box.fmt_ctx).streams;
451 let data_stream = *streams.offset(data_index as isize);
452
453 let index = (*data_stream).index;
454 let time_base = (*data_stream).time_base;
455 let start_time = (*data_stream).start_time;
456 let duration = (*data_stream).duration;
457 let metadata = av_dict_to_hashmap((*data_stream).metadata);
458
459 Ok(Some(StreamInfo::Data {
460 index,
461 time_base,
462 start_time,
463 duration,
464 metadata,
465 }))
466 }
467}
468
469pub fn find_attachment_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
484 let in_fmt_ctx_box = init_format_context(url)?;
485
486 unsafe {
487 let attachment_index =
488 av_find_best_stream(in_fmt_ctx_box.fmt_ctx, AVMEDIA_TYPE_ATTACHMENT, -1, -1, null_mut(), 0);
489 if attachment_index < 0 {
490 return Ok(None);
491 }
492
493 let streams = (*in_fmt_ctx_box.fmt_ctx).streams;
494 let attachment_stream = *streams.offset(attachment_index as isize);
495
496 let index = (*attachment_stream).index;
497 let metadata = av_dict_to_hashmap((*attachment_stream).metadata);
498
499 let codec_parameters = (*attachment_stream).codecpar;
500 let codec_id = (*codec_parameters).codec_id;
501 let codec_name = CStr::from_ptr(avcodec_get_name(codec_id))
502 .to_str()
503 .unwrap_or("Unknown codec")
504 .to_string();
505
506 Ok(Some(StreamInfo::Attachment {
507 index,
508 metadata,
509 codec_id,
510 codec_name,
511 }))
512 }
513}
514
515pub fn find_unknown_stream_info(url: impl Into<String>) -> Result<Option<StreamInfo>> {
529 let in_fmt_ctx_box = init_format_context(url)?;
530
531 unsafe {
532 let unknown_index =
533 av_find_best_stream(in_fmt_ctx_box.fmt_ctx, AVMEDIA_TYPE_UNKNOWN, -1, -1, null_mut(), 0);
534 if unknown_index < 0 {
535 return Ok(None);
536 }
537
538 let streams = (*in_fmt_ctx_box.fmt_ctx).streams;
539 let unknown_stream = *streams.offset(unknown_index as isize);
540
541 let index = (*unknown_stream).index;
542 let metadata = av_dict_to_hashmap((*unknown_stream).metadata);
543
544 Ok(Some(StreamInfo::Unknown { index, metadata }))
545 }
546}
547
548pub fn find_all_stream_infos(url: impl Into<String>) -> Result<Vec<StreamInfo>> {
562 let in_fmt_ctx_box = init_format_context(url)?;
563
564 unsafe {
565 let mut stream_infos = Vec::new();
566
567 let stream_count = (*in_fmt_ctx_box.fmt_ctx).nb_streams;
568
569 for i in 0..stream_count {
570 let stream = *(*in_fmt_ctx_box.fmt_ctx).streams.add(i as usize);
571 let codec_parameters = (*stream).codecpar;
572 let codec_id = (*codec_parameters).codec_id;
573 let codec_name = CStr::from_ptr(avcodec_get_name(codec_id))
574 .to_str()
575 .unwrap_or("Unknown codec")
576 .to_string();
577
578 let index = (*stream).index;
579 let time_base = (*stream).time_base;
580 let start_time = (*stream).start_time;
581 let duration = (*stream).duration;
582 let nb_frames = (*stream).nb_frames;
583 let avg_frame_rate = (*stream).avg_frame_rate;
584 let metadata = av_dict_to_hashmap((*stream).metadata);
585
586 match (*codec_parameters).codec_type {
587 AVMEDIA_TYPE_VIDEO => {
588 let width = (*codec_parameters).width;
589 let height = (*codec_parameters).height;
590 let bit_rate = (*codec_parameters).bit_rate;
591 let pixel_format = (*codec_parameters).format;
592 let video_delay = (*codec_parameters).video_delay;
593 let r_frame_rate = (*stream).r_frame_rate;
594 let sample_aspect_ratio = (*stream).sample_aspect_ratio;
595 let fps = if avg_frame_rate.den == 0 {
596 0.0
597 } else {
598 avg_frame_rate.num as f64 / avg_frame_rate.den as f64
599 };
600
601 let rotate = metadata
603 .get("rotate")
604 .and_then(|rotate| rotate.parse::<i32>().ok())
605 .unwrap_or(0); stream_infos.push(StreamInfo::Video {
608 index,
609 time_base,
610 start_time,
611 duration,
612 nb_frames,
613 r_frame_rate,
614 sample_aspect_ratio,
615 metadata,
616 avg_frame_rate,
617 codec_id,
618 codec_name,
619 width,
620 height,
621 bit_rate,
622 pixel_format,
623 video_delay,
624 fps,
625 rotate,
626 });
627 }
628 AVMEDIA_TYPE_AUDIO => {
629 let sample_rate = (*codec_parameters).sample_rate;
630 #[cfg(not(feature = "docs-rs"))]
631 let ch_layout = (*codec_parameters).ch_layout;
632 let sample_format = (*codec_parameters).format;
633 let frame_size = (*codec_parameters).frame_size;
634 let bit_rate = (*codec_parameters).bit_rate;
635
636 stream_infos.push(StreamInfo::Audio {
637 index,
638 time_base,
639 start_time,
640 duration,
641 nb_frames,
642 metadata,
643 avg_frame_rate,
644 codec_id,
645 codec_name,
646 sample_rate,
647 #[cfg(not(feature = "docs-rs"))]
648 order: ch_layout.order,
649 #[cfg(feature = "docs-rs")]
650 nb_channels: 0,
651 #[cfg(not(feature = "docs-rs"))]
652 nb_channels: ch_layout.nb_channels,
653 bit_rate,
654 sample_format,
655 frame_size,
656 });
657 }
658 AVMEDIA_TYPE_SUBTITLE => {
659 stream_infos.push(StreamInfo::Subtitle {
660 index,
661 time_base,
662 start_time,
663 duration,
664 nb_frames,
665 metadata,
666 codec_id,
667 codec_name,
668 });
669 }
670 AVMEDIA_TYPE_DATA => {
671 stream_infos.push(StreamInfo::Data {
672 index,
673 time_base,
674 start_time,
675 duration,
676 metadata,
677 });
678 }
679 AVMEDIA_TYPE_ATTACHMENT => {
680 stream_infos.push(StreamInfo::Attachment {
681 index,
682 metadata,
683 codec_id,
684 codec_name,
685 });
686 }
687 AVMEDIA_TYPE_UNKNOWN => {
688 stream_infos.push(StreamInfo::Unknown { index, metadata });
689 }
690 _ => {}
691 }
692 }
693
694 Ok(stream_infos)
695 }
696}
697
698fn init_format_context(url: impl Into<String>) -> Result<AVFormatContextBox> {
699 unsafe {
700 let mut in_fmt_ctx = avformat_alloc_context();
701 if in_fmt_ctx.is_null() {
702 return Err(OpenInputError::OutOfMemory.into());
703 }
704
705 let url_cstr = CString::new(url.into())?;
706
707 let mut format_opts = null_mut();
708 let scan_all_pmts_key = CString::new("scan_all_pmts")?;
709 if av_dict_get(
710 format_opts,
711 scan_all_pmts_key.as_ptr(),
712 null(),
713 ffmpeg_sys_next::AV_DICT_MATCH_CASE,
714 )
715 .is_null()
716 {
717 let scan_all_pmts_value = CString::new("1")?;
718 ffmpeg_sys_next::av_dict_set(
719 &mut format_opts,
720 scan_all_pmts_key.as_ptr(),
721 scan_all_pmts_value.as_ptr(),
722 ffmpeg_sys_next::AV_DICT_DONT_OVERWRITE,
723 );
724 };
725
726 #[cfg(not(feature = "docs-rs"))]
727 let mut ret =
728 { avformat_open_input(&mut in_fmt_ctx, url_cstr.as_ptr(), null(), &mut format_opts) };
729 #[cfg(feature = "docs-rs")]
730 let mut ret = 0;
731
732 if ret < 0 {
733 avformat_close_input(&mut in_fmt_ctx);
734 return Err(OpenInputError::from(ret).into());
735 }
736
737 ret = avformat_find_stream_info(in_fmt_ctx, null_mut());
738 if ret < 0 {
739 avformat_close_input(&mut in_fmt_ctx);
740 return Err(FindStreamError::from(ret).into());
741 }
742
743 Ok(AVFormatContextBox::new(in_fmt_ctx, true, false))
744 }
745}
746
747unsafe fn av_dict_to_hashmap(dict: *mut AVDictionary) -> HashMap<String, String> {
748 let mut map = HashMap::new();
749 let mut entry: *mut AVDictionaryEntry = null_mut();
750
751 while {
752 entry = av_dict_get(dict, null(), entry, AV_DICT_IGNORE_SUFFIX);
753 !entry.is_null()
754 } {
755 let key = CStr::from_ptr((*entry).key).to_string_lossy().into_owned();
756 let value = CStr::from_ptr((*entry).value)
757 .to_string_lossy()
758 .into_owned();
759
760 map.insert(key, value);
761 }
762
763 map
764}
765
766#[cfg(test)]
767mod tests {
768 use super::*;
769
770 #[test]
771 fn test_not_found() {
772 let result = find_all_stream_infos("not_found.mp4");
773 assert!(result.is_err());
774
775 let error = result.err().unwrap();
776 println!("{error}");
777 assert!(matches!(
778 error,
779 crate::error::Error::OpenInputStream(OpenInputError::NotFound)
780 ))
781 }
782
783 #[test]
784 fn test_find_all_stream_infos() {
785 let stream_infos = find_all_stream_infos("test.mp4").unwrap();
786 assert_eq!(2, stream_infos.len());
787 for stream_info in stream_infos {
788 println!("{:?}", stream_info);
789 }
790 }
791
792 #[test]
793 fn test_find_video_stream_info() {
794 let option = find_video_stream_info("test.mp4").unwrap();
795 assert!(option.is_some());
796 let video_stream_info = option.unwrap();
797 println!("video_stream_info:{:?}", video_stream_info);
798 }
799
800 #[test]
801 fn test_find_audio_stream_info() {
802 let option = find_audio_stream_info("test.mp4").unwrap();
803 assert!(option.is_some());
804 let audio_stream_info = option.unwrap();
805 println!("audio_stream_info:{:?}", audio_stream_info);
806 }
807
808 #[test]
809 fn test_find_subtitle_stream_info() {
810 let option = find_subtitle_stream_info("test.mp4").unwrap();
811 assert!(option.is_none())
812 }
813
814 #[test]
815 fn test_find_data_stream_info() {
816 let option = find_data_stream_info("test.mp4").unwrap();
817 assert!(option.is_none());
818 }
819
820 #[test]
821 fn test_find_attachment_stream_info() {
822 let option = find_attachment_stream_info("test.mp4").unwrap();
823 assert!(option.is_none())
824 }
825
826 #[test]
827 fn test_find_unknown_stream_info() {
828 let option = find_unknown_stream_info("test.mp4").unwrap();
829 assert!(option.is_none())
830 }
831}