Skip to main content

ff_format/
stream.rs

1//! Video and audio stream information.
2//!
3//! This module provides structs for representing metadata about video and
4//! audio streams within media files.
5//!
6//! # Examples
7//!
8//! ```
9//! use ff_format::stream::{VideoStreamInfo, AudioStreamInfo};
10//! use ff_format::{PixelFormat, SampleFormat, Rational};
11//! use ff_format::codec::{VideoCodec, AudioCodec};
12//! use ff_format::color::{ColorSpace, ColorRange, ColorPrimaries};
13//! use ff_format::channel::ChannelLayout;
14//! use std::time::Duration;
15//!
16//! // Create video stream info
17//! let video = VideoStreamInfo::builder()
18//!     .index(0)
19//!     .codec(VideoCodec::H264)
20//!     .width(1920)
21//!     .height(1080)
22//!     .frame_rate(Rational::new(30, 1))
23//!     .pixel_format(PixelFormat::Yuv420p)
24//!     .build();
25//!
26//! assert_eq!(video.width(), 1920);
27//! assert_eq!(video.height(), 1080);
28//!
29//! // Create audio stream info
30//! let audio = AudioStreamInfo::builder()
31//!     .index(1)
32//!     .codec(AudioCodec::Aac)
33//!     .sample_rate(48000)
34//!     .channels(2)
35//!     .sample_format(SampleFormat::F32)
36//!     .build();
37//!
38//! assert_eq!(audio.sample_rate(), 48000);
39//! assert_eq!(audio.channels(), 2);
40//! ```
41
42use std::time::Duration;
43
44use crate::channel::ChannelLayout;
45use crate::codec::{AudioCodec, SubtitleCodec, VideoCodec};
46use crate::color::{ColorPrimaries, ColorRange, ColorSpace};
47use crate::pixel::PixelFormat;
48use crate::sample::SampleFormat;
49use crate::time::Rational;
50
51/// Information about a video stream within a media file.
52///
53/// This struct contains all metadata needed to understand and process
54/// a video stream, including resolution, codec, frame rate, and color
55/// characteristics.
56///
57/// # Construction
58///
59/// Use [`VideoStreamInfo::builder()`] for fluent construction:
60///
61/// ```
62/// use ff_format::stream::VideoStreamInfo;
63/// use ff_format::{PixelFormat, Rational};
64/// use ff_format::codec::VideoCodec;
65///
66/// let info = VideoStreamInfo::builder()
67///     .index(0)
68///     .codec(VideoCodec::H264)
69///     .width(1920)
70///     .height(1080)
71///     .frame_rate(Rational::new(30, 1))
72///     .build();
73/// ```
74#[derive(Debug, Clone)]
75pub struct VideoStreamInfo {
76    /// Stream index within the container
77    index: u32,
78    /// Video codec
79    codec: VideoCodec,
80    /// Codec name as reported by the demuxer
81    codec_name: String,
82    /// Frame width in pixels
83    width: u32,
84    /// Frame height in pixels
85    height: u32,
86    /// Pixel format
87    pixel_format: PixelFormat,
88    /// Frame rate (frames per second)
89    frame_rate: Rational,
90    /// Stream duration (if known)
91    duration: Option<Duration>,
92    /// Bitrate in bits per second (if known)
93    bitrate: Option<u64>,
94    /// Total number of frames (if known)
95    frame_count: Option<u64>,
96    /// Color space (matrix coefficients)
97    color_space: ColorSpace,
98    /// Color range (limited/full)
99    color_range: ColorRange,
100    /// Color primaries
101    color_primaries: ColorPrimaries,
102}
103
104impl VideoStreamInfo {
105    /// Creates a new builder for constructing `VideoStreamInfo`.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// use ff_format::stream::VideoStreamInfo;
111    /// use ff_format::codec::VideoCodec;
112    /// use ff_format::{PixelFormat, Rational};
113    ///
114    /// let info = VideoStreamInfo::builder()
115    ///     .index(0)
116    ///     .codec(VideoCodec::H264)
117    ///     .width(1920)
118    ///     .height(1080)
119    ///     .frame_rate(Rational::new(30, 1))
120    ///     .build();
121    /// ```
122    #[must_use]
123    pub fn builder() -> VideoStreamInfoBuilder {
124        VideoStreamInfoBuilder::default()
125    }
126
127    /// Returns the stream index within the container.
128    #[must_use]
129    #[inline]
130    pub const fn index(&self) -> u32 {
131        self.index
132    }
133
134    /// Returns the video codec.
135    #[must_use]
136    #[inline]
137    pub const fn codec(&self) -> VideoCodec {
138        self.codec
139    }
140
141    /// Returns the codec name as reported by the demuxer.
142    #[must_use]
143    #[inline]
144    pub fn codec_name(&self) -> &str {
145        &self.codec_name
146    }
147
148    /// Returns the frame width in pixels.
149    #[must_use]
150    #[inline]
151    pub const fn width(&self) -> u32 {
152        self.width
153    }
154
155    /// Returns the frame height in pixels.
156    #[must_use]
157    #[inline]
158    pub const fn height(&self) -> u32 {
159        self.height
160    }
161
162    /// Returns the pixel format.
163    #[must_use]
164    #[inline]
165    pub const fn pixel_format(&self) -> PixelFormat {
166        self.pixel_format
167    }
168
169    /// Returns the frame rate as a rational number.
170    #[must_use]
171    #[inline]
172    pub const fn frame_rate(&self) -> Rational {
173        self.frame_rate
174    }
175
176    /// Returns the frame rate as frames per second (f64).
177    #[must_use]
178    #[inline]
179    pub fn fps(&self) -> f64 {
180        self.frame_rate.as_f64()
181    }
182
183    /// Returns the stream duration, if known.
184    #[must_use]
185    #[inline]
186    pub const fn duration(&self) -> Option<Duration> {
187        self.duration
188    }
189
190    /// Returns the bitrate in bits per second, if known.
191    #[must_use]
192    #[inline]
193    pub const fn bitrate(&self) -> Option<u64> {
194        self.bitrate
195    }
196
197    /// Returns the total number of frames, if known.
198    #[must_use]
199    #[inline]
200    pub const fn frame_count(&self) -> Option<u64> {
201        self.frame_count
202    }
203
204    /// Returns the color space (matrix coefficients).
205    #[must_use]
206    #[inline]
207    pub const fn color_space(&self) -> ColorSpace {
208        self.color_space
209    }
210
211    /// Returns the color range (limited/full).
212    #[must_use]
213    #[inline]
214    pub const fn color_range(&self) -> ColorRange {
215        self.color_range
216    }
217
218    /// Returns the color primaries.
219    #[must_use]
220    #[inline]
221    pub const fn color_primaries(&self) -> ColorPrimaries {
222        self.color_primaries
223    }
224
225    /// Returns the aspect ratio as width/height.
226    #[must_use]
227    #[inline]
228    pub fn aspect_ratio(&self) -> f64 {
229        if self.height == 0 {
230            log::warn!(
231                "aspect_ratio unavailable, height is 0, returning 0.0 \
232                 width={} height=0 fallback=0.0",
233                self.width
234            );
235            0.0
236        } else {
237            f64::from(self.width) / f64::from(self.height)
238        }
239    }
240
241    /// Returns `true` if the video is HD (720p or higher).
242    #[must_use]
243    #[inline]
244    pub const fn is_hd(&self) -> bool {
245        self.height >= 720
246    }
247
248    /// Returns `true` if the video is Full HD (1080p or higher).
249    #[must_use]
250    #[inline]
251    pub const fn is_full_hd(&self) -> bool {
252        self.height >= 1080
253    }
254
255    /// Returns `true` if the video is 4K UHD (2160p or higher).
256    #[must_use]
257    #[inline]
258    pub const fn is_4k(&self) -> bool {
259        self.height >= 2160
260    }
261
262    /// Returns `true` if this video stream appears to be HDR (High Dynamic Range).
263    ///
264    /// HDR detection is based on two primary indicators:
265    /// 1. **Wide color gamut**: BT.2020 color primaries
266    /// 2. **High bit depth**: 10-bit or higher pixel format
267    ///
268    /// Both conditions must be met for a stream to be considered HDR.
269    /// This is a heuristic detection - for definitive HDR identification,
270    /// additional metadata like transfer characteristics (PQ/HLG) should be checked.
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// use ff_format::stream::VideoStreamInfo;
276    /// use ff_format::color::ColorPrimaries;
277    /// use ff_format::PixelFormat;
278    ///
279    /// let hdr_video = VideoStreamInfo::builder()
280    ///     .width(3840)
281    ///     .height(2160)
282    ///     .color_primaries(ColorPrimaries::Bt2020)
283    ///     .pixel_format(PixelFormat::Yuv420p10le)
284    ///     .build();
285    ///
286    /// assert!(hdr_video.is_hdr());
287    ///
288    /// // Standard HD video with BT.709 is not HDR
289    /// let sdr_video = VideoStreamInfo::builder()
290    ///     .width(1920)
291    ///     .height(1080)
292    ///     .color_primaries(ColorPrimaries::Bt709)
293    ///     .pixel_format(PixelFormat::Yuv420p)
294    ///     .build();
295    ///
296    /// assert!(!sdr_video.is_hdr());
297    /// ```
298    #[must_use]
299    #[inline]
300    pub fn is_hdr(&self) -> bool {
301        // HDR requires wide color gamut (BT.2020) and high bit depth (10-bit or higher)
302        self.color_primaries.is_wide_gamut() && self.pixel_format.is_high_bit_depth()
303    }
304}
305
306impl Default for VideoStreamInfo {
307    fn default() -> Self {
308        Self {
309            index: 0,
310            codec: VideoCodec::default(),
311            codec_name: String::new(),
312            width: 0,
313            height: 0,
314            pixel_format: PixelFormat::default(),
315            frame_rate: Rational::new(30, 1),
316            duration: None,
317            bitrate: None,
318            frame_count: None,
319            color_space: ColorSpace::default(),
320            color_range: ColorRange::default(),
321            color_primaries: ColorPrimaries::default(),
322        }
323    }
324}
325
326/// Builder for constructing `VideoStreamInfo`.
327#[derive(Debug, Clone, Default)]
328pub struct VideoStreamInfoBuilder {
329    index: u32,
330    codec: VideoCodec,
331    codec_name: String,
332    width: u32,
333    height: u32,
334    pixel_format: PixelFormat,
335    frame_rate: Rational,
336    duration: Option<Duration>,
337    bitrate: Option<u64>,
338    frame_count: Option<u64>,
339    color_space: ColorSpace,
340    color_range: ColorRange,
341    color_primaries: ColorPrimaries,
342}
343
344impl VideoStreamInfoBuilder {
345    /// Sets the stream index.
346    #[must_use]
347    pub fn index(mut self, index: u32) -> Self {
348        self.index = index;
349        self
350    }
351
352    /// Sets the video codec.
353    #[must_use]
354    pub fn codec(mut self, codec: VideoCodec) -> Self {
355        self.codec = codec;
356        self
357    }
358
359    /// Sets the codec name string.
360    #[must_use]
361    pub fn codec_name(mut self, name: impl Into<String>) -> Self {
362        self.codec_name = name.into();
363        self
364    }
365
366    /// Sets the frame width in pixels.
367    #[must_use]
368    pub fn width(mut self, width: u32) -> Self {
369        self.width = width;
370        self
371    }
372
373    /// Sets the frame height in pixels.
374    #[must_use]
375    pub fn height(mut self, height: u32) -> Self {
376        self.height = height;
377        self
378    }
379
380    /// Sets the pixel format.
381    #[must_use]
382    pub fn pixel_format(mut self, format: PixelFormat) -> Self {
383        self.pixel_format = format;
384        self
385    }
386
387    /// Sets the frame rate.
388    #[must_use]
389    pub fn frame_rate(mut self, rate: Rational) -> Self {
390        self.frame_rate = rate;
391        self
392    }
393
394    /// Sets the stream duration.
395    #[must_use]
396    pub fn duration(mut self, duration: Duration) -> Self {
397        self.duration = Some(duration);
398        self
399    }
400
401    /// Sets the bitrate in bits per second.
402    #[must_use]
403    pub fn bitrate(mut self, bitrate: u64) -> Self {
404        self.bitrate = Some(bitrate);
405        self
406    }
407
408    /// Sets the total frame count.
409    #[must_use]
410    pub fn frame_count(mut self, count: u64) -> Self {
411        self.frame_count = Some(count);
412        self
413    }
414
415    /// Sets the color space.
416    #[must_use]
417    pub fn color_space(mut self, space: ColorSpace) -> Self {
418        self.color_space = space;
419        self
420    }
421
422    /// Sets the color range.
423    #[must_use]
424    pub fn color_range(mut self, range: ColorRange) -> Self {
425        self.color_range = range;
426        self
427    }
428
429    /// Sets the color primaries.
430    #[must_use]
431    pub fn color_primaries(mut self, primaries: ColorPrimaries) -> Self {
432        self.color_primaries = primaries;
433        self
434    }
435
436    /// Builds the `VideoStreamInfo`.
437    #[must_use]
438    pub fn build(self) -> VideoStreamInfo {
439        VideoStreamInfo {
440            index: self.index,
441            codec: self.codec,
442            codec_name: self.codec_name,
443            width: self.width,
444            height: self.height,
445            pixel_format: self.pixel_format,
446            frame_rate: self.frame_rate,
447            duration: self.duration,
448            bitrate: self.bitrate,
449            frame_count: self.frame_count,
450            color_space: self.color_space,
451            color_range: self.color_range,
452            color_primaries: self.color_primaries,
453        }
454    }
455}
456
457/// Information about an audio stream within a media file.
458///
459/// This struct contains all metadata needed to understand and process
460/// an audio stream, including sample rate, channel layout, and codec
461/// information.
462///
463/// # Construction
464///
465/// Use [`AudioStreamInfo::builder()`] for fluent construction:
466///
467/// ```
468/// use ff_format::stream::AudioStreamInfo;
469/// use ff_format::SampleFormat;
470/// use ff_format::codec::AudioCodec;
471///
472/// let info = AudioStreamInfo::builder()
473///     .index(1)
474///     .codec(AudioCodec::Aac)
475///     .sample_rate(48000)
476///     .channels(2)
477///     .build();
478/// ```
479#[derive(Debug, Clone)]
480pub struct AudioStreamInfo {
481    /// Stream index within the container
482    index: u32,
483    /// Audio codec
484    codec: AudioCodec,
485    /// Codec name as reported by the demuxer
486    codec_name: String,
487    /// Sample rate in Hz
488    sample_rate: u32,
489    /// Number of channels
490    channels: u32,
491    /// Channel layout
492    channel_layout: ChannelLayout,
493    /// Sample format
494    sample_format: SampleFormat,
495    /// Stream duration (if known)
496    duration: Option<Duration>,
497    /// Bitrate in bits per second (if known)
498    bitrate: Option<u64>,
499    /// Language code (e.g., "eng", "jpn")
500    language: Option<String>,
501}
502
503impl AudioStreamInfo {
504    /// Creates a new builder for constructing `AudioStreamInfo`.
505    ///
506    /// # Examples
507    ///
508    /// ```
509    /// use ff_format::stream::AudioStreamInfo;
510    /// use ff_format::codec::AudioCodec;
511    /// use ff_format::SampleFormat;
512    ///
513    /// let info = AudioStreamInfo::builder()
514    ///     .index(1)
515    ///     .codec(AudioCodec::Aac)
516    ///     .sample_rate(48000)
517    ///     .channels(2)
518    ///     .build();
519    /// ```
520    #[must_use]
521    pub fn builder() -> AudioStreamInfoBuilder {
522        AudioStreamInfoBuilder::default()
523    }
524
525    /// Returns the stream index within the container.
526    #[must_use]
527    #[inline]
528    pub const fn index(&self) -> u32 {
529        self.index
530    }
531
532    /// Returns the audio codec.
533    #[must_use]
534    #[inline]
535    pub const fn codec(&self) -> AudioCodec {
536        self.codec
537    }
538
539    /// Returns the codec name as reported by the demuxer.
540    #[must_use]
541    #[inline]
542    pub fn codec_name(&self) -> &str {
543        &self.codec_name
544    }
545
546    /// Returns the sample rate in Hz.
547    #[must_use]
548    #[inline]
549    pub const fn sample_rate(&self) -> u32 {
550        self.sample_rate
551    }
552
553    /// Returns the number of audio channels.
554    ///
555    /// The type is `u32` to match `FFmpeg`'s `AVCodecParameters::ch_layout.nb_channels`
556    /// and professional audio APIs. When passing to `rodio` or `cpal` (which require
557    /// `u16`), cast with `info.channels() as u16` — channel counts never exceed
558    /// `u16::MAX` in practice.
559    #[must_use]
560    #[inline]
561    pub const fn channels(&self) -> u32 {
562        self.channels
563    }
564
565    /// Returns the channel layout.
566    #[must_use]
567    #[inline]
568    pub const fn channel_layout(&self) -> ChannelLayout {
569        self.channel_layout
570    }
571
572    /// Returns the sample format.
573    #[must_use]
574    #[inline]
575    pub const fn sample_format(&self) -> SampleFormat {
576        self.sample_format
577    }
578
579    /// Returns the stream duration, if known.
580    #[must_use]
581    #[inline]
582    pub const fn duration(&self) -> Option<Duration> {
583        self.duration
584    }
585
586    /// Returns the bitrate in bits per second, if known.
587    #[must_use]
588    #[inline]
589    pub const fn bitrate(&self) -> Option<u64> {
590        self.bitrate
591    }
592
593    /// Returns the language code, if specified.
594    #[must_use]
595    #[inline]
596    pub fn language(&self) -> Option<&str> {
597        self.language.as_deref()
598    }
599
600    /// Returns `true` if this is a mono stream.
601    #[must_use]
602    #[inline]
603    pub const fn is_mono(&self) -> bool {
604        self.channels == 1
605    }
606
607    /// Returns `true` if this is a stereo stream.
608    #[must_use]
609    #[inline]
610    pub const fn is_stereo(&self) -> bool {
611        self.channels == 2
612    }
613
614    /// Returns `true` if this is a surround sound stream (more than 2 channels).
615    #[must_use]
616    #[inline]
617    pub const fn is_surround(&self) -> bool {
618        self.channels > 2
619    }
620}
621
622impl Default for AudioStreamInfo {
623    fn default() -> Self {
624        Self {
625            index: 0,
626            codec: AudioCodec::default(),
627            codec_name: String::new(),
628            sample_rate: 48000,
629            channels: 2,
630            channel_layout: ChannelLayout::default(),
631            sample_format: SampleFormat::default(),
632            duration: None,
633            bitrate: None,
634            language: None,
635        }
636    }
637}
638
639/// Builder for constructing `AudioStreamInfo`.
640#[derive(Debug, Clone, Default)]
641pub struct AudioStreamInfoBuilder {
642    index: u32,
643    codec: AudioCodec,
644    codec_name: String,
645    sample_rate: u32,
646    channels: u32,
647    channel_layout: Option<ChannelLayout>,
648    sample_format: SampleFormat,
649    duration: Option<Duration>,
650    bitrate: Option<u64>,
651    language: Option<String>,
652}
653
654impl AudioStreamInfoBuilder {
655    /// Sets the stream index.
656    #[must_use]
657    pub fn index(mut self, index: u32) -> Self {
658        self.index = index;
659        self
660    }
661
662    /// Sets the audio codec.
663    #[must_use]
664    pub fn codec(mut self, codec: AudioCodec) -> Self {
665        self.codec = codec;
666        self
667    }
668
669    /// Sets the codec name string.
670    #[must_use]
671    pub fn codec_name(mut self, name: impl Into<String>) -> Self {
672        self.codec_name = name.into();
673        self
674    }
675
676    /// Sets the sample rate in Hz.
677    #[must_use]
678    pub fn sample_rate(mut self, rate: u32) -> Self {
679        self.sample_rate = rate;
680        self
681    }
682
683    /// Sets the number of channels.
684    ///
685    /// This also updates the channel layout if not explicitly set.
686    #[must_use]
687    pub fn channels(mut self, channels: u32) -> Self {
688        self.channels = channels;
689        self
690    }
691
692    /// Sets the channel layout explicitly.
693    #[must_use]
694    pub fn channel_layout(mut self, layout: ChannelLayout) -> Self {
695        self.channel_layout = Some(layout);
696        self
697    }
698
699    /// Sets the sample format.
700    #[must_use]
701    pub fn sample_format(mut self, format: SampleFormat) -> Self {
702        self.sample_format = format;
703        self
704    }
705
706    /// Sets the stream duration.
707    #[must_use]
708    pub fn duration(mut self, duration: Duration) -> Self {
709        self.duration = Some(duration);
710        self
711    }
712
713    /// Sets the bitrate in bits per second.
714    #[must_use]
715    pub fn bitrate(mut self, bitrate: u64) -> Self {
716        self.bitrate = Some(bitrate);
717        self
718    }
719
720    /// Sets the language code.
721    #[must_use]
722    pub fn language(mut self, lang: impl Into<String>) -> Self {
723        self.language = Some(lang.into());
724        self
725    }
726
727    /// Builds the `AudioStreamInfo`.
728    #[must_use]
729    pub fn build(self) -> AudioStreamInfo {
730        let channel_layout = self.channel_layout.unwrap_or_else(|| {
731            log::warn!(
732                "channel_layout not set, deriving from channel count \
733                 channels={} fallback=from_channels",
734                self.channels
735            );
736            ChannelLayout::from_channels(self.channels)
737        });
738
739        AudioStreamInfo {
740            index: self.index,
741            codec: self.codec,
742            codec_name: self.codec_name,
743            sample_rate: self.sample_rate,
744            channels: self.channels,
745            channel_layout,
746            sample_format: self.sample_format,
747            duration: self.duration,
748            bitrate: self.bitrate,
749            language: self.language,
750        }
751    }
752}
753
754/// Information about a subtitle stream within a media file.
755///
756/// This struct contains all metadata needed to identify and categorize
757/// a subtitle stream, including codec, language, and forced flag.
758///
759/// # Construction
760///
761/// Use [`SubtitleStreamInfo::builder()`] for fluent construction:
762///
763/// ```
764/// use ff_format::stream::SubtitleStreamInfo;
765/// use ff_format::codec::SubtitleCodec;
766///
767/// let info = SubtitleStreamInfo::builder()
768///     .index(2)
769///     .codec(SubtitleCodec::Srt)
770///     .codec_name("srt")
771///     .language("eng")
772///     .build();
773/// ```
774#[derive(Debug, Clone)]
775pub struct SubtitleStreamInfo {
776    /// Stream index within the container
777    index: u32,
778    /// Subtitle codec
779    codec: SubtitleCodec,
780    /// Codec name as reported by the demuxer
781    codec_name: String,
782    /// Language code (e.g., "eng", "jpn")
783    language: Option<String>,
784    /// Stream title (e.g., "English (Forced)")
785    title: Option<String>,
786    /// Stream duration (if known)
787    duration: Option<Duration>,
788    /// Whether this is a forced subtitle track
789    forced: bool,
790}
791
792impl SubtitleStreamInfo {
793    /// Creates a new builder for constructing `SubtitleStreamInfo`.
794    #[must_use]
795    pub fn builder() -> SubtitleStreamInfoBuilder {
796        SubtitleStreamInfoBuilder::default()
797    }
798
799    /// Returns the stream index within the container.
800    #[must_use]
801    #[inline]
802    pub const fn index(&self) -> u32 {
803        self.index
804    }
805
806    /// Returns the subtitle codec.
807    #[must_use]
808    #[inline]
809    pub fn codec(&self) -> &SubtitleCodec {
810        &self.codec
811    }
812
813    /// Returns the codec name as reported by the demuxer.
814    #[must_use]
815    #[inline]
816    pub fn codec_name(&self) -> &str {
817        &self.codec_name
818    }
819
820    /// Returns the language code, if specified.
821    #[must_use]
822    #[inline]
823    pub fn language(&self) -> Option<&str> {
824        self.language.as_deref()
825    }
826
827    /// Returns the stream title, if specified.
828    #[must_use]
829    #[inline]
830    pub fn title(&self) -> Option<&str> {
831        self.title.as_deref()
832    }
833
834    /// Returns the stream duration, if known.
835    #[must_use]
836    #[inline]
837    pub const fn duration(&self) -> Option<Duration> {
838        self.duration
839    }
840
841    /// Returns `true` if this is a forced subtitle track.
842    #[must_use]
843    #[inline]
844    pub const fn is_forced(&self) -> bool {
845        self.forced
846    }
847
848    /// Returns `true` if the codec is text-based.
849    #[must_use]
850    #[inline]
851    pub fn is_text_based(&self) -> bool {
852        self.codec.is_text_based()
853    }
854}
855
856/// Builder for constructing `SubtitleStreamInfo`.
857#[derive(Debug, Clone)]
858pub struct SubtitleStreamInfoBuilder {
859    index: u32,
860    codec: SubtitleCodec,
861    codec_name: String,
862    language: Option<String>,
863    title: Option<String>,
864    duration: Option<Duration>,
865    forced: bool,
866}
867
868impl Default for SubtitleStreamInfoBuilder {
869    fn default() -> Self {
870        Self {
871            index: 0,
872            codec: SubtitleCodec::Other(String::new()),
873            codec_name: String::new(),
874            language: None,
875            title: None,
876            duration: None,
877            forced: false,
878        }
879    }
880}
881
882impl SubtitleStreamInfoBuilder {
883    /// Sets the stream index.
884    #[must_use]
885    pub fn index(mut self, index: u32) -> Self {
886        self.index = index;
887        self
888    }
889
890    /// Sets the subtitle codec.
891    #[must_use]
892    pub fn codec(mut self, codec: SubtitleCodec) -> Self {
893        self.codec = codec;
894        self
895    }
896
897    /// Sets the codec name string.
898    #[must_use]
899    pub fn codec_name(mut self, name: impl Into<String>) -> Self {
900        self.codec_name = name.into();
901        self
902    }
903
904    /// Sets the language code.
905    #[must_use]
906    pub fn language(mut self, lang: impl Into<String>) -> Self {
907        self.language = Some(lang.into());
908        self
909    }
910
911    /// Sets the stream title.
912    #[must_use]
913    pub fn title(mut self, title: impl Into<String>) -> Self {
914        self.title = Some(title.into());
915        self
916    }
917
918    /// Sets the stream duration.
919    #[must_use]
920    pub fn duration(mut self, duration: Duration) -> Self {
921        self.duration = Some(duration);
922        self
923    }
924
925    /// Sets the forced flag.
926    #[must_use]
927    pub fn forced(mut self, forced: bool) -> Self {
928        self.forced = forced;
929        self
930    }
931
932    /// Builds the `SubtitleStreamInfo`.
933    #[must_use]
934    pub fn build(self) -> SubtitleStreamInfo {
935        SubtitleStreamInfo {
936            index: self.index,
937            codec: self.codec,
938            codec_name: self.codec_name,
939            language: self.language,
940            title: self.title,
941            duration: self.duration,
942            forced: self.forced,
943        }
944    }
945}
946
947#[cfg(test)]
948mod tests {
949    use super::*;
950
951    mod video_stream_info_tests {
952        use super::*;
953
954        #[test]
955        fn test_builder_basic() {
956            let info = VideoStreamInfo::builder()
957                .index(0)
958                .codec(VideoCodec::H264)
959                .codec_name("h264")
960                .width(1920)
961                .height(1080)
962                .frame_rate(Rational::new(30, 1))
963                .pixel_format(PixelFormat::Yuv420p)
964                .build();
965
966            assert_eq!(info.index(), 0);
967            assert_eq!(info.codec(), VideoCodec::H264);
968            assert_eq!(info.codec_name(), "h264");
969            assert_eq!(info.width(), 1920);
970            assert_eq!(info.height(), 1080);
971            assert!((info.fps() - 30.0).abs() < 0.001);
972            assert_eq!(info.pixel_format(), PixelFormat::Yuv420p);
973        }
974
975        #[test]
976        fn test_builder_full() {
977            let info = VideoStreamInfo::builder()
978                .index(0)
979                .codec(VideoCodec::H265)
980                .codec_name("hevc")
981                .width(3840)
982                .height(2160)
983                .frame_rate(Rational::new(60, 1))
984                .pixel_format(PixelFormat::Yuv420p10le)
985                .duration(Duration::from_secs(120))
986                .bitrate(50_000_000)
987                .frame_count(7200)
988                .color_space(ColorSpace::Bt2020)
989                .color_range(ColorRange::Full)
990                .color_primaries(ColorPrimaries::Bt2020)
991                .build();
992
993            assert_eq!(info.codec(), VideoCodec::H265);
994            assert_eq!(info.width(), 3840);
995            assert_eq!(info.height(), 2160);
996            assert_eq!(info.duration(), Some(Duration::from_secs(120)));
997            assert_eq!(info.bitrate(), Some(50_000_000));
998            assert_eq!(info.frame_count(), Some(7200));
999            assert_eq!(info.color_space(), ColorSpace::Bt2020);
1000            assert_eq!(info.color_range(), ColorRange::Full);
1001            assert_eq!(info.color_primaries(), ColorPrimaries::Bt2020);
1002        }
1003
1004        #[test]
1005        fn test_default() {
1006            let info = VideoStreamInfo::default();
1007            assert_eq!(info.index(), 0);
1008            assert_eq!(info.codec(), VideoCodec::default());
1009            assert_eq!(info.width(), 0);
1010            assert_eq!(info.height(), 0);
1011            assert!(info.duration().is_none());
1012        }
1013
1014        #[test]
1015        fn test_aspect_ratio() {
1016            let info = VideoStreamInfo::builder().width(1920).height(1080).build();
1017            assert!((info.aspect_ratio() - (16.0 / 9.0)).abs() < 0.01);
1018
1019            let info = VideoStreamInfo::builder().width(1280).height(720).build();
1020            assert!((info.aspect_ratio() - (16.0 / 9.0)).abs() < 0.01);
1021
1022            // Zero height
1023            let info = VideoStreamInfo::builder().width(1920).height(0).build();
1024            assert_eq!(info.aspect_ratio(), 0.0);
1025        }
1026
1027        #[test]
1028        fn test_resolution_checks() {
1029            // SD
1030            let sd = VideoStreamInfo::builder().width(720).height(480).build();
1031            assert!(!sd.is_hd());
1032            assert!(!sd.is_full_hd());
1033            assert!(!sd.is_4k());
1034
1035            // HD
1036            let hd = VideoStreamInfo::builder().width(1280).height(720).build();
1037            assert!(hd.is_hd());
1038            assert!(!hd.is_full_hd());
1039            assert!(!hd.is_4k());
1040
1041            // Full HD
1042            let fhd = VideoStreamInfo::builder().width(1920).height(1080).build();
1043            assert!(fhd.is_hd());
1044            assert!(fhd.is_full_hd());
1045            assert!(!fhd.is_4k());
1046
1047            // 4K
1048            let uhd = VideoStreamInfo::builder().width(3840).height(2160).build();
1049            assert!(uhd.is_hd());
1050            assert!(uhd.is_full_hd());
1051            assert!(uhd.is_4k());
1052        }
1053
1054        #[test]
1055        fn test_is_hdr() {
1056            // HDR video: BT.2020 color primaries + 10-bit pixel format
1057            let hdr = VideoStreamInfo::builder()
1058                .width(3840)
1059                .height(2160)
1060                .color_primaries(ColorPrimaries::Bt2020)
1061                .pixel_format(PixelFormat::Yuv420p10le)
1062                .build();
1063            assert!(hdr.is_hdr());
1064
1065            // HDR video with P010le format
1066            let hdr_p010 = VideoStreamInfo::builder()
1067                .width(3840)
1068                .height(2160)
1069                .color_primaries(ColorPrimaries::Bt2020)
1070                .pixel_format(PixelFormat::P010le)
1071                .build();
1072            assert!(hdr_p010.is_hdr());
1073
1074            // SDR video: BT.709 color primaries (standard HD)
1075            let sdr_hd = VideoStreamInfo::builder()
1076                .width(1920)
1077                .height(1080)
1078                .color_primaries(ColorPrimaries::Bt709)
1079                .pixel_format(PixelFormat::Yuv420p)
1080                .build();
1081            assert!(!sdr_hd.is_hdr());
1082
1083            // BT.2020 but 8-bit (not HDR - missing high bit depth)
1084            let wide_gamut_8bit = VideoStreamInfo::builder()
1085                .width(3840)
1086                .height(2160)
1087                .color_primaries(ColorPrimaries::Bt2020)
1088                .pixel_format(PixelFormat::Yuv420p) // 8-bit
1089                .build();
1090            assert!(!wide_gamut_8bit.is_hdr());
1091
1092            // 10-bit but BT.709 (not HDR - missing wide gamut)
1093            let hd_10bit = VideoStreamInfo::builder()
1094                .width(1920)
1095                .height(1080)
1096                .color_primaries(ColorPrimaries::Bt709)
1097                .pixel_format(PixelFormat::Yuv420p10le)
1098                .build();
1099            assert!(!hd_10bit.is_hdr());
1100
1101            // Default video stream is not HDR
1102            let default = VideoStreamInfo::default();
1103            assert!(!default.is_hdr());
1104        }
1105
1106        #[test]
1107        fn test_debug() {
1108            let info = VideoStreamInfo::builder()
1109                .index(0)
1110                .codec(VideoCodec::H264)
1111                .width(1920)
1112                .height(1080)
1113                .build();
1114            let debug = format!("{info:?}");
1115            assert!(debug.contains("VideoStreamInfo"));
1116            assert!(debug.contains("1920"));
1117            assert!(debug.contains("1080"));
1118        }
1119
1120        #[test]
1121        fn test_clone() {
1122            let info = VideoStreamInfo::builder()
1123                .index(0)
1124                .codec(VideoCodec::H264)
1125                .codec_name("h264")
1126                .width(1920)
1127                .height(1080)
1128                .build();
1129            let cloned = info.clone();
1130            assert_eq!(info.width(), cloned.width());
1131            assert_eq!(info.height(), cloned.height());
1132            assert_eq!(info.codec_name(), cloned.codec_name());
1133        }
1134    }
1135
1136    mod audio_stream_info_tests {
1137        use super::*;
1138
1139        #[test]
1140        fn test_builder_basic() {
1141            let info = AudioStreamInfo::builder()
1142                .index(1)
1143                .codec(AudioCodec::Aac)
1144                .codec_name("aac")
1145                .sample_rate(48000)
1146                .channels(2)
1147                .sample_format(SampleFormat::F32)
1148                .build();
1149
1150            assert_eq!(info.index(), 1);
1151            assert_eq!(info.codec(), AudioCodec::Aac);
1152            assert_eq!(info.codec_name(), "aac");
1153            assert_eq!(info.sample_rate(), 48000);
1154            assert_eq!(info.channels(), 2);
1155            assert_eq!(info.sample_format(), SampleFormat::F32);
1156            assert_eq!(info.channel_layout(), ChannelLayout::Stereo);
1157        }
1158
1159        #[test]
1160        fn test_builder_full() {
1161            let info = AudioStreamInfo::builder()
1162                .index(2)
1163                .codec(AudioCodec::Flac)
1164                .codec_name("flac")
1165                .sample_rate(96000)
1166                .channels(6)
1167                .channel_layout(ChannelLayout::Surround5_1)
1168                .sample_format(SampleFormat::I32)
1169                .duration(Duration::from_secs(300))
1170                .bitrate(1_411_200)
1171                .language("jpn")
1172                .build();
1173
1174            assert_eq!(info.codec(), AudioCodec::Flac);
1175            assert_eq!(info.sample_rate(), 96000);
1176            assert_eq!(info.channels(), 6);
1177            assert_eq!(info.channel_layout(), ChannelLayout::Surround5_1);
1178            assert_eq!(info.duration(), Some(Duration::from_secs(300)));
1179            assert_eq!(info.bitrate(), Some(1_411_200));
1180            assert_eq!(info.language(), Some("jpn"));
1181        }
1182
1183        #[test]
1184        fn test_default() {
1185            let info = AudioStreamInfo::default();
1186            assert_eq!(info.index(), 0);
1187            assert_eq!(info.codec(), AudioCodec::default());
1188            assert_eq!(info.sample_rate(), 48000);
1189            assert_eq!(info.channels(), 2);
1190            assert!(info.duration().is_none());
1191        }
1192
1193        #[test]
1194        fn test_auto_channel_layout() {
1195            // Should auto-detect layout from channel count
1196            let mono = AudioStreamInfo::builder().channels(1).build();
1197            assert_eq!(mono.channel_layout(), ChannelLayout::Mono);
1198
1199            let stereo = AudioStreamInfo::builder().channels(2).build();
1200            assert_eq!(stereo.channel_layout(), ChannelLayout::Stereo);
1201
1202            let surround = AudioStreamInfo::builder().channels(6).build();
1203            assert_eq!(surround.channel_layout(), ChannelLayout::Surround5_1);
1204
1205            // Explicit layout should override
1206            let custom = AudioStreamInfo::builder()
1207                .channels(6)
1208                .channel_layout(ChannelLayout::Other(6))
1209                .build();
1210            assert_eq!(custom.channel_layout(), ChannelLayout::Other(6));
1211        }
1212
1213        #[test]
1214        fn test_channel_checks() {
1215            let mono = AudioStreamInfo::builder().channels(1).build();
1216            assert!(mono.is_mono());
1217            assert!(!mono.is_stereo());
1218            assert!(!mono.is_surround());
1219
1220            let stereo = AudioStreamInfo::builder().channels(2).build();
1221            assert!(!stereo.is_mono());
1222            assert!(stereo.is_stereo());
1223            assert!(!stereo.is_surround());
1224
1225            let surround = AudioStreamInfo::builder().channels(6).build();
1226            assert!(!surround.is_mono());
1227            assert!(!surround.is_stereo());
1228            assert!(surround.is_surround());
1229        }
1230
1231        #[test]
1232        fn test_debug() {
1233            let info = AudioStreamInfo::builder()
1234                .index(1)
1235                .codec(AudioCodec::Aac)
1236                .sample_rate(48000)
1237                .channels(2)
1238                .build();
1239            let debug = format!("{info:?}");
1240            assert!(debug.contains("AudioStreamInfo"));
1241            assert!(debug.contains("48000"));
1242        }
1243
1244        #[test]
1245        fn test_clone() {
1246            let info = AudioStreamInfo::builder()
1247                .index(1)
1248                .codec(AudioCodec::Aac)
1249                .codec_name("aac")
1250                .sample_rate(48000)
1251                .channels(2)
1252                .language("eng")
1253                .build();
1254            let cloned = info.clone();
1255            assert_eq!(info.sample_rate(), cloned.sample_rate());
1256            assert_eq!(info.channels(), cloned.channels());
1257            assert_eq!(info.language(), cloned.language());
1258            assert_eq!(info.codec_name(), cloned.codec_name());
1259        }
1260    }
1261
1262    mod subtitle_stream_info_tests {
1263        use super::*;
1264
1265        #[test]
1266        fn builder_should_store_all_fields() {
1267            let info = SubtitleStreamInfo::builder()
1268                .index(2)
1269                .codec(SubtitleCodec::Srt)
1270                .codec_name("srt")
1271                .language("eng")
1272                .title("English")
1273                .duration(Duration::from_secs(120))
1274                .forced(true)
1275                .build();
1276
1277            assert_eq!(info.index(), 2);
1278            assert_eq!(info.codec(), &SubtitleCodec::Srt);
1279            assert_eq!(info.codec_name(), "srt");
1280            assert_eq!(info.language(), Some("eng"));
1281            assert_eq!(info.title(), Some("English"));
1282            assert_eq!(info.duration(), Some(Duration::from_secs(120)));
1283            assert!(info.is_forced());
1284        }
1285
1286        #[test]
1287        fn is_forced_should_default_to_false() {
1288            let info = SubtitleStreamInfo::builder()
1289                .codec(SubtitleCodec::Ass)
1290                .build();
1291            assert!(!info.is_forced());
1292        }
1293
1294        #[test]
1295        fn is_text_based_should_delegate_to_codec() {
1296            let text = SubtitleStreamInfo::builder()
1297                .codec(SubtitleCodec::Srt)
1298                .build();
1299            assert!(text.is_text_based());
1300
1301            let bitmap = SubtitleStreamInfo::builder()
1302                .codec(SubtitleCodec::Hdmv)
1303                .build();
1304            assert!(!bitmap.is_text_based());
1305        }
1306
1307        #[test]
1308        fn optional_fields_should_default_to_none() {
1309            let info = SubtitleStreamInfo::builder().build();
1310            assert!(info.language().is_none());
1311            assert!(info.title().is_none());
1312            assert!(info.duration().is_none());
1313        }
1314    }
1315}