1use std::collections::HashMap;
55use std::path::{Path, PathBuf};
56use std::time::Duration;
57
58use crate::stream::{AudioStreamInfo, VideoStreamInfo};
59
60#[derive(Debug, Clone)]
81pub struct MediaInfo {
82 path: PathBuf,
84 format: String,
86 format_long_name: Option<String>,
88 duration: Duration,
90 file_size: u64,
92 bitrate: Option<u64>,
94 video_streams: Vec<VideoStreamInfo>,
96 audio_streams: Vec<AudioStreamInfo>,
98 metadata: HashMap<String, String>,
100}
101
102impl MediaInfo {
103 #[must_use]
119 pub fn builder() -> MediaInfoBuilder {
120 MediaInfoBuilder::default()
121 }
122
123 #[must_use]
125 #[inline]
126 pub fn path(&self) -> &Path {
127 &self.path
128 }
129
130 #[must_use]
132 #[inline]
133 pub fn format(&self) -> &str {
134 &self.format
135 }
136
137 #[must_use]
139 #[inline]
140 pub fn format_long_name(&self) -> Option<&str> {
141 self.format_long_name.as_deref()
142 }
143
144 #[must_use]
146 #[inline]
147 pub const fn duration(&self) -> Duration {
148 self.duration
149 }
150
151 #[must_use]
153 #[inline]
154 pub const fn file_size(&self) -> u64 {
155 self.file_size
156 }
157
158 #[must_use]
160 #[inline]
161 pub const fn bitrate(&self) -> Option<u64> {
162 self.bitrate
163 }
164
165 #[must_use]
167 #[inline]
168 pub fn video_streams(&self) -> &[VideoStreamInfo] {
169 &self.video_streams
170 }
171
172 #[must_use]
174 #[inline]
175 pub fn audio_streams(&self) -> &[AudioStreamInfo] {
176 &self.audio_streams
177 }
178
179 #[must_use]
181 #[inline]
182 pub fn metadata(&self) -> &HashMap<String, String> {
183 &self.metadata
184 }
185
186 #[must_use]
188 #[inline]
189 pub fn metadata_value(&self, key: &str) -> Option<&str> {
190 self.metadata.get(key).map(String::as_str)
191 }
192
193 #[must_use]
197 #[inline]
198 pub fn has_video(&self) -> bool {
199 !self.video_streams.is_empty()
200 }
201
202 #[must_use]
204 #[inline]
205 pub fn has_audio(&self) -> bool {
206 !self.audio_streams.is_empty()
207 }
208
209 #[must_use]
211 #[inline]
212 pub fn video_stream_count(&self) -> usize {
213 self.video_streams.len()
214 }
215
216 #[must_use]
218 #[inline]
219 pub fn audio_stream_count(&self) -> usize {
220 self.audio_streams.len()
221 }
222
223 #[must_use]
225 #[inline]
226 pub fn stream_count(&self) -> usize {
227 self.video_streams.len() + self.audio_streams.len()
228 }
229
230 #[must_use]
237 #[inline]
238 pub fn primary_video(&self) -> Option<&VideoStreamInfo> {
239 self.video_streams.first()
240 }
241
242 #[must_use]
247 #[inline]
248 pub fn primary_audio(&self) -> Option<&AudioStreamInfo> {
249 self.audio_streams.first()
250 }
251
252 #[must_use]
254 #[inline]
255 pub fn video_stream(&self, index: usize) -> Option<&VideoStreamInfo> {
256 self.video_streams.get(index)
257 }
258
259 #[must_use]
261 #[inline]
262 pub fn audio_stream(&self, index: usize) -> Option<&AudioStreamInfo> {
263 self.audio_streams.get(index)
264 }
265
266 #[must_use]
272 #[inline]
273 pub fn resolution(&self) -> Option<(u32, u32)> {
274 self.primary_video().map(|v| (v.width(), v.height()))
275 }
276
277 #[must_use]
281 #[inline]
282 pub fn frame_rate(&self) -> Option<f64> {
283 self.primary_video().map(VideoStreamInfo::fps)
284 }
285
286 #[must_use]
290 #[inline]
291 pub fn sample_rate(&self) -> Option<u32> {
292 self.primary_audio().map(AudioStreamInfo::sample_rate)
293 }
294
295 #[must_use]
299 #[inline]
300 pub fn channels(&self) -> Option<u32> {
301 self.primary_audio().map(AudioStreamInfo::channels)
302 }
303
304 #[must_use]
306 #[inline]
307 pub fn is_video_only(&self) -> bool {
308 self.has_video() && !self.has_audio()
309 }
310
311 #[must_use]
313 #[inline]
314 pub fn is_audio_only(&self) -> bool {
315 self.has_audio() && !self.has_video()
316 }
317
318 #[must_use]
320 #[inline]
321 pub fn file_name(&self) -> Option<&str> {
322 self.path.file_name().and_then(|n| n.to_str())
323 }
324
325 #[must_use]
327 #[inline]
328 pub fn extension(&self) -> Option<&str> {
329 self.path.extension().and_then(|e| e.to_str())
330 }
331
332 #[must_use]
339 #[inline]
340 pub fn title(&self) -> Option<&str> {
341 self.metadata_value("title")
342 }
343
344 #[must_use]
348 #[inline]
349 pub fn artist(&self) -> Option<&str> {
350 self.metadata_value("artist")
351 }
352
353 #[must_use]
357 #[inline]
358 pub fn album(&self) -> Option<&str> {
359 self.metadata_value("album")
360 }
361
362 #[must_use]
368 #[inline]
369 pub fn creation_time(&self) -> Option<&str> {
370 self.metadata_value("creation_time")
371 }
372
373 #[must_use]
377 #[inline]
378 pub fn date(&self) -> Option<&str> {
379 self.metadata_value("date")
380 }
381
382 #[must_use]
386 #[inline]
387 pub fn comment(&self) -> Option<&str> {
388 self.metadata_value("comment")
389 }
390
391 #[must_use]
396 #[inline]
397 pub fn encoder(&self) -> Option<&str> {
398 self.metadata_value("encoder")
399 }
400}
401
402impl Default for MediaInfo {
403 fn default() -> Self {
404 Self {
405 path: PathBuf::new(),
406 format: String::new(),
407 format_long_name: None,
408 duration: Duration::ZERO,
409 file_size: 0,
410 bitrate: None,
411 video_streams: Vec::new(),
412 audio_streams: Vec::new(),
413 metadata: HashMap::new(),
414 }
415 }
416}
417
418#[derive(Debug, Clone, Default)]
437pub struct MediaInfoBuilder {
438 path: PathBuf,
439 format: String,
440 format_long_name: Option<String>,
441 duration: Duration,
442 file_size: u64,
443 bitrate: Option<u64>,
444 video_streams: Vec<VideoStreamInfo>,
445 audio_streams: Vec<AudioStreamInfo>,
446 metadata: HashMap<String, String>,
447}
448
449impl MediaInfoBuilder {
450 #[must_use]
452 pub fn path(mut self, path: impl Into<PathBuf>) -> Self {
453 self.path = path.into();
454 self
455 }
456
457 #[must_use]
459 pub fn format(mut self, format: impl Into<String>) -> Self {
460 self.format = format.into();
461 self
462 }
463
464 #[must_use]
466 pub fn format_long_name(mut self, name: impl Into<String>) -> Self {
467 self.format_long_name = Some(name.into());
468 self
469 }
470
471 #[must_use]
473 pub fn duration(mut self, duration: Duration) -> Self {
474 self.duration = duration;
475 self
476 }
477
478 #[must_use]
480 pub fn file_size(mut self, size: u64) -> Self {
481 self.file_size = size;
482 self
483 }
484
485 #[must_use]
487 pub fn bitrate(mut self, bitrate: u64) -> Self {
488 self.bitrate = Some(bitrate);
489 self
490 }
491
492 #[must_use]
494 pub fn video_stream(mut self, stream: VideoStreamInfo) -> Self {
495 self.video_streams.push(stream);
496 self
497 }
498
499 #[must_use]
501 pub fn video_streams(mut self, streams: Vec<VideoStreamInfo>) -> Self {
502 self.video_streams = streams;
503 self
504 }
505
506 #[must_use]
508 pub fn audio_stream(mut self, stream: AudioStreamInfo) -> Self {
509 self.audio_streams.push(stream);
510 self
511 }
512
513 #[must_use]
515 pub fn audio_streams(mut self, streams: Vec<AudioStreamInfo>) -> Self {
516 self.audio_streams = streams;
517 self
518 }
519
520 #[must_use]
522 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
523 self.metadata.insert(key.into(), value.into());
524 self
525 }
526
527 #[must_use]
529 pub fn metadata_map(mut self, metadata: HashMap<String, String>) -> Self {
530 self.metadata = metadata;
531 self
532 }
533
534 #[must_use]
536 pub fn build(self) -> MediaInfo {
537 MediaInfo {
538 path: self.path,
539 format: self.format,
540 format_long_name: self.format_long_name,
541 duration: self.duration,
542 file_size: self.file_size,
543 bitrate: self.bitrate,
544 video_streams: self.video_streams,
545 audio_streams: self.audio_streams,
546 metadata: self.metadata,
547 }
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554 use crate::codec::{AudioCodec, VideoCodec};
555 use crate::time::Rational;
556 use crate::{PixelFormat, SampleFormat};
557
558 fn sample_video_stream() -> VideoStreamInfo {
559 VideoStreamInfo::builder()
560 .index(0)
561 .codec(VideoCodec::H264)
562 .codec_name("h264")
563 .width(1920)
564 .height(1080)
565 .frame_rate(Rational::new(30, 1))
566 .pixel_format(PixelFormat::Yuv420p)
567 .duration(Duration::from_secs(120))
568 .build()
569 }
570
571 fn sample_audio_stream() -> AudioStreamInfo {
572 AudioStreamInfo::builder()
573 .index(1)
574 .codec(AudioCodec::Aac)
575 .codec_name("aac")
576 .sample_rate(48000)
577 .channels(2)
578 .sample_format(SampleFormat::F32)
579 .duration(Duration::from_secs(120))
580 .build()
581 }
582
583 mod media_info_tests {
584 use super::*;
585
586 #[test]
587 fn test_builder_basic() {
588 let info = MediaInfo::builder()
589 .path("/path/to/video.mp4")
590 .format("mp4")
591 .duration(Duration::from_secs(120))
592 .file_size(1_000_000)
593 .build();
594
595 assert_eq!(info.path(), Path::new("/path/to/video.mp4"));
596 assert_eq!(info.format(), "mp4");
597 assert_eq!(info.duration(), Duration::from_secs(120));
598 assert_eq!(info.file_size(), 1_000_000);
599 assert!(info.format_long_name().is_none());
600 assert!(info.bitrate().is_none());
601 }
602
603 #[test]
604 fn test_builder_full() {
605 let video = sample_video_stream();
606 let audio = sample_audio_stream();
607
608 let info = MediaInfo::builder()
609 .path("/path/to/video.mp4")
610 .format("mp4")
611 .format_long_name("QuickTime / MOV")
612 .duration(Duration::from_secs(120))
613 .file_size(150_000_000)
614 .bitrate(10_000_000)
615 .video_stream(video)
616 .audio_stream(audio)
617 .metadata("title", "Test Video")
618 .metadata("artist", "Test Artist")
619 .build();
620
621 assert_eq!(info.format_long_name(), Some("QuickTime / MOV"));
622 assert_eq!(info.bitrate(), Some(10_000_000));
623 assert_eq!(info.video_stream_count(), 1);
624 assert_eq!(info.audio_stream_count(), 1);
625 assert_eq!(info.metadata_value("title"), Some("Test Video"));
626 assert_eq!(info.metadata_value("artist"), Some("Test Artist"));
627 assert!(info.metadata_value("nonexistent").is_none());
628 }
629
630 #[test]
631 fn test_default() {
632 let info = MediaInfo::default();
633 assert_eq!(info.path(), Path::new(""));
634 assert_eq!(info.format(), "");
635 assert_eq!(info.duration(), Duration::ZERO);
636 assert_eq!(info.file_size(), 0);
637 assert!(!info.has_video());
638 assert!(!info.has_audio());
639 }
640
641 #[test]
642 fn test_has_streams() {
643 let empty = MediaInfo::default();
645 assert!(!empty.has_video());
646 assert!(!empty.has_audio());
647
648 let video_only = MediaInfo::builder()
650 .video_stream(sample_video_stream())
651 .build();
652 assert!(video_only.has_video());
653 assert!(!video_only.has_audio());
654 assert!(video_only.is_video_only());
655 assert!(!video_only.is_audio_only());
656
657 let audio_only = MediaInfo::builder()
659 .audio_stream(sample_audio_stream())
660 .build();
661 assert!(!audio_only.has_video());
662 assert!(audio_only.has_audio());
663 assert!(!audio_only.is_video_only());
664 assert!(audio_only.is_audio_only());
665
666 let both = MediaInfo::builder()
668 .video_stream(sample_video_stream())
669 .audio_stream(sample_audio_stream())
670 .build();
671 assert!(both.has_video());
672 assert!(both.has_audio());
673 assert!(!both.is_video_only());
674 assert!(!both.is_audio_only());
675 }
676
677 #[test]
678 fn test_primary_streams() {
679 let video1 = VideoStreamInfo::builder()
680 .index(0)
681 .width(1920)
682 .height(1080)
683 .build();
684 let video2 = VideoStreamInfo::builder()
685 .index(2)
686 .width(1280)
687 .height(720)
688 .build();
689 let audio1 = AudioStreamInfo::builder()
690 .index(1)
691 .sample_rate(48000)
692 .build();
693 let audio2 = AudioStreamInfo::builder()
694 .index(3)
695 .sample_rate(44100)
696 .build();
697
698 let info = MediaInfo::builder()
699 .video_stream(video1)
700 .video_stream(video2)
701 .audio_stream(audio1)
702 .audio_stream(audio2)
703 .build();
704
705 let primary_video = info.primary_video().unwrap();
707 assert_eq!(primary_video.width(), 1920);
708 assert_eq!(primary_video.index(), 0);
709
710 let primary_audio = info.primary_audio().unwrap();
711 assert_eq!(primary_audio.sample_rate(), 48000);
712 assert_eq!(primary_audio.index(), 1);
713 }
714
715 #[test]
716 fn test_stream_access_by_index() {
717 let video1 = VideoStreamInfo::builder().width(1920).build();
718 let video2 = VideoStreamInfo::builder().width(1280).build();
719 let audio1 = AudioStreamInfo::builder().sample_rate(48000).build();
720
721 let info = MediaInfo::builder()
722 .video_stream(video1)
723 .video_stream(video2)
724 .audio_stream(audio1)
725 .build();
726
727 assert_eq!(info.video_stream(0).unwrap().width(), 1920);
728 assert_eq!(info.video_stream(1).unwrap().width(), 1280);
729 assert!(info.video_stream(2).is_none());
730
731 assert_eq!(info.audio_stream(0).unwrap().sample_rate(), 48000);
732 assert!(info.audio_stream(1).is_none());
733 }
734
735 #[test]
736 fn test_resolution_and_frame_rate() {
737 let info = MediaInfo::builder()
738 .video_stream(sample_video_stream())
739 .build();
740
741 assert_eq!(info.resolution(), Some((1920, 1080)));
742 assert!((info.frame_rate().unwrap() - 30.0).abs() < 0.001);
743
744 let no_video = MediaInfo::default();
746 assert!(no_video.resolution().is_none());
747 assert!(no_video.frame_rate().is_none());
748 }
749
750 #[test]
751 fn test_sample_rate_and_channels() {
752 let info = MediaInfo::builder()
753 .audio_stream(sample_audio_stream())
754 .build();
755
756 assert_eq!(info.sample_rate(), Some(48000));
757 assert_eq!(info.channels(), Some(2));
758
759 let no_audio = MediaInfo::default();
761 assert!(no_audio.sample_rate().is_none());
762 assert!(no_audio.channels().is_none());
763 }
764
765 #[test]
766 fn test_stream_counts() {
767 let info = MediaInfo::builder()
768 .video_stream(sample_video_stream())
769 .video_stream(sample_video_stream())
770 .audio_stream(sample_audio_stream())
771 .audio_stream(sample_audio_stream())
772 .audio_stream(sample_audio_stream())
773 .build();
774
775 assert_eq!(info.video_stream_count(), 2);
776 assert_eq!(info.audio_stream_count(), 3);
777 assert_eq!(info.stream_count(), 5);
778 }
779
780 #[test]
781 fn test_file_name_and_extension() {
782 let info = MediaInfo::builder().path("/path/to/my_video.mp4").build();
783
784 assert_eq!(info.file_name(), Some("my_video.mp4"));
785 assert_eq!(info.extension(), Some("mp4"));
786
787 let empty = MediaInfo::default();
789 assert!(empty.file_name().is_none());
790 assert!(empty.extension().is_none());
791 }
792
793 #[test]
794 fn test_metadata_operations() {
795 let mut map = HashMap::new();
796 map.insert("key1".to_string(), "value1".to_string());
797 map.insert("key2".to_string(), "value2".to_string());
798
799 let info = MediaInfo::builder()
800 .metadata_map(map)
801 .metadata("key3", "value3")
802 .build();
803
804 assert_eq!(info.metadata().len(), 3);
805 assert_eq!(info.metadata_value("key1"), Some("value1"));
806 assert_eq!(info.metadata_value("key2"), Some("value2"));
807 assert_eq!(info.metadata_value("key3"), Some("value3"));
808 }
809
810 #[test]
811 fn test_clone() {
812 let info = MediaInfo::builder()
813 .path("/path/to/video.mp4")
814 .format("mp4")
815 .format_long_name("QuickTime / MOV")
816 .duration(Duration::from_secs(120))
817 .file_size(1_000_000)
818 .video_stream(sample_video_stream())
819 .audio_stream(sample_audio_stream())
820 .metadata("title", "Test")
821 .build();
822
823 let cloned = info.clone();
824 assert_eq!(info.path(), cloned.path());
825 assert_eq!(info.format(), cloned.format());
826 assert_eq!(info.format_long_name(), cloned.format_long_name());
827 assert_eq!(info.duration(), cloned.duration());
828 assert_eq!(info.file_size(), cloned.file_size());
829 assert_eq!(info.video_stream_count(), cloned.video_stream_count());
830 assert_eq!(info.audio_stream_count(), cloned.audio_stream_count());
831 assert_eq!(info.metadata_value("title"), cloned.metadata_value("title"));
832 }
833
834 #[test]
835 fn test_debug() {
836 let info = MediaInfo::builder()
837 .path("/path/to/video.mp4")
838 .format("mp4")
839 .duration(Duration::from_secs(120))
840 .file_size(1_000_000)
841 .build();
842
843 let debug = format!("{info:?}");
844 assert!(debug.contains("MediaInfo"));
845 assert!(debug.contains("mp4"));
846 }
847
848 #[test]
849 fn test_video_streams_setter() {
850 let streams = vec![sample_video_stream(), sample_video_stream()];
851
852 let info = MediaInfo::builder().video_streams(streams).build();
853
854 assert_eq!(info.video_stream_count(), 2);
855 }
856
857 #[test]
858 fn test_audio_streams_setter() {
859 let streams = vec![
860 sample_audio_stream(),
861 sample_audio_stream(),
862 sample_audio_stream(),
863 ];
864
865 let info = MediaInfo::builder().audio_streams(streams).build();
866
867 assert_eq!(info.audio_stream_count(), 3);
868 }
869 }
870
871 mod media_info_builder_tests {
872 use super::*;
873
874 #[test]
875 fn test_builder_default() {
876 let builder = MediaInfoBuilder::default();
877 let info = builder.build();
878 assert_eq!(info.path(), Path::new(""));
879 assert_eq!(info.format(), "");
880 assert_eq!(info.duration(), Duration::ZERO);
881 }
882
883 #[test]
884 fn test_builder_clone() {
885 let builder = MediaInfo::builder()
886 .path("/path/to/video.mp4")
887 .format("mp4")
888 .duration(Duration::from_secs(120));
889
890 let cloned = builder.clone();
891 let info1 = builder.build();
892 let info2 = cloned.build();
893
894 assert_eq!(info1.path(), info2.path());
895 assert_eq!(info1.format(), info2.format());
896 assert_eq!(info1.duration(), info2.duration());
897 }
898
899 #[test]
900 fn test_builder_debug() {
901 let builder = MediaInfo::builder()
902 .path("/path/to/video.mp4")
903 .format("mp4");
904
905 let debug = format!("{builder:?}");
906 assert!(debug.contains("MediaInfoBuilder"));
907 }
908 }
909
910 mod metadata_convenience_tests {
911 use super::*;
912
913 #[test]
914 fn test_title() {
915 let info = MediaInfo::builder()
916 .metadata("title", "Sample Video Title")
917 .build();
918
919 assert_eq!(info.title(), Some("Sample Video Title"));
920 }
921
922 #[test]
923 fn test_title_missing() {
924 let info = MediaInfo::default();
925 assert!(info.title().is_none());
926 }
927
928 #[test]
929 fn test_artist() {
930 let info = MediaInfo::builder()
931 .metadata("artist", "Test Artist")
932 .build();
933
934 assert_eq!(info.artist(), Some("Test Artist"));
935 }
936
937 #[test]
938 fn test_album() {
939 let info = MediaInfo::builder().metadata("album", "Test Album").build();
940
941 assert_eq!(info.album(), Some("Test Album"));
942 }
943
944 #[test]
945 fn test_creation_time() {
946 let info = MediaInfo::builder()
947 .metadata("creation_time", "2024-01-15T10:30:00.000000Z")
948 .build();
949
950 assert_eq!(info.creation_time(), Some("2024-01-15T10:30:00.000000Z"));
951 }
952
953 #[test]
954 fn test_date() {
955 let info = MediaInfo::builder().metadata("date", "2024-01-15").build();
956
957 assert_eq!(info.date(), Some("2024-01-15"));
958 }
959
960 #[test]
961 fn test_comment() {
962 let info = MediaInfo::builder()
963 .metadata("comment", "This is a test comment")
964 .build();
965
966 assert_eq!(info.comment(), Some("This is a test comment"));
967 }
968
969 #[test]
970 fn test_encoder() {
971 let info = MediaInfo::builder()
972 .metadata("encoder", "Lavf58.76.100")
973 .build();
974
975 assert_eq!(info.encoder(), Some("Lavf58.76.100"));
976 }
977
978 #[test]
979 fn test_multiple_metadata_fields() {
980 let info = MediaInfo::builder()
981 .metadata("title", "My Video")
982 .metadata("artist", "John Doe")
983 .metadata("album", "My Collection")
984 .metadata("date", "2024")
985 .metadata("comment", "A great video")
986 .metadata("encoder", "FFmpeg")
987 .metadata("custom_field", "custom_value")
988 .build();
989
990 assert_eq!(info.title(), Some("My Video"));
991 assert_eq!(info.artist(), Some("John Doe"));
992 assert_eq!(info.album(), Some("My Collection"));
993 assert_eq!(info.date(), Some("2024"));
994 assert_eq!(info.comment(), Some("A great video"));
995 assert_eq!(info.encoder(), Some("FFmpeg"));
996 assert_eq!(info.metadata_value("custom_field"), Some("custom_value"));
997 assert_eq!(info.metadata().len(), 7);
998 }
999 }
1000}