rust_ffprobe/
types.rs

1use ffmpeg_common::Duration;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fmt;
5
6/// Sections that can be probed
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ProbeSection {
9    Format,
10    Streams,
11    Packets,
12    Frames,
13    Programs,
14    Chapters,
15    Error,
16}
17
18impl ProbeSection {
19    pub fn as_str(&self) -> &'static str {
20        match self {
21            Self::Format => "format",
22            Self::Streams => "streams",
23            Self::Packets => "packets",
24            Self::Frames => "frames",
25            Self::Programs => "programs",
26            Self::Chapters => "chapters",
27            Self::Error => "error",
28        }
29    }
30}
31
32/// Read interval specification
33#[derive(Debug, Clone)]
34pub struct ReadInterval {
35    /// Start position (None = from beginning)
36    pub start: Option<IntervalPosition>,
37    /// End position (None = to end)
38    pub end: Option<IntervalPosition>,
39}
40
41#[derive(Debug, Clone)]
42pub enum IntervalPosition {
43    /// Absolute position
44    Absolute(Duration),
45    /// Relative offset from current position
46    Relative(Duration),
47    /// Number of packets
48    Packets(u64),
49}
50
51impl ReadInterval {
52    /// Create interval from start to end
53    pub fn new(start: Option<IntervalPosition>, end: Option<IntervalPosition>) -> Self {
54        Self { start, end }
55    }
56
57    /// Read from beginning to position
58    pub fn to(end: IntervalPosition) -> Self {
59        Self {
60            start: None,
61            end: Some(end),
62        }
63    }
64
65    /// Read from position to end
66    pub fn from(start: IntervalPosition) -> Self {
67        Self {
68            start: Some(start),
69            end: None,
70        }
71    }
72
73    /// Read entire input
74    pub fn all() -> Self {
75        Self {
76            start: None,
77            end: None,
78        }
79    }
80
81    /// Convert to FFprobe format
82    pub fn to_string(&self) -> String {
83        let mut result = String::new();
84
85        if let Some(ref start) = self.start {
86            match start {
87                IntervalPosition::Absolute(d) => result.push_str(&d.to_ffmpeg_format()),
88                IntervalPosition::Relative(d) => result.push_str(&format!("+{}", d.to_ffmpeg_format())),
89                IntervalPosition::Packets(n) => result.push_str(&format!("#{}", n)),
90            }
91        }
92
93        result.push('%');
94
95        if let Some(ref end) = self.end {
96            match end {
97                IntervalPosition::Absolute(d) => result.push_str(&d.to_ffmpeg_format()),
98                IntervalPosition::Relative(d) => result.push_str(&format!("+{}", d.to_ffmpeg_format())),
99                IntervalPosition::Packets(n) => result.push_str(&format!("#{}", n)),
100            }
101        }
102
103        result
104    }
105}
106
107impl fmt::Display for ReadInterval {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        write!(f, "{}", self.to_string())
110    }
111}
112
113/// Main probe result structure
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct ProbeResult {
116    /// Format information
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub format: Option<FormatInfo>,
119
120    /// Stream information
121    #[serde(default, skip_serializing_if = "Vec::is_empty")]
122    pub streams: Vec<StreamInfo>,
123
124    /// Packet information
125    #[serde(default, skip_serializing_if = "Vec::is_empty")]
126    pub packets: Vec<PacketInfo>,
127
128    /// Frame information
129    #[serde(default, skip_serializing_if = "Vec::is_empty")]
130    pub frames: Vec<FrameInfo>,
131
132    /// Program information
133    #[serde(default, skip_serializing_if = "Vec::is_empty")]
134    pub programs: Vec<ProgramInfo>,
135
136    /// Chapter information
137    #[serde(default, skip_serializing_if = "Vec::is_empty")]
138    pub chapters: Vec<ChapterInfo>,
139
140    /// Error information
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub error: Option<ErrorInfo>,
143}
144
145impl ProbeResult {
146    /// Get video streams
147    pub fn video_streams(&self) -> Vec<&StreamInfo> {
148        self.streams
149            .iter()
150            .filter(|s| s.codec_type == Some("video".to_string()))
151            .collect()
152    }
153
154    /// Get audio streams
155    pub fn audio_streams(&self) -> Vec<&StreamInfo> {
156        self.streams
157            .iter()
158            .filter(|s| s.codec_type == Some("audio".to_string()))
159            .collect()
160    }
161
162    /// Get subtitle streams
163    pub fn subtitle_streams(&self) -> Vec<&StreamInfo> {
164        self.streams
165            .iter()
166            .filter(|s| s.codec_type == Some("subtitle".to_string()))
167            .collect()
168    }
169
170    /// Get the primary video stream
171    pub fn primary_video_stream(&self) -> Option<&StreamInfo> {
172        self.video_streams().into_iter().next()
173    }
174
175    /// Get the primary audio stream
176    pub fn primary_audio_stream(&self) -> Option<&StreamInfo> {
177        self.audio_streams().into_iter().next()
178    }
179
180    /// Get total duration
181    pub fn duration(&self) -> Option<f64> {
182        self.format.as_ref()?.duration.as_ref()?.parse().ok()
183    }
184
185    /// Get format name
186    pub fn format_name(&self) -> Option<&str> {
187        self.format.as_ref()?.format_name.as_deref()
188    }
189
190    /// Get format long name
191    pub fn format_long_name(&self) -> Option<&str> {
192        self.format.as_ref()?.format_long_name.as_deref()
193    }
194}
195
196/// Format information
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct FormatInfo {
199    /// Filename
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub filename: Option<String>,
202
203    /// Number of streams
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub nb_streams: Option<u32>,
206
207    /// Number of programs
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub nb_programs: Option<u32>,
210
211    /// Format name
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub format_name: Option<String>,
214
215    /// Format long name
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub format_long_name: Option<String>,
218
219    /// Start time in seconds
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub start_time: Option<String>,
222
223    /// Duration in seconds
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub duration: Option<String>,
226
227    /// File size in bytes
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub size: Option<String>,
230
231    /// Bit rate
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub bit_rate: Option<String>,
234
235    /// Probe score
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub probe_score: Option<u32>,
238
239    /// Tags/metadata
240    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
241    pub tags: HashMap<String, String>,
242}
243
244/// Stream information
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct StreamInfo {
247    /// Stream index
248    pub index: u32,
249
250    /// Codec name
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub codec_name: Option<String>,
253
254    /// Codec long name
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub codec_long_name: Option<String>,
257
258    /// Profile
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub profile: Option<String>,
261
262    /// Codec type (video/audio/subtitle/data)
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub codec_type: Option<String>,
265
266    /// Codec tag string
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub codec_tag_string: Option<String>,
269
270    /// Codec tag
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub codec_tag: Option<String>,
273
274    // Video specific
275    /// Width
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub width: Option<u32>,
278
279    /// Height
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub height: Option<u32>,
282
283    /// Coded width
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub coded_width: Option<u32>,
286
287    /// Coded height
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub coded_height: Option<u32>,
290
291    /// Has B frames
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub has_b_frames: Option<u32>,
294
295    /// Sample aspect ratio
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub sample_aspect_ratio: Option<String>,
298
299    /// Display aspect ratio
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub display_aspect_ratio: Option<String>,
302
303    /// Pixel format
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub pix_fmt: Option<String>,
306
307    /// Level
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub level: Option<i32>,
310
311    /// Color range
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub color_range: Option<String>,
314
315    /// Color space
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub color_space: Option<String>,
318
319    /// Color transfer
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub color_transfer: Option<String>,
322
323    /// Color primaries
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub color_primaries: Option<String>,
326
327    /// Chroma location
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub chroma_location: Option<String>,
330
331    /// Field order
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub field_order: Option<String>,
334
335    /// References
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub refs: Option<u32>,
338
339    /// Is AVC
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub is_avc: Option<String>,
342
343    /// NAL length size
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub nal_length_size: Option<String>,
346
347    // Audio specific
348    /// Sample format
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub sample_fmt: Option<String>,
351
352    /// Sample rate
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub sample_rate: Option<String>,
355
356    /// Number of channels
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub channels: Option<u32>,
359
360    /// Channel layout
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub channel_layout: Option<String>,
363
364    /// Bits per sample
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub bits_per_sample: Option<u32>,
367
368    // Common
369    /// Stream ID
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub id: Option<String>,
372
373    /// Frame rate ratio
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub r_frame_rate: Option<String>,
376
377    /// Average frame rate
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub avg_frame_rate: Option<String>,
380
381    /// Time base
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub time_base: Option<String>,
384
385    /// Start PTS
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub start_pts: Option<i64>,
388
389    /// Start time
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub start_time: Option<String>,
392
393    /// Duration timestamp
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub duration_ts: Option<i64>,
396
397    /// Duration
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub duration: Option<String>,
400
401    /// Bit rate
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub bit_rate: Option<String>,
404
405    /// Max bit rate
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub max_bit_rate: Option<String>,
408
409    /// Bits per raw sample
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub bits_per_raw_sample: Option<u32>,
412
413    /// Number of frames
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub nb_frames: Option<String>,
416
417    /// Number of read frames
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub nb_read_frames: Option<String>,
420
421    /// Number of read packets
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub nb_read_packets: Option<String>,
424
425    /// Disposition flags
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub disposition: Option<HashMap<String, u8>>,
428
429    /// Tags/metadata
430    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
431    pub tags: HashMap<String, String>,
432}
433
434impl StreamInfo {
435    /// Check if this is a video stream
436    pub fn is_video(&self) -> bool {
437        self.codec_type.as_deref() == Some("video")
438    }
439
440    /// Check if this is an audio stream
441    pub fn is_audio(&self) -> bool {
442        self.codec_type.as_deref() == Some("audio")
443    }
444
445    /// Check if this is a subtitle stream
446    pub fn is_subtitle(&self) -> bool {
447        self.codec_type.as_deref() == Some("subtitle")
448    }
449
450    /// Get the language tag
451    pub fn language(&self) -> Option<&str> {
452        self.tags.get("language").map(|s| s.as_str())
453    }
454
455    /// Get the title tag
456    pub fn title(&self) -> Option<&str> {
457        self.tags.get("title").map(|s| s.as_str())
458    }
459
460    /// Get resolution as (width, height)
461    pub fn resolution(&self) -> Option<(u32, u32)> {
462        match (self.width, self.height) {
463            (Some(w), Some(h)) => Some((w, h)),
464            _ => None,
465        }
466    }
467
468    /// Get frame rate as f64
469    pub fn frame_rate(&self) -> Option<f64> {
470        self.avg_frame_rate
471            .as_ref()
472            .or(self.r_frame_rate.as_ref())
473            .and_then(|r| parse_rational(r))
474    }
475
476    /// Get sample rate as u32
477    pub fn sample_rate_hz(&self) -> Option<u32> {
478        self.sample_rate.as_ref()?.parse().ok()
479    }
480
481    /// Get duration as f64 seconds
482    pub fn duration_seconds(&self) -> Option<f64> {
483        self.duration.as_ref()?.parse().ok()
484    }
485
486    /// Get bit rate as u64
487    pub fn bit_rate_bps(&self) -> Option<u64> {
488        self.bit_rate.as_ref()?.parse().ok()
489    }
490}
491
492/// Packet information
493#[derive(Debug, Clone, Serialize, Deserialize)]
494pub struct PacketInfo {
495    /// Codec type
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub codec_type: Option<String>,
498
499    /// Stream index
500    pub stream_index: u32,
501
502    /// Presentation timestamp
503    #[serde(skip_serializing_if = "Option::is_none")]
504    pub pts: Option<i64>,
505
506    /// Presentation time
507    #[serde(skip_serializing_if = "Option::is_none")]
508    pub pts_time: Option<String>,
509
510    /// Decoding timestamp
511    #[serde(skip_serializing_if = "Option::is_none")]
512    pub dts: Option<i64>,
513
514    /// Decoding time
515    #[serde(skip_serializing_if = "Option::is_none")]
516    pub dts_time: Option<String>,
517
518    /// Duration
519    #[serde(skip_serializing_if = "Option::is_none")]
520    pub duration: Option<i64>,
521
522    /// Duration time
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub duration_time: Option<String>,
525
526    /// Size in bytes
527    #[serde(skip_serializing_if = "Option::is_none")]
528    pub size: Option<String>,
529
530    /// Position
531    #[serde(skip_serializing_if = "Option::is_none")]
532    pub pos: Option<String>,
533
534    /// Flags
535    #[serde(skip_serializing_if = "Option::is_none")]
536    pub flags: Option<String>,
537
538    /// Data (if show_data enabled)
539    #[serde(skip_serializing_if = "Option::is_none")]
540    pub data: Option<String>,
541
542    /// Data hash (if show_data_hash enabled)
543    #[serde(skip_serializing_if = "Option::is_none")]
544    pub data_hash: Option<String>,
545}
546
547/// Frame information
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct FrameInfo {
550    /// Media type
551    #[serde(skip_serializing_if = "Option::is_none")]
552    pub media_type: Option<String>,
553
554    /// Stream index
555    pub stream_index: u32,
556
557    /// Key frame flag
558    #[serde(skip_serializing_if = "Option::is_none")]
559    pub key_frame: Option<u8>,
560
561    /// Presentation timestamp
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub pts: Option<i64>,
564
565    /// Presentation time
566    #[serde(skip_serializing_if = "Option::is_none")]
567    pub pts_time: Option<String>,
568
569    /// Packet timestamp
570    #[serde(skip_serializing_if = "Option::is_none")]
571    pub pkt_pts: Option<i64>,
572
573    /// Packet time
574    #[serde(skip_serializing_if = "Option::is_none")]
575    pub pkt_pts_time: Option<String>,
576
577    /// Packet DTS
578    #[serde(skip_serializing_if = "Option::is_none")]
579    pub pkt_dts: Option<i64>,
580
581    /// Packet DTS time
582    #[serde(skip_serializing_if = "Option::is_none")]
583    pub pkt_dts_time: Option<String>,
584
585    /// Best effort timestamp
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub best_effort_timestamp: Option<i64>,
588
589    /// Best effort time
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub best_effort_timestamp_time: Option<String>,
592
593    /// Packet duration
594    #[serde(skip_serializing_if = "Option::is_none")]
595    pub pkt_duration: Option<i64>,
596
597    /// Packet duration time
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub pkt_duration_time: Option<String>,
600
601    /// Packet position
602    #[serde(skip_serializing_if = "Option::is_none")]
603    pub pkt_pos: Option<String>,
604
605    /// Packet size
606    #[serde(skip_serializing_if = "Option::is_none")]
607    pub pkt_size: Option<String>,
608
609    // Video specific
610    /// Width
611    #[serde(skip_serializing_if = "Option::is_none")]
612    pub width: Option<u32>,
613
614    /// Height
615    #[serde(skip_serializing_if = "Option::is_none")]
616    pub height: Option<u32>,
617
618    /// Pixel format
619    #[serde(skip_serializing_if = "Option::is_none")]
620    pub pix_fmt: Option<String>,
621
622    /// Picture type
623    #[serde(skip_serializing_if = "Option::is_none")]
624    pub pict_type: Option<String>,
625
626    /// Coded picture number
627    #[serde(skip_serializing_if = "Option::is_none")]
628    pub coded_picture_number: Option<u32>,
629
630    /// Display picture number
631    #[serde(skip_serializing_if = "Option::is_none")]
632    pub display_picture_number: Option<u32>,
633
634    /// Interlaced frame
635    #[serde(skip_serializing_if = "Option::is_none")]
636    pub interlaced_frame: Option<u8>,
637
638    /// Top field first
639    #[serde(skip_serializing_if = "Option::is_none")]
640    pub top_field_first: Option<u8>,
641
642    /// Repeat picture
643    #[serde(skip_serializing_if = "Option::is_none")]
644    pub repeat_pict: Option<u8>,
645
646    // Audio specific
647    /// Sample format
648    #[serde(skip_serializing_if = "Option::is_none")]
649    pub sample_fmt: Option<String>,
650
651    /// Number of samples
652    #[serde(skip_serializing_if = "Option::is_none")]
653    pub nb_samples: Option<u32>,
654
655    /// Channels
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub channels: Option<u32>,
658
659    /// Channel layout
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub channel_layout: Option<String>,
662}
663
664/// Program information
665#[derive(Debug, Clone, Serialize, Deserialize)]
666pub struct ProgramInfo {
667    /// Program ID
668    pub program_id: u32,
669
670    /// Program number
671    #[serde(skip_serializing_if = "Option::is_none")]
672    pub program_num: Option<u32>,
673
674    /// Number of streams
675    #[serde(skip_serializing_if = "Option::is_none")]
676    pub nb_streams: Option<u32>,
677
678    /// PMT PID
679    #[serde(skip_serializing_if = "Option::is_none")]
680    pub pmt_pid: Option<u32>,
681
682    /// PCR PID
683    #[serde(skip_serializing_if = "Option::is_none")]
684    pub pcr_pid: Option<u32>,
685
686    /// Start PTS
687    #[serde(skip_serializing_if = "Option::is_none")]
688    pub start_pts: Option<i64>,
689
690    /// Start time
691    #[serde(skip_serializing_if = "Option::is_none")]
692    pub start_time: Option<String>,
693
694    /// End PTS
695    #[serde(skip_serializing_if = "Option::is_none")]
696    pub end_pts: Option<i64>,
697
698    /// End time
699    #[serde(skip_serializing_if = "Option::is_none")]
700    pub end_time: Option<String>,
701
702    /// Tags
703    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
704    pub tags: HashMap<String, String>,
705
706    /// Streams in this program
707    #[serde(default, skip_serializing_if = "Vec::is_empty")]
708    pub streams: Vec<StreamInfo>,
709}
710
711/// Chapter information
712#[derive(Debug, Clone, Serialize, Deserialize)]
713pub struct ChapterInfo {
714    /// Chapter ID
715    pub id: i64,
716
717    /// Time base
718    #[serde(skip_serializing_if = "Option::is_none")]
719    pub time_base: Option<String>,
720
721    /// Start time
722    pub start: i64,
723
724    /// Start time string
725    #[serde(skip_serializing_if = "Option::is_none")]
726    pub start_time: Option<String>,
727
728    /// End time
729    pub end: i64,
730
731    /// End time string
732    #[serde(skip_serializing_if = "Option::is_none")]
733    pub end_time: Option<String>,
734
735    /// Tags
736    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
737    pub tags: HashMap<String, String>,
738}
739
740impl ChapterInfo {
741    /// Get chapter title
742    pub fn title(&self) -> Option<&str> {
743        self.tags.get("title").map(|s| s.as_str())
744    }
745}
746
747/// Error information
748#[derive(Debug, Clone, Serialize, Deserialize)]
749pub struct ErrorInfo {
750    /// Error code
751    #[serde(skip_serializing_if = "Option::is_none")]
752    pub code: Option<i32>,
753
754    /// Error string
755    #[serde(skip_serializing_if = "Option::is_none")]
756    pub string: Option<String>,
757}
758
759/// Parse a rational number string (e.g., "30/1" or "30000/1001")
760fn parse_rational(s: &str) -> Option<f64> {
761    let parts: Vec<&str> = s.split('/').collect();
762    if parts.len() == 2 {
763        let num: f64 = parts[0].parse().ok()?;
764        let den: f64 = parts[1].parse().ok()?;
765        if den != 0.0 {
766            Some(num / den)
767        } else {
768            None
769        }
770    } else {
771        s.parse().ok()
772    }
773}
774
775#[cfg(test)]
776mod tests {
777    use super::*;
778
779    #[test]
780    fn test_read_interval() {
781        let interval = ReadInterval::all();
782        assert_eq!(interval.to_string(), "%");
783
784        let interval = ReadInterval::to(IntervalPosition::Absolute(Duration::from_secs(30)));
785        assert_eq!(interval.to_string(), "%00:00:30");
786
787        let interval = ReadInterval::from(IntervalPosition::Relative(Duration::from_secs(10)));
788        assert_eq!(interval.to_string(), "+00:00:10%");
789
790        let interval = ReadInterval::new(
791            Some(IntervalPosition::Absolute(Duration::from_secs(10))),
792            Some(IntervalPosition::Packets(100)),
793        );
794        assert_eq!(interval.to_string(), "00:00:10%#100");
795    }
796
797    #[test]
798    fn test_stream_info_helpers() {
799        let mut stream = StreamInfo {
800            index: 0,
801            codec_type: Some("video".to_string()),
802            width: Some(1920),
803            height: Some(1080),
804            avg_frame_rate: Some("30/1".to_string()),
805            bit_rate: Some("5000000".to_string()),
806            tags: HashMap::new(),
807            ..Default::default()
808        };
809
810        assert!(stream.is_video());
811        assert!(!stream.is_audio());
812        assert_eq!(stream.resolution(), Some((1920, 1080)));
813        assert_eq!(stream.frame_rate(), Some(30.0));
814        assert_eq!(stream.bit_rate_bps(), Some(5000000));
815
816        stream.tags.insert("language".to_string(), "eng".to_string());
817        assert_eq!(stream.language(), Some("eng"));
818    }
819
820    #[test]
821    fn test_parse_rational() {
822        assert_eq!(parse_rational("30/1"), Some(30.0));
823        assert_eq!(parse_rational("30000/1001"), Some(29.97002997002997));
824        assert_eq!(parse_rational("25"), Some(25.0));
825        assert_eq!(parse_rational("0/1"), Some(0.0));
826        assert_eq!(parse_rational("1/0"), None);
827    }
828}
829
830impl Default for StreamInfo {
831    fn default() -> Self {
832        Self {
833            index: 0,
834            codec_name: None,
835            codec_long_name: None,
836            profile: None,
837            codec_type: None,
838            codec_tag_string: None,
839            codec_tag: None,
840            width: None,
841            height: None,
842            coded_width: None,
843            coded_height: None,
844            has_b_frames: None,
845            sample_aspect_ratio: None,
846            display_aspect_ratio: None,
847            pix_fmt: None,
848            level: None,
849            color_range: None,
850            color_space: None,
851            color_transfer: None,
852            color_primaries: None,
853            chroma_location: None,
854            field_order: None,
855            refs: None,
856            is_avc: None,
857            nal_length_size: None,
858            sample_fmt: None,
859            sample_rate: None,
860            channels: None,
861            channel_layout: None,
862            bits_per_sample: None,
863            id: None,
864            r_frame_rate: None,
865            avg_frame_rate: None,
866            time_base: None,
867            start_pts: None,
868            start_time: None,
869            duration_ts: None,
870            duration: None,
871            bit_rate: None,
872            max_bit_rate: None,
873            bits_per_raw_sample: None,
874            nb_frames: None,
875            nb_read_frames: None,
876            nb_read_packets: None,
877            disposition: None,
878            tags: HashMap::new(),
879        }
880    }
881}