yt_live_rs/
quality.rs

1//! Video quality definitions and selection.
2//!
3//! This module provides types for specifying video quality preferences
4//! and selecting the best available stream format.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use yt_live_rs::{Quality, QualitySelector, CodecPreference};
10//!
11//! // Select 1080p60, falling back to 1080p, then 720p
12//! let selector = QualitySelector::new(Quality::P1080F60)
13//!     .fallback(Quality::P1080)
14//!     .fallback(Quality::P720)
15//!     .codec(CodecPreference::H264);
16//! ```
17
18use crate::types::{StreamFormat, StreamInfo};
19use std::fmt;
20use std::str::FromStr;
21
22/// Audio itag constant (AAC 128kbps).
23pub const AUDIO_ITAG: u32 = 140;
24
25/// Video itag mappings for different codecs at a given quality level.
26#[derive(Debug, Clone, Copy)]
27pub struct VideoItags {
28    /// H.264/AVC itag.
29    pub h264: u32,
30    /// VP9 itag.
31    pub vp9: u32,
32    /// AV1 itag.
33    pub av1: u32,
34}
35
36/// Video quality level.
37///
38/// Represents the resolution and frame rate of a video stream.
39/// Use with [`QualitySelector`] to specify your preferred quality.
40///
41/// # Example
42///
43/// ```
44/// use yt_live_rs::Quality;
45/// use std::str::FromStr;
46///
47/// let quality = Quality::from_str("1080p60").unwrap();
48/// assert_eq!(quality, Quality::P1080F60);
49/// assert_eq!(quality.to_string(), "1080p60");
50/// ```
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub enum Quality {
53    /// Audio only, no video.
54    AudioOnly,
55    /// 144p (256x144).
56    P144,
57    /// 240p (426x240).
58    P240,
59    /// 360p (640x360).
60    P360,
61    /// 480p (854x480).
62    P480,
63    /// 720p at 30fps (1280x720).
64    P720,
65    /// 720p at 60fps (1280x720).
66    P720F60,
67    /// 1080p at 30fps (1920x1080).
68    P1080,
69    /// 1080p at 60fps (1920x1080).
70    P1080F60,
71    /// 1440p at 30fps (2560x1440).
72    P1440,
73    /// 1440p at 60fps (2560x1440).
74    P1440F60,
75    /// 2160p/4K at 30fps (3840x2160).
76    P2160,
77    /// 2160p/4K at 60fps (3840x2160).
78    P2160F60,
79    /// Automatically select the best available quality.
80    Best,
81}
82
83impl Quality {
84    /// Get the quality label string (e.g., "1080p60").
85    #[must_use]
86    pub fn label(&self) -> &'static str {
87        match self {
88            Quality::AudioOnly => "audio_only",
89            Quality::P144 => "144p",
90            Quality::P240 => "240p",
91            Quality::P360 => "360p",
92            Quality::P480 => "480p",
93            Quality::P720 => "720p",
94            Quality::P720F60 => "720p60",
95            Quality::P1080 => "1080p",
96            Quality::P1080F60 => "1080p60",
97            Quality::P1440 => "1440p",
98            Quality::P1440F60 => "1440p60",
99            Quality::P2160 => "2160p",
100            Quality::P2160F60 => "2160p60",
101            Quality::Best => "best",
102        }
103    }
104
105    /// Get the itag mappings for this quality level.
106    ///
107    /// Returns `None` for `AudioOnly` and `Best`.
108    #[must_use]
109    pub fn itags(&self) -> Option<VideoItags> {
110        match self {
111            Quality::AudioOnly | Quality::Best => None,
112            Quality::P144 => Some(VideoItags {
113                h264: 160,
114                vp9: 278,
115                av1: 394,
116            }),
117            Quality::P240 => Some(VideoItags {
118                h264: 133,
119                vp9: 242,
120                av1: 395,
121            }),
122            Quality::P360 => Some(VideoItags {
123                h264: 134,
124                vp9: 243,
125                av1: 396,
126            }),
127            Quality::P480 => Some(VideoItags {
128                h264: 135,
129                vp9: 244,
130                av1: 397,
131            }),
132            Quality::P720 => Some(VideoItags {
133                h264: 136,
134                vp9: 247,
135                av1: 398,
136            }),
137            Quality::P720F60 => Some(VideoItags {
138                h264: 298,
139                vp9: 302,
140                av1: 398,
141            }),
142            Quality::P1080 => Some(VideoItags {
143                h264: 137,
144                vp9: 248,
145                av1: 399,
146            }),
147            Quality::P1080F60 => Some(VideoItags {
148                h264: 299,
149                vp9: 303,
150                av1: 399,
151            }),
152            Quality::P1440 => Some(VideoItags {
153                h264: 264,
154                vp9: 271,
155                av1: 400,
156            }),
157            Quality::P1440F60 => Some(VideoItags {
158                h264: 304,
159                vp9: 308,
160                av1: 400,
161            }),
162            Quality::P2160 => Some(VideoItags {
163                h264: 266,
164                vp9: 313,
165                av1: 401,
166            }),
167            Quality::P2160F60 => Some(VideoItags {
168                h264: 305,
169                vp9: 315,
170                av1: 401,
171            }),
172        }
173    }
174
175    /// Get all video quality levels in order (lowest to highest).
176    #[must_use]
177    pub fn all() -> &'static [Quality] {
178        &[
179            Quality::AudioOnly,
180            Quality::P144,
181            Quality::P240,
182            Quality::P360,
183            Quality::P480,
184            Quality::P720,
185            Quality::P720F60,
186            Quality::P1080,
187            Quality::P1080F60,
188            Quality::P1440,
189            Quality::P1440F60,
190            Quality::P2160,
191            Quality::P2160F60,
192        ]
193    }
194}
195
196impl FromStr for Quality {
197    type Err = String;
198
199    fn from_str(s: &str) -> Result<Self, Self::Err> {
200        match s.to_lowercase().as_str() {
201            "audio_only" | "audio" => Ok(Quality::AudioOnly),
202            "144p" | "144" => Ok(Quality::P144),
203            "240p" | "240" => Ok(Quality::P240),
204            "360p" | "360" => Ok(Quality::P360),
205            "480p" | "480" => Ok(Quality::P480),
206            "720p" | "720" => Ok(Quality::P720),
207            "720p60" => Ok(Quality::P720F60),
208            "1080p" | "1080" => Ok(Quality::P1080),
209            "1080p60" => Ok(Quality::P1080F60),
210            "1440p" | "1440" => Ok(Quality::P1440),
211            "1440p60" => Ok(Quality::P1440F60),
212            "2160p" | "2160" | "4k" => Ok(Quality::P2160),
213            "2160p60" | "4k60" => Ok(Quality::P2160F60),
214            "best" => Ok(Quality::Best),
215            _ => Err(format!("Unknown quality: {}", s)),
216        }
217    }
218}
219
220impl fmt::Display for Quality {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        write!(f, "{}", self.label())
223    }
224}
225
226/// Preferred video codec for downloading.
227///
228/// When multiple codecs are available for a quality level,
229/// this preference determines which one to select.
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
231pub enum CodecPreference {
232    /// Prefer H.264/AVC. Most compatible with players.
233    #[default]
234    H264,
235    /// Prefer VP9. Better compression than H.264.
236    VP9,
237    /// Prefer AV1. Best compression but higher CPU usage.
238    AV1,
239}
240
241impl FromStr for CodecPreference {
242    type Err = String;
243
244    fn from_str(s: &str) -> Result<Self, Self::Err> {
245        match s.to_lowercase().as_str() {
246            "h264" | "avc" => Ok(CodecPreference::H264),
247            "vp9" => Ok(CodecPreference::VP9),
248            "av1" => Ok(CodecPreference::AV1),
249            _ => Err(format!("Unknown codec preference: {}", s)),
250        }
251    }
252}
253
254impl fmt::Display for CodecPreference {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        match self {
257            CodecPreference::H264 => write!(f, "h264"),
258            CodecPreference::VP9 => write!(f, "vp9"),
259            CodecPreference::AV1 => write!(f, "av1"),
260        }
261    }
262}
263
264/// Builder for selecting video quality with fallbacks.
265///
266/// Use this to specify your preferred quality and codec, with automatic
267/// fallback to lower qualities if your preferred one isn't available.
268///
269/// # Example
270///
271/// ```no_run
272/// use yt_live_rs::{Quality, QualitySelector, CodecPreference, StreamInfo};
273///
274/// // Create a selector that prefers 1080p60, falls back to 1080p, then 720p
275/// let selector = QualitySelector::new(Quality::P1080F60)
276///     .fallback(Quality::P1080)
277///     .fallback(Quality::P720)
278///     .codec(CodecPreference::H264);
279///
280/// // Later, use it to select from available streams
281/// // let format = selector.select(&stream_info);
282/// ```
283#[derive(Debug, Clone)]
284pub struct QualitySelector {
285    qualities: Vec<Quality>,
286    codec: CodecPreference,
287}
288
289impl QualitySelector {
290    /// Create a new quality selector with a preferred quality.
291    ///
292    /// # Arguments
293    ///
294    /// * `quality` - The preferred quality level.
295    #[must_use]
296    pub fn new(quality: Quality) -> Self {
297        Self {
298            qualities: vec![quality],
299            codec: CodecPreference::default(),
300        }
301    }
302
303    /// Add a fallback quality level.
304    ///
305    /// Fallbacks are tried in order if higher preferences are unavailable.
306    #[must_use]
307    pub fn fallback(mut self, quality: Quality) -> Self {
308        self.qualities.push(quality);
309        self
310    }
311
312    /// Set the preferred codec.
313    ///
314    /// Defaults to H.264 for maximum compatibility.
315    #[must_use]
316    pub fn codec(mut self, codec: CodecPreference) -> Self {
317        self.codec = codec;
318        self
319    }
320
321    /// Select the best matching stream format from available streams.
322    ///
323    /// Returns `None` if no matching format is found.
324    #[must_use]
325    pub fn select(&self, stream_info: &StreamInfo) -> Option<StreamFormat> {
326        // Handle audio-only
327        if self.qualities.first() == Some(&Quality::AudioOnly) {
328            return stream_info.audio.clone();
329        }
330
331        // Expand "Best" to all qualities in reverse order
332        let qualities: Vec<Quality> = self
333            .qualities
334            .iter()
335            .flat_map(|q| {
336                if *q == Quality::Best {
337                    Quality::all().iter().rev().copied().collect::<Vec<_>>()
338                } else {
339                    vec![*q]
340                }
341            })
342            .collect();
343
344        for quality in qualities {
345            if quality == Quality::AudioOnly {
346                continue;
347            }
348
349            let label = quality.label();
350
351            // Try preferred codec first, then fall back to others
352            let codec_order = match self.codec {
353                CodecPreference::AV1 => ["AV1", "VP9", "h264"],
354                CodecPreference::VP9 => ["VP9", "h264", "AV1"],
355                CodecPreference::H264 => ["h264", "VP9", "AV1"],
356            };
357
358            for codec in codec_order {
359                let key = format!("{} ({})", label, codec);
360                if let Some(format) = stream_info.video.get(&key) {
361                    return Some(format.clone());
362                }
363            }
364        }
365
366        None
367    }
368
369    /// Get the codec preference.
370    #[must_use]
371    pub fn codec_preference(&self) -> CodecPreference {
372        self.codec
373    }
374}
375
376impl Default for QualitySelector {
377    fn default() -> Self {
378        Self::new(Quality::Best)
379    }
380}
381
382/// Parse a quality selection string with fallbacks.
383///
384/// Accepts formats like "1080p60/720p/best" (slash-separated fallbacks).
385///
386/// # Example
387///
388/// ```
389/// use yt_live_rs::quality::parse_quality_selection;
390///
391/// let qualities = parse_quality_selection("1080p60/720p/best");
392/// assert_eq!(qualities.len(), 3);
393/// ```
394#[must_use]
395pub fn parse_quality_selection(selection: &str) -> Vec<Quality> {
396    selection
397        .split('/')
398        .filter_map(|s| s.trim().parse().ok())
399        .collect()
400}
401
402/// Get the itag for a quality and codec preference from available URLs.
403///
404/// Returns the itag and URL if found.
405#[must_use]
406pub fn get_itag_for_quality(
407    quality: Quality,
408    codec_pref: CodecPreference,
409    available_itags: &std::collections::HashMap<u32, String>,
410) -> Option<(u32, String)> {
411    let itags = quality.itags()?;
412
413    let order = match codec_pref {
414        CodecPreference::AV1 => [itags.av1, itags.vp9, itags.h264],
415        CodecPreference::VP9 => [itags.vp9, itags.h264, itags.av1],
416        CodecPreference::H264 => [itags.h264, itags.vp9, itags.av1],
417    };
418
419    for itag in order {
420        if let Some(url) = available_itags.get(&itag) {
421            return Some((itag, url.clone()));
422        }
423    }
424
425    None
426}