Skip to main content

oximedia_packager/
variant_stream.rs

1#![allow(dead_code)]
2//! Variant stream management for multi-bitrate adaptive packaging.
3//!
4//! This module provides structures for tracking variant streams (renditions)
5//! in an adaptive bitrate ladder, including video, audio, and subtitle
6//! alternate renditions used in HLS and DASH packaging.
7
8use crate::config::SegmentFormat;
9use crate::error::{PackagerError, PackagerResult};
10
11/// Codec identifier for a variant stream.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum StreamCodec {
14    /// AV1 video codec.
15    Av1,
16    /// VP9 video codec.
17    Vp9,
18    /// VP8 video codec.
19    Vp8,
20    /// Opus audio codec.
21    Opus,
22    /// Vorbis audio codec.
23    Vorbis,
24    /// FLAC audio codec.
25    Flac,
26    /// WebVTT subtitle format.
27    WebVtt,
28}
29
30impl StreamCodec {
31    /// MIME codecs string for HLS/DASH manifests.
32    #[must_use]
33    pub const fn codecs_string(&self) -> &'static str {
34        match self {
35            Self::Av1 => "av01.0.08M.08",
36            Self::Vp9 => "vp09.00.31.08",
37            Self::Vp8 => "vp8",
38            Self::Opus => "opus",
39            Self::Vorbis => "vorbis",
40            Self::Flac => "flac",
41            Self::WebVtt => "wvtt",
42        }
43    }
44
45    /// Whether this codec represents video.
46    #[must_use]
47    pub const fn is_video(&self) -> bool {
48        matches!(self, Self::Av1 | Self::Vp9 | Self::Vp8)
49    }
50
51    /// Whether this codec represents audio.
52    #[must_use]
53    pub const fn is_audio(&self) -> bool {
54        matches!(self, Self::Opus | Self::Vorbis | Self::Flac)
55    }
56
57    /// Whether this codec represents subtitles.
58    #[must_use]
59    pub const fn is_subtitle(&self) -> bool {
60        matches!(self, Self::WebVtt)
61    }
62}
63
64/// A single variant stream in the adaptive ladder.
65#[derive(Debug, Clone)]
66pub struct VariantStream {
67    /// Unique identifier for this variant.
68    pub id: String,
69    /// Video codec (if video variant).
70    pub video_codec: Option<StreamCodec>,
71    /// Audio codec (if audio variant or muxed).
72    pub audio_codec: Option<StreamCodec>,
73    /// Width in pixels (video only).
74    pub width: Option<u32>,
75    /// Height in pixels (video only).
76    pub height: Option<u32>,
77    /// Frame rate (video only).
78    pub frame_rate: Option<f64>,
79    /// Peak video bitrate in bits/s.
80    pub video_bitrate: u64,
81    /// Audio bitrate in bits/s.
82    pub audio_bitrate: u64,
83    /// Segment format for this variant.
84    pub segment_format: SegmentFormat,
85    /// Language tag (BCP 47) for audio/subtitle variants.
86    pub language: Option<String>,
87    /// Whether this variant is the default selection.
88    pub is_default: bool,
89}
90
91impl VariantStream {
92    /// Create a new video variant.
93    #[must_use]
94    pub fn video(id: &str, codec: StreamCodec, width: u32, height: u32, bitrate: u64) -> Self {
95        Self {
96            id: id.to_string(),
97            video_codec: Some(codec),
98            audio_codec: None,
99            width: Some(width),
100            height: Some(height),
101            frame_rate: None,
102            video_bitrate: bitrate,
103            audio_bitrate: 0,
104            segment_format: SegmentFormat::Fmp4,
105            language: None,
106            is_default: false,
107        }
108    }
109
110    /// Create a new audio-only variant.
111    #[must_use]
112    pub fn audio(id: &str, codec: StreamCodec, bitrate: u64, language: &str) -> Self {
113        Self {
114            id: id.to_string(),
115            video_codec: None,
116            audio_codec: Some(codec),
117            width: None,
118            height: None,
119            frame_rate: None,
120            video_bitrate: 0,
121            audio_bitrate: bitrate,
122            segment_format: SegmentFormat::Fmp4,
123            language: Some(language.to_string()),
124            is_default: false,
125        }
126    }
127
128    /// Set this variant as the default.
129    #[must_use]
130    pub fn as_default(mut self) -> Self {
131        self.is_default = true;
132        self
133    }
134
135    /// Set the frame rate.
136    #[must_use]
137    pub fn with_frame_rate(mut self, fps: f64) -> Self {
138        self.frame_rate = Some(fps);
139        self
140    }
141
142    /// Total bandwidth for this variant.
143    #[must_use]
144    pub fn total_bandwidth(&self) -> u64 {
145        self.video_bitrate + self.audio_bitrate
146    }
147
148    /// Build the combined codecs string for manifests.
149    #[must_use]
150    pub fn combined_codecs(&self) -> String {
151        let mut parts = Vec::new();
152        if let Some(vc) = &self.video_codec {
153            parts.push(vc.codecs_string().to_string());
154        }
155        if let Some(ac) = &self.audio_codec {
156            parts.push(ac.codecs_string().to_string());
157        }
158        parts.join(",")
159    }
160
161    /// Resolution string (e.g. "1920x1080").
162    #[must_use]
163    pub fn resolution_string(&self) -> Option<String> {
164        match (self.width, self.height) {
165            (Some(w), Some(h)) => Some(format!("{w}x{h}")),
166            _ => None,
167        }
168    }
169
170    /// Validate the variant stream.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if required fields are missing.
175    pub fn validate(&self) -> PackagerResult<()> {
176        if self.id.is_empty() {
177            return Err(PackagerError::InvalidConfig(
178                "Variant stream ID must not be empty".into(),
179            ));
180        }
181        if self.video_codec.is_none() && self.audio_codec.is_none() {
182            return Err(PackagerError::InvalidConfig(
183                "Variant must have at least one codec".into(),
184            ));
185        }
186        if self.video_codec.is_some() && (self.width.is_none() || self.height.is_none()) {
187            return Err(PackagerError::InvalidConfig(
188                "Video variant must specify width and height".into(),
189            ));
190        }
191        Ok(())
192    }
193}
194
195/// A collection of variant streams forming an adaptive set.
196#[derive(Debug, Clone)]
197pub struct VariantSet {
198    /// The variant streams.
199    pub variants: Vec<VariantStream>,
200}
201
202impl VariantSet {
203    /// Create a new empty variant set.
204    #[must_use]
205    pub fn new() -> Self {
206        Self {
207            variants: Vec::new(),
208        }
209    }
210
211    /// Add a variant stream.
212    pub fn add(&mut self, variant: VariantStream) {
213        self.variants.push(variant);
214    }
215
216    /// Number of variants.
217    #[must_use]
218    pub fn len(&self) -> usize {
219        self.variants.len()
220    }
221
222    /// Whether the set is empty.
223    #[must_use]
224    pub fn is_empty(&self) -> bool {
225        self.variants.is_empty()
226    }
227
228    /// Get all video variants, sorted by bandwidth (ascending).
229    #[must_use]
230    pub fn video_variants(&self) -> Vec<&VariantStream> {
231        let mut vids: Vec<&VariantStream> = self
232            .variants
233            .iter()
234            .filter(|v| v.video_codec.is_some())
235            .collect();
236        vids.sort_by_key(|v| v.video_bitrate);
237        vids
238    }
239
240    /// Get all audio variants.
241    #[must_use]
242    pub fn audio_variants(&self) -> Vec<&VariantStream> {
243        self.variants
244            .iter()
245            .filter(|v| v.video_codec.is_none() && v.audio_codec.is_some())
246            .collect()
247    }
248
249    /// Validate all variants.
250    ///
251    /// # Errors
252    ///
253    /// Returns the first validation error encountered.
254    pub fn validate(&self) -> PackagerResult<()> {
255        if self.variants.is_empty() {
256            return Err(PackagerError::InvalidConfig(
257                "Variant set must have at least one variant".into(),
258            ));
259        }
260        for v in &self.variants {
261            v.validate()?;
262        }
263        Ok(())
264    }
265}
266
267impl Default for VariantSet {
268    fn default() -> Self {
269        Self::new()
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn test_stream_codec_properties() {
279        assert!(StreamCodec::Av1.is_video());
280        assert!(!StreamCodec::Av1.is_audio());
281        assert!(StreamCodec::Opus.is_audio());
282        assert!(StreamCodec::WebVtt.is_subtitle());
283    }
284
285    #[test]
286    fn test_codecs_string() {
287        assert_eq!(StreamCodec::Av1.codecs_string(), "av01.0.08M.08");
288        assert_eq!(StreamCodec::Opus.codecs_string(), "opus");
289    }
290
291    #[test]
292    fn test_video_variant_creation() {
293        let v = VariantStream::video("v1", StreamCodec::Av1, 1920, 1080, 5_000_000);
294        assert_eq!(v.width, Some(1920));
295        assert_eq!(v.height, Some(1080));
296        assert!(v.validate().is_ok());
297    }
298
299    #[test]
300    fn test_audio_variant_creation() {
301        let v = VariantStream::audio("a1", StreamCodec::Opus, 128_000, "en");
302        assert_eq!(v.language, Some("en".to_string()));
303        assert!(v.validate().is_ok());
304    }
305
306    #[test]
307    fn test_variant_total_bandwidth() {
308        let mut v = VariantStream::video("v1", StreamCodec::Vp9, 1280, 720, 3_000_000);
309        v.audio_bitrate = 128_000;
310        assert_eq!(v.total_bandwidth(), 3_128_000);
311    }
312
313    #[test]
314    fn test_combined_codecs() {
315        let mut v = VariantStream::video("v1", StreamCodec::Av1, 1920, 1080, 5_000_000);
316        v.audio_codec = Some(StreamCodec::Opus);
317        let codecs = v.combined_codecs();
318        assert!(codecs.contains("av01"));
319        assert!(codecs.contains("opus"));
320    }
321
322    #[test]
323    fn test_resolution_string() {
324        let v = VariantStream::video("v1", StreamCodec::Av1, 1920, 1080, 5_000_000);
325        assert_eq!(v.resolution_string(), Some("1920x1080".to_string()));
326    }
327
328    #[test]
329    fn test_audio_variant_no_resolution() {
330        let v = VariantStream::audio("a1", StreamCodec::Opus, 128_000, "en");
331        assert_eq!(v.resolution_string(), None);
332    }
333
334    #[test]
335    fn test_variant_validate_empty_id() {
336        let v = VariantStream::video("", StreamCodec::Av1, 1920, 1080, 5_000_000);
337        assert!(v.validate().is_err());
338    }
339
340    #[test]
341    fn test_variant_validate_no_codec() {
342        let v = VariantStream {
343            id: "x".to_string(),
344            video_codec: None,
345            audio_codec: None,
346            width: None,
347            height: None,
348            frame_rate: None,
349            video_bitrate: 0,
350            audio_bitrate: 0,
351            segment_format: SegmentFormat::Fmp4,
352            language: None,
353            is_default: false,
354        };
355        assert!(v.validate().is_err());
356    }
357
358    #[test]
359    fn test_variant_set() {
360        let mut set = VariantSet::new();
361        set.add(VariantStream::video(
362            "v1",
363            StreamCodec::Av1,
364            1920,
365            1080,
366            5_000_000,
367        ));
368        set.add(VariantStream::video(
369            "v2",
370            StreamCodec::Av1,
371            1280,
372            720,
373            3_000_000,
374        ));
375        assert_eq!(set.len(), 2);
376        assert!(set.validate().is_ok());
377    }
378
379    #[test]
380    fn test_variant_set_video_sorted() {
381        let mut set = VariantSet::new();
382        set.add(VariantStream::video(
383            "hi",
384            StreamCodec::Av1,
385            1920,
386            1080,
387            5_000_000,
388        ));
389        set.add(VariantStream::video(
390            "lo",
391            StreamCodec::Av1,
392            640,
393            360,
394            500_000,
395        ));
396        let vids = set.video_variants();
397        assert!(vids[0].video_bitrate < vids[1].video_bitrate);
398    }
399
400    #[test]
401    fn test_variant_set_empty_validation() {
402        let set = VariantSet::new();
403        assert!(set.validate().is_err());
404    }
405
406    #[test]
407    fn test_default_variant() {
408        let v = VariantStream::video("v1", StreamCodec::Av1, 1920, 1080, 5_000_000).as_default();
409        assert!(v.is_default);
410    }
411}