feat: add auto-dubbed audio tracks, improved StreamFilter

This commit is contained in:
ThetaDev 2024-12-19 01:30:35 +01:00
parent 1b60c97a18
commit 1d1ae17ffc
No known key found for this signature in database
GPG key ID: E319D3C5148D65B6
2 changed files with 172 additions and 173 deletions

View file

@ -332,7 +332,10 @@ pub struct AudioTrack {
pub lang: Option<String>,
/// Language name (e.g. "English")
pub lang_name: String,
/// True if this is the default audio track
/// True if this is the default audio track chosen by YouTube
///
/// Note that YouTube's selection depends on the client type used to fetch the player.
/// Some players
pub is_default: bool,
/// Audio track type (e.g. *Original*, *Dubbed*)
pub track_type: Option<AudioTrackType>,
@ -351,13 +354,15 @@ pub enum AudioFormat {
/// Audio track type
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "snake_case")]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum AudioTrackType {
/// An original audio track of the video
Original,
/// An audio track with the original voices replaced, typically in a different language
Dubbed,
/// An audio track dubbed using YouTube's AI powered AutoDub feature
DubbedAuto,
/// A descriptive audio track
///
/// A descriptive audio track is an audio track in which descriptions of visual elements of

View file

@ -3,8 +3,8 @@
use std::cmp::Ordering;
use crate::model::{
traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, VideoCodec, VideoFormat, VideoPlayer,
VideoStream,
traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrackType, VideoCodec,
VideoFormat, VideoPlayer, VideoStream,
};
/// The StreamFilter is used for selecting audio/video streams from an extracted video
@ -13,7 +13,9 @@ pub struct StreamFilter {
audio_max_bitrate: Option<u32>,
audio_formats: Option<Vec<AudioFormat>>,
audio_codecs: Option<Vec<AudioCodec>>,
audio_language: Option<String>,
audio_languages: Vec<String>,
audio_autodub: bool,
audio_descriptive: bool,
video_max_res: Option<u32>,
video_max_fps: Option<u8>,
video_formats: Option<Vec<VideoFormat>>,
@ -22,47 +24,10 @@ pub struct StreamFilter {
video_none: bool,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
enum FilterResult {
Deny,
AllowLowest,
Allow,
Match,
}
impl FilterResult {
fn hard(val: bool) -> Self {
if val {
Self::Match
} else {
Self::Deny
}
}
fn soft(val: bool) -> Self {
if val {
Self::Match
} else {
Self::AllowLowest
}
}
fn allow(val: bool) -> Self {
if val {
Self::Allow
} else {
Self::Deny
}
}
fn join(self, other: Self) -> Self {
if self == Self::Deny {
Self::Deny
} else {
self.min(other)
}
}
}
const N_RES_AUDIO: usize = 4;
const N_RES_VIDEO: usize = 5;
type AudioRes = Option<[i64; N_RES_AUDIO]>;
type VideoRes = Option<[i64; N_RES_VIDEO]>;
impl StreamFilter {
/// Create a new [`StreamFilter`]
@ -81,13 +46,6 @@ impl StreamFilter {
self
}
fn apply_audio_max_bitrate(&self, stream: &AudioStream) -> FilterResult {
match self.audio_max_bitrate {
Some(max_bitrate) => FilterResult::soft(stream.average_bitrate <= max_bitrate),
None => FilterResult::Match,
}
}
/// Set the supported audio container formats
#[must_use]
pub fn audio_formats<F: Into<Vec<AudioFormat>>>(mut self, formats: F) -> Self {
@ -95,13 +53,6 @@ impl StreamFilter {
self
}
fn apply_audio_formats(&self, stream: &AudioStream) -> FilterResult {
match &self.audio_formats {
Some(formats) => FilterResult::hard(formats.contains(&stream.format)),
None => FilterResult::Match,
}
}
/// Set the supported audio codecs
#[must_use]
pub fn audio_codecs<C: Into<Vec<AudioCodec>>>(mut self, codecs: C) -> Self {
@ -109,14 +60,18 @@ impl StreamFilter {
self
}
fn apply_audio_codecs(&self, stream: &AudioStream) -> FilterResult {
match &self.audio_codecs {
Some(codecs) => FilterResult::hard(codecs.contains(&stream.codec)),
None => FilterResult::Match,
}
/// Set the preferred audio languages
/// Some YouTube videos feature multiple audio streams in
/// different languages (e.g. <https://www.youtube.com/watch?v=tVWWp1PqDus>).
///
/// If this filter is unset or no stream matches,
/// the filter returns the default audio stream.
#[must_use]
pub fn audio_languages<S: Into<Vec<String>>>(mut self, languages: S) -> Self {
self.audio_languages = languages.into();
self
}
/// Set the preferred audio language (2 letter ISO 639-1 code, e.g. `en`, `fr`).
/// Set the preferred audio language
/// Some YouTube videos feature multiple audio streams in
/// different languages (e.g. <https://www.youtube.com/watch?v=tVWWp1PqDus>).
///
@ -124,27 +79,24 @@ impl StreamFilter {
/// the filter returns the default audio stream.
#[must_use]
pub fn audio_language<S: Into<String>>(mut self, language: S) -> Self {
self.audio_language = Some(language.into());
self.audio_languages = vec![language.into()];
self
}
fn apply_audio_language(&self, stream: &AudioStream) -> FilterResult {
match &self.audio_language {
Some(language) => match &stream.track {
Some(track) => match &track.lang {
Some(track_lang) => {
if track_lang == language {
FilterResult::Match
} else {
FilterResult::allow(track.is_default)
}
}
None => FilterResult::allow(track.is_default),
},
None => FilterResult::Match,
},
None => FilterResult::hard(stream.track.as_ref().map_or(true, |t| t.is_default)),
}
/// Select the descriptive audio track for visually impaired people if available
#[must_use]
pub fn audio_descriptive(mut self) -> Self {
self.audio_descriptive = true;
self
}
/// Allow audio tracks that are AI-dubbed by YouTube
///
/// By default these are never selected.
#[must_use]
pub fn audio_autodub(mut self) -> Self {
self.audio_autodub = true;
self
}
/// Set the maximum video resolution. Resolution is determined by the
@ -158,13 +110,6 @@ impl StreamFilter {
self
}
fn apply_video_max_res(&self, stream: &VideoStream) -> FilterResult {
match self.video_max_res {
Some(max_res) => FilterResult::soft(stream.height.min(stream.width) <= max_res),
None => FilterResult::Match,
}
}
/// Set the maximum video framerate.
///
/// This is a soft filter, so if there is no stream with a framerate
@ -175,13 +120,6 @@ impl StreamFilter {
self
}
fn apply_video_max_fps(&self, stream: &VideoStream) -> FilterResult {
match self.video_max_fps {
Some(max_fps) => FilterResult::soft(stream.fps <= max_fps),
None => FilterResult::Match,
}
}
/// Set the supported video container formats
#[must_use]
pub fn video_formats<F: Into<Vec<VideoFormat>>>(mut self, formats: F) -> Self {
@ -189,13 +127,6 @@ impl StreamFilter {
self
}
fn apply_video_formats(&self, stream: &VideoStream) -> FilterResult {
match &self.video_formats {
Some(formats) => FilterResult::hard(formats.contains(&stream.format)),
None => FilterResult::Match,
}
}
/// Set the supported video codecs
#[must_use]
pub fn video_codecs<C: Into<Vec<VideoCodec>>>(mut self, codecs: C) -> Self {
@ -203,13 +134,6 @@ impl StreamFilter {
self
}
fn apply_video_codecs(&self, stream: &VideoStream) -> FilterResult {
match &self.video_codecs {
Some(codecs) => FilterResult::hard(codecs.contains(&stream.codec)),
None => FilterResult::Match,
}
}
/// Allow HDR videos
#[must_use]
pub fn video_hdr(mut self) -> Self {
@ -217,13 +141,6 @@ impl StreamFilter {
self
}
fn apply_video_hdr(&self, stream: &VideoStream) -> FilterResult {
match &self.video_hdr {
true => FilterResult::Match,
false => FilterResult::hard(!&stream.hdr),
}
}
/// Output no video stream (audio only)
#[must_use]
pub fn no_video(mut self) -> Self {
@ -231,24 +148,123 @@ impl StreamFilter {
self
}
fn apply_audio(&self, stream: &AudioStream) -> FilterResult {
self.apply_audio_max_bitrate(stream).join(
self.apply_audio_formats(stream).join(
self.apply_audio_codecs(stream)
.join(self.apply_audio_language(stream)),
),
)
fn apply_audio(&self, stream: &AudioStream) -> AudioRes {
let bitrate = match self.audio_max_bitrate {
Some(max) => filter_max(stream.average_bitrate, max),
None => stream.average_bitrate.into(),
};
if let Some(formats) = &self.audio_formats {
if !formats.contains(&stream.format) {
return None;
}
}
if let Some(codecs) = &self.audio_codecs {
if !codecs.contains(&stream.codec) {
return None;
}
}
let codecs = match stream.codec {
AudioCodec::Unknown => -10,
AudioCodec::Mp4a => 0,
AudioCodec::Opus => 10,
};
let language = if self.audio_languages.is_empty() {
0
} else {
match &stream.track {
Some(track) => match &track.lang {
Some(lang) => {
if self.audio_languages.contains(lang) {
10
} else if let Some((lang, _)) = lang.split_once('-') {
if self.audio_languages.contains(&lang.to_owned()) {
5
} else {
-1
}
} else {
-10
}
}
None => 0,
},
None => 0,
}
};
let track_type = match &stream.track {
Some(track) => match track.track_type {
Some(AudioTrackType::Original) => 5,
Some(AudioTrackType::Descriptive) => {
if self.audio_descriptive {
10
} else {
return None;
}
}
Some(AudioTrackType::Dubbed) => 0,
Some(AudioTrackType::DubbedAuto) => {
if self.audio_autodub {
-1
} else {
return None;
}
}
None => 0,
},
None => 0,
};
Some([language, track_type, codecs, bitrate])
}
fn apply_video(&self, stream: &VideoStream) -> FilterResult {
self.apply_video_max_res(stream).join(
self.apply_video_formats(stream).join(
self.apply_video_codecs(stream).join(
self.apply_video_hdr(stream)
.join(self.apply_video_max_fps(stream)),
),
),
)
fn apply_video(&self, stream: &VideoStream) -> VideoRes {
let vres = stream.height.min(stream.width);
let res = match self.video_max_res {
Some(max) => filter_max(vres, max),
None => vres.into(),
};
let fps = match self.video_max_fps {
Some(max) => filter_max(stream.fps.into(), max.into()),
None => i64::from(stream.fps),
};
if let Some(formats) = &self.video_formats {
if !formats.contains(&stream.format) {
return None;
}
}
if let Some(codecs) = &self.video_codecs {
if !codecs.contains(&stream.codec) {
return None;
}
}
let codecs = match stream.codec {
VideoCodec::Unknown => -1,
VideoCodec::Mp4v => 1,
VideoCodec::Avc1 => 2,
VideoCodec::Vp9 => 3,
VideoCodec::Av01 => 4,
};
let hdr = if self.video_hdr {
if stream.hdr {
10
} else {
0
}
} else if stream.hdr {
return None;
} else {
0
};
Some([hdr, res, fps, codecs, stream.average_bitrate.into()])
}
/// Return true if no video stream should be selected
@ -257,30 +273,23 @@ impl StreamFilter {
}
}
fn filter_max(val: u32, max: u32) -> i64 {
if val > max {
i64::from(max).wrapping_sub(val.into())
} else {
val.into()
}
}
impl VideoPlayer {
/// Select the audio stream which is the best match for the given [`StreamFilter`]
#[must_use]
pub fn select_audio_stream(&self, filter: &StreamFilter) -> Option<&AudioStream> {
let mut fallback: Option<&AudioStream> = None;
self.audio_streams
.iter()
.rev()
.find(|s| match filter.apply_audio(s) {
FilterResult::Deny => false,
FilterResult::AllowLowest => {
fallback = Some(s);
false
}
FilterResult::Allow => {
if fallback.is_none() {
fallback = Some(s);
}
false
}
FilterResult::Match => true,
})
.or(fallback)
.filter_map(|s| filter.apply_audio(s).map(|r| (s, r)))
.max_by_key(|(_, r)| *r)
.map(|(s, _)| s)
}
fn _select_video_stream<'a>(
@ -291,26 +300,11 @@ impl VideoPlayer {
return None;
}
let mut fallback: Option<&VideoStream> = None;
streams
.iter()
.rev()
.find(|s| match filter.apply_video(s) {
FilterResult::Deny => false,
FilterResult::AllowLowest => {
fallback = Some(s);
false
}
FilterResult::Allow => {
if fallback.is_none() {
fallback = Some(s);
}
false
}
FilterResult::Match => true,
})
.or(fallback)
.filter_map(|s| filter.apply_video(s).map(|r| (s, r)))
.max_by_key(|(_, r)| *r)
.map(|(s, _)| s)
}
/// Select the video stream which is the best match for the given [`StreamFilter`]
@ -399,7 +393,7 @@ mod tests {
#[case::hdr(StreamFilter::default().video_hdr().clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=976824147&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=701&keepalive=yes&lmt=1647469891607029&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAOax_lAWCW5ENOYxe3gZfBHgHA5oZJPyMlYQFy73t7-pAiEA46J7dsT-1pv9smuoP3Kx5T7c_IJ6cEZN4U9UkSNuT7o%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))]
#[case::resolution(StreamFilter::default().video_max_res(720).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=76313586&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=302&keepalive=yes&lmt=1647455155369524&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgW0H1434eh9Axw6zw95qezJB0D2aVd2bxEIs4T5bcfFACIDOjha9WLycp0L188FZyFGa1RBkLPoGrrJOppsaXqwDR&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))]
#[case::resolution_fps(StreamFilter::default().video_max_res(720).video_max_fps(30).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=47531179&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=247&keepalive=yes&lmt=1647458657499381&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAMUsmcl1zgbr3YQranPWNV1kcxT5IdEoLL7FTFEDdHHPAiEAhQnrfYMU0A9xZ69MfBujWA4pXtCOQCg2Jn6ve9J_vBQ%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))]
#[case::res_fallback(StreamFilter::default().video_max_res(100).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=2763284&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=160&keepalive=yes&lmt=1647456833049253&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgLPNxzLxppSSpnDEHxVblrQ38890NMbGnLXlmxljprfQCIQDn4Ir_sjYh7S3ms-Rynm-K0nJpHpQGYsz1nv4TiqeELQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))]
#[case::res_fallback(StreamFilter::default().video_max_res(100).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=3182932&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=278&keepalive=yes&lmt=1647458650479323&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAKcXzSIMQGA4R_rvoVg3ONpXOjpbaNZ5y9WJHLiQDTTVAiA6ePO9vuh5_zYE3Dw-QoRfqhT0CBDkg6w4dIo0MEfWnA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))]
#[case::webm_format(StreamFilter::default().video_formats([VideoFormat::Webm]).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=998696577&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=315&keepalive=yes&lmt=1647476955807851&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIfP4IVSo-00_kq_JIkuh032hcLoJzNEhYjvwgLiDpEzQIhALPVrvDBjRwiFddXiAyADmRtYygte4HvlJ3XOrkOf_TR&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))]
#[case::vp9_codec(StreamFilter::default().video_codecs([VideoCodec::Vp9]).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=998696577&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=315&keepalive=yes&lmt=1647476955807851&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIfP4IVSo-00_kq_JIkuh032hcLoJzNEhYjvwgLiDpEzQIhALPVrvDBjRwiFddXiAyADmRtYygte4HvlJ3XOrkOf_TR&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))]
#[case::noformat(StreamFilter::default().video_formats([]).clone(), None)]