1use std::collections::HashMap;
55use std::path::{Path, PathBuf};
56use std::time::Duration;
57
58use crate::chapter::ChapterInfo;
59use crate::stream::{AudioStreamInfo, VideoStreamInfo};
60
61#[derive(Debug, Clone)]
82pub struct MediaInfo {
83 path: PathBuf,
85 format: String,
87 format_long_name: Option<String>,
89 duration: Duration,
91 file_size: u64,
93 bitrate: Option<u64>,
95 video_streams: Vec<VideoStreamInfo>,
97 audio_streams: Vec<AudioStreamInfo>,
99 chapters: Vec<ChapterInfo>,
101 metadata: HashMap<String, String>,
103}
104
105impl MediaInfo {
106 #[must_use]
122 pub fn builder() -> MediaInfoBuilder {
123 MediaInfoBuilder::default()
124 }
125
126 #[must_use]
128 #[inline]
129 pub fn path(&self) -> &Path {
130 &self.path
131 }
132
133 #[must_use]
135 #[inline]
136 pub fn format(&self) -> &str {
137 &self.format
138 }
139
140 #[must_use]
142 #[inline]
143 pub fn format_long_name(&self) -> Option<&str> {
144 self.format_long_name.as_deref()
145 }
146
147 #[must_use]
149 #[inline]
150 pub const fn duration(&self) -> Duration {
151 self.duration
152 }
153
154 #[must_use]
156 #[inline]
157 pub const fn file_size(&self) -> u64 {
158 self.file_size
159 }
160
161 #[must_use]
163 #[inline]
164 pub const fn bitrate(&self) -> Option<u64> {
165 self.bitrate
166 }
167
168 #[must_use]
170 #[inline]
171 pub fn video_streams(&self) -> &[VideoStreamInfo] {
172 &self.video_streams
173 }
174
175 #[must_use]
177 #[inline]
178 pub fn audio_streams(&self) -> &[AudioStreamInfo] {
179 &self.audio_streams
180 }
181
182 #[must_use]
184 #[inline]
185 pub fn chapters(&self) -> &[ChapterInfo] {
186 &self.chapters
187 }
188
189 #[must_use]
191 #[inline]
192 pub fn has_chapters(&self) -> bool {
193 !self.chapters.is_empty()
194 }
195
196 #[must_use]
198 #[inline]
199 pub fn chapter_count(&self) -> usize {
200 self.chapters.len()
201 }
202
203 #[must_use]
205 #[inline]
206 pub fn metadata(&self) -> &HashMap<String, String> {
207 &self.metadata
208 }
209
210 #[must_use]
212 #[inline]
213 pub fn metadata_value(&self, key: &str) -> Option<&str> {
214 self.metadata.get(key).map(String::as_str)
215 }
216
217 #[must_use]
221 #[inline]
222 pub fn has_video(&self) -> bool {
223 !self.video_streams.is_empty()
224 }
225
226 #[must_use]
228 #[inline]
229 pub fn has_audio(&self) -> bool {
230 !self.audio_streams.is_empty()
231 }
232
233 #[must_use]
235 #[inline]
236 pub fn video_stream_count(&self) -> usize {
237 self.video_streams.len()
238 }
239
240 #[must_use]
242 #[inline]
243 pub fn audio_stream_count(&self) -> usize {
244 self.audio_streams.len()
245 }
246
247 #[must_use]
249 #[inline]
250 pub fn stream_count(&self) -> usize {
251 self.video_streams.len() + self.audio_streams.len()
252 }
253
254 #[must_use]
261 #[inline]
262 pub fn primary_video(&self) -> Option<&VideoStreamInfo> {
263 self.video_streams.first()
264 }
265
266 #[must_use]
271 #[inline]
272 pub fn primary_audio(&self) -> Option<&AudioStreamInfo> {
273 self.audio_streams.first()
274 }
275
276 #[must_use]
278 #[inline]
279 pub fn video_stream(&self, index: usize) -> Option<&VideoStreamInfo> {
280 self.video_streams.get(index)
281 }
282
283 #[must_use]
285 #[inline]
286 pub fn audio_stream(&self, index: usize) -> Option<&AudioStreamInfo> {
287 self.audio_streams.get(index)
288 }
289
290 #[must_use]
296 #[inline]
297 pub fn resolution(&self) -> Option<(u32, u32)> {
298 self.primary_video().map(|v| (v.width(), v.height()))
299 }
300
301 #[must_use]
305 #[inline]
306 pub fn frame_rate(&self) -> Option<f64> {
307 self.primary_video().map(VideoStreamInfo::fps)
308 }
309
310 #[must_use]
314 #[inline]
315 pub fn sample_rate(&self) -> Option<u32> {
316 self.primary_audio().map(AudioStreamInfo::sample_rate)
317 }
318
319 #[must_use]
323 #[inline]
324 pub fn channels(&self) -> Option<u32> {
325 self.primary_audio().map(AudioStreamInfo::channels)
326 }
327
328 #[must_use]
330 #[inline]
331 pub fn is_video_only(&self) -> bool {
332 self.has_video() && !self.has_audio()
333 }
334
335 #[must_use]
337 #[inline]
338 pub fn is_audio_only(&self) -> bool {
339 self.has_audio() && !self.has_video()
340 }
341
342 #[must_use]
344 #[inline]
345 pub fn file_name(&self) -> Option<&str> {
346 self.path.file_name().and_then(|n| n.to_str())
347 }
348
349 #[must_use]
351 #[inline]
352 pub fn extension(&self) -> Option<&str> {
353 self.path.extension().and_then(|e| e.to_str())
354 }
355
356 #[must_use]
363 #[inline]
364 pub fn title(&self) -> Option<&str> {
365 self.metadata_value("title")
366 }
367
368 #[must_use]
372 #[inline]
373 pub fn artist(&self) -> Option<&str> {
374 self.metadata_value("artist")
375 }
376
377 #[must_use]
381 #[inline]
382 pub fn album(&self) -> Option<&str> {
383 self.metadata_value("album")
384 }
385
386 #[must_use]
392 #[inline]
393 pub fn creation_time(&self) -> Option<&str> {
394 self.metadata_value("creation_time")
395 }
396
397 #[must_use]
401 #[inline]
402 pub fn date(&self) -> Option<&str> {
403 self.metadata_value("date")
404 }
405
406 #[must_use]
410 #[inline]
411 pub fn comment(&self) -> Option<&str> {
412 self.metadata_value("comment")
413 }
414
415 #[must_use]
420 #[inline]
421 pub fn encoder(&self) -> Option<&str> {
422 self.metadata_value("encoder")
423 }
424}
425
426impl Default for MediaInfo {
427 fn default() -> Self {
428 Self {
429 path: PathBuf::new(),
430 format: String::new(),
431 format_long_name: None,
432 duration: Duration::ZERO,
433 file_size: 0,
434 bitrate: None,
435 video_streams: Vec::new(),
436 audio_streams: Vec::new(),
437 chapters: Vec::new(),
438 metadata: HashMap::new(),
439 }
440 }
441}
442
443#[derive(Debug, Clone, Default)]
462pub struct MediaInfoBuilder {
463 path: PathBuf,
464 format: String,
465 format_long_name: Option<String>,
466 duration: Duration,
467 file_size: u64,
468 bitrate: Option<u64>,
469 video_streams: Vec<VideoStreamInfo>,
470 audio_streams: Vec<AudioStreamInfo>,
471 chapters: Vec<ChapterInfo>,
472 metadata: HashMap<String, String>,
473}
474
475impl MediaInfoBuilder {
476 #[must_use]
478 pub fn path(mut self, path: impl Into<PathBuf>) -> Self {
479 self.path = path.into();
480 self
481 }
482
483 #[must_use]
485 pub fn format(mut self, format: impl Into<String>) -> Self {
486 self.format = format.into();
487 self
488 }
489
490 #[must_use]
492 pub fn format_long_name(mut self, name: impl Into<String>) -> Self {
493 self.format_long_name = Some(name.into());
494 self
495 }
496
497 #[must_use]
499 pub fn duration(mut self, duration: Duration) -> Self {
500 self.duration = duration;
501 self
502 }
503
504 #[must_use]
506 pub fn file_size(mut self, size: u64) -> Self {
507 self.file_size = size;
508 self
509 }
510
511 #[must_use]
513 pub fn bitrate(mut self, bitrate: u64) -> Self {
514 self.bitrate = Some(bitrate);
515 self
516 }
517
518 #[must_use]
520 pub fn video_stream(mut self, stream: VideoStreamInfo) -> Self {
521 self.video_streams.push(stream);
522 self
523 }
524
525 #[must_use]
527 pub fn video_streams(mut self, streams: Vec<VideoStreamInfo>) -> Self {
528 self.video_streams = streams;
529 self
530 }
531
532 #[must_use]
534 pub fn audio_stream(mut self, stream: AudioStreamInfo) -> Self {
535 self.audio_streams.push(stream);
536 self
537 }
538
539 #[must_use]
541 pub fn audio_streams(mut self, streams: Vec<AudioStreamInfo>) -> Self {
542 self.audio_streams = streams;
543 self
544 }
545
546 #[must_use]
548 pub fn chapter(mut self, chapter: ChapterInfo) -> Self {
549 self.chapters.push(chapter);
550 self
551 }
552
553 #[must_use]
555 pub fn chapters(mut self, chapters: Vec<ChapterInfo>) -> Self {
556 self.chapters = chapters;
557 self
558 }
559
560 #[must_use]
562 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
563 self.metadata.insert(key.into(), value.into());
564 self
565 }
566
567 #[must_use]
569 pub fn metadata_map(mut self, metadata: HashMap<String, String>) -> Self {
570 self.metadata = metadata;
571 self
572 }
573
574 #[must_use]
576 pub fn build(self) -> MediaInfo {
577 MediaInfo {
578 path: self.path,
579 format: self.format,
580 format_long_name: self.format_long_name,
581 duration: self.duration,
582 file_size: self.file_size,
583 bitrate: self.bitrate,
584 video_streams: self.video_streams,
585 audio_streams: self.audio_streams,
586 chapters: self.chapters,
587 metadata: self.metadata,
588 }
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595 use crate::codec::{AudioCodec, VideoCodec};
596 use crate::time::Rational;
597 use crate::{PixelFormat, SampleFormat};
598
599 fn sample_video_stream() -> VideoStreamInfo {
600 VideoStreamInfo::builder()
601 .index(0)
602 .codec(VideoCodec::H264)
603 .codec_name("h264")
604 .width(1920)
605 .height(1080)
606 .frame_rate(Rational::new(30, 1))
607 .pixel_format(PixelFormat::Yuv420p)
608 .duration(Duration::from_secs(120))
609 .build()
610 }
611
612 fn sample_audio_stream() -> AudioStreamInfo {
613 AudioStreamInfo::builder()
614 .index(1)
615 .codec(AudioCodec::Aac)
616 .codec_name("aac")
617 .sample_rate(48000)
618 .channels(2)
619 .sample_format(SampleFormat::F32)
620 .duration(Duration::from_secs(120))
621 .build()
622 }
623
624 mod media_info_tests {
625 use super::*;
626
627 #[test]
628 fn test_builder_basic() {
629 let info = MediaInfo::builder()
630 .path("/path/to/video.mp4")
631 .format("mp4")
632 .duration(Duration::from_secs(120))
633 .file_size(1_000_000)
634 .build();
635
636 assert_eq!(info.path(), Path::new("/path/to/video.mp4"));
637 assert_eq!(info.format(), "mp4");
638 assert_eq!(info.duration(), Duration::from_secs(120));
639 assert_eq!(info.file_size(), 1_000_000);
640 assert!(info.format_long_name().is_none());
641 assert!(info.bitrate().is_none());
642 }
643
644 #[test]
645 fn test_builder_full() {
646 let video = sample_video_stream();
647 let audio = sample_audio_stream();
648
649 let info = MediaInfo::builder()
650 .path("/path/to/video.mp4")
651 .format("mp4")
652 .format_long_name("QuickTime / MOV")
653 .duration(Duration::from_secs(120))
654 .file_size(150_000_000)
655 .bitrate(10_000_000)
656 .video_stream(video)
657 .audio_stream(audio)
658 .metadata("title", "Test Video")
659 .metadata("artist", "Test Artist")
660 .build();
661
662 assert_eq!(info.format_long_name(), Some("QuickTime / MOV"));
663 assert_eq!(info.bitrate(), Some(10_000_000));
664 assert_eq!(info.video_stream_count(), 1);
665 assert_eq!(info.audio_stream_count(), 1);
666 assert_eq!(info.metadata_value("title"), Some("Test Video"));
667 assert_eq!(info.metadata_value("artist"), Some("Test Artist"));
668 assert!(info.metadata_value("nonexistent").is_none());
669 }
670
671 #[test]
672 fn test_default() {
673 let info = MediaInfo::default();
674 assert_eq!(info.path(), Path::new(""));
675 assert_eq!(info.format(), "");
676 assert_eq!(info.duration(), Duration::ZERO);
677 assert_eq!(info.file_size(), 0);
678 assert!(!info.has_video());
679 assert!(!info.has_audio());
680 }
681
682 #[test]
683 fn test_has_streams() {
684 let empty = MediaInfo::default();
686 assert!(!empty.has_video());
687 assert!(!empty.has_audio());
688
689 let video_only = MediaInfo::builder()
691 .video_stream(sample_video_stream())
692 .build();
693 assert!(video_only.has_video());
694 assert!(!video_only.has_audio());
695 assert!(video_only.is_video_only());
696 assert!(!video_only.is_audio_only());
697
698 let audio_only = MediaInfo::builder()
700 .audio_stream(sample_audio_stream())
701 .build();
702 assert!(!audio_only.has_video());
703 assert!(audio_only.has_audio());
704 assert!(!audio_only.is_video_only());
705 assert!(audio_only.is_audio_only());
706
707 let both = MediaInfo::builder()
709 .video_stream(sample_video_stream())
710 .audio_stream(sample_audio_stream())
711 .build();
712 assert!(both.has_video());
713 assert!(both.has_audio());
714 assert!(!both.is_video_only());
715 assert!(!both.is_audio_only());
716 }
717
718 #[test]
719 fn test_primary_streams() {
720 let video1 = VideoStreamInfo::builder()
721 .index(0)
722 .width(1920)
723 .height(1080)
724 .build();
725 let video2 = VideoStreamInfo::builder()
726 .index(2)
727 .width(1280)
728 .height(720)
729 .build();
730 let audio1 = AudioStreamInfo::builder()
731 .index(1)
732 .sample_rate(48000)
733 .build();
734 let audio2 = AudioStreamInfo::builder()
735 .index(3)
736 .sample_rate(44100)
737 .build();
738
739 let info = MediaInfo::builder()
740 .video_stream(video1)
741 .video_stream(video2)
742 .audio_stream(audio1)
743 .audio_stream(audio2)
744 .build();
745
746 let primary_video = info.primary_video().unwrap();
748 assert_eq!(primary_video.width(), 1920);
749 assert_eq!(primary_video.index(), 0);
750
751 let primary_audio = info.primary_audio().unwrap();
752 assert_eq!(primary_audio.sample_rate(), 48000);
753 assert_eq!(primary_audio.index(), 1);
754 }
755
756 #[test]
757 fn test_stream_access_by_index() {
758 let video1 = VideoStreamInfo::builder().width(1920).build();
759 let video2 = VideoStreamInfo::builder().width(1280).build();
760 let audio1 = AudioStreamInfo::builder().sample_rate(48000).build();
761
762 let info = MediaInfo::builder()
763 .video_stream(video1)
764 .video_stream(video2)
765 .audio_stream(audio1)
766 .build();
767
768 assert_eq!(info.video_stream(0).unwrap().width(), 1920);
769 assert_eq!(info.video_stream(1).unwrap().width(), 1280);
770 assert!(info.video_stream(2).is_none());
771
772 assert_eq!(info.audio_stream(0).unwrap().sample_rate(), 48000);
773 assert!(info.audio_stream(1).is_none());
774 }
775
776 #[test]
777 fn test_resolution_and_frame_rate() {
778 let info = MediaInfo::builder()
779 .video_stream(sample_video_stream())
780 .build();
781
782 assert_eq!(info.resolution(), Some((1920, 1080)));
783 assert!((info.frame_rate().unwrap() - 30.0).abs() < 0.001);
784
785 let no_video = MediaInfo::default();
787 assert!(no_video.resolution().is_none());
788 assert!(no_video.frame_rate().is_none());
789 }
790
791 #[test]
792 fn test_sample_rate_and_channels() {
793 let info = MediaInfo::builder()
794 .audio_stream(sample_audio_stream())
795 .build();
796
797 assert_eq!(info.sample_rate(), Some(48000));
798 assert_eq!(info.channels(), Some(2));
799
800 let no_audio = MediaInfo::default();
802 assert!(no_audio.sample_rate().is_none());
803 assert!(no_audio.channels().is_none());
804 }
805
806 #[test]
807 fn test_stream_counts() {
808 let info = MediaInfo::builder()
809 .video_stream(sample_video_stream())
810 .video_stream(sample_video_stream())
811 .audio_stream(sample_audio_stream())
812 .audio_stream(sample_audio_stream())
813 .audio_stream(sample_audio_stream())
814 .build();
815
816 assert_eq!(info.video_stream_count(), 2);
817 assert_eq!(info.audio_stream_count(), 3);
818 assert_eq!(info.stream_count(), 5);
819 }
820
821 #[test]
822 fn test_file_name_and_extension() {
823 let info = MediaInfo::builder().path("/path/to/my_video.mp4").build();
824
825 assert_eq!(info.file_name(), Some("my_video.mp4"));
826 assert_eq!(info.extension(), Some("mp4"));
827
828 let empty = MediaInfo::default();
830 assert!(empty.file_name().is_none());
831 assert!(empty.extension().is_none());
832 }
833
834 #[test]
835 fn test_metadata_operations() {
836 let mut map = HashMap::new();
837 map.insert("key1".to_string(), "value1".to_string());
838 map.insert("key2".to_string(), "value2".to_string());
839
840 let info = MediaInfo::builder()
841 .metadata_map(map)
842 .metadata("key3", "value3")
843 .build();
844
845 assert_eq!(info.metadata().len(), 3);
846 assert_eq!(info.metadata_value("key1"), Some("value1"));
847 assert_eq!(info.metadata_value("key2"), Some("value2"));
848 assert_eq!(info.metadata_value("key3"), Some("value3"));
849 }
850
851 #[test]
852 fn test_clone() {
853 let info = MediaInfo::builder()
854 .path("/path/to/video.mp4")
855 .format("mp4")
856 .format_long_name("QuickTime / MOV")
857 .duration(Duration::from_secs(120))
858 .file_size(1_000_000)
859 .video_stream(sample_video_stream())
860 .audio_stream(sample_audio_stream())
861 .metadata("title", "Test")
862 .build();
863
864 let cloned = info.clone();
865 assert_eq!(info.path(), cloned.path());
866 assert_eq!(info.format(), cloned.format());
867 assert_eq!(info.format_long_name(), cloned.format_long_name());
868 assert_eq!(info.duration(), cloned.duration());
869 assert_eq!(info.file_size(), cloned.file_size());
870 assert_eq!(info.video_stream_count(), cloned.video_stream_count());
871 assert_eq!(info.audio_stream_count(), cloned.audio_stream_count());
872 assert_eq!(info.metadata_value("title"), cloned.metadata_value("title"));
873 }
874
875 #[test]
876 fn test_debug() {
877 let info = MediaInfo::builder()
878 .path("/path/to/video.mp4")
879 .format("mp4")
880 .duration(Duration::from_secs(120))
881 .file_size(1_000_000)
882 .build();
883
884 let debug = format!("{info:?}");
885 assert!(debug.contains("MediaInfo"));
886 assert!(debug.contains("mp4"));
887 }
888
889 #[test]
890 fn test_video_streams_setter() {
891 let streams = vec![sample_video_stream(), sample_video_stream()];
892
893 let info = MediaInfo::builder().video_streams(streams).build();
894
895 assert_eq!(info.video_stream_count(), 2);
896 }
897
898 #[test]
899 fn test_audio_streams_setter() {
900 let streams = vec![
901 sample_audio_stream(),
902 sample_audio_stream(),
903 sample_audio_stream(),
904 ];
905
906 let info = MediaInfo::builder().audio_streams(streams).build();
907
908 assert_eq!(info.audio_stream_count(), 3);
909 }
910 }
911
912 mod media_info_builder_tests {
913 use super::*;
914
915 #[test]
916 fn test_builder_default() {
917 let builder = MediaInfoBuilder::default();
918 let info = builder.build();
919 assert_eq!(info.path(), Path::new(""));
920 assert_eq!(info.format(), "");
921 assert_eq!(info.duration(), Duration::ZERO);
922 }
923
924 #[test]
925 fn test_builder_clone() {
926 let builder = MediaInfo::builder()
927 .path("/path/to/video.mp4")
928 .format("mp4")
929 .duration(Duration::from_secs(120));
930
931 let cloned = builder.clone();
932 let info1 = builder.build();
933 let info2 = cloned.build();
934
935 assert_eq!(info1.path(), info2.path());
936 assert_eq!(info1.format(), info2.format());
937 assert_eq!(info1.duration(), info2.duration());
938 }
939
940 #[test]
941 fn test_builder_debug() {
942 let builder = MediaInfo::builder()
943 .path("/path/to/video.mp4")
944 .format("mp4");
945
946 let debug = format!("{builder:?}");
947 assert!(debug.contains("MediaInfoBuilder"));
948 }
949 }
950
951 mod metadata_convenience_tests {
952 use super::*;
953
954 #[test]
955 fn test_title() {
956 let info = MediaInfo::builder()
957 .metadata("title", "Sample Video Title")
958 .build();
959
960 assert_eq!(info.title(), Some("Sample Video Title"));
961 }
962
963 #[test]
964 fn test_title_missing() {
965 let info = MediaInfo::default();
966 assert!(info.title().is_none());
967 }
968
969 #[test]
970 fn test_artist() {
971 let info = MediaInfo::builder()
972 .metadata("artist", "Test Artist")
973 .build();
974
975 assert_eq!(info.artist(), Some("Test Artist"));
976 }
977
978 #[test]
979 fn test_album() {
980 let info = MediaInfo::builder().metadata("album", "Test Album").build();
981
982 assert_eq!(info.album(), Some("Test Album"));
983 }
984
985 #[test]
986 fn test_creation_time() {
987 let info = MediaInfo::builder()
988 .metadata("creation_time", "2024-01-15T10:30:00.000000Z")
989 .build();
990
991 assert_eq!(info.creation_time(), Some("2024-01-15T10:30:00.000000Z"));
992 }
993
994 #[test]
995 fn test_date() {
996 let info = MediaInfo::builder().metadata("date", "2024-01-15").build();
997
998 assert_eq!(info.date(), Some("2024-01-15"));
999 }
1000
1001 #[test]
1002 fn test_comment() {
1003 let info = MediaInfo::builder()
1004 .metadata("comment", "This is a test comment")
1005 .build();
1006
1007 assert_eq!(info.comment(), Some("This is a test comment"));
1008 }
1009
1010 #[test]
1011 fn test_encoder() {
1012 let info = MediaInfo::builder()
1013 .metadata("encoder", "Lavf58.76.100")
1014 .build();
1015
1016 assert_eq!(info.encoder(), Some("Lavf58.76.100"));
1017 }
1018
1019 #[test]
1020 fn test_multiple_metadata_fields() {
1021 let info = MediaInfo::builder()
1022 .metadata("title", "My Video")
1023 .metadata("artist", "John Doe")
1024 .metadata("album", "My Collection")
1025 .metadata("date", "2024")
1026 .metadata("comment", "A great video")
1027 .metadata("encoder", "FFmpeg")
1028 .metadata("custom_field", "custom_value")
1029 .build();
1030
1031 assert_eq!(info.title(), Some("My Video"));
1032 assert_eq!(info.artist(), Some("John Doe"));
1033 assert_eq!(info.album(), Some("My Collection"));
1034 assert_eq!(info.date(), Some("2024"));
1035 assert_eq!(info.comment(), Some("A great video"));
1036 assert_eq!(info.encoder(), Some("FFmpeg"));
1037 assert_eq!(info.metadata_value("custom_field"), Some("custom_value"));
1038 assert_eq!(info.metadata().len(), 7);
1039 }
1040 }
1041}