From 2ecb4b6d3fdb47affefc73205d7fa669ea227df4 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 17 Mar 2025 00:04:11 +0100 Subject: [PATCH 01/20] feat!: extract ABR streaming URL, make audio/video stream url optional --- downloader/src/lib.rs | 4 +- src/client/player.rs | 48 +++++++++++++++---- src/client/response/player.rs | 17 +++++++ ...layer__tests__map_player_data_android.snap | 34 ++++++------- ...layer__tests__map_player_data_desktop.snap | 42 ++++++++-------- ...__tests__map_player_data_desktopmusic.snap | 30 ++++++------ ...t__player__tests__map_player_data_ios.snap | 10 ++-- ...nt__player__tests__map_player_data_tv.snap | 40 ++++++++-------- src/model/mod.rs | 8 +++- src/model/traits.rs | 10 ++-- src/param/stream_filter.rs | 24 ++++++++-- tests/youtube.rs | 2 +- 12 files changed, 172 insertions(+), 97 deletions(-) diff --git a/downloader/src/lib.rs b/downloader/src/lib.rs index c63d5c5..2ba3590 100644 --- a/downloader/src/lib.rs +++ b/downloader/src/lib.rs @@ -815,7 +815,7 @@ impl DownloadQuery { if let Some(v) = video { downloads.push(StreamDownload { file: output_path.with_extension(format!("video{}", v.format.extension())), - url: v.url.clone(), + url: v.url.clone().unwrap(), video_codec: Some(v.codec), audio_codec: None, }); @@ -823,7 +823,7 @@ impl DownloadQuery { if let Some(a) = audio { downloads.push(StreamDownload { file: output_path.with_extension(format!("audio{}", a.format.extension())), - url: a.url.clone(), + url: a.url.clone().unwrap(), video_codec: None, audio_codec: Some(a.codec), }); diff --git a/src/client/player.rs b/src/client/player.rs index 9bae601..591d773 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -417,11 +417,27 @@ impl MapResponse for response::Player { is_live_content: video_details.is_live_content, }; + let mut mapper = StreamsMapper::new( + ctx.deobf, + ctx.session_po_token.as_ref().map(|t| t.po_token.as_str()), + )?; + + let abr_streaming_url = match &streaming_data.server_abr_streaming_url { + Some(url) => match mapper.map_url_abr(url) { + Ok(res) => Some(res), + Err(e) => { + warnings.push(format!("could not map abr_streaming_url: {e}")); + None + } + }, + None => None, + }; + let abr_ustreamer_config = self.player_config.media_common_config.map(|cfg| { + cfg.media_ustreamer_request_config + .video_playback_ustreamer_config + }); + let streams = if !is_live { - let mut mapper = StreamsMapper::new( - ctx.deobf, - ctx.session_po_token.as_ref().map(|t| t.po_token.as_str()), - )?; mapper.map_streams(streaming_data.formats); mapper.map_streams(streaming_data.adaptive_formats); let mut res = mapper.output()?; @@ -530,6 +546,8 @@ impl MapResponse for response::Player { valid_until, hls_manifest_url: streaming_data.hls_manifest_url, dash_manifest_url: streaming_data.dash_manifest_url, + abr_streaming_url, + abr_ustreamer_config, preview_frames, drm, client_type: ctx.client_type, @@ -708,9 +726,7 @@ impl<'a> StreamsMapper<'a> { ) }) } - None => Err(ExtractionError::InvalidData( - "stream contained neither url or cipher".into(), - )), + None => return Ok(UrlMapRes::default()), }, }?; @@ -723,11 +739,21 @@ impl<'a> StreamsMapper<'a> { .map_err(|_| ExtractionError::InvalidData("could not combine URL".into()))?; Ok(UrlMapRes { - url: url.to_string(), + url: Some(url.to_string()), xtags: url_params.get("xtags").cloned(), }) } + fn map_url_abr(&mut self, url: &str) -> Result { + let (url_base, mut url_params) = util::url_to_params(url).map_err(|_| { + ExtractionError::InvalidData(format!("Could not parse url `{url}`").into()) + })?; + self.deobf_nsig(&mut url_params)?; + let url = Url::parse_with_params(url_base.as_str(), url_params.iter()) + .map_err(|_| ExtractionError::InvalidData("could not combine URL".into()))?; + Ok(url.to_string()) + } + fn map_video_stream(&mut self, f: player::Format) -> Result { let Some((mtype, codecs)) = parse_mime(&f.mime_type) else { return Err(ExtractionError::InvalidData( @@ -850,8 +876,9 @@ impl<'a> StreamsMapper<'a> { } } +#[derive(Default)] struct UrlMapRes { - url: String, + url: Option, xtags: Option, } @@ -1013,7 +1040,8 @@ mod tests { let url = mapper .map_url(&None, &Some(signature_cipher.to_owned())) .unwrap() - .url; + .url + .unwrap(); assert_eq!(url, "https://rr5---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=3781277&dur=229.301&ei=vb7nYvH5BMK8gAfBj7ToBQ&expire=1659376413&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AB_BABwrXZJN428ZwDxq5ScPn2AbcGODnRlTVhCQ3mj2&initcwndbps=1588750&ip=2003%3Ade%3Aaf06%3A6300%3Ac750%3A1b77%3Ac74a%3A80e3&itag=251&keepalive=yes&lmt=1655510291473933&lsig=AG3C_xAwRQIgCKCGJ1iu4wlaGXy3jcJyU3inh9dr1FIfqYOZEG_MdmACIQCbungkQYFk7EhD6K2YvLaHFMjKOFWjw001_tLb0lPDtg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=hH&mime=audio%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5ednsl&ms=au%2Conr&mt=1659354538&mv=m&mvi=5&n=XzXGSfGusw6OCQ&ns=b_Mq_qlTFcSGlG9RpwpM9xQH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAPIsKd7-xi4xVHEC9gb__dU4hzfzsHEj9ytd3nt0gEceAiACJWBcw-wFEq9qir35bwKHJZxtQ9mOL7SKiVkLQNDa6A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-Khi831z8dTejFIRCvCEwx_6romtM&txp=4532434&vprv=1"); } diff --git a/src/client/response/player.rs b/src/client/response/player.rs index a880dd5..9654b21 100644 --- a/src/client/response/player.rs +++ b/src/client/response/player.rs @@ -92,6 +92,7 @@ pub(crate) struct StreamingData { pub dash_manifest_url: Option, /// Only on livestreams pub hls_manifest_url: Option, + pub server_abr_streaming_url: Option, pub drm_params: Option, #[serde(default)] #[serde_as(deserialize_as = "VecSkipError<_>")] @@ -294,9 +295,13 @@ pub(crate) struct StoryboardRenderer { pub spec: String, } +#[serde_as] #[derive(Default, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct PlayerConfig { + #[serde(default)] + #[serde_as(deserialize_as = "DefaultOnError")] + pub media_common_config: Option, pub web_drm_config: Option, } @@ -306,6 +311,18 @@ pub(crate) struct WebDrmConfig { pub widevine_service_cert: Option, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MediaCommonConfig { + pub media_ustreamer_request_config: UstreamerRequestConfig, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UstreamerRequestConfig { + pub video_playback_ustreamer_config: String, +} + #[derive(Default, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct HeartbeatParams { diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap index 56cbc1c..7d3fbe6 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap @@ -61,7 +61,7 @@ VideoPlayer( ), video_streams: [ VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=1619781&dur=163.143&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=17&lmt=1580005480199246&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2F3gpp&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAJ2s7Pm4w42X3u3PWYViDeqIaE2tE9J6oIGpd0KB9gFsAiAH84QaJ4oUNivdRDUBi1ZYI7JSxESsPQ53mLInajKzcQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&txp=2211222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=1619781&dur=163.143&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=17&lmt=1580005480199246&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2F3gpp&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAJ2s7Pm4w42X3u3PWYViDeqIaE2tE9J6oIGpd0KB9gFsAiAH84QaJ4oUNivdRDUBi1ZYI7JSxESsPQ53mLInajKzcQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&txp=2211222&vprv=1"), itag: 17, bitrate: 79452, average_bitrate: 79428, @@ -81,7 +81,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=11439331&dur=163.096&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=18&lmt=1580005476071743&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAJAH-tWof01vrs8phEoz51XkWwdMzQ77k1UTrdY5XiuTAiA38z-qANX0jtfCiAl4EVMZaKo1ncrzJFRrCffZ6LagrA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&txp=2211222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=11439331&dur=163.096&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=18&lmt=1580005476071743&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAJAH-tWof01vrs8phEoz51XkWwdMzQ77k1UTrdY5XiuTAiA38z-qANX0jtfCiAl4EVMZaKo1ncrzJFRrCffZ6LagrA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&txp=2211222&vprv=1"), itag: 18, bitrate: 561339, average_bitrate: 561109, @@ -101,7 +101,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&dur=163.096&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=22&lmt=1580005750956837&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgFlQZgR63Yz9UgY9gVqiyGDVkZmSmACRP3-MmKN7CRzQCIAMHAwZbHmWL1qNH4Nu3A0pXZwErXMVPzMIt-PyxeZqa&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cratebypass%2Cdur%2Clmt&txp=2211222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&dur=163.096&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=22&lmt=1580005750956837&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgFlQZgR63Yz9UgY9gVqiyGDVkZmSmACRP3-MmKN7CRzQCIAMHAwZbHmWL1qNH4Nu3A0pXZwErXMVPzMIt-PyxeZqa&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cratebypass%2Cdur%2Clmt&txp=2211222&vprv=1"), itag: 22, bitrate: 1574434, average_bitrate: 1574434, @@ -123,7 +123,7 @@ VideoPlayer( ], video_only_streams: [ VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=1224002&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=394&keepalive=yes&lmt=1608045375671513&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgYCPleG9F86UoDRvzFL2xSUUI-HLZGw_P7qBOixlcmKsCIQChdmrJ1NvKo5Ra4QJ9ivR5V8fEcQs0f_3aUiqMhGB5DQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=1224002&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=394&keepalive=yes&lmt=1608045375671513&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgYCPleG9F86UoDRvzFL2xSUUI-HLZGw_P7qBOixlcmKsCIQChdmrJ1NvKo5Ra4QJ9ivR5V8fEcQs0f_3aUiqMhGB5DQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"), itag: 394, bitrate: 67637, average_bitrate: 60063, @@ -149,7 +149,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=2238952&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=395&keepalive=yes&lmt=1608045728968690&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAKCXHOCh_P3VlNWebTeWw0WdSln-zYe3BjZeEm2QiltCAiAQNcJBI4G-8dK5z1IUoqBZctk6ddjkl_QYKRFAKXyOcw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=2238952&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=395&keepalive=yes&lmt=1608045728968690&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAKCXHOCh_P3VlNWebTeWw0WdSln-zYe3BjZeEm2QiltCAiAQNcJBI4G-8dK5z1IUoqBZctk6ddjkl_QYKRFAKXyOcw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"), itag: 395, bitrate: 135747, average_bitrate: 109867, @@ -175,7 +175,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=7808990&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAIjjrMvCEzSLlbvbrjItT4V9JdpggnO5IHye9i4PxTyzAiAmbaFCB2hH7evf9JX3JUx-tU9S6zv2IzSKz8ObGSVRjw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=7808990&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAIjjrMvCEzSLlbvbrjItT4V9JdpggnO5IHye9i4PxTyzAiAmbaFCB2hH7evf9JX3JUx-tU9S6zv2IzSKz8ObGSVRjw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1"), itag: 134, bitrate: 538143, average_bitrate: 383195, @@ -201,7 +201,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=4130385&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=396&keepalive=yes&lmt=1608045761576250&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgBrQhbygTP6RGjUk0lGbxBI5e3NdeR6C_SW8R_ckZ2PkCIQDaBg5cJxYVWfwRrrELQFgRMOJ4xS3oOOROayoQMjxaCA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=4130385&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=396&keepalive=yes&lmt=1608045761576250&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgBrQhbygTP6RGjUk0lGbxBI5e3NdeR6C_SW8R_ckZ2PkCIQDaBg5cJxYVWfwRrrELQFgRMOJ4xS3oOOROayoQMjxaCA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"), itag: 396, bitrate: 258097, average_bitrate: 202682, @@ -227,7 +227,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=6873325&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=397&keepalive=yes&lmt=1608045990917419&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAMqBb1hKVVzWl3Awrh1T8GQG9IrSWF84zW_ZfjgbAN5QAiAaP3jYyI4ox2aclcOCzYFzqWgByWCxj_FgTN-SfsARXw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=6873325&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=397&keepalive=yes&lmt=1608045990917419&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAMqBb1hKVVzWl3Awrh1T8GQG9IrSWF84zW_ZfjgbAN5QAiAaP3jYyI4ox2aclcOCzYFzqWgByWCxj_FgTN-SfsARXw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"), itag: 397, bitrate: 436843, average_bitrate: 337281, @@ -253,7 +253,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=22365208&dur=163.046&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=398&keepalive=yes&lmt=1608048380553749&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgR6KqCOoig_FMl2tWKa7qHSmCjIZa9S7ABzEI16qdO2sCIFXccwql4bqV9CHlqXY4tgxyMFUsp7vW4XUjxs3AyG6H&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=22365208&dur=163.046&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=398&keepalive=yes&lmt=1608048380553749&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgR6KqCOoig_FMl2tWKa7qHSmCjIZa9S7ABzEI16qdO2sCIFXccwql4bqV9CHlqXY4tgxyMFUsp7vW4XUjxs3AyG6H&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"), itag: 398, bitrate: 1348419, average_bitrate: 1097369, @@ -279,7 +279,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=65400181&dur=163.046&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAPjxbuzkozPDc1Nd_0q5X8x8H2SiDvAUFuqqMadtz3SNAiEA_3kXCeePb2kci-WB2779tzI56E6E0iKwoHnUSkKCzwU%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=65400181&dur=163.046&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAPjxbuzkozPDc1Nd_0q5X8x8H2SiDvAUFuqqMadtz3SNAiEA_3kXCeePb2kci-WB2779tzI56E6E0iKwoHnUSkKCzwU%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1"), itag: 299, bitrate: 4190323, average_bitrate: 3208919, @@ -305,7 +305,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=42567727&dur=163.046&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=399&keepalive=yes&lmt=1608052932785283&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgFguw-cmBNOQegpyRRzcCScp2WaSnq_o7FB1-AiBgFpICIAGlMj9-kzNCWb3nhpg98Mc239ls6YYyoL8z1QpM8VmL&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=42567727&dur=163.046&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=399&keepalive=yes&lmt=1608052932785283&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgFguw-cmBNOQegpyRRzcCScp2WaSnq_o7FB1-AiBgFpICIAGlMj9-kzNCWb3nhpg98Mc239ls6YYyoL8z1QpM8VmL&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"), itag: 399, bitrate: 2572342, average_bitrate: 2088624, @@ -333,7 +333,7 @@ VideoPlayer( ], audio_streams: [ AudioStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=995840&dur=163.189&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=139&keepalive=yes&lmt=1580005582214385&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhALhtrbIL9_CQBeXsEwxFqyPY1XqBCOceQc7y00h7XBS9AiAH9VkMnkuFCU1ACkYU__uApTwcYeDoYNU74VYmKED3Gw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=995840&dur=163.189&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=139&keepalive=yes&lmt=1580005582214385&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhALhtrbIL9_CQBeXsEwxFqyPY1XqBCOceQc7y00h7XBS9AiAH9VkMnkuFCU1ACkYU__uApTwcYeDoYNU74VYmKED3Gw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1"), itag: 139, bitrate: 49724, average_bitrate: 48818, @@ -357,7 +357,7 @@ VideoPlayer( drm_systems: [], ), AudioStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=934449&dur=163.061&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=249&keepalive=yes&lmt=1608509101590706&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgPmadKd9As393GMRmu1Ow4RfQkDQhY6SbPRnkLMYyZOoCIE9AIgMMJ7n5HD2gKv3c8-HrnkMeakq_uWUOivnWquJX&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=934449&dur=163.061&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=249&keepalive=yes&lmt=1608509101590706&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgPmadKd9As393GMRmu1Ow4RfQkDQhY6SbPRnkLMYyZOoCIE9AIgMMJ7n5HD2gKv3c8-HrnkMeakq_uWUOivnWquJX&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"), itag: 249, bitrate: 53039, average_bitrate: 45845, @@ -381,7 +381,7 @@ VideoPlayer( drm_systems: [], ), AudioStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=1245866&dur=163.061&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=250&keepalive=yes&lmt=1608509101111096&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIge8uetzhejNg3DegY9EQkpvVam1gp8Jm-q6oqtb6Rn9wCIQD_VeQle7Z2H1uXB6qsYMGDU4OWA4h6YTTwMDmw5DDvuA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=1245866&dur=163.061&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=250&keepalive=yes&lmt=1608509101111096&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIge8uetzhejNg3DegY9EQkpvVam1gp8Jm-q6oqtb6Rn9wCIQD_VeQle7Z2H1uXB6qsYMGDU4OWA4h6YTTwMDmw5DDvuA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"), itag: 250, bitrate: 71268, average_bitrate: 61123, @@ -405,7 +405,7 @@ VideoPlayer( drm_systems: [], ), AudioStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=2640283&dur=163.096&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=140&keepalive=yes&lmt=1580005579712232&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAI8YylDImOPxxRo251u_RX6ir_j0p-gP_yQPcI6SxareAiArCxOcgrF9pxYS5bNYEnLGEQxRiEFJ0sT2Ydpa1G7x5A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=2640283&dur=163.096&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=140&keepalive=yes&lmt=1580005579712232&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAI8YylDImOPxxRo251u_RX6ir_j0p-gP_yQPcI6SxareAiArCxOcgrF9pxYS5bNYEnLGEQxRiEFJ0sT2Ydpa1G7x5A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1"), itag: 140, bitrate: 130268, average_bitrate: 129508, @@ -429,7 +429,7 @@ VideoPlayer( drm_systems: [], ), AudioStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=2476314&dur=163.061&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=251&keepalive=yes&lmt=1608509101894140&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgErpt4HOgIybMGrMD2qg9JB4O53n0jsCxkiI9JBxbz8ECIQDixyFJ54m4NsxhyFtIYPscMVp_G6RyvwrfKzdoya-62Q%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=2476314&dur=163.061&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=251&keepalive=yes&lmt=1608509101894140&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgErpt4HOgIybMGrMD2qg9JB4O53n0jsCxkiI9JBxbz8ECIQDixyFJ54m4NsxhyFtIYPscMVp_G6RyvwrfKzdoya-62Q%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"), itag: 251, bitrate: 140633, average_bitrate: 121491, @@ -465,6 +465,8 @@ VideoPlayer( valid_until: "[date]", hls_manifest_url: None, dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659481355/ei/q1jpYtOPEYSBgQeHmqbwAQ/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr5---sn-h0jeenek.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jeenek%2Csn-h0jelnez/ms/au%2Crdu/mv/m/mvi/5/pl/37/hfr/1/as/fmp4_audio_clear%2Cfmp4_sd_hd_clear/initcwndbps/1527500/vprv/1/mt/1659459429/fvip/4/itag_bl/376%2C377%2C384%2C385%2C612%2C613%2C617%2C619%2C623%2C628%2C655%2C656%2C660%2C662%2C666%2C671/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cvprv%2Citag/sig/AOq0QJ8wRAIgMm4a_MIHA3YUszKeruSy3exs5JwNjJAyLAwxL0yPdNMCIANb9GDMSTp_NT-PPhbvYMwRULJ5a9BO6MYD9FuWprC1/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgETSOwhwWVMy7gmrFXZlJu655ToLzSwOEsT16oRyrWhACIQDkvOEw1fImz5omu4iVIRNFe-z-JC9v8WUyx281dW2NOw%3D%3D"), + abr_streaming_url: None, + abr_ustreamer_config: Some("Cs0CCAAlMZkqPi0AAIA_NT0Klz9YAXjoAoABAaABAbUB9ijcP-ABAegBA_ABAfkBAAAAAAAA0D-BAgAAAAAAABhAmALwAaAC6AK4AgDaAlUQsOoBGKhGIKCcASjYNjCYdXCIJ4AB9AO4AQHgAQGYAgyoAgGwAgG4AgHAAgHIAgHQAgLgAgHoAgLwAgGAAwaIA4gnmAMBqAMIsAMBuAMBwAMB2AMB-gIrCAwQGBgyIDItAABwQjUAAIxCQAFIAWUAAIBAaMBwzQEAAIA_8AEB6ALgA4IDAJADAaADAbADA-ADkE6wBAG4BAHKBFgKFQiA4gkQmHUYrAIlAAAAACgAMABAARDg1AMY0A8qNgoKdGJfY29zdF81MCAIKQAAAAAAAAAASAFQAV2amZk-ZQAAAD9tAAAAP3UAAAA_eMCpB5IBADAB6AQB8AQB-AQBkAUBGAEgATIMCKsCEI662NuboOcCMgwIjwMQg-na_r_Q7QIyDAiqAhCOutjbm6DnAjIMCI4DEJXUhISv0O0CMgwIhwEQjrrY25ug5wIyDAiNAxCr6siQptDtAjIMCIYBEO_L2NuboOcCMgwIjAMQuvqao6XQ7QIyDAiFARCOutjbm6DnAjIMCIsDEPLf1JOl0O0CMgwIoAEQjrrY25ug5wIyDAiKAxDZmZnro9DtAjIMCIsBEPGp4ruboOcCMgwIjAEQ6M3Jupug5wIyDAj5ARCykfms493tAjIMCPoBELju26zj3e0CMgwI-wEQ_NOLrePd7QI6AA=="), preview_frames: [ Frameset( url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwENSDfyq4qpAwVwAcABBqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLAXobPyrylgm8IEvjlZzqYTiPe1Ow", diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap index d140467..3ceaadf 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap @@ -66,7 +66,7 @@ VideoPlayer( ), video_streams: [ VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=11439331&dur=163.096&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=18&lmt=1580005476071743&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=ig_QojS86GYHYg&ns=cOm8mnsR9HFwfq55dDyGyqYH&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgNqstD2C4HG7Vn5En5Z4aUyH2mk7gAB9cyfOAWGCaWeoCIQDbxxJZuOnz_3RJNviFYADvgTO7u8YBYKtpFtp9Ujmk2w%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=2211222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=11439331&dur=163.096&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=18&lmt=1580005476071743&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=ig_QojS86GYHYg&ns=cOm8mnsR9HFwfq55dDyGyqYH&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgNqstD2C4HG7Vn5En5Z4aUyH2mk7gAB9cyfOAWGCaWeoCIQDbxxJZuOnz_3RJNviFYADvgTO7u8YBYKtpFtp9Ujmk2w%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=2211222&vprv=1"), itag: 18, bitrate: 561339, average_bitrate: 561109, @@ -88,7 +88,7 @@ VideoPlayer( ], video_only_streams: [ VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=1484736&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=278&keepalive=yes&lmt=1608509388295661&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgDYs7xrSqi-Co90zypk9zutEJ-aaEpNAHWnE57zVIfxgCIQDE0exs9SN8JH5OEJ8728Ke6bfa0CsUucFETHLk3IFF7w%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=1484736&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=278&keepalive=yes&lmt=1608509388295661&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgDYs7xrSqi-Co90zypk9zutEJ-aaEpNAHWnE57zVIfxgCIQDE0exs9SN8JH5OEJ8728Ke6bfa0CsUucFETHLk3IFF7w%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 278, bitrate: 87458, average_bitrate: 72857, @@ -114,7 +114,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=1224002&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=394&keepalive=yes&lmt=1608045375671513&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAI-uoNLUkMHpH35niVh1tBvwwFLtmSbeHyknmyCvccFVAiB2XriyJd0u2q-tGIRTx5qtKt6bJCs5ndXtMsdSxOheuA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=1224002&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=394&keepalive=yes&lmt=1608045375671513&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAI-uoNLUkMHpH35niVh1tBvwwFLtmSbeHyknmyCvccFVAiB2XriyJd0u2q-tGIRTx5qtKt6bJCs5ndXtMsdSxOheuA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 394, bitrate: 67637, average_bitrate: 60063, @@ -140,7 +140,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=2973283&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=242&keepalive=yes&lmt=1608509388282028&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgEleuqkeo7x7BsHur5aGPfHaT6KjKEG4c1d_xXwqlrsYCIQD85X_m050XwWyYlfLiWtZz-TX--H8H0UvfZCWKpY7m4Q%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=2973283&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=242&keepalive=yes&lmt=1608509388282028&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgEleuqkeo7x7BsHur5aGPfHaT6KjKEG4c1d_xXwqlrsYCIQD85X_m050XwWyYlfLiWtZz-TX--H8H0UvfZCWKpY7m4Q%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 242, bitrate: 184064, average_bitrate: 145902, @@ -166,7 +166,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=2238952&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=395&keepalive=yes&lmt=1608045728968690&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAIBttTR02kTdGb4vdxQ9Gro88JOAY7u5z69nJbdmVS1sAiBr61rqkUtra4PHLdnp2w-s8ZSaN_4qZ3OEeeuIr5C13w%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=2238952&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=395&keepalive=yes&lmt=1608045728968690&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAIBttTR02kTdGb4vdxQ9Gro88JOAY7u5z69nJbdmVS1sAiBr61rqkUtra4PHLdnp2w-s8ZSaN_4qZ3OEeeuIr5C13w%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 395, bitrate: 135747, average_bitrate: 109867, @@ -192,7 +192,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=7808990&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAMBRhMAZ5GXFSZHN6D-XhXRdG_EWSNwnN2eLPlwVNQ6PAiEA75eH0iJLgwRkujaABZnaJxG2ni-4irYHEGD42x6uaQg%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=2211222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=7808990&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAMBRhMAZ5GXFSZHN6D-XhXRdG_EWSNwnN2eLPlwVNQ6PAiEA75eH0iJLgwRkujaABZnaJxG2ni-4irYHEGD42x6uaQg%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=2211222&vprv=1"), itag: 134, bitrate: 538143, average_bitrate: 383195, @@ -218,7 +218,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=5169510&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=243&keepalive=yes&lmt=1608509388282405&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgNi0fwQbep6oKsEeEGfms2Ay4x2OL2G0hUX5GFhycgKkCIANiC-j-Gz3-noxsNeSKKPxy--T9mFBu_8V7Vi5-zDYS&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=5169510&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=243&keepalive=yes&lmt=1608509388282405&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgNi0fwQbep6oKsEeEGfms2Ay4x2OL2G0hUX5GFhycgKkCIANiC-j-Gz3-noxsNeSKKPxy--T9mFBu_8V7Vi5-zDYS&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 243, bitrate: 319085, average_bitrate: 253673, @@ -244,7 +244,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=4130385&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=396&keepalive=yes&lmt=1608045761576250&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgFuBoOIkqwq0D1_OmnNJx3C0jmhHUyskpzPrTMoaWRYECIFZ1Y4QbQ41GsWS8yRHox8l_nGVosfXhXfKu3v18AyeT&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=4130385&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=396&keepalive=yes&lmt=1608045761576250&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgFuBoOIkqwq0D1_OmnNJx3C0jmhHUyskpzPrTMoaWRYECIFZ1Y4QbQ41GsWS8yRHox8l_nGVosfXhXfKu3v18AyeT&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 396, bitrate: 258097, average_bitrate: 202682, @@ -270,7 +270,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=8890590&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=244&keepalive=yes&lmt=1608509388284632&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgMYF0KQQNgYI8oOhgdCwyRY6E_hvFnJiaAadyMf89MRoCIHnDnROTvUoy0iIBM3MzFAxJh_bLA-2vFl9KFDrHOf1B&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=8890590&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=244&keepalive=yes&lmt=1608509388284632&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgMYF0KQQNgYI8oOhgdCwyRY6E_hvFnJiaAadyMf89MRoCIHnDnROTvUoy0iIBM3MzFAxJh_bLA-2vFl9KFDrHOf1B&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 244, bitrate: 539056, average_bitrate: 436270, @@ -296,7 +296,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=6873325&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=397&keepalive=yes&lmt=1608045990917419&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAOtLGFoFtLHIXzNRoSrR7ULbIz91OYmaVQkcSatqNKAiAiEA23ZF7h2BZZCAGc0Zdd2p3PWRotmwLDyH6yYCuQpE8xw%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=6873325&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=397&keepalive=yes&lmt=1608045990917419&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAOtLGFoFtLHIXzNRoSrR7ULbIz91OYmaVQkcSatqNKAiAiEA23ZF7h2BZZCAGc0Zdd2p3PWRotmwLDyH6yYCuQpE8xw%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 397, bitrate: 436843, average_bitrate: 337281, @@ -322,7 +322,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=16547577&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=247&keepalive=yes&lmt=1608509388326822&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgfYKbT_196P-2EtjuqcTKdataiM480y65Ko0a73dv7WECIQC6nqWienQvu7swC1OW9HlwFWRH7VwTwj6H4yjY6FYvzg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=16547577&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=247&keepalive=yes&lmt=1608509388326822&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgfYKbT_196P-2EtjuqcTKdataiM480y65Ko0a73dv7WECIQC6nqWienQvu7swC1OW9HlwFWRH7VwTwj6H4yjY6FYvzg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 247, bitrate: 982813, average_bitrate: 812006, @@ -348,7 +348,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=35955780&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=302&keepalive=yes&lmt=1608509234088626&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgQG8GPj3w_5_Lr2apagmte66IFBY3bYcZ2KnhwnUpshYCIFgvHYIZsz8WdYGSk9adpfMNKX0pzSP_l8cW47Gq2RTi&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=35955780&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=302&keepalive=yes&lmt=1608509234088626&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgQG8GPj3w_5_Lr2apagmte66IFBY3bYcZ2KnhwnUpshYCIFgvHYIZsz8WdYGSk9adpfMNKX0pzSP_l8cW47Gq2RTi&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 302, bitrate: 2354009, average_bitrate: 1764202, @@ -374,7 +374,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=22365208&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=398&keepalive=yes&lmt=1608048380553749&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAI-VhcBU6o8LGmeuVYC2_zbxeGvC6XWf7yIOQ1RvjURhAiEA0YcZlVOI2ZUtKl-31__Hzax2SOUPeekCRjqjfw4m15s%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=22365208&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=398&keepalive=yes&lmt=1608048380553749&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAI-VhcBU6o8LGmeuVYC2_zbxeGvC6XWf7yIOQ1RvjURhAiEA0YcZlVOI2ZUtKl-31__Hzax2SOUPeekCRjqjfw4m15s%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 398, bitrate: 1348419, average_bitrate: 1097369, @@ -400,7 +400,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=65400181&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAIdbG-deTvLhp7mD2b-QZYQamPFv75l1bNBEEOMihrxPAiEA1NYvRlFphbRRvFIBCP-Ij9-5q8OTwUskgsL6LyIrD7c%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=2211222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=65400181&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAIdbG-deTvLhp7mD2b-QZYQamPFv75l1bNBEEOMihrxPAiEA1NYvRlFphbRRvFIBCP-Ij9-5q8OTwUskgsL6LyIrD7c%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=2211222&vprv=1"), itag: 299, bitrate: 4190323, average_bitrate: 3208919, @@ -426,7 +426,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=62993617&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=303&keepalive=yes&lmt=1608509371758331&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAJ8n34LQhg6iEg1Ux9rDkk48e8l3vBR4WwuHeIpKnorlAiBopK4z-nq-pJTPTmrdbbKPW1Lfufdz2f9sGUKY-dzk5A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=62993617&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=303&keepalive=yes&lmt=1608509371758331&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAJ8n34LQhg6iEg1Ux9rDkk48e8l3vBR4WwuHeIpKnorlAiBopK4z-nq-pJTPTmrdbbKPW1Lfufdz2f9sGUKY-dzk5A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 303, bitrate: 3832648, average_bitrate: 3090839, @@ -452,7 +452,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=42567727&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=399&keepalive=yes&lmt=1608052932785283&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAMewAT3SgJRGn7wqDaDzNWcsAfrjFRu6k0wm7O_5YJeQAiANVhGmILp_gmNXnmixDesxsZ44_72YBT2SqjLLSZV32w%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=42567727&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=399&keepalive=yes&lmt=1608052932785283&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAMewAT3SgJRGn7wqDaDzNWcsAfrjFRu6k0wm7O_5YJeQAiANVhGmILp_gmNXnmixDesxsZ44_72YBT2SqjLLSZV32w%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 399, bitrate: 2572342, average_bitrate: 2088624, @@ -480,7 +480,7 @@ VideoPlayer( ], audio_streams: [ AudioStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=934449&dur=163.061&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=249&keepalive=yes&lmt=1608509101590706&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAOdVu1woKNveQspV4WPm1PHrOBuzlrnPu2ZLvyiYZCSbAiAYODN_y5t1oU334SHUqqgyc4Wnt-9If-W98Fd966fy2A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=934449&dur=163.061&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=249&keepalive=yes&lmt=1608509101590706&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAOdVu1woKNveQspV4WPm1PHrOBuzlrnPu2ZLvyiYZCSbAiAYODN_y5t1oU334SHUqqgyc4Wnt-9If-W98Fd966fy2A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 249, bitrate: 53039, average_bitrate: 45845, @@ -504,7 +504,7 @@ VideoPlayer( drm_systems: [], ), AudioStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=1245866&dur=163.061&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=250&keepalive=yes&lmt=1608509101111096&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAMJJ-uGQureE70LIxHjHP9hFxqcWwsSlxXX6EjGKmFfEAiAvQ98YKkqUrweNnBZOI7pXJk1kuU_1hSsQ0KeNU4CbyQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=1245866&dur=163.061&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=250&keepalive=yes&lmt=1608509101111096&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAMJJ-uGQureE70LIxHjHP9hFxqcWwsSlxXX6EjGKmFfEAiAvQ98YKkqUrweNnBZOI7pXJk1kuU_1hSsQ0KeNU4CbyQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 250, bitrate: 71268, average_bitrate: 61123, @@ -528,7 +528,7 @@ VideoPlayer( drm_systems: [], ), AudioStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=2640283&dur=163.096&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=140&keepalive=yes&lmt=1580005579712232&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhANXIw4pIwIPvMGWnJSrA_bnmBX6KPBPqak18aPtKsI8jAiBvisRnEtFax7OTrwKbOiktCihoMraLkCi7Rnnu6JGmeQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=2211222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=2640283&dur=163.096&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=140&keepalive=yes&lmt=1580005579712232&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhANXIw4pIwIPvMGWnJSrA_bnmBX6KPBPqak18aPtKsI8jAiBvisRnEtFax7OTrwKbOiktCihoMraLkCi7Rnnu6JGmeQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=2211222&vprv=1"), itag: 140, bitrate: 130268, average_bitrate: 129508, @@ -552,7 +552,7 @@ VideoPlayer( drm_systems: [], ), AudioStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=2476314&dur=163.061&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=251&keepalive=yes&lmt=1608509101894140&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAKP_KjT_SSnz5WGXaveO56pJAEw166qT3cpBdAZI1zwCAiBWZKVQZxfOPWnqSp5FnRDyQBQ-6a2nZopyA1eHicgHtw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=2476314&dur=163.061&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=251&keepalive=yes&lmt=1608509101894140&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAKP_KjT_SSnz5WGXaveO56pJAEw166qT3cpBdAZI1zwCAiBWZKVQZxfOPWnqSp5FnRDyQBQ-6a2nZopyA1eHicgHtw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"), itag: 251, bitrate: 140633, average_bitrate: 121491, @@ -588,6 +588,8 @@ VideoPlayer( valid_until: "[date]", hls_manifest_url: None, dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659481355/ei/q1jpYtq3BJCX1gKVyJGQDg/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr4---sn-h0jelnez.googlevideo.com/mh/mQ/mm/31%2C26/mn/sn-h0jelnez%2Csn-4g5edn6k/ms/au%2Conr/mv/m/mvi/4/pl/37/hfr/all/as/fmp4_audio_clear%2Cwebm_audio_clear%2Cwebm2_audio_clear%2Cfmp4_sd_hd_clear%2Cwebm2_sd_hd_clear/initcwndbps/1513750/spc/lT-KhrZGE2opztWyVdAtyUNlb8dXPDs/vprv/1/mt/1659459429/fvip/4/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cspc%2Cvprv%2Citag/sig/AOq0QJ8wRgIhAPEjHK19PKVHqQeia6WF4qubuMYk74LGi8F8lk5ZMPkFAiEAsaB2pKQWBvuPnNUnbdQXHc-izgsHJUP793woC2xNJlg%3D/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgOY4xu4H9wqPVZ7vF2i0hFcOnqrur1XGoA43a7ZEuuSUCIQCyPxBKXUQrKFmknNEGpX5GSWySKgMw_xHBikWpKpKwvg%3D%3D"), + abr_streaming_url: None, + abr_ustreamer_config: None, preview_frames: [ Frameset( url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwENSDfyq4qpAwVwAcABBqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLAXobPyrylgm8IEvjlZzqYTiPe1Ow", diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap index 47fbb1e..a6c2df6 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap @@ -34,7 +34,7 @@ VideoPlayer( ), video_streams: [ VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=11439331&dur=163.096&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=18&lmt=1580005476071743&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=mAEwZepBJSQPkQ&ns=orl5qWACo00YlHHyQZ7a6awH&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhANfXubbDcXpc25m3F5xQ97ygJRjrTvm8ruVxgnxgFAUBAiAEnj_3KacDNTTLUk-6ZEbL-52zxmBLU1iuTEDx0NvJzA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=2211222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=11439331&dur=163.096&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=18&lmt=1580005476071743&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=mAEwZepBJSQPkQ&ns=orl5qWACo00YlHHyQZ7a6awH&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhANfXubbDcXpc25m3F5xQ97ygJRjrTvm8ruVxgnxgFAUBAiAEnj_3KacDNTTLUk-6ZEbL-52zxmBLU1iuTEDx0NvJzA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=2211222&vprv=1"), itag: 18, bitrate: 561339, average_bitrate: 561109, @@ -56,7 +56,7 @@ VideoPlayer( ], video_only_streams: [ VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=1484736&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=278&keepalive=yes&lmt=1608509388295661&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgEQ0-VVvo41T4l2X26p5zP8Wo8sXOPmBWvCf2OW33ilgCIH2bIFOYgpmsml7FvRQj_SoLzPh7yBvmLZ61Kgsj4FUe&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=1484736&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=278&keepalive=yes&lmt=1608509388295661&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgEQ0-VVvo41T4l2X26p5zP8Wo8sXOPmBWvCf2OW33ilgCIH2bIFOYgpmsml7FvRQj_SoLzPh7yBvmLZ61Kgsj4FUe&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1"), itag: 278, bitrate: 87458, average_bitrate: 72857, @@ -82,7 +82,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=2973283&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=242&keepalive=yes&lmt=1608509388282028&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAO7DI5E91yHpLhgiWg9C99NsMoJBVOWsNTNF3os9kREQAiAr2oC8vFtXIHwkJJt45q0sdmjiJdkTO2i8VAjUodk6Xw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=2973283&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=242&keepalive=yes&lmt=1608509388282028&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAO7DI5E91yHpLhgiWg9C99NsMoJBVOWsNTNF3os9kREQAiAr2oC8vFtXIHwkJJt45q0sdmjiJdkTO2i8VAjUodk6Xw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1"), itag: 242, bitrate: 184064, average_bitrate: 145902, @@ -108,7 +108,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=7808990&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgTkOjFd0nExEtpr8sBIaNu9HhkxWNdjhSKufHMhLR8-8CIHJAmOuCD7VBv_krH6rn5zqXFqAfsq9rQPXlC3CcQrjM&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=2211222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=7808990&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgTkOjFd0nExEtpr8sBIaNu9HhkxWNdjhSKufHMhLR8-8CIHJAmOuCD7VBv_krH6rn5zqXFqAfsq9rQPXlC3CcQrjM&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=2211222&vprv=1"), itag: 134, bitrate: 538143, average_bitrate: 383195, @@ -134,7 +134,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=5169510&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=243&keepalive=yes&lmt=1608509388282405&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAPqQfxwIANgIC3DrQ6avaWOhCvIMLdzMPQtFOx2gwEXNAiAwJp2mgN9-zl4vPOB2uoQXOfmGsYDB470q1zg7wRW4Sw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=5169510&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=243&keepalive=yes&lmt=1608509388282405&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAPqQfxwIANgIC3DrQ6avaWOhCvIMLdzMPQtFOx2gwEXNAiAwJp2mgN9-zl4vPOB2uoQXOfmGsYDB470q1zg7wRW4Sw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1"), itag: 243, bitrate: 319085, average_bitrate: 253673, @@ -160,7 +160,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=8890590&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=244&keepalive=yes&lmt=1608509388284632&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAIjdvhcThMxoo_v2bzEjaR_w0ryWFQDs0f0INaI5WPcVAiApQZUYTqcQJdfxZlNSsp7cl3FK8XPfDZ-qbVvj9GuauQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=8890590&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=244&keepalive=yes&lmt=1608509388284632&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAIjdvhcThMxoo_v2bzEjaR_w0ryWFQDs0f0INaI5WPcVAiApQZUYTqcQJdfxZlNSsp7cl3FK8XPfDZ-qbVvj9GuauQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1"), itag: 244, bitrate: 539056, average_bitrate: 436270, @@ -186,7 +186,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=16547577&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=247&keepalive=yes&lmt=1608509388326822&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgBV4Oa1IQ0YNDvRrKO5ec3Pfbg65MxzmIxCcm0gOuwT0CIFysQdow6DQXzz1W9KZVuqACTdjXQ3-yiBj9GcmNw3HE&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=16547577&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=247&keepalive=yes&lmt=1608509388326822&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgBV4Oa1IQ0YNDvRrKO5ec3Pfbg65MxzmIxCcm0gOuwT0CIFysQdow6DQXzz1W9KZVuqACTdjXQ3-yiBj9GcmNw3HE&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1"), itag: 247, bitrate: 982813, average_bitrate: 812006, @@ -212,7 +212,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=35955780&dur=163.046&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=302&keepalive=yes&lmt=1608509234088626&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAOiqSNfGfOprZ9InWVMc7gY0KrTf8weLibcpK0W2Hfa6AiAFHW213qsByzlar5ivCAYttjo1rPciQnLEnh-izJ3ZhA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=35955780&dur=163.046&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=302&keepalive=yes&lmt=1608509234088626&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAOiqSNfGfOprZ9InWVMc7gY0KrTf8weLibcpK0W2Hfa6AiAFHW213qsByzlar5ivCAYttjo1rPciQnLEnh-izJ3ZhA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1"), itag: 302, bitrate: 2354009, average_bitrate: 1764202, @@ -238,7 +238,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=65400181&dur=163.046&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgdkJv6w9_Azf0m6poA-ULyX0eH_GKBtSJRwUY1lNBAZgCIDCrC0lnu__ycTaIhg0pUcsRUqay60S3QMo5084EWifd&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=2211222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=65400181&dur=163.046&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgdkJv6w9_Azf0m6poA-ULyX0eH_GKBtSJRwUY1lNBAZgCIDCrC0lnu__ycTaIhg0pUcsRUqay60S3QMo5084EWifd&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=2211222&vprv=1"), itag: 299, bitrate: 4190323, average_bitrate: 3208919, @@ -264,7 +264,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=62993617&dur=163.046&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=303&keepalive=yes&lmt=1608509371758331&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgZi9dDSMWh10NID8-QNn3azIH1zw5UooZrRTPZjVn7hYCIAm9bFc6NBwJ_DzY4V2R_zGmJSpOwQl8LEsfCb7hf6i7&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=62993617&dur=163.046&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=303&keepalive=yes&lmt=1608509371758331&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgZi9dDSMWh10NID8-QNn3azIH1zw5UooZrRTPZjVn7hYCIAm9bFc6NBwJ_DzY4V2R_zGmJSpOwQl8LEsfCb7hf6i7&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1"), itag: 303, bitrate: 3832648, average_bitrate: 3090839, @@ -292,7 +292,7 @@ VideoPlayer( ], audio_streams: [ AudioStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=934449&dur=163.061&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=249&keepalive=yes&lmt=1608509101590706&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAMUfr2X-eQJt1abn-IK1H4d5DtvKZuBaETo4opNi6mqCAiEAvBmrmuaoFjB1CJ2Kug87w-Uv6OCdxyrJ_3HIaHX9KBI%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=934449&dur=163.061&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=249&keepalive=yes&lmt=1608509101590706&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAMUfr2X-eQJt1abn-IK1H4d5DtvKZuBaETo4opNi6mqCAiEAvBmrmuaoFjB1CJ2Kug87w-Uv6OCdxyrJ_3HIaHX9KBI%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1"), itag: 249, bitrate: 53039, average_bitrate: 45845, @@ -316,7 +316,7 @@ VideoPlayer( drm_systems: [], ), AudioStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=1245866&dur=163.061&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=250&keepalive=yes&lmt=1608509101111096&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAIflUU4t4Ukf4CXW3ttB5c8SnP4z4z3ef-7EFVMFv4U8AiAlcKmmofCTzzr2NRRsRVosQdpDBphUqWyVzS7noGrixw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=1245866&dur=163.061&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=250&keepalive=yes&lmt=1608509101111096&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAIflUU4t4Ukf4CXW3ttB5c8SnP4z4z3ef-7EFVMFv4U8AiAlcKmmofCTzzr2NRRsRVosQdpDBphUqWyVzS7noGrixw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1"), itag: 250, bitrate: 71268, average_bitrate: 61123, @@ -340,7 +340,7 @@ VideoPlayer( drm_systems: [], ), AudioStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=2640283&dur=163.096&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=140&keepalive=yes&lmt=1580005579712232&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhALBveArAZ7DP9r1BIpNz6ZXst5MtzvUM72jhtYMrildCAiEArvwqaqcowZwR_UrRM-O7jq2CMxgBtbnmul27AEcBqEI%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=2211222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=2640283&dur=163.096&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=140&keepalive=yes&lmt=1580005579712232&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhALBveArAZ7DP9r1BIpNz6ZXst5MtzvUM72jhtYMrildCAiEArvwqaqcowZwR_UrRM-O7jq2CMxgBtbnmul27AEcBqEI%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=2211222&vprv=1"), itag: 140, bitrate: 130268, average_bitrate: 129508, @@ -364,7 +364,7 @@ VideoPlayer( drm_systems: [], ), AudioStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=2476314&dur=163.061&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=251&keepalive=yes&lmt=1608509101894140&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgARPqD-6172OshMHeV8DpONV7tnPvdsxcg8QlaIGxcuMCICSe8LWvhRTEO2bdAQ43OzOoc5XfJcj3veyhScVXVz8O&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=2476314&dur=163.061&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=251&keepalive=yes&lmt=1608509101894140&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgARPqD-6172OshMHeV8DpONV7tnPvdsxcg8QlaIGxcuMCICSe8LWvhRTEO2bdAQ43OzOoc5XfJcj3veyhScVXVz8O&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1"), itag: 251, bitrate: 140633, average_bitrate: 121491, @@ -400,6 +400,8 @@ VideoPlayer( valid_until: "[date]", hls_manifest_url: None, dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659487474/ei/knDpYub6BojEgAf6jbLgDw/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr5---sn-h0jeenek.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jeenek%2Csn-h0jelnez/ms/au%2Crdu/mv/m/mvi/5/pl/37/hfr/all/as/fmp4_audio_clear%2Cwebm_audio_clear%2Cwebm2_audio_clear%2Cfmp4_sd_hd_clear%2Cwebm2_sd_hd_clear/initcwndbps/1418750/spc/lT-Khox4YuJQ2wmH79zYALRvsWTPCUc/vprv/1/mt/1659465669/fvip/4/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cspc%2Cvprv%2Citag/sig/AOq0QJ8wRAIgErABhAEaoKHUDu9dDbpxE_8gR4b8WWAi61fnu8UKnuICIEYrEKcHvqHdO4V3R7cvSGwi_HGH34IlQsKbziOfMBov/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgJxHmH0Sxo3cY_pW_ZzQ3hW9-7oz6K_pZWcUdrDDQ2sQCIQDJYNINQwLgKelgbO3CZYx7sMxdUAFpWdokmRBQ77vwvw%3D%3D"), + abr_streaming_url: None, + abr_ustreamer_config: None, preview_frames: [ Frameset( url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwENSDfyq4qpAwVwAcABBqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLAXobPyrylgm8IEvjlZzqYTiPe1Ow", diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap index 94d9f38..72669c4 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap @@ -57,7 +57,7 @@ VideoPlayer( video_streams: [], video_only_streams: [ VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=IOS&clen=7808990&dur=163.029&ei=q1jpYq-xHs7NgQev0bfwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-ANNg3iPHI56jhLSlPQk4pi4mdub5iAby0hmJBVrtiJgY&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRQIgWKVoDpyI6QmVnkdGzdirFtjMAXhmLex64VTO7UUJd-4CIQDoJKkT2-Kpa7j0merJJoZDs4IkkXSjdNm3bvdCL8t2Pg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAMFQo_RyC3Ud44QVGtKckMcuq5UQ3J7CgYsYl0bXaWMUAiEAhMi1h0ru4zoIGX0jBZT23dozvtrhf_m61p4qbAfEhZo%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&svpuc=1&txp=2211222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=IOS&clen=7808990&dur=163.029&ei=q1jpYq-xHs7NgQev0bfwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-ANNg3iPHI56jhLSlPQk4pi4mdub5iAby0hmJBVrtiJgY&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRQIgWKVoDpyI6QmVnkdGzdirFtjMAXhmLex64VTO7UUJd-4CIQDoJKkT2-Kpa7j0merJJoZDs4IkkXSjdNm3bvdCL8t2Pg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAMFQo_RyC3Ud44QVGtKckMcuq5UQ3J7CgYsYl0bXaWMUAiEAhMi1h0ru4zoIGX0jBZT23dozvtrhf_m61p4qbAfEhZo%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&svpuc=1&txp=2211222&vprv=1"), itag: 134, bitrate: 538143, average_bitrate: 383195, @@ -83,7 +83,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=IOS&clen=65400181&dur=163.046&ei=q1jpYq-xHs7NgQev0bfwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-ANNg3iPHI56jhLSlPQk4pi4mdub5iAby0hmJBVrtiJgY&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRQIgWKVoDpyI6QmVnkdGzdirFtjMAXhmLex64VTO7UUJd-4CIQDoJKkT2-Kpa7j0merJJoZDs4IkkXSjdNm3bvdCL8t2Pg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAP6zxXXA18ToZWUfalauhhsgOsDHTu-R0QrqNrJR7D5kAiEAi8HBa9OkYwmA0bcRxhgvXfN9JsFlXwCWJ-x4ty6TjoY%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&svpuc=1&txp=2211222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=IOS&clen=65400181&dur=163.046&ei=q1jpYq-xHs7NgQev0bfwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-ANNg3iPHI56jhLSlPQk4pi4mdub5iAby0hmJBVrtiJgY&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRQIgWKVoDpyI6QmVnkdGzdirFtjMAXhmLex64VTO7UUJd-4CIQDoJKkT2-Kpa7j0merJJoZDs4IkkXSjdNm3bvdCL8t2Pg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAP6zxXXA18ToZWUfalauhhsgOsDHTu-R0QrqNrJR7D5kAiEAi8HBa9OkYwmA0bcRxhgvXfN9JsFlXwCWJ-x4ty6TjoY%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&svpuc=1&txp=2211222&vprv=1"), itag: 299, bitrate: 4190323, average_bitrate: 3208919, @@ -111,7 +111,7 @@ VideoPlayer( ], audio_streams: [ AudioStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=IOS&clen=995840&dur=163.189&ei=q1jpYq-xHs7NgQev0bfwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-ANNg3iPHI56jhLSlPQk4pi4mdub5iAby0hmJBVrtiJgY&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=139&keepalive=yes&lmt=1580005582214385&lsig=AG3C_xAwRQIgWKVoDpyI6QmVnkdGzdirFtjMAXhmLex64VTO7UUJd-4CIQDoJKkT2-Kpa7j0merJJoZDs4IkkXSjdNm3bvdCL8t2Pg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAMXDvZZznm1LafIKh_pdGf-TjY5Kz-F9N67o6gXenKouAiEA5qji45i5ABmAytPDOORjw0OaBmwX88S7bgUWcmF-_LU%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&svpuc=1&txp=2211222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=IOS&clen=995840&dur=163.189&ei=q1jpYq-xHs7NgQev0bfwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-ANNg3iPHI56jhLSlPQk4pi4mdub5iAby0hmJBVrtiJgY&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=139&keepalive=yes&lmt=1580005582214385&lsig=AG3C_xAwRQIgWKVoDpyI6QmVnkdGzdirFtjMAXhmLex64VTO7UUJd-4CIQDoJKkT2-Kpa7j0merJJoZDs4IkkXSjdNm3bvdCL8t2Pg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAMXDvZZznm1LafIKh_pdGf-TjY5Kz-F9N67o6gXenKouAiEA5qji45i5ABmAytPDOORjw0OaBmwX88S7bgUWcmF-_LU%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&svpuc=1&txp=2211222&vprv=1"), itag: 139, bitrate: 49724, average_bitrate: 48818, @@ -135,7 +135,7 @@ VideoPlayer( drm_systems: [], ), AudioStream( - url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=IOS&clen=2640283&dur=163.096&ei=q1jpYq-xHs7NgQev0bfwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-ANNg3iPHI56jhLSlPQk4pi4mdub5iAby0hmJBVrtiJgY&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=140&keepalive=yes&lmt=1580005579712232&lsig=AG3C_xAwRQIgWKVoDpyI6QmVnkdGzdirFtjMAXhmLex64VTO7UUJd-4CIQDoJKkT2-Kpa7j0merJJoZDs4IkkXSjdNm3bvdCL8t2Pg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgcsPm6rrUAwCi1VTGf0FMDTjzjM01__iTC13PnzDTFeQCIQCJ_EGeKVZztkmK3Cr7gVuxUP83XCSlP01YLx5FO-PPcQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&svpuc=1&txp=2211222&vprv=1", + url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=IOS&clen=2640283&dur=163.096&ei=q1jpYq-xHs7NgQev0bfwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-ANNg3iPHI56jhLSlPQk4pi4mdub5iAby0hmJBVrtiJgY&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=140&keepalive=yes&lmt=1580005579712232&lsig=AG3C_xAwRQIgWKVoDpyI6QmVnkdGzdirFtjMAXhmLex64VTO7UUJd-4CIQDoJKkT2-Kpa7j0merJJoZDs4IkkXSjdNm3bvdCL8t2Pg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgcsPm6rrUAwCi1VTGf0FMDTjzjM01__iTC13PnzDTFeQCIQCJ_EGeKVZztkmK3Cr7gVuxUP83XCSlP01YLx5FO-PPcQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&svpuc=1&txp=2211222&vprv=1"), itag: 140, bitrate: 130268, average_bitrate: 129508, @@ -171,6 +171,8 @@ VideoPlayer( valid_until: "[date]", hls_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1659481355/ei/q1jpYq-xHs7NgQev0bfwAQ/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr4---sn-h0jelnez.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jelnez%2Csn-h0jeenek/ms/au%2Crdu/mv/m/mvi/4/pl/37/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1513750/vprv/1/go/1/mt/1659459429/fvip/5/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24001373%2C24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRQIhAIYnEHvIgJtJ8hehAXNtVY3qsgsq_GdOhWf2hkJZe6lCAiBxaRY_nubYp6hBizcAg_KFkKnkG-t2XYLRQ5wGdM3AjA%3D%3D/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRgIhAM_91Kk_0VLuSsR6nLCY7LdtWojyRAzXSScd_X9ShRROAiEA1AF4VY04F71NsAI8_j3iqjuXnWL9s6NoXHq7P8-bHx8%3D/file/index.m3u8"), dash_manifest_url: None, + abr_streaming_url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=IOS&ei=q1jpYq-xHs7NgQev0bfwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=5&id=o-ANNg3iPHI56jhLSlPQk4pi4mdub5iAby0hmJBVrtiJgY&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&keepalive=yes&lsig=AG3C_xAwRgIhAK6T5ehnFBsc0FOurPHH1ME_vGcVysI-g5jrtEsvX64sAiEArY-iAvQCsc4R8yg8dvMdpnuHPIcPMCnRgyh8E527HF0%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&pl=37&rbqsm=fr&requiressl=yes&sabr=1&sig=AOq0QJ8wRgIhAJCJpb5gE12jQc2qOUy-Y61vEHeiAP_F78weNCzj8VklAiEAwR2PK52CmwsVHfRVk75OOYOxwYKNW2g1eDBw3COBP9w%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Csvpuc%2Csabr&svpuc=1"), + abr_ustreamer_config: Some("CpYECp0DCAAQgAUY6AIl-n6qPi0AAIA_NT0Klz9oAXI6ChJtZnMyX2NtZnNfdjNfMV8wNDMSIAoeCgxkZXZpY2VfbW9kZWwSDgoMCgppcGhvbmUxNCw1GAAgAXIWChJtZnMyX2NtZnNfdjNfMV8wNDMYAHjoAoABAagBAbUB9ijcP6AC6AK4AgDaAmEQsOoBGKhGIKCcASjYNjCYdXCIJ4AB9AO4AQHgAQP4AQGQAgGYAgygAgGoAgGwAgG4AgHAAgHIAgHQAgLgAgHoAgLwAgGAAwKIA4gnmAMBqAMIwAMByAMB2AMB-LWR5QwB-gIvCAwQGBgyIDItAABwQjUAAIxCQAFIAVAKWAplAACAQGjAcM0BAACAP_ABAegC0AWCAwCQAwGgAwGoAwGwAwPQAwHYAwHgA5BOuAQBygRWChUI4NQDEJh1GOgHJQAAAAAoADAAQAEQ4NQDGNAPKjYKCnRiX2Nvc3RfNTAgDykAAAAAAAAAAEgBUAFdZmbmPmXNzEw-bZqZGT91mpkZP3jAqQeSAQDoBAHwBAH4BAGQBQGgBQEYASABMgwIqwIQjrrY25ug5wIyDAiqAhCOutjbm6DnAjIMCIcBEI662NuboOcCMgwIhgEQ78vY25ug5wIyDAiFARCOutjbm6DnAjIMCKABEI662NuboOcCMgwIiwEQ8aniu5ug5wIyDAiMARDozcm6m6DnAjoAEkwA8tjvwTBFAiAyo6mtW-M8Y8noBO_6Y3RmNo5U2LntCwiKcI2yye_zRgIhAK27d1UbqzhtkQhminIBAajD70ONeAQNwjt7VRpJRDimGgJlaQ=="), preview_frames: [ Frameset( url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLCsCT8Lprh2S0ptmCRsWH7VtDl3YQ", diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tv.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tv.snap index b7d5a29..32ba960 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tv.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tv.snap @@ -24,7 +24,7 @@ VideoPlayer( ), video_streams: [ VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IT4iUCpJNJWUitTMgIi6njuKSsi3MNed1Szyf0qysTX0v1Nf6AyCvjIGbek5Fn50kuBrGtRJ5q&c=TVHTML5&clen=10262148&dur=163.096&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=18&lmt=1700885551970466&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=BMzwItzIOB1HhmG&ns=YmgbZhlLp0C-9ilsQWGAyUAQ&pl=26&ratebypass=yes&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgUah4qH8RqPzmo75ExCWSiRYlUlsAk0v9gl638LitVNICICxFs5lK3CsmOAja0bsXavXkyykzpdhHZKGXOZQYT1f8&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&svpuc=1&txp=1318224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IT4iUCpJNJWUitTMgIi6njuKSsi3MNed1Szyf0qysTX0v1Nf6AyCvjIGbek5Fn50kuBrGtRJ5q&c=TVHTML5&clen=10262148&dur=163.096&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=18&lmt=1700885551970466&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=BMzwItzIOB1HhmG&ns=YmgbZhlLp0C-9ilsQWGAyUAQ&pl=26&ratebypass=yes&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgUah4qH8RqPzmo75ExCWSiRYlUlsAk0v9gl638LitVNICICxFs5lK3CsmOAja0bsXavXkyykzpdhHZKGXOZQYT1f8&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&svpuc=1&txp=1318224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 18, bitrate: 503574, average_bitrate: 503367, @@ -46,7 +46,7 @@ VideoPlayer( ], video_only_streams: [ VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2273274&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=160&keepalive=yes&lmt=1705967288821438&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgb8eXnQ6MSJ3PuvFVBdYIWTnFobH8mTC9zbZpBNxLbBYCICkPLKEm3gNbW5HIFXs7bwF5rSqUKHHnXNK91qMslQog&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2273274&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=160&keepalive=yes&lmt=1705967288821438&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgb8eXnQ6MSJ3PuvFVBdYIWTnFobH8mTC9zbZpBNxLbBYCICkPLKEm3gNbW5HIFXs7bwF5rSqUKHHnXNK91qMslQog&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 160, bitrate: 114816, average_bitrate: 111551, @@ -72,7 +72,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=1151892&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=278&keepalive=yes&lmt=1705966620402771&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAP4IybR7cZRpx7IX1ke6UIu_hdFZN3LOuHBDywg_xv5WAiB8_XEx8VhT9OlFxmM-cY0fl6-7GT9uj3clMIPDk2w7cA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=1151892&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=278&keepalive=yes&lmt=1705966620402771&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAP4IybR7cZRpx7IX1ke6UIu_hdFZN3LOuHBDywg_xv5WAiB8_XEx8VhT9OlFxmM-cY0fl6-7GT9uj3clMIPDk2w7cA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 278, bitrate: 70630, average_bitrate: 56524, @@ -98,7 +98,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=5026513&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=133&keepalive=yes&lmt=1705967298859029&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgPF0ms4OEe15BTjOFVCkvf52UeTUf0b62_pavCfEyGjcCIH-0AoxzyT8iioWFFaX7iYjqzzaUTpo8rgAPQ0uX8DJa&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=5026513&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=133&keepalive=yes&lmt=1705967298859029&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgPF0ms4OEe15BTjOFVCkvf52UeTUf0b62_pavCfEyGjcCIH-0AoxzyT8iioWFFaX7iYjqzzaUTpo8rgAPQ0uX8DJa&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 133, bitrate: 257417, average_bitrate: 246656, @@ -124,7 +124,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2541351&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=242&keepalive=yes&lmt=1705966614837727&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgKj1JyMGwYtf16zLJsmbnizz5_v3jaZSa7-j-ls8-qzECIQDKUd50iIc52h7zOX50Hf1SkbV9h-hP4QHs-wkik1fk6Q%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2541351&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=242&keepalive=yes&lmt=1705966614837727&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgKj1JyMGwYtf16zLJsmbnizz5_v3jaZSa7-j-ls8-qzECIQDKUd50iIc52h7zOX50Hf1SkbV9h-hP4QHs-wkik1fk6Q%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 242, bitrate: 149589, average_bitrate: 124706, @@ -150,7 +150,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=7810925&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=134&keepalive=yes&lmt=1705967286812435&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAJ92IgZgdk3_WLsfzJV_ZyrSFSbzpsoJh3DkRKDHbNxzAiEA9UbnVlXQ2S3BUimLmWC5TZQfhIkc-PlLnZ81fL0S5yA%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=7810925&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=134&keepalive=yes&lmt=1705967286812435&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAJ92IgZgdk3_WLsfzJV_ZyrSFSbzpsoJh3DkRKDHbNxzAiEA9UbnVlXQ2S3BUimLmWC5TZQfhIkc-PlLnZ81fL0S5yA%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 134, bitrate: 537902, average_bitrate: 383290, @@ -176,7 +176,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=4188954&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=243&keepalive=yes&lmt=1705966624121874&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgSCLGQvdZKNXym0zt7c3Yw_4e0J8-wNxtPagPRRn4dRoCIQCOj0IzalNG4EcowBIyK2LC6NLFDr8Zt6sNVkqPjw6lGg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=4188954&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=243&keepalive=yes&lmt=1705966624121874&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgSCLGQvdZKNXym0zt7c3Yw_4e0J8-wNxtPagPRRn4dRoCIQCOj0IzalNG4EcowBIyK2LC6NLFDr8Zt6sNVkqPjw6lGg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 243, bitrate: 248858, average_bitrate: 205556, @@ -202,7 +202,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=14723538&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=135&keepalive=yes&lmt=1705967282545273&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAM843wAa1e7Gc1S69gfXckm7hdgIKPXp0bUSh3hO6W5zAiEA-DDEPGsZBmF5N8VbPy75dhy3rLpE1F18KtWgmrUm2Pg%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=14723538&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=135&keepalive=yes&lmt=1705967282545273&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAM843wAa1e7Gc1S69gfXckm7hdgIKPXp0bUSh3hO6W5zAiEA-DDEPGsZBmF5N8VbPy75dhy3rLpE1F18KtWgmrUm2Pg%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 135, bitrate: 978945, average_bitrate: 722499, @@ -228,7 +228,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=7788899&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=244&keepalive=yes&lmt=1705966622098793&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAKGyn799bfkVHYE195sPmD60dCMppqJrBM0O-sjgYTzzAiAoBjkNAtL90sXw2YP9UTW9JrMhPSvPiBI_KiCVMJAkFQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=7788899&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=244&keepalive=yes&lmt=1705966622098793&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAKGyn799bfkVHYE195sPmD60dCMppqJrBM0O-sjgYTzzAiAoBjkNAtL90sXw2YP9UTW9JrMhPSvPiBI_KiCVMJAkFQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 244, bitrate: 467884, average_bitrate: 382209, @@ -254,7 +254,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=24616305&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=136&keepalive=yes&lmt=1705967307531372&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAM57L2Utesn4xVyT0HSwR9Khv_S-efx4uFAbCPkZFoRXAiEAtIu63-jF2_FZkOMmZAqGU3SRU9QgxoajRjBhMFwcOuk%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=24616305&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=136&keepalive=yes&lmt=1705967307531372&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAM57L2Utesn4xVyT0HSwR9Khv_S-efx4uFAbCPkZFoRXAiEAtIu63-jF2_FZkOMmZAqGU3SRU9QgxoajRjBhMFwcOuk%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 136, bitrate: 1560439, average_bitrate: 1207947, @@ -280,7 +280,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=14723992&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=247&keepalive=yes&lmt=1705966613897741&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAL-upITxk7r9FQL5F4WL0A6SjPw673qyyzmXIC48eKfTAiEAlkdkx7IFYtehbhKakbffvIebpPXRtxSgBWLl7WEHCrE%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=14723992&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=247&keepalive=yes&lmt=1705966613897741&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAL-upITxk7r9FQL5F4WL0A6SjPw673qyyzmXIC48eKfTAiEAlkdkx7IFYtehbhKakbffvIebpPXRtxSgBWLl7WEHCrE%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 247, bitrate: 929607, average_bitrate: 722521, @@ -306,7 +306,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=34544823&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=298&keepalive=yes&lmt=1705967092637061&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAIIGU41JunuODw9qIlSoYQcwkCYO6k9XOVlDn1Nxqnu7AiEAoiMOgYU8s8lp01fW0L86hHrSrtlvOLSI9XA50iyIGBc%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=34544823&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=298&keepalive=yes&lmt=1705967092637061&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAIIGU41JunuODw9qIlSoYQcwkCYO6k9XOVlDn1Nxqnu7AiEAoiMOgYU8s8lp01fW0L86hHrSrtlvOLSI9XA50iyIGBc%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 298, bitrate: 2188961, average_bitrate: 1694973, @@ -332,7 +332,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=30205331&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=302&keepalive=yes&lmt=1705966545733919&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAL428Az_BKxxff4FlH4WleHSy4Igq3mR71NuTMOc9xU3AiBN4lXfH9DklGaQUMnOT8wAhiMuzR73bW3cwr744TSoNA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=30205331&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=302&keepalive=yes&lmt=1705966545733919&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAL428Az_BKxxff4FlH4WleHSy4Igq3mR71NuTMOc9xU3AiBN4lXfH9DklGaQUMnOT8wAhiMuzR73bW3cwr744TSoNA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 302, bitrate: 2250391, average_bitrate: 1482051, @@ -358,7 +358,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=62057888&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=299&keepalive=yes&lmt=1705967093743693&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgBEemc0Cvd3KhNooNRblgX64_fjNSP30RmWDfFwDR7qYCIQCXpQ9FO0_X93ZHcyvRZCKX5gbJuusCReaRcJbRLFsM_g%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=62057888&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=299&keepalive=yes&lmt=1705967093743693&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgBEemc0Cvd3KhNooNRblgX64_fjNSP30RmWDfFwDR7qYCIQCXpQ9FO0_X93ZHcyvRZCKX5gbJuusCReaRcJbRLFsM_g%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 299, bitrate: 3926810, average_bitrate: 3044926, @@ -384,7 +384,7 @@ VideoPlayer( drm_systems: [], ), VideoStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=55300085&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=303&keepalive=yes&lmt=1705966651743358&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgTZlmOcsLYJ_a9SnVLehXnaoajtreQO97qawEIDPEi8sCIQDKFdtBWWMuQUb9X8H-x92B3q-y0g8TvAPanR95cfklXQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=55300085&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=303&keepalive=yes&lmt=1705966651743358&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgTZlmOcsLYJ_a9SnVLehXnaoajtreQO97qawEIDPEi8sCIQDKFdtBWWMuQUb9X8H-x92B3q-y0g8TvAPanR95cfklXQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 303, bitrate: 3473307, average_bitrate: 2713348, @@ -412,7 +412,7 @@ VideoPlayer( ], audio_streams: [ AudioStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=934750&dur=163.061&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=249&keepalive=yes&lmt=1714877357172339&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAItfaWkRs94vqyae7GR4M1xHoQO2lduvNRFugRSf0h-IAiA9fdLOJMwPI8vAO2C13igyv2qGSpOlKQptS4sN6p5Ffw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=934750&dur=163.061&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=249&keepalive=yes&lmt=1714877357172339&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAItfaWkRs94vqyae7GR4M1xHoQO2lduvNRFugRSf0h-IAiA9fdLOJMwPI8vAO2C13igyv2qGSpOlKQptS4sN6p5Ffw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 249, bitrate: 53073, average_bitrate: 45860, @@ -436,7 +436,7 @@ VideoPlayer( drm_systems: [], ), AudioStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=1245582&dur=163.061&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=250&keepalive=yes&lmt=1714877466693058&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgdJ1SjWwaloQecEblSIMFp2qFmpG_kKYZP1vX_M55dE0CIQCDSfa_FsaiFRcNL-1LRTgCIRSO7dj5vrpKR1Ya-KbmMw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=1245582&dur=163.061&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=250&keepalive=yes&lmt=1714877466693058&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgdJ1SjWwaloQecEblSIMFp2qFmpG_kKYZP1vX_M55dE0CIQCDSfa_FsaiFRcNL-1LRTgCIRSO7dj5vrpKR1Ya-KbmMw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 250, bitrate: 71197, average_bitrate: 61109, @@ -460,7 +460,7 @@ VideoPlayer( drm_systems: [], ), AudioStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2640283&dur=163.096&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=140&keepalive=yes&lmt=1705966477945761&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgSxdbLrbojMVJcyRzsI2TrzOf78LN28bWcsHpbs4QXDwCIHidfXoriWMHfuiktUCdzLuUmksU7r5vITdh6u0puNmx&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2640283&dur=163.096&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=140&keepalive=yes&lmt=1705966477945761&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgSxdbLrbojMVJcyRzsI2TrzOf78LN28bWcsHpbs4QXDwCIHidfXoriWMHfuiktUCdzLuUmksU7r5vITdh6u0puNmx&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 140, bitrate: 130268, average_bitrate: 129508, @@ -484,7 +484,7 @@ VideoPlayer( drm_systems: [], ), AudioStream( - url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2480393&dur=163.061&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=251&keepalive=yes&lmt=1714877359450110&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgO0jG-x2l6AF7tjryIX_oM3np78WgNDiseezppLfbQrgCIQCVLdpDhclKc8vQgWGzKXcqsAxgNl5S3MlLT8u1Jeok2A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D", + url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2480393&dur=163.061&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=251&keepalive=yes&lmt=1714877359450110&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgO0jG-x2l6AF7tjryIX_oM3np78WgNDiseezppLfbQrgCIQCVLdpDhclKc8vQgWGzKXcqsAxgNl5S3MlLT8u1Jeok2A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D"), itag: 251, bitrate: 140833, average_bitrate: 121691, @@ -520,6 +520,8 @@ VideoPlayer( valid_until: "[date]", hls_manifest_url: None, dash_manifest_url: None, + abr_streaming_url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=TVHTML5&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&keepalive=yes&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=eBXmY26Y0c3VPyt&ns=Kl83P0QZk1oI9742KUD7ly8Q&pl=26&requiressl=yes&rqh=1&sabr=1&sig=AJfQdSswRAIgJRK55pIkQ3Pak9jZ4fHPDsxXv0YgkxKE-FFdIN12ph8CIFHlFEvAoUOoX4Fd1RmyCJqgLZhDkSLwD6s-xVW25kYL&smc=1&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Cxpc%2Csvpuc%2Cns%2Csabr%2Crqh&svpuc=1&xpc=EgVo2aDSNQ%3D%3D"), + abr_ustreamer_config: Some("CswJCpcHCAAlAACAPy1SuF4_NQAAwD9YAWABaAFyFgoSbWZzMl9jbWZzX3YzXzJfMTA5GAB4j06gAQGoAQCQAgG4AgDIAgHaAroBELDqARioRiCgnAEoiCcwmHVwiCeAAfQDuAEB4AEDkAIBmAIMoAIBwAIB0AIC4AIB6AIEgAMCiAOIJ5gDAagDA8ADAcgDAdADAfgDAYAEAYgEAZAEAZgEAaAEAagEAcgEAdAEAdgEAeAEAOgEAfgEB4AFfYgFAbAFAbgFAcAFAcgFAdAFAdgFAeAF0A_oBQH4BdAPgAYBuAYBwAYB0AYB2AYB6AYB8AYB-AYBkAcBqAcB2AcB-LWR5QwB-gKeAi0AAIJCNQAAlkJIAWUAAIBAaMBwqAHQhgOwAeADuAEBzQEAAIA_8AEB_QEAAIA_hQKamRk-jQIAAIA_lQIAAAJCmAIBtQIAAIA_wALgA9ICEbD__________wEePEZaXF1e2gIFMjA6MDDgAnjoAugC9QIK16M7_QLNzMw9gAMBkAMBnQMK1yM9oAMBuAMByAMB2AMB5QNiSkRA7QMyyvM-8AMB_QNmZoY_hQQAAIBAmAQB1QQAACBB6ATwEPAEAb0Fo0Afu8UF308tP8gFAeAFAZgGAaAGAagGAbUGvTeGNb0GMzODQJAHAcAHAcgHAdUHAICdQ-UHAIAJRKEIAAAAAAAA8L-pCAAAAAAAAPC_sAjwAbgIAdgI8AHoCAGCAwCQAwGoAwGwAwPQAwHYAwHgA5BOuAQBygQcChMIwKkHEJh1GOgHJQAAAAAoADAAEODUAxjQD9IEDQoICLAJELAJIAEgiCfaBAsKBgjwLhDwLiCIJ-gEAfgEAYAFAYgFAZAFAagFAbAFAdAFAdgFAegFAfAFAYgGAZgGAagGgIACwAYByAYB0gYUCOgHEGQaDQiIJxUAAAA_Hc3MTD-CBwoVAACAPxhkIJBOiAcBoAcBsAcBuAcBwAcB-AcBgAgBoAgBsAgBuAgB0ggGCAEQARgBmAkBqQkAAAAAAADwv7EJAAAAAAAA8L_ICQHaCSRFRzRmTDl1Sm9tL2NWdklmNjg4bnB6c2t4SVQrMXl0N09POHXgCQGwCgHYCgHwCgGICwGYCwG4CwHICwHQCwHYCwHqCwSLBowG8AsB-AsBkAwBoAwBqAyQAbAMAbgMAcAMAdAMAeAMAegMAYANAaANAdANAeANAYgOAZAOAbAOAYinocoLARgBIAEyDAirAhDNgPzUlvKDAzIMCK8CEP64moKV8oMDMgwIiAEQ7Mj0upfygwMyDAj3ARCNxJTwlPKDAzIMCKoCEIW7uNSW8oMDMgwIrgIQn5LUz5TygwMyDAiHARD5xP-ul_KDAzIMCPQBEOmKifSU8oMDMgwIhgEQk_6DsZfygwMyDAjzARCSyIT1lPKDAzIMCIUBEJWg47aX8oMDMgwI8gEQ3_PN8JTygwMyDAigARC-zf6xl_KDAzIMCJYCENPIofOU8oMDMgwIjAEQodeqr5TygwMyDAj5ARDzzNT9v_WFAzIMCPoBEMKb8bHA9YUDMgwI-wEQ_s_f_r_1hQM6AEgAUiYaAmVuKAAyGFVDYnh4RWktSW1QbGJMeDVGLWZIZXRFZzgAQABYAJDL048OARJNAKEYC4YwRgIhAIj4Ug4dw_gq15NXvgcfXpI1Fm_fhmwl-4ad-rX3Ffg_AiEAkZDsUgoAGLOXIvWZlNyuyfu8HLWt-snFl3gkTiPo2acaAmVp"), preview_frames: [ Frameset( url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLCsCT8Lprh2S0ptmCRsWH7VtDl3YQ", diff --git a/src/model/mod.rs b/src/model/mod.rs index 1829adc..7f3b277 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -147,6 +147,10 @@ pub struct VideoPlayer { pub hls_manifest_url: Option, /// Dash manifest URL (for livestreams) pub dash_manifest_url: Option, + /// ABR (adaptive bitrate) streaming URL + pub abr_streaming_url: Option, + /// ABR streaming config + pub abr_ustreamer_config: Option, /// Video frames for seek preview pub preview_frames: Vec, /// Video player DRM config @@ -194,7 +198,7 @@ pub struct VideoPlayerDetails { #[non_exhaustive] pub struct VideoStream { /// Video stream URL - pub url: String, + pub url: Option, /// YouTube stream format identifier pub itag: u32, /// Stream bitrate (in bits/second) @@ -238,7 +242,7 @@ pub struct VideoStream { #[non_exhaustive] pub struct AudioStream { /// Audio stream URL - pub url: String, + pub url: Option, /// YouTube stream format identifier pub itag: u32, /// Stream bitrate (in bits/second) diff --git a/src/model/traits.rs b/src/model/traits.rs index d4f6253..43d4236 100644 --- a/src/model/traits.rs +++ b/src/model/traits.rs @@ -9,7 +9,7 @@ use super::*; /// Trait for YouTube streams (video and audio) pub trait YtStream { /// Stream URL - fn url(&self) -> &str; + fn url(&self) -> Option<&str>; /// YouTube stream format identifier fn itag(&self) -> u32; /// Stream bitrate (in bits/second) @@ -29,8 +29,8 @@ pub trait YtStream { } impl YtStream for VideoStream { - fn url(&self) -> &str { - &self.url + fn url(&self) -> Option<&str> { + self.url.as_deref() } fn itag(&self) -> u32 { @@ -67,8 +67,8 @@ impl YtStream for VideoStream { } impl YtStream for AudioStream { - fn url(&self) -> &str { - &self.url + fn url(&self) -> Option<&str> { + self.url.as_deref() } fn itag(&self) -> u32 { diff --git a/src/param/stream_filter.rs b/src/param/stream_filter.rs index b690bb0..99e9a31 100644 --- a/src/param/stream_filter.rs +++ b/src/param/stream_filter.rs @@ -23,6 +23,7 @@ pub struct StreamFilter { video_codecs: Option>, video_hdr: bool, video_none: bool, + abr_only: bool, drm_track_types: Vec, drm_system: Option, } @@ -176,6 +177,13 @@ impl StreamFilter { self } + /// Allow ABR-only streams without URL + #[must_use] + pub fn abr_only(mut self) -> Self { + self.abr_only = true; + self + } + fn check_drm(&self, track_type: Option, drm_systems: &[DrmSystem]) -> Option<()> { if let Some(track_type) = track_type { if !self.drm_track_types.contains(&track_type) { @@ -189,6 +197,10 @@ impl StreamFilter { } fn apply_audio(&self, stream: &AudioStream) -> AudioRes { + if stream.url.is_none() && !self.abr_only { + return None; + } + let bitrate = match self.audio_max_bitrate { Some(max) => { if stream.average_bitrate > max { @@ -272,6 +284,10 @@ impl StreamFilter { } fn apply_video(&self, stream: &VideoStream) -> VideoRes { + if stream.url.is_none() && !self.abr_only { + return None; + } + let vres = stream.height.min(stream.width); let res = match self.video_max_res { Some(max) => filter_max(vres, max), @@ -466,7 +482,7 @@ mod tests { let selection = player.select_audio_stream(&filter); match expect_url { - Some(expect_url) => assert_eq!(selection.unwrap().url, expect_url), + Some(expect_url) => assert_eq!(selection.unwrap().url.as_deref().unwrap(), expect_url), None => assert_eq!(selection, None), } } @@ -491,7 +507,7 @@ mod tests { let selection = player.select_video_only_stream(&filter); match expect_url { - Some(expect_url) => assert_eq!(selection.unwrap().url, expect_url), + Some(expect_url) => assert_eq!(selection.unwrap().url.as_deref().unwrap(), expect_url), None => assert_eq!(selection, None), } } @@ -526,12 +542,12 @@ mod tests { let (video, audio) = PLAYER_HDR.select_video_audio_stream(&filter); match expect_video_url { - Some(expect_url) => assert_eq!(video.unwrap().url, expect_url), + Some(expect_url) => assert_eq!(video.unwrap().url.as_deref().unwrap(), expect_url), None => assert_eq!(video, None), } match expect_audio_url { - Some(expect_url) => assert_eq!(audio.unwrap().url, expect_url), + Some(expect_url) => assert_eq!(audio.unwrap().url.as_deref().unwrap(), expect_url), None => assert_eq!(audio, None), } } diff --git a/tests/youtube.rs b/tests/youtube.rs index fb7f860..423cc65 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -149,7 +149,7 @@ async fn check_video_stream(s: impl YtStream) { let http = reqwest::Client::new(); let resp = http - .get(s.url()) + .get(s.url().expect("no url")) .send() .await .unwrap() From 3a41899ce7a88edb06845a2062e6dff15cd74c2b Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 17 Mar 2025 00:06:10 +0100 Subject: [PATCH 02/20] feat: add rustypipe-abr-proto crate --- Cargo.toml | 3 +- abr-proto/Cargo.toml | 16 +++++ abr-proto/LICENSE | 21 ++++++ abr-proto/README.md | 5 ++ abr-proto/build.rs | 64 ++++++++++++++++++ abr-proto/proto/misc/common.proto | 28 ++++++++ .../video_streaming/buffered_range.proto | 32 +++++++++ .../video_streaming/client_abr_state.proto | 43 ++++++++++++ .../proto/video_streaming/crypto_params.proto | 13 ++++ .../encrypted_player_request.proto | 18 +++++ .../format_initialization_metadata.proto | 17 +++++ .../proto/video_streaming/live_metadata.proto | 13 ++++ .../video_streaming/media_capabilities.proto | 24 +++++++ .../proto/video_streaming/media_header.proto | 30 +++++++++ .../video_streaming/next_request_policy.proto | 12 ++++ .../proto/video_streaming/onesie_header.proto | 27 ++++++++ .../video_streaming/onesie_header_type.proto | 31 +++++++++ .../onesie_player_request.proto | 12 ++++ .../onesie_player_response.proto | 28 ++++++++ .../video_streaming/onesie_request.proto | 19 ++++++ .../video_streaming/playback_cookie.proto | 11 ++++ .../playback_start_policy.proto | 12 ++++ .../proto/video_streaming/proxy_status.proto | 15 +++++ .../request_cancellation_policy.proto | 14 ++++ .../proto/video_streaming/sabr_error.proto | 7 ++ .../proto/video_streaming/sabr_redirect.proto | 6 ++ .../stream_protection_status.proto | 7 ++ .../video_streaming/streamer_context.proto | 66 +++++++++++++++++++ .../proto/video_streaming/time_range.proto | 8 +++ .../video_playback_abr_request.proto | 51 ++++++++++++++ abr-proto/src/lib.rs | 3 + 31 files changed, 655 insertions(+), 1 deletion(-) create mode 100644 abr-proto/Cargo.toml create mode 100644 abr-proto/LICENSE create mode 100644 abr-proto/README.md create mode 100644 abr-proto/build.rs create mode 100644 abr-proto/proto/misc/common.proto create mode 100644 abr-proto/proto/video_streaming/buffered_range.proto create mode 100644 abr-proto/proto/video_streaming/client_abr_state.proto create mode 100644 abr-proto/proto/video_streaming/crypto_params.proto create mode 100644 abr-proto/proto/video_streaming/encrypted_player_request.proto create mode 100644 abr-proto/proto/video_streaming/format_initialization_metadata.proto create mode 100644 abr-proto/proto/video_streaming/live_metadata.proto create mode 100644 abr-proto/proto/video_streaming/media_capabilities.proto create mode 100644 abr-proto/proto/video_streaming/media_header.proto create mode 100644 abr-proto/proto/video_streaming/next_request_policy.proto create mode 100644 abr-proto/proto/video_streaming/onesie_header.proto create mode 100644 abr-proto/proto/video_streaming/onesie_header_type.proto create mode 100644 abr-proto/proto/video_streaming/onesie_player_request.proto create mode 100644 abr-proto/proto/video_streaming/onesie_player_response.proto create mode 100644 abr-proto/proto/video_streaming/onesie_request.proto create mode 100644 abr-proto/proto/video_streaming/playback_cookie.proto create mode 100644 abr-proto/proto/video_streaming/playback_start_policy.proto create mode 100644 abr-proto/proto/video_streaming/proxy_status.proto create mode 100644 abr-proto/proto/video_streaming/request_cancellation_policy.proto create mode 100644 abr-proto/proto/video_streaming/sabr_error.proto create mode 100644 abr-proto/proto/video_streaming/sabr_redirect.proto create mode 100644 abr-proto/proto/video_streaming/stream_protection_status.proto create mode 100644 abr-proto/proto/video_streaming/streamer_context.proto create mode 100644 abr-proto/proto/video_streaming/time_range.proto create mode 100644 abr-proto/proto/video_streaming/video_playback_abr_request.proto create mode 100644 abr-proto/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index ae8bbec..3b7c5c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ description = "Client for the public YouTube / YouTube Music API (Innertube), in include = ["/src", "README.md", "CHANGELOG.md", "LICENSE", "!snapshots"] [workspace] -members = [".", "codegen", "downloader", "cli"] +members = [".", "codegen", "downloader", "cli", "abr-proto"] [workspace.package] edition = "2021" @@ -56,6 +56,7 @@ urlencoding = "2.1.0" quick-xml = { version = "0.37.0", features = ["serialize"] } tracing = { version = "0.1.0", features = ["log"] } localzone = "0.3.1" +protobuf = "3.0.0" # CLI indicatif = "0.17.0" diff --git a/abr-proto/Cargo.toml b/abr-proto/Cargo.toml new file mode 100644 index 0000000..d5624f0 --- /dev/null +++ b/abr-proto/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "rustypipe-abr-proto" +version = "0.1.0" +edition.workspace = true +authors.workspace = true +license = "MIT" +repository.workspace = true +keywords.workspace = true +categories.workspace = true +description = "Protobuf definitions for YouTube adaptive bitrate streaming" + +[dependencies] +protobuf.workspace = true + +[build-dependencies] +protobuf-codegen = "3" diff --git a/abr-proto/LICENSE b/abr-proto/LICENSE new file mode 100644 index 0000000..dbb5408 --- /dev/null +++ b/abr-proto/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2025 ThetaDev + +MIT LICENSE +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/abr-proto/README.md b/abr-proto/README.md new file mode 100644 index 0000000..39e70f4 --- /dev/null +++ b/abr-proto/README.md @@ -0,0 +1,5 @@ +# ![RustyPipe](https://codeberg.org/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg) abr-proto + +[![Current crates.io version](https://img.shields.io/crates/v/rustypipe-abr-proto.svg)](https://crates.io/crates/rustypipe-abr-proto) +[![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](https://opensource.org/licenses/MIT) +[![CI status](https://codeberg.org/ThetaDev/rustypipe/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://codeberg.org/ThetaDev/rustypipe/actions/?workflow=ci.yaml) diff --git a/abr-proto/build.rs b/abr-proto/build.rs new file mode 100644 index 0000000..93174f1 --- /dev/null +++ b/abr-proto/build.rs @@ -0,0 +1,64 @@ +use std::{ + env, fs, + ops::Deref, + path::{Path, PathBuf}, +}; + +fn out_dir() -> PathBuf { + Path::new(&env::var("OUT_DIR").expect("env")).to_path_buf() +} + +fn cleanup() { + let _ = fs::remove_dir_all(out_dir()); +} + +fn compile() { + let proto_dir = Path::new(&env::var("CARGO_MANIFEST_DIR").expect("env")).join("proto"); + let vsdir = proto_dir.join("video_streaming"); + + let files = &[ + proto_dir.join("misc").join("common.proto"), + vsdir.join("buffered_range.proto"), + vsdir.join("client_abr_state.proto"), + vsdir.join("crypto_params.proto"), + vsdir.join("encrypted_player_request.proto"), + vsdir.join("format_initialization_metadata.proto"), + vsdir.join("live_metadata.proto"), + vsdir.join("media_capabilities.proto"), + vsdir.join("media_header.proto"), + vsdir.join("next_request_policy.proto"), + vsdir.join("onesie_header_type.proto"), + vsdir.join("onesie_header.proto"), + vsdir.join("onesie_player_request.proto"), + vsdir.join("onesie_player_response.proto"), + vsdir.join("onesie_request.proto"), + vsdir.join("playback_cookie.proto"), + vsdir.join("playback_start_policy.proto"), + vsdir.join("proxy_status.proto"), + vsdir.join("request_cancellation_policy.proto"), + vsdir.join("sabr_error.proto"), + vsdir.join("sabr_redirect.proto"), + vsdir.join("stream_protection_status.proto"), + vsdir.join("streamer_context.proto"), + vsdir.join("time_range.proto"), + vsdir.join("video_playback_abr_request.proto"), + ]; + + let slices = files.iter().map(Deref::deref).collect::>(); + + let out_dir = out_dir(); + fs::create_dir(&out_dir).expect("create_dir"); + + protobuf_codegen::Codegen::new() + .pure() + .out_dir(&out_dir) + .inputs(&slices) + .include(&proto_dir) + .run() + .expect("Codegen failed."); +} + +fn main() { + cleanup(); + compile(); +} diff --git a/abr-proto/proto/misc/common.proto b/abr-proto/proto/misc/common.proto new file mode 100644 index 0000000..e494b7f --- /dev/null +++ b/abr-proto/proto/misc/common.proto @@ -0,0 +1,28 @@ +syntax = "proto2"; +package misc; + +message HttpHeader { + optional string name = 1; + optional string value = 2; +} + +message FormatId { + optional int32 itag = 1; + optional uint64 last_modified = 2; + optional string xtags = 3; +} + +message InitRange { + optional int32 start = 1; + optional int32 end = 2; +} + +message IndexRange { + optional int32 start = 1; + optional int32 end = 2; +} + +message KeyValuePair { + optional string key = 1; + optional string value = 2; +} diff --git a/abr-proto/proto/video_streaming/buffered_range.proto b/abr-proto/proto/video_streaming/buffered_range.proto new file mode 100644 index 0000000..1cef4eb --- /dev/null +++ b/abr-proto/proto/video_streaming/buffered_range.proto @@ -0,0 +1,32 @@ + +syntax = "proto2"; +package video_streaming; + +import "misc/common.proto"; +import "video_streaming/time_range.proto"; + +message BufferedRange { + required .misc.FormatId format_id = 1; + required int64 start_time_ms = 2; + required int64 duration_ms = 3; + required int32 start_segment_index = 4; + required int32 end_segment_index = 5; + optional TimeRange time_range = 6; + optional Kob field9 = 9; + optional YPa field11 = 11; + optional YPa field12 = 12; +} + +message Kob { + message Pa { + optional string video_id = 1; + optional uint64 lmt = 2; + } + repeated Pa EW = 1; +} + +message YPa { + optional int32 field1 = 1; + optional int32 field2 = 2; + optional int32 field3 = 3; +} diff --git a/abr-proto/proto/video_streaming/client_abr_state.proto b/abr-proto/proto/video_streaming/client_abr_state.proto new file mode 100644 index 0000000..3bd3cfb --- /dev/null +++ b/abr-proto/proto/video_streaming/client_abr_state.proto @@ -0,0 +1,43 @@ +syntax = "proto2"; +package video_streaming; + +message ClientAbrState { + optional int32 time_since_last_manual_format_selection_ms = 13; + optional int32 last_manual_direction = 14; + optional int32 last_manual_selected_resolution = 16; + optional int32 detailed_network_type = 17; + optional int32 client_viewport_width = 18; + optional int32 client_viewport_height = 19; + optional int64 client_bitrate_cap = 20; + optional int32 sticky_resolution = 21; + optional bool client_viewport_is_flexible = 22; + optional int32 bandwidth_estimate = 23; + optional int64 player_time_ms = 28; + optional int64 time_since_last_seek = 29; + optional bool data_saver_mode = 30; + optional int32 visibility = 34; + optional float playback_rate = 35; + optional int64 elapsed_wall_time_ms = 36; + optional bytes media_capabilities = 38; + optional int64 time_since_last_action_ms = 39; + optional int32 enabled_track_types_bitfield = 40; + optional int32 max_pacing_rate = 43; + optional int64 player_state = 44; + optional bool drc_enabled = 46; + optional int32 Jda = 48; + optional int32 qw = 50; + optional int32 Ky = 51; + optional int32 sabr_report_request_cancellation_info = 54; + optional bool l = 56; + optional int64 G7 = 57; + optional bool prefer_vp9 = 58; + optional int32 qj = 59; + optional int32 Hx = 60; + optional bool is_prefetch = 61; + optional int32 sabr_support_quality_constraints = 62; + optional bytes sabr_license_constraint = 63; + optional int32 allow_proxima_live_latency = 64; + optional int32 sabr_force_proxima = 66; + optional int32 Tqb = 67; + optional int64 sabr_force_max_network_interruption_duration_ms = 68; +} diff --git a/abr-proto/proto/video_streaming/crypto_params.proto b/abr-proto/proto/video_streaming/crypto_params.proto new file mode 100644 index 0000000..8c3b077 --- /dev/null +++ b/abr-proto/proto/video_streaming/crypto_params.proto @@ -0,0 +1,13 @@ +syntax = "proto2"; +package video_streaming; + +message CryptoParams { + enum CompressionType { + VAL_0 = 0; + VAL_1 = 1; + VAL_2 = 2; + } + optional bytes hmac = 4; + optional bytes iv = 5; + optional CompressionType compression_type = 6; +} diff --git a/abr-proto/proto/video_streaming/encrypted_player_request.proto b/abr-proto/proto/video_streaming/encrypted_player_request.proto new file mode 100644 index 0000000..bc553e2 --- /dev/null +++ b/abr-proto/proto/video_streaming/encrypted_player_request.proto @@ -0,0 +1,18 @@ +syntax = "proto2"; +package video_streaming; + +import "video_streaming/onesie_player_request.proto"; + +message EncryptedPlayerRequest { + optional bytes context = 1; // InnerTubeContext proto? + optional bytes encrypted_onesie_player_request = 2; + optional bytes encrypted_client_key = 5; + optional bytes iv = 6; + optional bytes hmac = 7; + optional string reverse_proxy_config = 9; + optional bool serialize_response_as_json = 10; + optional bool pM = 13; + optional bool enable_compression = 14; + optional bytes unencrypted_onesie_player_request = 16; + optional bool TQ = 17; +} diff --git a/abr-proto/proto/video_streaming/format_initialization_metadata.proto b/abr-proto/proto/video_streaming/format_initialization_metadata.proto new file mode 100644 index 0000000..b13fa6b --- /dev/null +++ b/abr-proto/proto/video_streaming/format_initialization_metadata.proto @@ -0,0 +1,17 @@ +syntax = "proto2"; +package video_streaming; + +import "misc/common.proto"; + +message FormatInitializationMetadata { + optional string video_id = 1; + optional .misc.FormatId format_id = 2; + optional int32 end_time_ms = 3; + optional int32 field4 = 4; + optional string mime_type = 5; + optional .misc.InitRange init_range = 6; + optional .misc.IndexRange index_range = 7; + optional int32 field8 = 8; + optional int32 duration_ms = 9; + optional int32 field10 = 10; +} diff --git a/abr-proto/proto/video_streaming/live_metadata.proto b/abr-proto/proto/video_streaming/live_metadata.proto new file mode 100644 index 0000000..83c1540 --- /dev/null +++ b/abr-proto/proto/video_streaming/live_metadata.proto @@ -0,0 +1,13 @@ +syntax = "proto2"; +package video_streaming; + +message LiveMetadata { + optional uint32 head_sequence_number = 3; + optional uint64 head_time_ms = 4; + optional uint64 wall_time_ms = 5; + optional uint64 field10 = 10; + optional uint64 field12 = 12; + optional uint64 field13 = 13; + optional uint64 head_time_usec = 14; + optional uint64 field15 = 15; +} diff --git a/abr-proto/proto/video_streaming/media_capabilities.proto b/abr-proto/proto/video_streaming/media_capabilities.proto new file mode 100644 index 0000000..78dfcec --- /dev/null +++ b/abr-proto/proto/video_streaming/media_capabilities.proto @@ -0,0 +1,24 @@ +syntax = "proto2"; +package video_streaming; + +message MediaCapabilities { + repeated VideoFormatCapability video_format_capabilities = 1; + repeated AudioFormatCapability audio_format_capabilities = 2; + optional int32 hdr_mode_bitmask = 5; + + message VideoFormatCapability { + optional int32 video_codec = 1; + optional int32 max_height = 3; + optional int32 max_width = 4; + optional int32 max_framerate = 11; + optional int32 max_bitrate_bps = 12; + optional bool is_10_bit_supported = 15; + } + + message AudioFormatCapability { + optional int32 audio_codec = 1; + optional int32 num_channels = 2; + optional int32 max_bitrate_bps = 3; + optional int32 spatial_capability_bitmask = 6; + } +} diff --git a/abr-proto/proto/video_streaming/media_header.proto b/abr-proto/proto/video_streaming/media_header.proto new file mode 100644 index 0000000..58aa007 --- /dev/null +++ b/abr-proto/proto/video_streaming/media_header.proto @@ -0,0 +1,30 @@ +syntax = "proto2"; +package video_streaming; + +import "misc/common.proto"; +import "video_streaming/time_range.proto"; + +message MediaHeader { + optional uint32 header_id = 1; + optional string video_id = 2; + optional int32 itag = 3; + optional uint64 lmt = 4; + optional string xtags = 5; + optional int32 start_data_range = 6; + optional Compression compression = 7; + optional bool is_init_seg = 8; + optional int64 sequence_number = 9; + optional int64 field10 = 10; + optional int32 start_ms = 11; + optional int32 duration_ms = 12; + optional .misc.FormatId format_id = 13; + optional int64 content_length = 14; + optional TimeRange time_range = 15; + + enum Compression { + VAL0 = 0; + VAL1 = 1; + GZIP = 2; + } + +} diff --git a/abr-proto/proto/video_streaming/next_request_policy.proto b/abr-proto/proto/video_streaming/next_request_policy.proto new file mode 100644 index 0000000..6556912 --- /dev/null +++ b/abr-proto/proto/video_streaming/next_request_policy.proto @@ -0,0 +1,12 @@ +syntax = "proto2"; +package video_streaming; + +import "video_streaming/playback_cookie.proto"; + +message NextRequestPolicy { + optional int32 target_audio_readahead_ms = 1; + optional int32 target_video_readahead_ms = 2; + optional int32 backoff_time_ms = 4; + optional .video_streaming.PlaybackCookie playback_cookie = 7; + optional string video_id = 8; +} diff --git a/abr-proto/proto/video_streaming/onesie_header.proto b/abr-proto/proto/video_streaming/onesie_header.proto new file mode 100644 index 0000000..5cd8167 --- /dev/null +++ b/abr-proto/proto/video_streaming/onesie_header.proto @@ -0,0 +1,27 @@ +syntax = "proto2"; +package video_streaming; + +import "video_streaming/onesie_header_type.proto"; +import "video_streaming/crypto_params.proto"; + +message OnesieHeader { + message Field23 { + optional string video_id = 2; + } + + message Field34 { + repeated string itag_denylist = 1; + } + + optional OnesieHeaderType type = 1; + optional string video_id = 2; + optional string itag = 3; + optional CryptoParams crypto_params = 4; + optional uint64 last_modified = 5; + optional int64 media_size_bytes = 7; + repeated string restricted_formats = 11; + optional string xtags = 15; + optional int64 sequence_number = 18; + optional Field23 field23 = 23; + optional Field34 field34 = 34; +} diff --git a/abr-proto/proto/video_streaming/onesie_header_type.proto b/abr-proto/proto/video_streaming/onesie_header_type.proto new file mode 100644 index 0000000..021f01b --- /dev/null +++ b/abr-proto/proto/video_streaming/onesie_header_type.proto @@ -0,0 +1,31 @@ +syntax = "proto2"; +package video_streaming; + +enum OnesieHeaderType { + PLAYER_RESPONSE = 0; + HEADER_TYPE_1 = 1; + MEDIA_DECRYPTION_KEY = 2; + HEADER_TYPE_3 = 3; + HEADER_TYPE_4 = 4; + HEADER_TYPE_5 = 5; + NEW_HOST = 6; + HEADER_TYPE_7 = 7; + HEADER_TYPE_8 = 8; + HEADER_TYPE_9 = 9; + HEADER_TYPE_10 = 10; + HEADER_TYPE_11 = 11; + HEADER_TYPE_12 = 12; + HEADER_TYPE_13 = 13; + RESTRICTED_FORMATS_HINT = 14; + HEADER_TYPE_15 = 15; + STREAM_METADATA = 16; + HEADER_TYPE_17 = 17; + HEADER_TYPE_18 = 18; + HEADER_TYPE_19 = 19; + HEADER_TYPE_20 = 20; + HEADER_TYPE_21 = 21; + HEADER_TYPE_22 = 22; + HEADER_TYPE_23 = 23; + HEADER_TYPE_24 = 24; + ENCRYPTED_INNERTUBE_RESPONSE_PART = 25; +} diff --git a/abr-proto/proto/video_streaming/onesie_player_request.proto b/abr-proto/proto/video_streaming/onesie_player_request.proto new file mode 100644 index 0000000..844bd66 --- /dev/null +++ b/abr-proto/proto/video_streaming/onesie_player_request.proto @@ -0,0 +1,12 @@ +syntax = "proto2"; +package video_streaming; + +import "misc/common.proto"; + +message OnesiePlayerRequest { + optional string url = 1; + repeated misc.HttpHeader headers = 2; + optional string body = 3; + optional bool proxied_by_trusted_bandaid = 4; + optional bool skip_response_encryption = 6; +} diff --git a/abr-proto/proto/video_streaming/onesie_player_response.proto b/abr-proto/proto/video_streaming/onesie_player_response.proto new file mode 100644 index 0000000..41d97a4 --- /dev/null +++ b/abr-proto/proto/video_streaming/onesie_player_response.proto @@ -0,0 +1,28 @@ +syntax = "proto2"; +package video_streaming; + +import "misc/common.proto"; + +enum OnesieProxyStatus { + ONESIE_PROXY_STATUS_UNKNOWN = 0; + ONESIE_PROXY_STATUS_OK = 1; + ONESIE_PROXY_STATUS_DECRYPTION_FAILED = 2; + ONESIE_PROXY_STATUS_PARSING_FAILED = 3; + ONESIE_PROXY_STATUS_MISSING_X_FORWARDED_FOR = 4; + ONESIE_PROXY_STATUS_INVALID_X_FORWARDED_FOR = 5; + ONESIE_PROXY_STATUS_INVALID_CONTENT_TYPE = 6; + ONESIE_PROXY_STATUS_BACKEND_ERROR = 7; + ONESIE_PROXY_STATUS_CLIENT_ERROR = 8; + ONESIE_PROXY_STATUS_MISSING_CRYPTER = 9; + ONESIE_PROXY_STATUS_RESPONSE_JSON_SERIALIZATION_FAILED = 10; + ONESIE_PROXY_STATUS_DECOMPRESSION_FAILED = 11; + ONESIE_PROXY_STATUS_JSON_PARSING_FAILED = 12; + ONESIE_PROXY_STATUS_UNKNOWN_COMPRESSION_TYPE = 13; +} + +message OnesiePlayerResponse { + optional OnesieProxyStatus onesie_proxy_status = 1; + optional int32 http_status = 2; + repeated .misc.HttpHeader headers = 3; + optional bytes body = 4; +} diff --git a/abr-proto/proto/video_streaming/onesie_request.proto b/abr-proto/proto/video_streaming/onesie_request.proto new file mode 100644 index 0000000..5dbf396 --- /dev/null +++ b/abr-proto/proto/video_streaming/onesie_request.proto @@ -0,0 +1,19 @@ +syntax = "proto2"; +package video_streaming; + +import "video_streaming/client_abr_state.proto"; +import "video_streaming/encrypted_player_request.proto"; +import "video_streaming/streamer_context.proto"; +import "video_streaming/buffered_range.proto"; + +message OnesieRequest { + repeated string urls = 1; + optional ClientAbrState client_abr_state = 2; + optional EncryptedPlayerRequest player_request = 3; + optional bytes onesie_ustreamer_config = 4; + optional int32 max_vp9_height = 5; + optional int32 client_display_height = 6; + optional StreamerContext streamer_context = 10; + optional int32 request_target = 13; // MLOnesieRequestTarget + repeated BufferedRange buffered_ranges = 14; +} diff --git a/abr-proto/proto/video_streaming/playback_cookie.proto b/abr-proto/proto/video_streaming/playback_cookie.proto new file mode 100644 index 0000000..eb8ca09 --- /dev/null +++ b/abr-proto/proto/video_streaming/playback_cookie.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; +package video_streaming; + +import "misc/common.proto"; + +message PlaybackCookie { + optional int32 field1 = 1; // Always 999999?? + optional int32 field2 = 2; + optional .misc.FormatId video_fmt = 7; + optional .misc.FormatId audio_fmt = 8; +} diff --git a/abr-proto/proto/video_streaming/playback_start_policy.proto b/abr-proto/proto/video_streaming/playback_start_policy.proto new file mode 100644 index 0000000..2b43367 --- /dev/null +++ b/abr-proto/proto/video_streaming/playback_start_policy.proto @@ -0,0 +1,12 @@ +syntax = "proto2"; +package video_streaming; + +message PlaybackStartPolicy { + message ReadaheadPolicy { + optional int32 min_readahead_ms = 2; + optional int32 min_bandwidth_bytes_per_sec = 1; + } + + optional ReadaheadPolicy start_min_readahead_policy = 1; + optional ReadaheadPolicy resume_min_readahead_policy = 2; +} diff --git a/abr-proto/proto/video_streaming/proxy_status.proto b/abr-proto/proto/video_streaming/proxy_status.proto new file mode 100644 index 0000000..18fe610 --- /dev/null +++ b/abr-proto/proto/video_streaming/proxy_status.proto @@ -0,0 +1,15 @@ +syntax = "proto2"; +package video_streaming; + +enum ProxyStatus { + VAL_0 = 0; + OK = 1; + VAL_2 = 2; + VAL_3 = 3; + VAL_4 = 4; + VAL_5 = 5; + VAL_6 = 6; + VAL_7 = 7; + VAL_8 = 8; + VAL_9 = 9; +} diff --git a/abr-proto/proto/video_streaming/request_cancellation_policy.proto b/abr-proto/proto/video_streaming/request_cancellation_policy.proto new file mode 100644 index 0000000..110c259 --- /dev/null +++ b/abr-proto/proto/video_streaming/request_cancellation_policy.proto @@ -0,0 +1,14 @@ +syntax = "proto2"; +package video_streaming; + +message RequestCancellationPolicy { + message Item { + optional int32 fR = 1; + optional int32 NK = 2; + optional int32 minReadaheadMs = 3; + } + + optional int32 N0 = 1; + repeated Item items = 2; + optional int32 jq = 3; +} diff --git a/abr-proto/proto/video_streaming/sabr_error.proto b/abr-proto/proto/video_streaming/sabr_error.proto new file mode 100644 index 0000000..f05311b --- /dev/null +++ b/abr-proto/proto/video_streaming/sabr_error.proto @@ -0,0 +1,7 @@ +syntax = "proto2"; +package video_streaming; + +message SabrError { + optional string type = 1; + optional int32 code = 2; +} diff --git a/abr-proto/proto/video_streaming/sabr_redirect.proto b/abr-proto/proto/video_streaming/sabr_redirect.proto new file mode 100644 index 0000000..e660ad3 --- /dev/null +++ b/abr-proto/proto/video_streaming/sabr_redirect.proto @@ -0,0 +1,6 @@ +syntax = "proto2"; +package video_streaming; + +message SabrRedirect { + optional string url = 1; +} diff --git a/abr-proto/proto/video_streaming/stream_protection_status.proto b/abr-proto/proto/video_streaming/stream_protection_status.proto new file mode 100644 index 0000000..2f2cba4 --- /dev/null +++ b/abr-proto/proto/video_streaming/stream_protection_status.proto @@ -0,0 +1,7 @@ +syntax = "proto2"; +package video_streaming; + +message StreamProtectionStatus { + optional int32 status = 1; + optional int32 field2 = 2; +} diff --git a/abr-proto/proto/video_streaming/streamer_context.proto b/abr-proto/proto/video_streaming/streamer_context.proto new file mode 100644 index 0000000..3a766c3 --- /dev/null +++ b/abr-proto/proto/video_streaming/streamer_context.proto @@ -0,0 +1,66 @@ +syntax = "proto2"; +package video_streaming; + +message StreamerContext { + message ClientInfo { + optional string device_make = 12; + optional string device_model = 13; + optional int32 client_name = 16; + optional string client_version = 17; + optional string os_name = 18; + optional string os_version = 19; + optional string accept_language = 21; + optional string accept_region = 22; + optional int32 screen_width_points = 37; + optional int32 screen_height_points = 38; + optional float screen_width_inches = 39; + optional float screen_height_inches = 40; + optional int32 screen_pixel_density = 41; + optional ClientFormFactor client_form_factor = 46; + optional int32 gmscore_version_code = 50; // e.g. 243731017 + optional int32 window_width_points = 55; + optional int32 window_height_points = 56; + optional int32 android_sdk_version = 64; + optional float screen_density_float = 65; + optional int64 utc_offset_minutes = 67; + optional string time_zone = 80; + optional string chipset = 92; // e.g. "qcom;taro" + optional GLDeviceInfo gl_device_info = 102; + } + + enum ClientFormFactor { + UNKNOWN_FORM_FACTOR = 0; + FORM_FACTOR_VAL1 = 1; + FORM_FACTOR_VAL2 = 2; + } + + message GLDeviceInfo { + optional string gl_renderer = 1; + optional int32 gl_es_version_major = 2; + optional int32 gl_es_version_minor = 3; + } + + message Fqa { + optional int32 type = 1; + optional bytes value = 2; + } + + message Gqa { + message Hqa { + optional int32 code = 1; + optional string message = 2; + } + + optional bytes field1 = 1; + optional Hqa field2 = 2; + } + + optional ClientInfo client_info = 1; + optional bytes po_token = 2; + optional bytes playback_cookie = 3; + optional bytes gp = 4; + repeated Fqa field5 = 5; + repeated int32 field6 = 6; + optional string field7 = 7; + optional Gqa field8 = 8; +} diff --git a/abr-proto/proto/video_streaming/time_range.proto b/abr-proto/proto/video_streaming/time_range.proto new file mode 100644 index 0000000..e85d2f3 --- /dev/null +++ b/abr-proto/proto/video_streaming/time_range.proto @@ -0,0 +1,8 @@ +syntax = "proto2"; +package video_streaming; + +message TimeRange { + optional int64 start = 1; + optional int64 duration = 2; + optional int32 timescale = 3; +} diff --git a/abr-proto/proto/video_streaming/video_playback_abr_request.proto b/abr-proto/proto/video_streaming/video_playback_abr_request.proto new file mode 100644 index 0000000..cc16499 --- /dev/null +++ b/abr-proto/proto/video_streaming/video_playback_abr_request.proto @@ -0,0 +1,51 @@ +syntax = "proto2"; +package video_streaming; + +import "misc/common.proto"; +import "video_streaming/client_abr_state.proto"; +import "video_streaming/streamer_context.proto"; +import "video_streaming/buffered_range.proto"; + +message VideoPlaybackAbrRequest { + optional ClientAbrState client_abr_state = 1; + repeated .misc.FormatId selected_format_ids = 2; + repeated BufferedRange buffered_ranges = 3; + optional int64 player_time_ms = 4; + optional bytes video_playback_ustreamer_config = 5; + optional Lo lo = 6; + repeated .misc.FormatId selected_audio_format_ids = 16; + repeated .misc.FormatId selected_video_format_ids = 17; + optional StreamerContext streamer_context = 19; + optional OQa field21 = 21; + optional int32 field22 = 22; + optional int32 field23 = 23; + repeated Pqa field1000 = 1000; +} + +message Lo { + message Field4 { + optional int32 field1 = 1; + optional int32 field2 = 2; + optional int32 field3 = 3; + } + optional .misc.FormatId format_id = 1; + optional int32 Lj = 2; + optional int32 sequence_number = 3; + optional Field4 field4 = 4; + optional int32 MZ = 5; +} + +message OQa { + repeated string field1 = 1; + optional bytes field2 = 2; + optional string field3 = 3; + optional int32 field4 = 4; + optional int32 field5 = 5; + optional string field6 = 6; +} + +message Pqa { + repeated .misc.FormatId formats = 1; + repeated BufferedRange ud = 2; + optional string clip_id = 3; +} diff --git a/abr-proto/src/lib.rs b/abr-proto/src/lib.rs new file mode 100644 index 0000000..16a04e9 --- /dev/null +++ b/abr-proto/src/lib.rs @@ -0,0 +1,3 @@ +#![doc = include_str!("../README.md")] + +include!(concat!(env!("OUT_DIR"), "/mod.rs")); From f4f3e4f25671a241fbc153434b2aaa328d96fe77 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 22 Mar 2025 21:46:05 +0100 Subject: [PATCH 03/20] wip: working prototype --- Cargo.toml | 1 + .../format_initialization_metadata.proto | 4 +- codegen/src/abtest.rs | 9 + downloader/Cargo.toml | 7 + downloader/src/abr.rs | 676 +++++++++++++++ downloader/src/abr2.rs | 799 ++++++++++++++++++ downloader/src/abr_model.rs | 87 ++ downloader/src/error.rs | 18 + downloader/src/lib.rs | 3 + downloader/src/util.rs | 145 ++++ notes/AB_Tests.md | 7 + src/client/player.rs | 12 +- src/client/response/player.rs | 7 +- src/model/mod.rs | 10 + src/model/traits.rs | 20 + src/param/stream_filter.rs | 13 +- 16 files changed, 1805 insertions(+), 13 deletions(-) create mode 100644 downloader/src/abr.rs create mode 100644 downloader/src/abr2.rs create mode 100644 downloader/src/abr_model.rs diff --git a/Cargo.toml b/Cargo.toml index 3b7c5c0..547b84e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ rustypipe-downloader = { path = "./downloader", version = "0.3.1", default-featu "indicatif", "audiotag", ] } +rustypipe-abr-proto = { path = "./abr-proto", version = "0.1.0" } [features] default = ["default-tls"] diff --git a/abr-proto/proto/video_streaming/format_initialization_metadata.proto b/abr-proto/proto/video_streaming/format_initialization_metadata.proto index b13fa6b..ce7e08e 100644 --- a/abr-proto/proto/video_streaming/format_initialization_metadata.proto +++ b/abr-proto/proto/video_streaming/format_initialization_metadata.proto @@ -7,11 +7,11 @@ message FormatInitializationMetadata { optional string video_id = 1; optional .misc.FormatId format_id = 2; optional int32 end_time_ms = 3; - optional int32 field4 = 4; + optional int32 sequence_count = 4; optional string mime_type = 5; optional .misc.InitRange init_range = 6; optional .misc.IndexRange index_range = 7; optional int32 field8 = 8; optional int32 duration_ms = 9; - optional int32 field10 = 10; + optional int32 field10 = 10; } diff --git a/codegen/src/abtest.rs b/codegen/src/abtest.rs index de8001a..0f90b13 100644 --- a/codegen/src/abtest.rs +++ b/codegen/src/abtest.rs @@ -42,6 +42,7 @@ pub enum ABTest { MusicContinuationItemRenderer = 20, AlbumRecommends = 21, CommandExecutorCommand = 22, + AbrStreamOnly = 23, } /// List of active A/B tests that are run when none is manually specified @@ -49,6 +50,7 @@ const TESTS_TO_RUN: &[ABTest] = &[ ABTest::MusicAlbumGroupsReordered, ABTest::AlbumRecommends, ABTest::CommandExecutorCommand, + ABTest::AbrStreamOnly, ]; #[derive(Debug, Serialize, Deserialize)] @@ -126,6 +128,7 @@ pub async fn run_test( } ABTest::AlbumRecommends => album_recommends(&query).await, ABTest::CommandExecutorCommand => command_executor_command(&query).await, + ABTest::AbrStreamOnly => abr_stream_only(&query).await, } .unwrap(); pb.inc(1); @@ -478,3 +481,9 @@ pub async fn command_executor_command(rp: &RustyPipeQuery) -> Result { .await?; Ok(res.contains("\"commandExecutorCommand\"")) } + +pub async fn abr_stream_only(rp: &RustyPipeQuery) -> Result { + let id = "pxY4OXVyMe4"; + let res = rp.player_from_client(id, ClientType::Desktop).await?; + Ok(res.video_streams.iter().all(|s| s.url.is_none())) +} diff --git a/downloader/Cargo.toml b/downloader/Cargo.toml index dba74ce..d5d1613 100644 --- a/downloader/Cargo.toml +++ b/downloader/Cargo.toml @@ -34,6 +34,7 @@ audiotag = ["dep:lofty", "dep:image", "dep:smartcrop2"] [dependencies] rustypipe.workspace = true +rustypipe-abr-proto.workspace = true once_cell.workspace = true regex.workspace = true thiserror.workspace = true @@ -52,10 +53,16 @@ image = { version = "0.25.0", optional = true, default-features = false, feature "webp", ] } smartcrop2 = { version = "0.4.0", optional = true } +data-encoding.workspace = true +protobuf.workspace = true +bytes = "1.0.0" +byteorder = "1.0.0" +async-stream = "0.3.6" [dev-dependencies] path_macro.workspace = true rstest.workspace = true +tracing-test.workspace = true serde_json.workspace = true temp_testdir = "0.2.3" diff --git a/downloader/src/abr.rs b/downloader/src/abr.rs new file mode 100644 index 0000000..350d9b0 --- /dev/null +++ b/downloader/src/abr.rs @@ -0,0 +1,676 @@ +#![allow(unused)] + +use std::collections::{HashMap, HashSet}; + +use bytes::Bytes; +use data_encoding::BASE64URL; +use protobuf::{Message, MessageField}; +use reqwest::Client; +use rustypipe::model::{AudioStream, VideoStream}; + +pub use rustypipe_abr_proto::client_abr_state::ClientAbrState; +use rustypipe_abr_proto::{ + buffered_range::BufferedRange, + common::FormatId, + format_initialization_metadata::FormatInitializationMetadata, + media_header::MediaHeader, + next_request_policy::NextRequestPolicy, + playback_cookie::PlaybackCookie, + sabr_error::SabrError, + sabr_redirect::SabrRedirect, + stream_protection_status::StreamProtectionStatus, + streamer_context::{streamer_context::ClientInfo, StreamerContext}, + time_range::TimeRange, + video_playback_abr_request::VideoPlaybackAbrRequest, +}; + +use crate::abr_model::PartType; +use crate::error::AbrError; + +pub struct AbrStream { + http: Client, + initial_url: String, + ustreamer_config: Vec, + po_token: Option>, +} + +#[derive(Debug)] +pub struct StreamingState { + abr_streaming_url: String, + abr_state: ClientAbrState, + playback_cookie: Option, + backoff_time_ms: i32, + formats_by_key: HashMap, + header_id_to_format_key_map: HashMap, + previous_sequences: HashMap>, +} + +struct InitializedFormat { + format_id: FormatId, + duration_ms: Option, + mime_type: Option, + sequence_count: Option, + sequence_list: Vec, + media_chunks: Vec, + state: BufferedRange, +} + +#[derive(Debug)] +struct Sequence { + itag: Option, + format_id: FormatId, + is_init_segment: bool, + duration_ms: Option, + start_ms: Option, + start_data_range: Option, + sequence_number: Option, + content_length: Option, + time_range: Option, +} + +struct UmpPart<'a> { + part_type: PartType, + size: u64, + data: &'a [u8], +} + +impl std::fmt::Debug for InitializedFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("InitializedFormat") + .field("format_id", &self.format_id) + .field("duration_ms", &self.duration_ms) + .field("mime_type", &self.mime_type) + .field("sequence_count", &self.sequence_count) + .field("sequence_list", &self.sequence_list) + .field("media_chunks", &self.media_chunks.len()) + .field("state", &self.state) + .finish() + } +} + +impl std::fmt::Debug for UmpPart<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UmpPart") + .field("part_type", &self.part_type) + .field("size", &self.size) + .field("data", &self.data.len()) + .finish() + } +} + +impl AbrStream { + pub fn new( + url: String, + ustreamer_config: &str, + po_token: Option<&str>, + http: Client, + ) -> Result { + let ustreamer_config = BASE64URL.decode(ustreamer_config.as_bytes()).map_err(|e| { + AbrError::Invalid(format!("could not parse ustreamer_config: {e}").into()) + })?; + let po_token = + match po_token { + Some(pot) => Some(BASE64URL.decode(pot.as_bytes()).map_err(|e| { + AbrError::Invalid(format!("could not parse po_token: {e}").into()) + })?), + None => None, + }; + Ok(Self { + http, + initial_url: url, + ustreamer_config, + po_token, + }) + } + + pub async fn init( + &self, + audio_streams: &[&AudioStream], + video_streams: &[&VideoStream], + mut abr_state: ClientAbrState, + ) -> Result { + if !abr_state.has_last_manual_direction() { + abr_state.set_last_manual_direction(0); + } + if !abr_state.has_time_since_last_manual_format_selection_ms() { + abr_state.set_time_since_last_manual_format_selection_ms(0); + } + if !abr_state.has_player_time_ms() { + abr_state.set_player_time_ms(0); + } + if !abr_state.has_visibility() { + abr_state.set_visibility(0); + } + if !abr_state.has_enabled_track_types_bitfield() { + abr_state.set_enabled_track_types_bitfield(i32::from(video_streams.is_empty())); + } + if let Some(v) = video_streams.first() { + if !abr_state.has_last_manual_selected_resolution() { + abr_state.set_last_manual_selected_resolution(v.height as i32); + } + if !abr_state.has_sticky_resolution() { + abr_state.set_sticky_resolution(v.height as i32); + } + } + + let audio_format_ids = audio_streams + .iter() + .map(|s| FormatId { + itag: Some(s.itag as i32), + last_modified: s.last_modified, + xtags: s.xtags.clone(), + ..Default::default() + }) + .collect::>(); + let video_format_ids = video_streams + .iter() + .map(|s| FormatId { + itag: Some(s.itag as i32), + last_modified: s.last_modified, + xtags: s.xtags.clone(), + ..Default::default() + }) + .collect::>(); + let mut state = StreamingState::new(self.initial_url.to_owned(), abr_state); + let mut ri = 0; + + loop { + let data = self + .fetch_media(audio_format_ids.clone(), video_format_ids.clone(), &state) + .await?; + tracing::debug!("request #{ri} fetched {} bytes", data.len()); + + let ump_parts = Self::parse_ump(&data); + if state.process_ump_parts(&ump_parts)? { + break; + } + ri += 1; + } + + Ok(state) + } + + async fn fetch_media( + &self, + audio_format_ids: Vec, + video_format_ids: Vec, + state: &StreamingState, + ) -> Result { + let body = VideoPlaybackAbrRequest { + client_abr_state: MessageField::some(state.abr_state.clone()), + selected_format_ids: state + .formats_by_key + .values() + .map(|f| f.format_id.clone()) + .collect(), + buffered_ranges: state + .formats_by_key + .values() + .map(|f| f.state.clone()) + .collect(), + // player_time_ms: todo!(), + video_playback_ustreamer_config: Some(self.ustreamer_config.clone()), + selected_audio_format_ids: audio_format_ids, + selected_video_format_ids: video_format_ids, + streamer_context: MessageField::some(StreamerContext { + client_info: MessageField::some(ClientInfo { + client_name: Some(1), + client_version: Some("2.20250314.01.00".to_owned()), + ..Default::default() + }), + po_token: self.po_token.clone(), + playback_cookie: state + .playback_cookie + .as_ref() + .and_then(|c| c.write_to_bytes().ok()), + ..Default::default() + }), + ..Default::default() + }; + + let res = self + .http + .post(&state.abr_streaming_url) + .body(body.write_to_bytes()?) + .send() + .await? + .error_for_status()?; + + // let dstream = res.bytes_stream(); + let data = res.bytes().await?; + tracing::debug!("got {} bytes", data.len()); + + Ok(data) + } + + fn parse_ump(data: &[u8]) -> Vec> { + let mut parts = Vec::new(); + let mut offset = 0; + while offset < data.len() { + let (part_type, new_offset) = Self::read_varint(&data[offset..]); + if new_offset == 0 { + break; + } + offset += new_offset; + let (part_size, new_offset) = Self::read_varint(&data[offset..]); + if new_offset == 0 { + break; + } + offset += new_offset; + + let data_offset = offset + part_size as usize; + let part_data = &data[offset..data_offset.min(data.len())]; + offset = data_offset; + + parts.push(UmpPart { + part_type: part_type.into(), + size: part_size, + data: part_data, + }); + } + parts + } + + fn read_varint(data: &[u8]) -> (u64, usize) { + /* + fn varint_size(byte: u8) -> u8 { + let mut lo = 0; + for i in (4u8..8).rev() { + if byte & (1 << i) == 0 { + break; + } else { + lo += 1; + } + } + return (lo + 1).min(5); + } + + if data.is_empty() { + return (0, 0); + } + + let prefix = data[0]; + let size = varint_size(prefix); + + let mut result = 0u64; + let mut shift = 0; + + if size != 5 { + shift = 8 - size; + let mask = (1 << shift) - 1; + result |= u64::from(prefix) & mask; + } + + let total_size = usize::from(size) + 1; + for byte in &data[1..total_size] { + result |= u64::from(*byte) << shift; + shift += 8; + } + + (result, total_size) + */ + + // Determine the length of the val + + if data.is_empty() { + return (0, 0); + } + + let fb = data[0]; + let byte_length = if fb < 128 { + 1 + } else if fb < 192 { + 2 + } else if fb < 224 { + 3 + } else if fb < 240 { + 4 + } else { + 5 + }; + + if data.len() < byte_length { + return (0, 0); + } + + let n = match byte_length { + 1 => data[0].into(), + 2 => { + let b1 = u64::from(data[0]); + let b2 = u64::from(data[1]); + (b1 & 0x3f) + 64 * b2 + } + 3 => { + let b1 = u64::from(data[0]); + let b2 = u64::from(data[1]); + let b3 = u64::from(data[2]); + (b1 & 0x1f) + 32 * (b2 + 256 * b3) + } + 4 => { + let b1 = u64::from(data[0]); + let b2 = u64::from(data[1]); + let b3 = u64::from(data[2]); + let b4 = u64::from(data[3]); + (b1 & 0x0f) + 16 * (b2 + 256 * (b3 + 256 * b4)) + } + _ => u32::from_be_bytes([data[1], data[2], data[3], data[4]]).into(), + }; + (n, byte_length) + } +} + +impl StreamingState { + fn new(url: String, abr_state: ClientAbrState) -> Self { + Self { + abr_streaming_url: url, + abr_state, + playback_cookie: None, + backoff_time_ms: 0, + formats_by_key: HashMap::new(), + header_id_to_format_key_map: HashMap::new(), + previous_sequences: HashMap::new(), + } + } + + fn process_ump_parts(&mut self, ump_parts: &[UmpPart]) -> Result { + for f in self.formats_by_key.values_mut() { + f.sequence_list.clear(); + } + + for part in ump_parts { + match part.part_type { + PartType::MEDIA_HEADER => { + self.process_media_header(part.data)?; + } + PartType::MEDIA => { + self.process_media_data(part.data)?; + } + PartType::MEDIA_END => { + self.process_media_end(part.data); + } + PartType::NEXT_REQUEST_POLICY => { + self.process_next_request_policy(part.data)?; + } + PartType::FORMAT_INITIALIZATION_METADATA => { + self.process_format_initialization(part.data)?; + } + PartType::SABR_ERROR => { + return Err(AbrError::Sabr(SabrError::parse_from_bytes(part.data)?)); + } + PartType::SABR_REDIRECT => { + self.process_sabr_redirect(part.data)?; + } + PartType::STREAM_PROTECTION_STATUS => { + let prot = StreamProtectionStatus::parse_from_bytes(part.data)?; + match prot.status() { + 1 => tracing::debug!("[StreamProtectionStatus]: Good"), + 2 => tracing::warn!("[StreamProtectionStatus]: Attestation pending"), + 3 => return Err(AbrError::Attestation), + _ => {} + } + } + _ => {} + } + } + + let main_format = self + .formats_by_key + .values() + .find(|fmt| { + fmt.mime_type + .as_deref() + .map(|t| t.contains("video")) + .unwrap_or_default() + }) + .or(self.formats_by_key.values().next()); + if let Some(main_format) = main_format { + self.abr_state.set_player_time_ms( + self.abr_state.player_time_ms() + + main_format.sequence_list.iter().fold(0, |acc, seq| { + acc + i64::from(seq.duration_ms.unwrap_or_default()) + }), + ); + + tracing::debug!( + "sequence_count={}, last={}", + main_format.sequence_count.unwrap_or_default(), + main_format + .sequence_list + .last() + .and_then(|x| x.sequence_number) + .unwrap_or_default() + ); + + let is_last = main_format + .sequence_count + .zip(main_format.sequence_list.last()) + .map(|(sc, last)| last.sequence_number == Some(sc.into())) + .unwrap_or_default(); + Ok(is_last) + } else { + Ok(true) + } + } + + fn process_media_header(&mut self, data: &[u8]) -> Result<(), AbrError> { + let mh = MediaHeader::parse_from_bytes(data)?; + + let format_id = mh + .format_id + .as_ref() + .ok_or(AbrError::Invalid("media header: no format_id".into()))?; + let header_id = mh + .header_id + .and_then(|hid| u8::try_from(hid).ok()) + .ok_or(AbrError::Invalid("media header: no header_id".into()))?; + + let format_key = get_format_key(format_id); + + if !self.formats_by_key.contains_key(&format_key) { + self.formats_by_key + .insert(format_key.to_owned(), InitializedFormat::try_from(&mh)?); + } + let current_format = self.formats_by_key.get_mut(&format_key).unwrap(); + + // This is a hacky workaround to prevent duplicate sequences from being added. This should be fixed in the future + // (preferably by figuring out how to make the server not send duplicates). + if let Some(sequence_number) = mh.sequence_number { + let pseq = self + .previous_sequences + .entry(format_key.to_owned()) + .or_default(); + if !pseq.insert(sequence_number) { + tracing::warn!("duplicate sequence {sequence_number}"); + return Ok(()); + } + } + + // Save the header's ID so we can identify its stream data later. + self.header_id_to_format_key_map + .entry(header_id) + .or_insert(format_key); + + if current_format + .sequence_list + .iter() + .all(|x| x.sequence_number != Some(mh.sequence_number())) + { + current_format.sequence_list.push(Sequence { + itag: mh.itag, + format_id: format_id.clone(), + is_init_segment: mh.is_init_seg(), + duration_ms: mh.duration_ms, + start_ms: mh.start_ms, + start_data_range: mh.start_data_range, + sequence_number: mh.sequence_number, + content_length: mh.content_length, + time_range: mh.time_range.as_ref().cloned(), + }); + + if mh.has_sequence_number() { + current_format.state.set_duration_ms( + current_format.state.duration_ms() + i64::from(mh.duration_ms()), + ); + current_format + .state + .set_end_segment_index(current_format.state.end_segment_index() + 1); + } + } + + Ok(()) + } + + fn process_media_data(&mut self, data: &[u8]) -> Result<(), AbrError> { + let header_id = data[0]; + let format_key = self + .header_id_to_format_key_map + .get(&header_id) + .ok_or_else(|| { + AbrError::Invalid(format!("media: unknown header id {header_id}").into()) + })?; + let current_format = self + .formats_by_key + .get_mut(format_key) + .ok_or(AbrError::Invalid("no current format".into()))?; + + current_format.media_chunks.extend_from_slice(&data[1..]); + Ok(()) + } + + fn process_media_end(&mut self, data: &[u8]) { + let header_id = data[0]; + self.header_id_to_format_key_map.remove(&header_id); + } + + fn process_next_request_policy(&mut self, data: &[u8]) -> Result<(), AbrError> { + let mut policy = NextRequestPolicy::parse_from_bytes(data)?; + self.playback_cookie = policy.playback_cookie.take(); + self.backoff_time_ms = policy.backoff_time_ms(); + Ok(()) + } + + fn process_format_initialization(&mut self, data: &[u8]) -> Result<(), AbrError> { + let init = FormatInitializationMetadata::parse_from_bytes(data)?; + let format = InitializedFormat::try_from(init)?; + let format_key = get_format_key(&format.format_id); + self.formats_by_key.insert(format_key, format); + Ok(()) + } + + fn process_sabr_redirect(&mut self, data: &[u8]) -> Result<(), AbrError> { + let redir = SabrRedirect::parse_from_bytes(data)?; + self.abr_streaming_url = redir + .url + .ok_or(AbrError::Invalid("sabr redirect: no URL".into()))?; + Ok(()) + } +} + +fn get_format_key(format_id: &FormatId) -> String { + format!("{};{};", format_id.itag(), format_id.last_modified()) +} + +impl TryFrom<&MediaHeader> for InitializedFormat { + type Error = AbrError; + + fn try_from(value: &MediaHeader) -> Result { + let format_id = value + .format_id + .as_ref() + .ok_or(AbrError::Invalid("media header: no format_id".into()))?; + Ok(InitializedFormat { + format_id: format_id.clone(), + duration_ms: value.duration_ms, + mime_type: None, + sequence_count: None, + sequence_list: Vec::new(), + media_chunks: Vec::new(), + state: BufferedRange { + format_id: MessageField::some(format_id.clone()), + start_time_ms: Some(0), + duration_ms: Some(0), + start_segment_index: Some(1), + end_segment_index: Some(0), + ..Default::default() + }, + }) + } +} + +impl TryFrom for InitializedFormat { + type Error = AbrError; + + fn try_from(value: FormatInitializationMetadata) -> Result { + let format_id = *value.format_id.0.ok_or(AbrError::Invalid( + "format initialization: no format_id".into(), + ))?; + Ok(InitializedFormat { + format_id: format_id.clone(), + duration_ms: value.duration_ms, + mime_type: value.mime_type.clone(), + sequence_count: value.sequence_count, + sequence_list: Vec::new(), + media_chunks: Vec::new(), + state: BufferedRange { + format_id: MessageField::some(format_id), + start_time_ms: Some(0), + duration_ms: Some(0), + start_segment_index: Some(1), + end_segment_index: Some(0), + ..Default::default() + }, + }) + } +} + +#[cfg(test)] +mod tests { + use rustypipe::{ + client::RustyPipe, + model::{AudioCodec, VideoFormat}, + param::StreamFilter, + }; + + use crate::abr::{AbrStream, StreamingState}; + + #[tokio::test] + #[tracing_test::traced_test] + async fn experiment() { + let rp = RustyPipe::new(); + let player = rp.query().player("UMm7Pq0BdRg").await.unwrap(); + let stream_filter = StreamFilter::new() + .allow_abr_only() + .video_max_res(360) + .video_formats([VideoFormat::Webm]) + .audio_codecs([AudioCodec::Opus]); + let (video, audio) = player.select_video_audio_stream(&stream_filter); + let _video = video.unwrap(); + let audio = audio.unwrap(); + + let http = reqwest::Client::new(); + let abr = AbrStream::new( + player.abr_streaming_url.clone().unwrap(), + player.abr_ustreamer_config.as_deref().unwrap(), + player.po_token.as_deref(), + http, + ) + .unwrap(); + let state = abr.init(&[audio], &[], Default::default()).await.unwrap(); + dbg!(&state); + + // let f = state.formats_by_key.values().next().unwrap(); + // std::fs::write("test.webm", &f.media_chunks); + } + + #[test] + #[tracing_test::traced_test] + fn parse_ump() { + let data = std::fs::read("abr.bin").unwrap(); + let ump_parts = AbrStream::parse_ump(&data); + dbg!(&ump_parts); + + let mut state = StreamingState::new(Default::default(), Default::default()); + assert!(!state.process_ump_parts(&ump_parts).unwrap()); + dbg!(&state); + } +} diff --git a/downloader/src/abr2.rs b/downloader/src/abr2.rs new file mode 100644 index 0000000..a6ebfef --- /dev/null +++ b/downloader/src/abr2.rs @@ -0,0 +1,799 @@ +#![allow(unused)] + +use std::{ + collections::{HashMap, HashSet}, + marker::PhantomData, + pin::Pin, + task::{ready, Poll}, +}; + +use async_stream::try_stream; +use bytes::{Buf, Bytes}; +use data_encoding::BASE64URL; +use futures_util::{stream::BoxStream, FutureExt, Stream, StreamExt, TryStreamExt}; +use protobuf::{Message, MessageField}; +use reqwest::Client; +use rustypipe::model::{AudioStream, VideoStream}; +use rustypipe_abr_proto::{ + buffered_range::BufferedRange, + client_abr_state::ClientAbrState, + common::FormatId, + format_initialization_metadata::FormatInitializationMetadata, + media_header::MediaHeader, + next_request_policy::NextRequestPolicy, + playback_cookie::PlaybackCookie, + sabr_error::SabrError, + sabr_redirect::SabrRedirect, + stream_protection_status::StreamProtectionStatus, + streamer_context::{streamer_context::ClientInfo, StreamerContext}, + time_range::TimeRange, + video_playback_abr_request::VideoPlaybackAbrRequest, +}; + +use crate::{abr_model::PartType, error::AbrError, util::BytesBuffer}; + +pub struct AbrStream { + http: Client, + url: String, + ustreamer_config: Vec, + po_token: Option>, + abr_state: ClientAbrState, + audio_format_ids: Vec, + video_format_ids: Vec, + playback_cookie: Option, + backoff_time_ms: i32, + formats_by_key: HashMap, + header_id_to_format_key_map: HashMap, + previous_sequences: HashMap>, + // http_stream: Option>>, + buffer: BytesBuffer, + last_header: Option, +} + +#[derive(Default, Clone)] +pub struct AbrStreamOptions<'a> { + http: Client, + url: String, + ustreamer_config: &'a str, + po_token: Option<&'a str>, + audio_streams: &'a [&'a AudioStream], + video_streams: &'a [&'a VideoStream], + abr_state: ClientAbrState, +} + +#[derive(Debug, Clone)] +pub struct AbrStreamItem { + format: FormatInfo, + data: Bytes, +} + +#[derive(Debug, Clone)] +pub struct FormatInfo { + itag: i32, + last_modified: u64, + xtags: Option, + mime_type: Option, +} + +#[derive(Debug)] +struct InitializedFormat { + format_id: FormatId, + duration_ms: Option, + mime_type: Option, + sequence_count: Option, + sequence_list: Vec, + state: BufferedRange, +} + +#[derive(Debug)] +struct Sequence { + itag: Option, + format_id: FormatId, + is_init_segment: bool, + duration_ms: Option, + start_ms: Option, + start_data_range: Option, + sequence_number: Option, + content_length: Option, + time_range: Option, +} + +#[derive(Debug, Clone)] +struct UmpHeader { + part_type: PartType, + size: u64, + header_id: Option, +} + +impl From<&InitializedFormat> for FormatInfo { + fn from(value: &InitializedFormat) -> Self { + Self { + itag: value.format_id.itag(), + last_modified: value.format_id.last_modified(), + xtags: value.format_id.xtags.clone(), + mime_type: value.mime_type.clone(), + } + } +} + +impl AbrStream { + pub fn new(options: AbrStreamOptions) -> Result { + let ustreamer_config = BASE64URL + .decode(options.ustreamer_config.as_bytes()) + .map_err(|e| { + AbrError::Invalid(format!("could not parse ustreamer_config: {e}").into()) + })?; + let po_token = + match options.po_token { + Some(pot) => Some(BASE64URL.decode(pot.as_bytes()).map_err(|e| { + AbrError::Invalid(format!("could not parse po_token: {e}").into()) + })?), + None => None, + }; + + let mut abr_state = options.abr_state; + if !abr_state.has_last_manual_direction() { + abr_state.set_last_manual_direction(0); + } + if !abr_state.has_time_since_last_manual_format_selection_ms() { + abr_state.set_time_since_last_manual_format_selection_ms(0); + } + if !abr_state.has_player_time_ms() { + abr_state.set_player_time_ms(0); + } + if !abr_state.has_visibility() { + abr_state.set_visibility(0); + } + if !abr_state.has_enabled_track_types_bitfield() { + abr_state.set_enabled_track_types_bitfield(i32::from(options.video_streams.is_empty())); + } + if let Some(v) = options.video_streams.first() { + if !abr_state.has_last_manual_selected_resolution() { + abr_state.set_last_manual_selected_resolution(v.height as i32); + } + if !abr_state.has_sticky_resolution() { + abr_state.set_sticky_resolution(v.height as i32); + } + } + + let audio_format_ids = options + .audio_streams + .iter() + .map(|s| FormatId { + itag: Some(s.itag as i32), + last_modified: s.last_modified, + xtags: s.xtags.clone(), + ..Default::default() + }) + .collect::>(); + let video_format_ids = options + .video_streams + .iter() + .map(|s| FormatId { + itag: Some(s.itag as i32), + last_modified: s.last_modified, + xtags: s.xtags.clone(), + ..Default::default() + }) + .collect::>(); + + Ok(Self { + http: options.http, + url: options.url, + ustreamer_config, + po_token, + abr_state, + audio_format_ids, + video_format_ids, + playback_cookie: None, + backoff_time_ms: 0, + formats_by_key: HashMap::new(), + header_id_to_format_key_map: HashMap::new(), + previous_sequences: HashMap::new(), + buffer: BytesBuffer::new(), + last_header: None, + }) + } + + fn read_varint(&mut self, offset: usize) -> (u64, usize) { + // Determine the length of the val + let buffer_len = self.buffer.remaining().saturating_sub(offset); + if buffer_len == 0 { + return (0, 0); + } + + let fb = self.buffer[offset]; + let byte_length = if fb < 128 { + 1 + } else if fb < 192 { + 2 + } else if fb < 224 { + 3 + } else if fb < 240 { + 4 + } else { + 5 + }; + + if buffer_len < byte_length { + return (0, 0); + } + + let n = match byte_length { + 1 => self.buffer[offset].into(), + 2 => { + let b1 = u64::from(self.buffer[offset]); + let b2 = u64::from(self.buffer[offset + 1]); + (b1 & 0x3f) + 64 * b2 + } + 3 => { + let b1 = u64::from(self.buffer[offset]); + let b2 = u64::from(self.buffer[offset + 1]); + let b3 = u64::from(self.buffer[offset + 2]); + (b1 & 0x1f) + 32 * (b2 + 256 * b3) + } + 4 => { + let b1 = u64::from(self.buffer[offset]); + let b2 = u64::from(self.buffer[offset + 1]); + let b3 = u64::from(self.buffer[offset + 2]); + let b4 = u64::from(self.buffer[offset + 3]); + (b1 & 0x0f) + 16 * (b2 + 256 * (b3 + 256 * b4)) + } + _ => u32::from_be_bytes([ + self.buffer[offset + 1], + self.buffer[offset + 2], + self.buffer[offset + 3], + self.buffer[offset + 4], + ]) + .into(), + }; + (n, offset + byte_length) + } + + fn parse_ump_header(&mut self) -> Option { + let (part_type, o1) = self.read_varint(0); + if o1 == 0 { + return None; + } + let (part_size, o2) = self.read_varint(o1); + if o2 == 0 { + return None; + } + self.buffer.advance(o2); + + Some(UmpHeader { + part_type: part_type.into(), + size: part_size, + header_id: None, + }) + } + + fn clear_sequence_list(&mut self) { + for f in self.formats_by_key.values_mut() { + f.sequence_list.clear(); + } + } + + fn process_ump_part(&mut self) -> Result<(Option, bool), AbrError> { + if let Some(last_header) = self.last_header.clone() { + let hsize = last_header.size as usize; + if last_header.part_type == PartType::MEDIA { + // Media data may be processed if the part is only partially downloaded + self.process_media_data().map(|x| (x, true)) + } else if self.buffer.remaining() < hsize { + // Wait for the entire part to be downloaded + Ok((None, true)) + } else { + let part_data = self.buffer.copy_to_bytes(hsize); + match last_header.part_type { + PartType::MEDIA_HEADER => self.process_media_header(&part_data)?, + PartType::MEDIA_END => self.process_media_end(&part_data), + PartType::NEXT_REQUEST_POLICY => { + self.process_next_request_policy(&part_data)? + } + PartType::FORMAT_INITIALIZATION_METADATA => { + self.process_format_initialization(&part_data)? + } + PartType::SABR_ERROR => { + return Err(AbrError::Sabr(SabrError::parse_from_bytes(&part_data)?)); + } + PartType::SABR_REDIRECT => self.process_sabr_redirect(&part_data)?, + PartType::STREAM_PROTECTION_STATUS => { + let prot = StreamProtectionStatus::parse_from_bytes(&part_data)?; + match prot.status() { + 1 => tracing::debug!("[StreamProtectionStatus]: Good"), + 2 => tracing::warn!("[StreamProtectionStatus]: Attestation pending"), + 3 => return Err(AbrError::Attestation), + _ => {} + } + } + _ => {} + }; + self.last_header = None; + Ok((None, false)) + } + } else { + Ok((None, true)) + } + } + + fn process_media_header(&mut self, data: &[u8]) -> Result<(), AbrError> { + let mh = MediaHeader::parse_from_bytes(data)?; + + let format_id = mh + .format_id + .as_ref() + .ok_or(AbrError::Invalid("media header: no format_id".into()))?; + let header_id = mh + .header_id + .and_then(|hid| u8::try_from(hid).ok()) + .ok_or(AbrError::Invalid("media header: no header_id".into()))?; + + let format_key = get_format_key(format_id); + + if !self.formats_by_key.contains_key(&format_key) { + self.formats_by_key + .insert(format_key.to_owned(), InitializedFormat::try_from(&mh)?); + } + let current_format = self.formats_by_key.get_mut(&format_key).unwrap(); + + // This is a hacky workaround to prevent duplicate sequences from being added. This should be fixed in the future + // (preferably by figuring out how to make the server not send duplicates). + if let Some(sequence_number) = mh.sequence_number { + let pseq = self + .previous_sequences + .entry(format_key.to_owned()) + .or_default(); + if !pseq.insert(sequence_number) { + tracing::warn!("duplicate sequence {sequence_number}"); + return Ok(()); + } + } + + // Save the header's ID so we can identify its stream data later. + self.header_id_to_format_key_map + .entry(header_id) + .or_insert(format_key); + + if current_format + .sequence_list + .iter() + .all(|x| x.sequence_number != Some(mh.sequence_number())) + { + current_format.sequence_list.push(Sequence { + itag: mh.itag, + format_id: format_id.clone(), + is_init_segment: mh.is_init_seg(), + duration_ms: mh.duration_ms, + start_ms: mh.start_ms, + start_data_range: mh.start_data_range, + sequence_number: mh.sequence_number, + content_length: mh.content_length, + time_range: mh.time_range.as_ref().cloned(), + }); + + if mh.has_sequence_number() { + current_format.state.set_duration_ms( + current_format.state.duration_ms() + i64::from(mh.duration_ms()), + ); + current_format + .state + .set_end_segment_index(current_format.state.end_segment_index() + 1); + } + } + + Ok(()) + } + + fn process_media_data(&mut self) -> Result, AbrError> { + if let Some(last_header) = &mut self.last_header { + if let Some(header_id) = last_header.header_id.or_else(|| { + let x = self.buffer.try_get_u8().ok(); + if x.is_some() { + last_header.header_id = x; + last_header.size -= 1; + } + x + }) { + let to_copy = self.buffer.remaining().min(last_header.size as usize); + if to_copy == 0 { + return Ok(None); + } + let format_key = self + .header_id_to_format_key_map + .get(&header_id) + .ok_or_else(|| { + AbrError::Invalid(format!("media: unknown header id {header_id}").into()) + })?; + let current_format = self + .formats_by_key + .get(format_key) + .ok_or(AbrError::Invalid("no current format".into()))?; + let format = FormatInfo::from(current_format); + let data = self.buffer.copy_to_bytes(to_copy); + last_header.size -= data.len() as u64; + if last_header.size == 0 { + self.last_header = None; + } + Ok(Some(AbrStreamItem { format, data })) + } else { + Ok(None) + } + } else { + Ok(None) + } + } + + fn process_media_end(&mut self, data: &[u8]) { + let header_id = data[0]; + self.header_id_to_format_key_map.remove(&header_id); + } + + fn process_next_request_policy(&mut self, data: &[u8]) -> Result<(), AbrError> { + let mut policy = NextRequestPolicy::parse_from_bytes(data)?; + self.playback_cookie = policy.playback_cookie.take(); + self.backoff_time_ms = policy.backoff_time_ms(); + Ok(()) + } + + fn process_format_initialization(&mut self, data: &[u8]) -> Result<(), AbrError> { + let init = FormatInitializationMetadata::parse_from_bytes(data)?; + let format = InitializedFormat::try_from(init)?; + let format_key = get_format_key(&format.format_id); + self.formats_by_key.insert(format_key, format); + Ok(()) + } + + fn process_sabr_redirect(&mut self, data: &[u8]) -> Result<(), AbrError> { + let redir = SabrRedirect::parse_from_bytes(data)?; + self.url = redir + .url + .ok_or(AbrError::Invalid("sabr redirect: no URL".into()))?; + Ok(()) + } + + fn main_format(&self) -> Option<&InitializedFormat> { + self.formats_by_key + .values() + .find(|fmt| { + fmt.mime_type + .as_deref() + .map(|t| t.contains("video")) + .unwrap_or_default() + }) + .or(self.formats_by_key.values().next()) + } + + pub fn stream(mut self) -> impl Stream> { + try_stream! { + let mut ri = 1; + loop { + let body = VideoPlaybackAbrRequest { + client_abr_state: MessageField::some(self.abr_state.clone()), + selected_format_ids: self + .formats_by_key + .values() + .map(|f| f.format_id.clone()) + .collect(), + buffered_ranges: self + .formats_by_key + .values() + .map(|f| f.state.clone()) + .collect(), + video_playback_ustreamer_config: Some(self.ustreamer_config.clone()), + selected_audio_format_ids: self.audio_format_ids.clone(), + selected_video_format_ids: self.video_format_ids.clone(), + streamer_context: MessageField::some(StreamerContext { + client_info: MessageField::some(ClientInfo { + client_name: Some(1), + client_version: Some("2.20250314.01.00".to_owned()), + ..Default::default() + }), + po_token: self.po_token.clone(), + playback_cookie: self + .playback_cookie + .as_ref() + .and_then(|c| c.write_to_bytes().ok()), + ..Default::default() + }), + ..Default::default() + }; + + let resp = self + .http + .post(&self.url) + .body(body.write_to_bytes()?) + .send() + .await? + .error_for_status()?; + let mut data_stream = resp.bytes_stream(); + let mut page_len = 0; + self.clear_sequence_list(); + + while let Some(chunk) = data_stream.next().await { + let chunk = chunk?; + let clen = chunk.len(); + page_len += clen; + tracing::debug!("chunk {clen}"); + self.buffer.push(chunk); + + loop { + if self.last_header.is_none() { + self.last_header = self.parse_ump_header(); + } + tracing::debug!("loop {:?}", self.last_header); + if self.last_header.is_none() { + break; + } + let (x, y) = self.process_ump_part()?; + if let Some(item) = x { + yield item; + } + if y { + break; + } + } + } + tracing::debug!("request #{ri} fetched {page_len} bytes"); + + let main_format = self.main_format().ok_or(AbrError::Invalid("no main format".into()))?; + let player_time = self.abr_state.player_time_ms() + + main_format.sequence_list.iter().fold(0, |acc, seq| { + acc + i64::from(seq.duration_ms.unwrap_or_default()) + }); + let is_last = main_format + .sequence_count + .zip(main_format.sequence_list.last()) + .map(|(sc, last)| last.sequence_number == Some(sc.into())) + .unwrap_or_default(); + tracing::debug!("player time: {player_time}; sequence_count={}, last={}", main_format.sequence_count.unwrap_or_default(), main_format + .sequence_list + .last() + .and_then(|x| x.sequence_number).unwrap_or_default()); + self.abr_state.set_player_time_ms(player_time); + if is_last { + break; + } + + if self.backoff_time_ms > 0 { + tracing::debug!("backoff: {} ms", self.backoff_time_ms); + tokio::time::sleep(std::time::Duration::from_millis( + self.backoff_time_ms.try_into().unwrap_or_default(), + )).await; + self.backoff_time_ms = 0; + } + + ri += 1; + } + } + } +} + +/* +impl<'a> Stream for AbrStream { + type Item = Result; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + loop { + if self.http_stream.is_none() { + let body = VideoPlaybackAbrRequest { + client_abr_state: MessageField::some(self.abr_state.clone()), + selected_format_ids: self + .formats_by_key + .values() + .map(|f| f.format_id.clone()) + .collect(), + buffered_ranges: self + .formats_by_key + .values() + .map(|f| f.state.clone()) + .collect(), + video_playback_ustreamer_config: Some(self.ustreamer_config.clone()), + selected_audio_format_ids: self.audio_format_ids.clone(), + selected_video_format_ids: self.video_format_ids.clone(), + streamer_context: MessageField::some(StreamerContext { + client_info: MessageField::some(ClientInfo { + client_name: Some(1), + client_version: Some("2.20250314.01.00".to_owned()), + ..Default::default() + }), + po_token: self.po_token.clone(), + playback_cookie: self + .playback_cookie + .as_ref() + .and_then(|c| c.write_to_bytes().ok()), + ..Default::default() + }), + ..Default::default() + }; + + match ready!(self + .http + .post(&self.url) + .body(body.write_to_bytes()?) + .send() + .poll_unpin(cx)) + { + Ok(resp) => { + let resp = resp.error_for_status()?; + let data_stream = resp.bytes_stream(); + self.http_stream = Some(Box::pin(data_stream)); + self.buffer = BytesBuffer::new() + } + Err(e) => return Poll::Ready(Some(Err(e.into()))), + } + } + + while let Some(http_stream) = &mut self.http_stream { + match ready!(http_stream.poll_next_unpin(cx)) { + Some(chunk) => { + let chunk = chunk?; + let clen = chunk.len(); + self.buffer.push(chunk); + if self.buffer.pos() == 0 { + self.last_header = self.parse_ump_header(); + } + if let Some(item) = self.process_ump_part()? { + return Poll::Ready(Some(Ok(item))); + } + } + None => { + self.http_stream = None; + } + } + } + + ready!( + Box::pin(tokio::time::sleep(std::time::Duration::from_millis( + self.backoff_time_ms.try_into().unwrap_or_default(), + ))) + .poll_unpin(cx) + ); + } + } +} +*/ + +fn read_varint(data: &[u8]) -> (u64, usize) { + // Determine the length of the val + if data.is_empty() { + return (0, 0); + } + + let fb = data[0]; + let byte_length = if fb < 128 { + 1 + } else if fb < 192 { + 2 + } else if fb < 224 { + 3 + } else if fb < 240 { + 4 + } else { + 5 + }; + + if data.len() < byte_length { + return (0, 0); + } + + let n = match byte_length { + 1 => data[0].into(), + 2 => { + let b1 = u64::from(data[0]); + let b2 = u64::from(data[1]); + (b1 & 0x3f) + 64 * b2 + } + 3 => { + let b1 = u64::from(data[0]); + let b2 = u64::from(data[1]); + let b3 = u64::from(data[2]); + (b1 & 0x1f) + 32 * (b2 + 256 * b3) + } + 4 => { + let b1 = u64::from(data[0]); + let b2 = u64::from(data[1]); + let b3 = u64::from(data[2]); + let b4 = u64::from(data[3]); + (b1 & 0x0f) + 16 * (b2 + 256 * (b3 + 256 * b4)) + } + _ => u32::from_be_bytes([data[1], data[2], data[3], data[4]]).into(), + }; + (n, byte_length) +} + +fn get_format_key(format_id: &FormatId) -> String { + format!("{};{};", format_id.itag(), format_id.last_modified()) +} + +impl TryFrom<&MediaHeader> for InitializedFormat { + type Error = AbrError; + + fn try_from(value: &MediaHeader) -> Result { + let format_id = value + .format_id + .as_ref() + .ok_or(AbrError::Invalid("media header: no format_id".into()))?; + Ok(InitializedFormat { + format_id: format_id.clone(), + duration_ms: value.duration_ms, + mime_type: None, + sequence_count: None, + sequence_list: Vec::new(), + state: BufferedRange { + format_id: MessageField::some(format_id.clone()), + start_time_ms: Some(0), + duration_ms: Some(0), + start_segment_index: Some(1), + end_segment_index: Some(0), + ..Default::default() + }, + }) + } +} + +impl TryFrom for InitializedFormat { + type Error = AbrError; + + fn try_from(value: FormatInitializationMetadata) -> Result { + let format_id = *value.format_id.0.ok_or(AbrError::Invalid( + "format initialization: no format_id".into(), + ))?; + Ok(InitializedFormat { + format_id: format_id.clone(), + duration_ms: value.duration_ms, + mime_type: value.mime_type.clone(), + sequence_count: value.sequence_count, + sequence_list: Vec::new(), + state: BufferedRange { + format_id: MessageField::some(format_id), + start_time_ms: Some(0), + duration_ms: Some(0), + start_segment_index: Some(1), + end_segment_index: Some(0), + ..Default::default() + }, + }) + } +} + +#[cfg(test)] +mod tests { + use rustypipe::{ + client::RustyPipe, + model::{AudioCodec, VideoFormat}, + param::StreamFilter, + }; + + use super::*; + + #[tokio::test] + #[tracing_test::traced_test] + async fn experiment() { + let rp = RustyPipe::new(); + let player = rp.query().player("UMm7Pq0BdRg").await.unwrap(); + let stream_filter = StreamFilter::new() + .allow_abr_only() + .video_max_res(360) + .video_formats([VideoFormat::Webm]) + .audio_codecs([AudioCodec::Opus]); + let (video, audio) = player.select_video_audio_stream(&stream_filter); + let _video = video.unwrap(); + let audio = audio.unwrap(); + + let mut abr = AbrStream::new(AbrStreamOptions { + url: player.abr_streaming_url.clone().unwrap(), + ustreamer_config: player.abr_ustreamer_config.as_deref().unwrap(), + po_token: player.po_token.as_deref(), + audio_streams: &[audio], + video_streams: &[], + ..Default::default() + }) + .unwrap(); + let mut stream = abr.stream().try_collect::>().await.unwrap(); + // dbg!(&stream); + } +} diff --git a/downloader/src/abr_model.rs b/downloader/src/abr_model.rs new file mode 100644 index 0000000..35df60e --- /dev/null +++ b/downloader/src/abr_model.rs @@ -0,0 +1,87 @@ +#[allow(non_camel_case_types, clippy::upper_case_acronyms)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PartType { + ONESIE_HEADER = 10, + ONESIE_DATA = 11, + MEDIA_HEADER = 20, + MEDIA = 21, + MEDIA_END = 22, + LIVE_METADATA = 31, + HOSTNAME_CHANGE_HINT = 32, + LIVE_METADATA_PROMISE = 33, + LIVE_METADATA_PROMISE_CANCELLATION = 34, + NEXT_REQUEST_POLICY = 35, + USTREAMER_VIDEO_AND_FORMAT_DATA = 36, + FORMAT_SELECTION_CONFIG = 37, + USTREAMER_SELECTED_MEDIA_STREAM = 38, + FORMAT_INITIALIZATION_METADATA = 42, + SABR_REDIRECT = 43, + SABR_ERROR = 44, + SABR_SEEK = 45, + RELOAD_PLAYER_RESPONSE = 46, + PLAYBACK_START_POLICY = 47, + ALLOWED_CACHED_FORMATS = 48, + START_BW_SAMPLING_HINT = 49, + PAUSE_BW_SAMPLING_HINT = 50, + SELECTABLE_FORMATS = 51, + REQUEST_IDENTIFIER = 52, + REQUEST_CANCELLATION_POLICY = 53, + ONESIE_PREFETCH_REJECTION = 54, + TIMELINE_CONTEXT = 55, + REQUEST_PIPELINING = 56, + SABR_CONTEXT_UPDATE = 57, + STREAM_PROTECTION_STATUS = 58, + SABR_CONTEXT_SENDING_POLICY = 59, + LAWNMOWER_POLICY = 60, + SABR_ACK = 61, + END_OF_TRACK = 62, + CACHE_LOAD_POLICY = 63, + LAWNMOWER_MESSAGING_POLICY = 64, + PREWARM_CONNECTION = 65, + UNKNOWN, +} + +impl From for PartType { + fn from(value: u64) -> Self { + match value { + 10 => Self::ONESIE_HEADER, + 11 => Self::ONESIE_DATA, + 20 => Self::MEDIA_HEADER, + 21 => Self::MEDIA, + 22 => Self::MEDIA_END, + 31 => Self::LIVE_METADATA, + 32 => Self::HOSTNAME_CHANGE_HINT, + 33 => Self::LIVE_METADATA_PROMISE, + 34 => Self::LIVE_METADATA_PROMISE_CANCELLATION, + 35 => Self::NEXT_REQUEST_POLICY, + 36 => Self::USTREAMER_VIDEO_AND_FORMAT_DATA, + 37 => Self::FORMAT_SELECTION_CONFIG, + 38 => Self::USTREAMER_SELECTED_MEDIA_STREAM, + 42 => Self::FORMAT_INITIALIZATION_METADATA, + 43 => Self::SABR_REDIRECT, + 44 => Self::SABR_ERROR, + 45 => Self::SABR_SEEK, + 46 => Self::RELOAD_PLAYER_RESPONSE, + 47 => Self::PLAYBACK_START_POLICY, + 48 => Self::ALLOWED_CACHED_FORMATS, + 49 => Self::START_BW_SAMPLING_HINT, + 50 => Self::PAUSE_BW_SAMPLING_HINT, + 51 => Self::SELECTABLE_FORMATS, + 52 => Self::REQUEST_IDENTIFIER, + 53 => Self::REQUEST_CANCELLATION_POLICY, + 54 => Self::ONESIE_PREFETCH_REJECTION, + 55 => Self::TIMELINE_CONTEXT, + 56 => Self::REQUEST_PIPELINING, + 57 => Self::SABR_CONTEXT_UPDATE, + 58 => Self::STREAM_PROTECTION_STATUS, + 59 => Self::SABR_CONTEXT_SENDING_POLICY, + 60 => Self::LAWNMOWER_POLICY, + 61 => Self::SABR_ACK, + 62 => Self::END_OF_TRACK, + 63 => Self::CACHE_LOAD_POLICY, + 64 => Self::LAWNMOWER_MESSAGING_POLICY, + 65 => Self::PREWARM_CONNECTION, + _ => Self::UNKNOWN, + } + } +} diff --git a/downloader/src/error.rs b/downloader/src/error.rs index d45405d..cf56f25 100644 --- a/downloader/src/error.rs +++ b/downloader/src/error.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, path::PathBuf}; use rustypipe::client::ClientType; +use rustypipe_abr_proto::sabr_error::SabrError; /// Error from the video downloader #[derive(thiserror::Error, Debug)] @@ -57,3 +58,20 @@ impl From for DownloadError { Self::AudioTag(value.to_string().into()) } } + +#[derive(thiserror::Error, Debug)] +pub enum AbrError { + /// Error encoding/decoding protobuf + #[error("protobuf: {0}")] + Protobuf(#[from] protobuf::Error), + /// Error from the HTTP client + #[error("http error: {0}")] + Http(#[from] reqwest::Error), + /// Received invalid data + #[error("invalid data: {0}")] + Invalid(Cow<'static, str>), + #[error("sabr error {}: {}", .0.code(), .0.type_())] + Sabr(SabrError), + #[error("attestation required")] + Attestation, +} diff --git a/downloader/src/lib.rs b/downloader/src/lib.rs index 2ba3590..b326cfb 100644 --- a/downloader/src/lib.rs +++ b/downloader/src/lib.rs @@ -2,6 +2,9 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #![warn(missing_docs, clippy::todo, clippy::dbg_macro)] +mod abr; +mod abr2; +mod abr_model; mod error; mod util; diff --git a/downloader/src/util.rs b/downloader/src/util.rs index 5f87339..9ce600a 100644 --- a/downloader/src/util.rs +++ b/downloader/src/util.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; +use bytes::{Buf, BufMut, Bytes, BytesMut}; use reqwest::Url; use crate::DownloadError; @@ -22,3 +23,147 @@ pub fn url_to_params(url: &str) -> Result<(Url, BTreeMap), Downl Ok((parsed_url, url_params)) } + +#[derive(Default, Debug, Clone)] +pub struct BytesBuffer { + b: Vec, + pos: usize, +} + +impl BytesBuffer { + pub fn new() -> Self { + Self::default() + } + + pub fn push(&mut self, bytes: Bytes) { + if !bytes.is_empty() { + self.b.push(bytes); + } + } + + fn get_ref(&self, index: usize) -> Option<&u8> { + let mut offset = 0; + for p in &self.b { + let pi = index - offset; + if pi < p.len() { + return Some(&p[pi]); + } + offset += p.len(); + } + None + } + + pub fn get(&self, index: usize) -> Option { + self.get_ref(index).cloned() + } + + pub fn pos(&self) -> usize { + self.pos + } +} + +impl std::ops::Index for BytesBuffer { + type Output = u8; + + fn index(&self, index: usize) -> &Self::Output { + self.get_ref(index).expect("index out of range") + } +} + +impl Buf for BytesBuffer { + fn remaining(&self) -> usize { + self.b + .iter() + .map(|b| b.remaining()) + .fold(0, |a, b| a.saturating_add(b)) + } + + fn chunk(&self) -> &[u8] { + for p in &self.b { + if p.has_remaining() { + return p.chunk(); + } + } + &[] + } + + fn advance(&mut self, mut cnt: usize) { + self.pos += cnt; + let mut to_trunc = 0; + for p in &mut self.b { + if cnt == 0 { + break; + } + + let to_adv = cnt.min(p.remaining()); + p.advance(to_adv); + cnt -= to_adv; + + if p.is_empty() { + to_trunc += 1; + } + } + + if to_trunc > 0 { + self.b.drain(0..to_trunc); + } + } + + fn chunks_vectored<'a>(&'a self, dst: &mut [std::io::IoSlice<'a>]) -> usize { + let mut n = 0; + for p in &self.b { + n += p.chunks_vectored(&mut dst[n..]); + } + n + } + + fn copy_to_bytes(&mut self, len: usize) -> bytes::Bytes { + self.pos += len; + if let Some(first) = self.b.first_mut() { + if first.remaining() >= len { + return first.copy_to_bytes(len); + } + } + + let rem = self.remaining(); + if rem < len { + assert!(len <= rem, "`len` greater than remaining"); + } + + let mut ret = BytesMut::with_capacity(len); + ret.put(self.take(len)); + ret.freeze() + } +} + +impl IntoIterator for BytesBuffer { + type Item = u8; + + type IntoIter = bytes::buf::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + bytes::buf::IntoIter::new(self) + } +} + +#[cfg(test)] +mod tests { + use bytes::{Buf, Bytes}; + + use super::BytesBuffer; + + #[test] + fn buffer() { + let mut buf = BytesBuffer::new(); + buf.push(Bytes::from_static(b"hello")); + buf.push(Bytes::from_static(b" ")); + buf.push(Bytes::from_static(b"world")); + + assert_eq!(buf[0], b'h'); + buf.advance(5); + let w1 = buf.copy_to_bytes(6); + assert_eq!(w1.as_ref(), b" world"); + assert!(buf.b.is_empty()); + assert_eq!(buf.pos(), 17); + } +} diff --git a/notes/AB_Tests.md b/notes/AB_Tests.md index 8742ef6..763622b 100644 --- a/notes/AB_Tests.md +++ b/notes/AB_Tests.md @@ -1101,3 +1101,10 @@ YouTube playlists may use a commandExecutorCommand which holds a list of command } } ``` + +## [23] YouTube Desktop only returns ABR streams + +- **Encountered on:** 16.03.2025 +- **Impact:** 🔴 High +- **Endpoint:** player (YT) +- **Status:** Experimental (<1%) diff --git a/src/client/player.rs b/src/client/player.rs index 591d773..9128a54 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -548,6 +548,10 @@ impl MapResponse for response::Player { dash_manifest_url: streaming_data.dash_manifest_url, abr_streaming_url, abr_ustreamer_config, + po_token: ctx + .session_po_token + .as_ref() + .map(|pot| pot.po_token.to_owned()), preview_frames, drm, client_type: ctx.client_type, @@ -777,6 +781,7 @@ impl<'a> StreamsMapper<'a> { bitrate: f.bitrate, average_bitrate: f.average_bitrate.unwrap_or(f.bitrate), size: f.content_length, + last_modified: f.last_modified, index_range: f.index_range, init_range: f.init_range, duration_ms: f.approx_duration_ms, @@ -791,6 +796,7 @@ impl<'a> StreamsMapper<'a> { format, codec: get_video_codec(codecs), mime: f.mime_type, + xtags: map_res.xtags, drm_track_type: f.drm_track_type.map(|t| t.into()), drm_systems: f.drm_families.into_iter().map(|t| t.into()).collect(), }) @@ -821,6 +827,7 @@ impl<'a> StreamsMapper<'a> { format!("no audio content length. itag: {}", f.itag).into(), ) })?, + last_modified: f.last_modified, index_range: f.index_range, init_range: f.init_range, duration_ms: f.approx_duration_ms, @@ -831,7 +838,8 @@ impl<'a> StreamsMapper<'a> { loudness_db: f.loudness_db, track: f .audio_track - .map(|t| self.map_audio_track(t, map_res.xtags)), + .map(|t| self.map_audio_track(t, map_res.xtags.as_deref())), + xtags: map_res.xtags, drm_track_type: f.drm_track_type.map(|t| t.into()), drm_systems: f.drm_families.into_iter().map(|t| t.into()).collect(), }) @@ -840,7 +848,7 @@ impl<'a> StreamsMapper<'a> { fn map_audio_track( &mut self, track: response::player::AudioTrack, - xtags: Option, + xtags: Option<&str>, ) -> AudioTrack { let mut lang = None; let mut track_type = None; diff --git a/src/client/response/player.rs b/src/client/response/player.rs index 9654b21..2cb460f 100644 --- a/src/client/response/player.rs +++ b/src/client/response/player.rs @@ -110,6 +110,10 @@ pub(crate) struct Format { pub format_type: FormatType, pub mime_type: String, + #[serde_as(as = "Option")] + pub last_modified: Option, + #[serde_as(as = "Option")] + pub content_length: Option, pub bitrate: u32, @@ -123,9 +127,6 @@ pub(crate) struct Format { #[serde_as(as = "Option")] pub init_range: Option>, - #[serde_as(as = "Option")] - pub content_length: Option, - #[serde(default)] #[serde_as(deserialize_as = "DefaultOnError")] pub quality: Option, diff --git a/src/model/mod.rs b/src/model/mod.rs index 7f3b277..f97090c 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -151,6 +151,8 @@ pub struct VideoPlayer { pub abr_streaming_url: Option, /// ABR streaming config pub abr_ustreamer_config: Option, + /// PO token for fetching ABR stream + pub po_token: Option, /// Video frames for seek preview pub preview_frames: Vec, /// Video player DRM config @@ -207,6 +209,8 @@ pub struct VideoStream { pub average_bitrate: u32, /// Video file size in bytes pub size: Option, + /// Last modified timestamp + pub last_modified: Option, /// Index range (used for DASH streaming) pub index_range: Option>, /// Init range (used for DASH streaming) @@ -229,6 +233,8 @@ pub struct VideoStream { pub format: VideoFormat, /// Video codec pub codec: VideoCodec, + /// Stream tags + pub xtags: Option, /// DRM track type /// /// [`None`] if the track is not DRM-protected @@ -251,6 +257,8 @@ pub struct AudioStream { pub average_bitrate: u32, /// Audio file size in bytes pub size: u64, + /// Last modified timestamp + pub last_modified: Option, /// Index range (used for DASH streaming) pub index_range: Option>, /// Init range (used for DASH streaming) @@ -287,6 +295,8 @@ pub struct AudioStream { /// /// This is None if the video contains only 1 audio track. pub track: Option, + /// Stream tags (e.g. audio track language and type) + pub xtags: Option, /// DRM track type /// /// [`None`] if the track is not DRM-protected diff --git a/src/model/traits.rs b/src/model/traits.rs index 43d4236..e621199 100644 --- a/src/model/traits.rs +++ b/src/model/traits.rs @@ -18,6 +18,8 @@ pub trait YtStream { fn averate_bitrate(&self) -> u32; /// File size in bytes fn size(&self) -> Option; + /// Last modified timestamp + fn last_modified(&self) -> Option; /// Index range (used for DASH streaming) fn index_range(&self) -> Option>; /// Init range (used for DASH streaming) @@ -26,6 +28,8 @@ pub trait YtStream { fn duration_ms(&self) -> Option; /// MIME file type fn mime(&self) -> &str; + /// Stream tags (e.g. audio track language and type) + fn xtags(&self) -> Option<&str>; } impl YtStream for VideoStream { @@ -49,6 +53,10 @@ impl YtStream for VideoStream { self.size } + fn last_modified(&self) -> Option { + self.last_modified + } + fn index_range(&self) -> Option> { self.index_range.clone() } @@ -64,6 +72,10 @@ impl YtStream for VideoStream { fn mime(&self) -> &str { &self.mime } + + fn xtags(&self) -> Option<&str> { + self.xtags.as_deref() + } } impl YtStream for AudioStream { @@ -87,6 +99,10 @@ impl YtStream for AudioStream { Some(self.size) } + fn last_modified(&self) -> Option { + self.last_modified + } + fn index_range(&self) -> Option> { self.index_range.clone() } @@ -102,6 +118,10 @@ impl YtStream for AudioStream { fn mime(&self) -> &str { &self.mime } + + fn xtags(&self) -> Option<&str> { + self.xtags.as_deref() + } } /// Trait for file types diff --git a/src/param/stream_filter.rs b/src/param/stream_filter.rs index 99e9a31..537c490 100644 --- a/src/param/stream_filter.rs +++ b/src/param/stream_filter.rs @@ -179,7 +179,7 @@ impl StreamFilter { /// Allow ABR-only streams without URL #[must_use] - pub fn abr_only(mut self) -> Self { + pub fn allow_abr_only(mut self) -> Self { self.abr_only = true; self } @@ -283,8 +283,8 @@ impl StreamFilter { Some([language, track_type, channels.into(), bitrate]) } - fn apply_video(&self, stream: &VideoStream) -> VideoRes { - if stream.url.is_none() && !self.abr_only { + fn apply_video(&self, stream: &VideoStream, video_only: bool) -> VideoRes { + if stream.url.is_none() && !(self.abr_only && video_only) { return None; } @@ -363,6 +363,7 @@ impl VideoPlayer { fn _select_video_stream<'a>( streams: &'a [VideoStream], filter: &StreamFilter, + video_only: bool, ) -> Option<&'a VideoStream> { if filter.video_none { return None; @@ -370,19 +371,19 @@ impl VideoPlayer { streams .iter() - .filter_map(|s| filter.apply_video(s).map(|r| (s, r))) + .filter_map(|s| filter.apply_video(s, video_only).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`] pub fn select_video_stream(&self, filter: &StreamFilter) -> Option<&VideoStream> { - Self::_select_video_stream(&self.video_streams, filter) + Self::_select_video_stream(&self.video_streams, filter, false) } /// Select the video-only stream which is the best match for the given [`StreamFilter`] pub fn select_video_only_stream(&self, filter: &StreamFilter) -> Option<&VideoStream> { - Self::_select_video_stream(&self.video_only_streams, filter) + Self::_select_video_stream(&self.video_only_streams, filter, true) } /// Select a video and audio stream which is the best match for the given [`StreamFilter`] From e9fc556d899dc39cc7ee76518687f78d8b9e7ec9 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 22 Mar 2025 23:23:48 +0100 Subject: [PATCH 04/20] wip: remove unused code --- downloader/src/abr2.rs | 170 +++++++---------------------------------- 1 file changed, 26 insertions(+), 144 deletions(-) diff --git a/downloader/src/abr2.rs b/downloader/src/abr2.rs index a6ebfef..d367d53 100644 --- a/downloader/src/abr2.rs +++ b/downloader/src/abr2.rs @@ -45,7 +45,6 @@ pub struct AbrStream { formats_by_key: HashMap, header_id_to_format_key_map: HashMap, previous_sequences: HashMap>, - // http_stream: Option>>, buffer: BytesBuffer, last_header: Option, } @@ -464,8 +463,8 @@ impl AbrStream { .or(self.formats_by_key.values().next()) } - pub fn stream(mut self) -> impl Stream> { - try_stream! { + fn stream(mut self) -> impl Stream> { + let stream = try_stream! { let mut ri = 1; loop { let body = VideoPlaybackAbrRequest { @@ -565,146 +564,11 @@ impl AbrStream { ri += 1; } - } + }; + Box::pin(stream) } } -/* -impl<'a> Stream for AbrStream { - type Item = Result; - - fn poll_next( - mut self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - loop { - if self.http_stream.is_none() { - let body = VideoPlaybackAbrRequest { - client_abr_state: MessageField::some(self.abr_state.clone()), - selected_format_ids: self - .formats_by_key - .values() - .map(|f| f.format_id.clone()) - .collect(), - buffered_ranges: self - .formats_by_key - .values() - .map(|f| f.state.clone()) - .collect(), - video_playback_ustreamer_config: Some(self.ustreamer_config.clone()), - selected_audio_format_ids: self.audio_format_ids.clone(), - selected_video_format_ids: self.video_format_ids.clone(), - streamer_context: MessageField::some(StreamerContext { - client_info: MessageField::some(ClientInfo { - client_name: Some(1), - client_version: Some("2.20250314.01.00".to_owned()), - ..Default::default() - }), - po_token: self.po_token.clone(), - playback_cookie: self - .playback_cookie - .as_ref() - .and_then(|c| c.write_to_bytes().ok()), - ..Default::default() - }), - ..Default::default() - }; - - match ready!(self - .http - .post(&self.url) - .body(body.write_to_bytes()?) - .send() - .poll_unpin(cx)) - { - Ok(resp) => { - let resp = resp.error_for_status()?; - let data_stream = resp.bytes_stream(); - self.http_stream = Some(Box::pin(data_stream)); - self.buffer = BytesBuffer::new() - } - Err(e) => return Poll::Ready(Some(Err(e.into()))), - } - } - - while let Some(http_stream) = &mut self.http_stream { - match ready!(http_stream.poll_next_unpin(cx)) { - Some(chunk) => { - let chunk = chunk?; - let clen = chunk.len(); - self.buffer.push(chunk); - if self.buffer.pos() == 0 { - self.last_header = self.parse_ump_header(); - } - if let Some(item) = self.process_ump_part()? { - return Poll::Ready(Some(Ok(item))); - } - } - None => { - self.http_stream = None; - } - } - } - - ready!( - Box::pin(tokio::time::sleep(std::time::Duration::from_millis( - self.backoff_time_ms.try_into().unwrap_or_default(), - ))) - .poll_unpin(cx) - ); - } - } -} -*/ - -fn read_varint(data: &[u8]) -> (u64, usize) { - // Determine the length of the val - if data.is_empty() { - return (0, 0); - } - - let fb = data[0]; - let byte_length = if fb < 128 { - 1 - } else if fb < 192 { - 2 - } else if fb < 224 { - 3 - } else if fb < 240 { - 4 - } else { - 5 - }; - - if data.len() < byte_length { - return (0, 0); - } - - let n = match byte_length { - 1 => data[0].into(), - 2 => { - let b1 = u64::from(data[0]); - let b2 = u64::from(data[1]); - (b1 & 0x3f) + 64 * b2 - } - 3 => { - let b1 = u64::from(data[0]); - let b2 = u64::from(data[1]); - let b3 = u64::from(data[2]); - (b1 & 0x1f) + 32 * (b2 + 256 * b3) - } - 4 => { - let b1 = u64::from(data[0]); - let b2 = u64::from(data[1]); - let b3 = u64::from(data[2]); - let b4 = u64::from(data[3]); - (b1 & 0x0f) + 16 * (b2 + 256 * (b3 + 256 * b4)) - } - _ => u32::from_be_bytes([data[1], data[2], data[3], data[4]]).into(), - }; - (n, byte_length) -} - fn get_format_key(format_id: &FormatId) -> String { format!("{};{};", format_id.itag(), format_id.last_modified()) } @@ -762,6 +626,8 @@ impl TryFrom for InitializedFormat { #[cfg(test)] mod tests { + use std::io::Write; + use rustypipe::{ client::RustyPipe, model::{AudioCodec, VideoFormat}, @@ -781,7 +647,7 @@ mod tests { .video_formats([VideoFormat::Webm]) .audio_codecs([AudioCodec::Opus]); let (video, audio) = player.select_video_audio_stream(&stream_filter); - let _video = video.unwrap(); + let video = video.unwrap(); let audio = audio.unwrap(); let mut abr = AbrStream::new(AbrStreamOptions { @@ -789,11 +655,27 @@ mod tests { ustreamer_config: player.abr_ustreamer_config.as_deref().unwrap(), po_token: player.po_token.as_deref(), audio_streams: &[audio], - video_streams: &[], + video_streams: &[video], ..Default::default() }) .unwrap(); - let mut stream = abr.stream().try_collect::>().await.unwrap(); - // dbg!(&stream); + + { + let mut stream = abr.stream(); + let mut files = HashMap::new(); + + while let Some(item) = stream.try_next().await.unwrap() { + let mut f = files.entry(item.format.itag).or_insert_with(|| { + std::fs::File::create(format!("test_{}.webm", item.format.itag)).unwrap() + }); + + f.write_all(&item.data).unwrap(); + } + } + + let audio_md = std::fs::metadata(format!("test_{}.webm", audio.itag)).unwrap(); + assert_eq!(audio_md.len(), audio.size); + let video_md = std::fs::metadata(format!("test_{}.webm", video.itag)).unwrap(); + assert_eq!(video_md.len(), video.size.unwrap()); } } From 1c2db9b88dfe21571fd1d7165f4f8dc42ce71b38 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 24 Mar 2025 01:33:42 +0100 Subject: [PATCH 05/20] Revert "fix: handle player returning no adaptive stream URLs" This reverts commit 07db7b1166e912e1554f98f2ae20c2c356fed38f. --- src/client/player.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/client/player.rs b/src/client/player.rs index 9128a54..70de86b 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -387,21 +387,6 @@ impl MapResponse for response::Player { video_details.video_id, ctx.id ))); } - // Sometimes YouTube Desktop does not output any URLs for adaptive streams. - // Since this is currently rare, it is best to retry the request in this case. - if !is_live - && !streaming_data.adaptive_formats.c.is_empty() - && streaming_data - .adaptive_formats - .c - .iter() - .all(|f| f.url.is_none() && f.signature_cipher.is_none()) - { - return Err(ExtractionError::Unavailable { - reason: UnavailabilityReason::TryAgain, - msg: "no adaptive stream URLs".to_owned(), - }); - } let video_info = VideoPlayerDetails { id: video_details.video_id, From cae99513f6f2da808abc544cf99c3cc8f8978334 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 24 Mar 2025 13:21:03 +0100 Subject: [PATCH 06/20] fix: A/B test 23 check --- codegen/src/abtest.rs | 2 +- notes/AB_Tests.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codegen/src/abtest.rs b/codegen/src/abtest.rs index 0f90b13..8c41913 100644 --- a/codegen/src/abtest.rs +++ b/codegen/src/abtest.rs @@ -485,5 +485,5 @@ pub async fn command_executor_command(rp: &RustyPipeQuery) -> Result { pub async fn abr_stream_only(rp: &RustyPipeQuery) -> Result { let id = "pxY4OXVyMe4"; let res = rp.player_from_client(id, ClientType::Desktop).await?; - Ok(res.video_streams.iter().all(|s| s.url.is_none())) + Ok(res.video_only_streams.iter().all(|s| s.url.is_none())) } diff --git a/notes/AB_Tests.md b/notes/AB_Tests.md index 763622b..8f1edf9 100644 --- a/notes/AB_Tests.md +++ b/notes/AB_Tests.md @@ -1107,4 +1107,4 @@ YouTube playlists may use a commandExecutorCommand which holds a list of command - **Encountered on:** 16.03.2025 - **Impact:** 🔴 High - **Endpoint:** player (YT) -- **Status:** Experimental (<1%) +- **Status:** Common (5%) From 2f3c01cb286530b3b0d09f53a4c98abf325aa488 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 26 Mar 2025 00:27:39 +0100 Subject: [PATCH 07/20] wip --- downloader/Cargo.toml | 1 + downloader/src/abr2.rs | 135 +++++++++++++++++++++++------------ downloader/src/lib.rs | 1 + downloader/src/streamtest.rs | 25 +++++++ 4 files changed, 116 insertions(+), 46 deletions(-) create mode 100644 downloader/src/streamtest.rs diff --git a/downloader/Cargo.toml b/downloader/Cargo.toml index d5d1613..67c60eb 100644 --- a/downloader/Cargo.toml +++ b/downloader/Cargo.toml @@ -58,6 +58,7 @@ protobuf.workspace = true bytes = "1.0.0" byteorder = "1.0.0" async-stream = "0.3.6" +pin-project-lite = "0.2.11" [dev-dependencies] path_macro.workspace = true diff --git a/downloader/src/abr2.rs b/downloader/src/abr2.rs index d367d53..b543632 100644 --- a/downloader/src/abr2.rs +++ b/downloader/src/abr2.rs @@ -359,7 +359,7 @@ impl AbrStream { .iter() .all(|x| x.sequence_number != Some(mh.sequence_number())) { - current_format.sequence_list.push(Sequence { + let seq = Sequence { itag: mh.itag, format_id: format_id.clone(), is_init_segment: mh.is_init_seg(), @@ -369,7 +369,16 @@ impl AbrStream { sequence_number: mh.sequence_number, content_length: mh.content_length, time_range: mh.time_range.as_ref().cloned(), - }); + }; + dbg!(&seq); + if mh.itag == Some(251) { + tracing::debug!( + "audio #{}: {}", + seq.sequence_number.unwrap_or_default(), + seq.content_length.unwrap_or_default() + ); + } + current_format.sequence_list.push(seq); if mh.has_sequence_number() { current_format.state.set_duration_ms( @@ -463,48 +472,53 @@ impl AbrStream { .or(self.formats_by_key.values().next()) } + async fn fetch_stream_data(&self) -> Result { + let body = VideoPlaybackAbrRequest { + client_abr_state: MessageField::some(self.abr_state.clone()), + selected_format_ids: self + .formats_by_key + .values() + .map(|f| f.format_id.clone()) + .collect(), + buffered_ranges: self + .formats_by_key + .values() + .map(|f| f.state.clone()) + .collect(), + video_playback_ustreamer_config: Some(self.ustreamer_config.clone()), + selected_audio_format_ids: self.audio_format_ids.clone(), + selected_video_format_ids: self.video_format_ids.clone(), + streamer_context: MessageField::some(StreamerContext { + client_info: MessageField::some(ClientInfo { + client_name: Some(1), + client_version: Some("2.20250314.01.00".to_owned()), + ..Default::default() + }), + po_token: self.po_token.clone(), + playback_cookie: self + .playback_cookie + .as_ref() + .and_then(|c| c.write_to_bytes().ok()), + ..Default::default() + }), + ..Default::default() + }; + + let resp = self + .http + .post(&self.url) + .body(body.write_to_bytes()?) + .send() + .await? + .error_for_status()?; + Ok(resp) + } + fn stream(mut self) -> impl Stream> { let stream = try_stream! { let mut ri = 1; loop { - let body = VideoPlaybackAbrRequest { - client_abr_state: MessageField::some(self.abr_state.clone()), - selected_format_ids: self - .formats_by_key - .values() - .map(|f| f.format_id.clone()) - .collect(), - buffered_ranges: self - .formats_by_key - .values() - .map(|f| f.state.clone()) - .collect(), - video_playback_ustreamer_config: Some(self.ustreamer_config.clone()), - selected_audio_format_ids: self.audio_format_ids.clone(), - selected_video_format_ids: self.video_format_ids.clone(), - streamer_context: MessageField::some(StreamerContext { - client_info: MessageField::some(ClientInfo { - client_name: Some(1), - client_version: Some("2.20250314.01.00".to_owned()), - ..Default::default() - }), - po_token: self.po_token.clone(), - playback_cookie: self - .playback_cookie - .as_ref() - .and_then(|c| c.write_to_bytes().ok()), - ..Default::default() - }), - ..Default::default() - }; - - let resp = self - .http - .post(&self.url) - .body(body.write_to_bytes()?) - .send() - .await? - .error_for_status()?; + let resp = self.fetch_stream_data().await?; let mut data_stream = resp.bytes_stream(); let mut page_len = 0; self.clear_sequence_list(); @@ -640,14 +654,14 @@ mod tests { #[tracing_test::traced_test] async fn experiment() { let rp = RustyPipe::new(); - let player = rp.query().player("UMm7Pq0BdRg").await.unwrap(); + let player = rp.query().player("pxY4OXVyMe4").await.unwrap(); let stream_filter = StreamFilter::new() .allow_abr_only() - .video_max_res(360) + // .video_max_res(360) .video_formats([VideoFormat::Webm]) .audio_codecs([AudioCodec::Opus]); let (video, audio) = player.select_video_audio_stream(&stream_filter); - let video = video.unwrap(); + // let video = video.unwrap(); let audio = audio.unwrap(); let mut abr = AbrStream::new(AbrStreamOptions { @@ -655,7 +669,8 @@ mod tests { ustreamer_config: player.abr_ustreamer_config.as_deref().unwrap(), po_token: player.po_token.as_deref(), audio_streams: &[audio], - video_streams: &[video], + // video_streams: &[video], + video_streams: &[], ..Default::default() }) .unwrap(); @@ -675,7 +690,35 @@ mod tests { let audio_md = std::fs::metadata(format!("test_{}.webm", audio.itag)).unwrap(); assert_eq!(audio_md.len(), audio.size); - let video_md = std::fs::metadata(format!("test_{}.webm", video.itag)).unwrap(); - assert_eq!(video_md.len(), video.size.unwrap()); + // let video_md = std::fs::metadata(format!("test_{}.webm", video.itag)).unwrap(); + // assert_eq!(video_md.len(), video.size.unwrap()); + } + + #[tokio::test] + #[tracing_test::traced_test] + async fn first_header() { + let rp = RustyPipe::new(); + let player = rp.query().player("pxY4OXVyMe4").await.unwrap(); + let stream_filter = StreamFilter::new() + .allow_abr_only() + // .video_max_res(360) + .video_formats([VideoFormat::Webm]) + .audio_codecs([AudioCodec::Opus]); + let (video, audio) = player.select_video_audio_stream(&stream_filter); + // let video = video.unwrap(); + let audio = audio.unwrap(); + + let mut abr = AbrStream::new(AbrStreamOptions { + url: player.abr_streaming_url.clone().unwrap(), + ustreamer_config: player.abr_ustreamer_config.as_deref().unwrap(), + po_token: player.po_token.as_deref(), + audio_streams: &[audio], + // video_streams: &[video], + video_streams: &[], + ..Default::default() + }) + .unwrap(); + + todo!() } } diff --git a/downloader/src/lib.rs b/downloader/src/lib.rs index b326cfb..f78df7d 100644 --- a/downloader/src/lib.rs +++ b/downloader/src/lib.rs @@ -6,6 +6,7 @@ mod abr; mod abr2; mod abr_model; mod error; +mod streamtest; mod util; use std::{ diff --git a/downloader/src/streamtest.rs b/downloader/src/streamtest.rs new file mode 100644 index 0000000..f9cd5b2 --- /dev/null +++ b/downloader/src/streamtest.rs @@ -0,0 +1,25 @@ +use async_stream::stream; +use futures_util::Stream; + +fn stream() -> impl Stream { + stream! { + let mut x = 0; + loop { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + yield x; + x += 1; + } + }; + + let (mut __yield_tx, __yield_rx) = unsafe { async_stream::__private::yielder::pair() }; + async_stream::__private::AsyncStream::new(__yield_rx, async move { + let mut x = 0; + loop { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + { + __yield_tx.send(x).await + }; + x += 1; + } + }) +} From 69fb82e39f7faaedb5974d261dd943b473c53d7a Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 31 Mar 2025 18:00:28 +0200 Subject: [PATCH 08/20] test: update player data snapshots --- ...layer__tests__map_player_data_android.snap | 33 +++++++++++++++ ...layer__tests__map_player_data_desktop.snap | 41 +++++++++++++++++++ ...__tests__map_player_data_desktopmusic.snap | 29 +++++++++++++ ...t__player__tests__map_player_data_ios.snap | 9 ++++ ...nt__player__tests__map_player_data_tv.snap | 39 ++++++++++++++++++ 5 files changed, 151 insertions(+) diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap index 7d3fbe6..937d6b0 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap @@ -66,6 +66,7 @@ VideoPlayer( bitrate: 79452, average_bitrate: 79428, size: Some(1619781), + last_modified: Some(1580005480199246), index_range: None, init_range: None, duration_ms: Some(163143), @@ -77,6 +78,7 @@ VideoPlayer( mime: "video/3gpp; codecs=\"mp4v.20.3, mp4a.40.2\"", format: r#3gp, codec: mp4v, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -86,6 +88,7 @@ VideoPlayer( bitrate: 561339, average_bitrate: 561109, size: Some(11439331), + last_modified: Some(1580005476071743), index_range: None, init_range: None, duration_ms: Some(163096), @@ -97,6 +100,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -106,6 +110,7 @@ VideoPlayer( bitrate: 1574434, average_bitrate: 1574434, size: None, + last_modified: Some(1580005750956837), index_range: None, init_range: None, duration_ms: Some(163096), @@ -117,6 +122,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.64001F, mp4a.40.2\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -128,6 +134,7 @@ VideoPlayer( bitrate: 67637, average_bitrate: 60063, size: Some(1224002), + last_modified: Some(1608045375671513), index_range: Some(Range( start: 700, end: 1115, @@ -145,6 +152,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.00M.08\"", format: mp4, codec: av01, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -154,6 +162,7 @@ VideoPlayer( bitrate: 135747, average_bitrate: 109867, size: Some(2238952), + last_modified: Some(1608045728968690), index_range: Some(Range( start: 700, end: 1115, @@ -171,6 +180,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.00M.08\"", format: mp4, codec: av01, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -180,6 +190,7 @@ VideoPlayer( bitrate: 538143, average_bitrate: 383195, size: Some(7808990), + last_modified: Some(1580005649163759), index_range: Some(Range( start: 740, end: 1155, @@ -197,6 +208,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.4d401e\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -206,6 +218,7 @@ VideoPlayer( bitrate: 258097, average_bitrate: 202682, size: Some(4130385), + last_modified: Some(1608045761576250), index_range: Some(Range( start: 700, end: 1115, @@ -223,6 +236,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.01M.08\"", format: mp4, codec: av01, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -232,6 +246,7 @@ VideoPlayer( bitrate: 436843, average_bitrate: 337281, size: Some(6873325), + last_modified: Some(1608045990917419), index_range: Some(Range( start: 700, end: 1115, @@ -249,6 +264,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.04M.08\"", format: mp4, codec: av01, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -258,6 +274,7 @@ VideoPlayer( bitrate: 1348419, average_bitrate: 1097369, size: Some(22365208), + last_modified: Some(1608048380553749), index_range: Some(Range( start: 700, end: 1115, @@ -275,6 +292,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.08M.08\"", format: mp4, codec: av01, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -284,6 +302,7 @@ VideoPlayer( bitrate: 4190323, average_bitrate: 3208919, size: Some(65400181), + last_modified: Some(1580005649161486), index_range: Some(Range( start: 740, end: 1155, @@ -301,6 +320,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.64002a\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -310,6 +330,7 @@ VideoPlayer( bitrate: 2572342, average_bitrate: 2088624, size: Some(42567727), + last_modified: Some(1608052932785283), index_range: Some(Range( start: 700, end: 1115, @@ -327,6 +348,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.09M.08\"", format: mp4, codec: av01, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -338,6 +360,7 @@ VideoPlayer( bitrate: 49724, average_bitrate: 48818, size: 995840, + last_modified: Some(1580005582214385), index_range: Some(Range( start: 641, end: 876, @@ -353,6 +376,7 @@ VideoPlayer( channels: Some(2), loudness_db: None, track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -362,6 +386,7 @@ VideoPlayer( bitrate: 53039, average_bitrate: 45845, size: 934449, + last_modified: Some(1608509101590706), index_range: Some(Range( start: 266, end: 551, @@ -377,6 +402,7 @@ VideoPlayer( channels: Some(2), loudness_db: None, track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -386,6 +412,7 @@ VideoPlayer( bitrate: 71268, average_bitrate: 61123, size: 1245866, + last_modified: Some(1608509101111096), index_range: Some(Range( start: 266, end: 551, @@ -401,6 +428,7 @@ VideoPlayer( channels: Some(2), loudness_db: None, track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -410,6 +438,7 @@ VideoPlayer( bitrate: 130268, average_bitrate: 129508, size: 2640283, + last_modified: Some(1580005579712232), index_range: Some(Range( start: 632, end: 867, @@ -425,6 +454,7 @@ VideoPlayer( channels: Some(2), loudness_db: None, track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -434,6 +464,7 @@ VideoPlayer( bitrate: 140633, average_bitrate: 121491, size: 2476314, + last_modified: Some(1608509101894140), index_range: Some(Range( start: 266, end: 551, @@ -449,6 +480,7 @@ VideoPlayer( channels: Some(2), loudness_db: None, track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -467,6 +499,7 @@ VideoPlayer( dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659481355/ei/q1jpYtOPEYSBgQeHmqbwAQ/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr5---sn-h0jeenek.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jeenek%2Csn-h0jelnez/ms/au%2Crdu/mv/m/mvi/5/pl/37/hfr/1/as/fmp4_audio_clear%2Cfmp4_sd_hd_clear/initcwndbps/1527500/vprv/1/mt/1659459429/fvip/4/itag_bl/376%2C377%2C384%2C385%2C612%2C613%2C617%2C619%2C623%2C628%2C655%2C656%2C660%2C662%2C666%2C671/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cvprv%2Citag/sig/AOq0QJ8wRAIgMm4a_MIHA3YUszKeruSy3exs5JwNjJAyLAwxL0yPdNMCIANb9GDMSTp_NT-PPhbvYMwRULJ5a9BO6MYD9FuWprC1/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgETSOwhwWVMy7gmrFXZlJu655ToLzSwOEsT16oRyrWhACIQDkvOEw1fImz5omu4iVIRNFe-z-JC9v8WUyx281dW2NOw%3D%3D"), abr_streaming_url: None, abr_ustreamer_config: Some("Cs0CCAAlMZkqPi0AAIA_NT0Klz9YAXjoAoABAaABAbUB9ijcP-ABAegBA_ABAfkBAAAAAAAA0D-BAgAAAAAAABhAmALwAaAC6AK4AgDaAlUQsOoBGKhGIKCcASjYNjCYdXCIJ4AB9AO4AQHgAQGYAgyoAgGwAgG4AgHAAgHIAgHQAgLgAgHoAgLwAgGAAwaIA4gnmAMBqAMIsAMBuAMBwAMB2AMB-gIrCAwQGBgyIDItAABwQjUAAIxCQAFIAWUAAIBAaMBwzQEAAIA_8AEB6ALgA4IDAJADAaADAbADA-ADkE6wBAG4BAHKBFgKFQiA4gkQmHUYrAIlAAAAACgAMABAARDg1AMY0A8qNgoKdGJfY29zdF81MCAIKQAAAAAAAAAASAFQAV2amZk-ZQAAAD9tAAAAP3UAAAA_eMCpB5IBADAB6AQB8AQB-AQBkAUBGAEgATIMCKsCEI662NuboOcCMgwIjwMQg-na_r_Q7QIyDAiqAhCOutjbm6DnAjIMCI4DEJXUhISv0O0CMgwIhwEQjrrY25ug5wIyDAiNAxCr6siQptDtAjIMCIYBEO_L2NuboOcCMgwIjAMQuvqao6XQ7QIyDAiFARCOutjbm6DnAjIMCIsDEPLf1JOl0O0CMgwIoAEQjrrY25ug5wIyDAiKAxDZmZnro9DtAjIMCIsBEPGp4ruboOcCMgwIjAEQ6M3Jupug5wIyDAj5ARCykfms493tAjIMCPoBELju26zj3e0CMgwI-wEQ_NOLrePd7QI6AA=="), + po_token: None, preview_frames: [ Frameset( url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwENSDfyq4qpAwVwAcABBqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLAXobPyrylgm8IEvjlZzqYTiPe1Ow", diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap index 3ceaadf..f2ded40 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap @@ -71,6 +71,7 @@ VideoPlayer( bitrate: 561339, average_bitrate: 561109, size: Some(11439331), + last_modified: Some(1580005476071743), index_range: None, init_range: None, duration_ms: Some(163096), @@ -82,6 +83,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -93,6 +95,7 @@ VideoPlayer( bitrate: 87458, average_bitrate: 72857, size: Some(1484736), + last_modified: Some(1608509388295661), index_range: Some(Range( start: 218, end: 751, @@ -110,6 +113,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -119,6 +123,7 @@ VideoPlayer( bitrate: 67637, average_bitrate: 60063, size: Some(1224002), + last_modified: Some(1608045375671513), index_range: Some(Range( start: 700, end: 1115, @@ -136,6 +141,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.00M.08\"", format: mp4, codec: av01, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -145,6 +151,7 @@ VideoPlayer( bitrate: 184064, average_bitrate: 145902, size: Some(2973283), + last_modified: Some(1608509388282028), index_range: Some(Range( start: 219, end: 753, @@ -162,6 +169,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -171,6 +179,7 @@ VideoPlayer( bitrate: 135747, average_bitrate: 109867, size: Some(2238952), + last_modified: Some(1608045728968690), index_range: Some(Range( start: 700, end: 1115, @@ -188,6 +197,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.00M.08\"", format: mp4, codec: av01, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -197,6 +207,7 @@ VideoPlayer( bitrate: 538143, average_bitrate: 383195, size: Some(7808990), + last_modified: Some(1580005649163759), index_range: Some(Range( start: 740, end: 1155, @@ -214,6 +225,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.4d401e\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -223,6 +235,7 @@ VideoPlayer( bitrate: 319085, average_bitrate: 253673, size: Some(5169510), + last_modified: Some(1608509388282405), index_range: Some(Range( start: 220, end: 754, @@ -240,6 +253,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -249,6 +263,7 @@ VideoPlayer( bitrate: 258097, average_bitrate: 202682, size: Some(4130385), + last_modified: Some(1608045761576250), index_range: Some(Range( start: 700, end: 1115, @@ -266,6 +281,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.01M.08\"", format: mp4, codec: av01, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -275,6 +291,7 @@ VideoPlayer( bitrate: 539056, average_bitrate: 436270, size: Some(8890590), + last_modified: Some(1608509388284632), index_range: Some(Range( start: 220, end: 754, @@ -292,6 +309,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -301,6 +319,7 @@ VideoPlayer( bitrate: 436843, average_bitrate: 337281, size: Some(6873325), + last_modified: Some(1608045990917419), index_range: Some(Range( start: 700, end: 1115, @@ -318,6 +337,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.04M.08\"", format: mp4, codec: av01, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -327,6 +347,7 @@ VideoPlayer( bitrate: 982813, average_bitrate: 812006, size: Some(16547577), + last_modified: Some(1608509388326822), index_range: Some(Range( start: 220, end: 754, @@ -344,6 +365,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -353,6 +375,7 @@ VideoPlayer( bitrate: 2354009, average_bitrate: 1764202, size: Some(35955780), + last_modified: Some(1608509234088626), index_range: Some(Range( start: 219, end: 771, @@ -370,6 +393,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -379,6 +403,7 @@ VideoPlayer( bitrate: 1348419, average_bitrate: 1097369, size: Some(22365208), + last_modified: Some(1608048380553749), index_range: Some(Range( start: 700, end: 1115, @@ -396,6 +421,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.08M.08\"", format: mp4, codec: av01, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -405,6 +431,7 @@ VideoPlayer( bitrate: 4190323, average_bitrate: 3208919, size: Some(65400181), + last_modified: Some(1580005649161486), index_range: Some(Range( start: 740, end: 1155, @@ -422,6 +449,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.64002a\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -431,6 +459,7 @@ VideoPlayer( bitrate: 3832648, average_bitrate: 3090839, size: Some(62993617), + last_modified: Some(1608509371758331), index_range: Some(Range( start: 219, end: 776, @@ -448,6 +477,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -457,6 +487,7 @@ VideoPlayer( bitrate: 2572342, average_bitrate: 2088624, size: Some(42567727), + last_modified: Some(1608052932785283), index_range: Some(Range( start: 700, end: 1115, @@ -474,6 +505,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.09M.08\"", format: mp4, codec: av01, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -485,6 +517,7 @@ VideoPlayer( bitrate: 53039, average_bitrate: 45845, size: 934449, + last_modified: Some(1608509101590706), index_range: Some(Range( start: 266, end: 551, @@ -500,6 +533,7 @@ VideoPlayer( channels: Some(2), loudness_db: Some(5.2200003), track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -509,6 +543,7 @@ VideoPlayer( bitrate: 71268, average_bitrate: 61123, size: 1245866, + last_modified: Some(1608509101111096), index_range: Some(Range( start: 266, end: 551, @@ -524,6 +559,7 @@ VideoPlayer( channels: Some(2), loudness_db: Some(5.2200003), track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -533,6 +569,7 @@ VideoPlayer( bitrate: 130268, average_bitrate: 129508, size: 2640283, + last_modified: Some(1580005579712232), index_range: Some(Range( start: 632, end: 867, @@ -548,6 +585,7 @@ VideoPlayer( channels: Some(2), loudness_db: Some(5.2159004), track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -557,6 +595,7 @@ VideoPlayer( bitrate: 140633, average_bitrate: 121491, size: 2476314, + last_modified: Some(1608509101894140), index_range: Some(Range( start: 266, end: 551, @@ -572,6 +611,7 @@ VideoPlayer( channels: Some(2), loudness_db: Some(5.2200003), track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -590,6 +630,7 @@ VideoPlayer( dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659481355/ei/q1jpYtq3BJCX1gKVyJGQDg/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr4---sn-h0jelnez.googlevideo.com/mh/mQ/mm/31%2C26/mn/sn-h0jelnez%2Csn-4g5edn6k/ms/au%2Conr/mv/m/mvi/4/pl/37/hfr/all/as/fmp4_audio_clear%2Cwebm_audio_clear%2Cwebm2_audio_clear%2Cfmp4_sd_hd_clear%2Cwebm2_sd_hd_clear/initcwndbps/1513750/spc/lT-KhrZGE2opztWyVdAtyUNlb8dXPDs/vprv/1/mt/1659459429/fvip/4/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cspc%2Cvprv%2Citag/sig/AOq0QJ8wRgIhAPEjHK19PKVHqQeia6WF4qubuMYk74LGi8F8lk5ZMPkFAiEAsaB2pKQWBvuPnNUnbdQXHc-izgsHJUP793woC2xNJlg%3D/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgOY4xu4H9wqPVZ7vF2i0hFcOnqrur1XGoA43a7ZEuuSUCIQCyPxBKXUQrKFmknNEGpX5GSWySKgMw_xHBikWpKpKwvg%3D%3D"), abr_streaming_url: None, abr_ustreamer_config: None, + po_token: None, preview_frames: [ Frameset( url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwENSDfyq4qpAwVwAcABBqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLAXobPyrylgm8IEvjlZzqYTiPe1Ow", diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap index a6c2df6..7308a6e 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap @@ -39,6 +39,7 @@ VideoPlayer( bitrate: 561339, average_bitrate: 561109, size: Some(11439331), + last_modified: Some(1580005476071743), index_range: None, init_range: None, duration_ms: Some(163096), @@ -50,6 +51,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -61,6 +63,7 @@ VideoPlayer( bitrate: 87458, average_bitrate: 72857, size: Some(1484736), + last_modified: Some(1608509388295661), index_range: Some(Range( start: 218, end: 751, @@ -78,6 +81,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -87,6 +91,7 @@ VideoPlayer( bitrate: 184064, average_bitrate: 145902, size: Some(2973283), + last_modified: Some(1608509388282028), index_range: Some(Range( start: 219, end: 753, @@ -104,6 +109,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -113,6 +119,7 @@ VideoPlayer( bitrate: 538143, average_bitrate: 383195, size: Some(7808990), + last_modified: Some(1580005649163759), index_range: Some(Range( start: 740, end: 1155, @@ -130,6 +137,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.4d401e\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -139,6 +147,7 @@ VideoPlayer( bitrate: 319085, average_bitrate: 253673, size: Some(5169510), + last_modified: Some(1608509388282405), index_range: Some(Range( start: 220, end: 754, @@ -156,6 +165,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -165,6 +175,7 @@ VideoPlayer( bitrate: 539056, average_bitrate: 436270, size: Some(8890590), + last_modified: Some(1608509388284632), index_range: Some(Range( start: 220, end: 754, @@ -182,6 +193,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -191,6 +203,7 @@ VideoPlayer( bitrate: 982813, average_bitrate: 812006, size: Some(16547577), + last_modified: Some(1608509388326822), index_range: Some(Range( start: 220, end: 754, @@ -208,6 +221,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -217,6 +231,7 @@ VideoPlayer( bitrate: 2354009, average_bitrate: 1764202, size: Some(35955780), + last_modified: Some(1608509234088626), index_range: Some(Range( start: 219, end: 771, @@ -234,6 +249,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -243,6 +259,7 @@ VideoPlayer( bitrate: 4190323, average_bitrate: 3208919, size: Some(65400181), + last_modified: Some(1580005649161486), index_range: Some(Range( start: 740, end: 1155, @@ -260,6 +277,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.64002a\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -269,6 +287,7 @@ VideoPlayer( bitrate: 3832648, average_bitrate: 3090839, size: Some(62993617), + last_modified: Some(1608509371758331), index_range: Some(Range( start: 219, end: 776, @@ -286,6 +305,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -297,6 +317,7 @@ VideoPlayer( bitrate: 53039, average_bitrate: 45845, size: 934449, + last_modified: Some(1608509101590706), index_range: Some(Range( start: 266, end: 551, @@ -312,6 +333,7 @@ VideoPlayer( channels: Some(2), loudness_db: Some(0.0006532669), track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -321,6 +343,7 @@ VideoPlayer( bitrate: 71268, average_bitrate: 61123, size: 1245866, + last_modified: Some(1608509101111096), index_range: Some(Range( start: 266, end: 551, @@ -336,6 +359,7 @@ VideoPlayer( channels: Some(2), loudness_db: Some(0.0006532669), track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -345,6 +369,7 @@ VideoPlayer( bitrate: 130268, average_bitrate: 129508, size: 2640283, + last_modified: Some(1580005579712232), index_range: Some(Range( start: 632, end: 867, @@ -360,6 +385,7 @@ VideoPlayer( channels: Some(2), loudness_db: Some(-0.003446579), track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -369,6 +395,7 @@ VideoPlayer( bitrate: 140633, average_bitrate: 121491, size: 2476314, + last_modified: Some(1608509101894140), index_range: Some(Range( start: 266, end: 551, @@ -384,6 +411,7 @@ VideoPlayer( channels: Some(2), loudness_db: Some(0.0006532669), track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -402,6 +430,7 @@ VideoPlayer( dash_manifest_url: Some("https://manifest.googlevideo.com/api/manifest/dash/expire/1659487474/ei/knDpYub6BojEgAf6jbLgDw/ip/2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e/id/a4fbddf14c6649b4/source/youtube/requiressl/yes/playback_host/rr5---sn-h0jeenek.googlevideo.com/mh/mQ/mm/31%2C29/mn/sn-h0jeenek%2Csn-h0jelnez/ms/au%2Crdu/mv/m/mvi/5/pl/37/hfr/all/as/fmp4_audio_clear%2Cwebm_audio_clear%2Cwebm2_audio_clear%2Cfmp4_sd_hd_clear%2Cwebm2_sd_hd_clear/initcwndbps/1418750/spc/lT-Khox4YuJQ2wmH79zYALRvsWTPCUc/vprv/1/mt/1659465669/fvip/4/keepalive/yes/fexp/24001373%2C24007246/itag/0/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cas%2Cspc%2Cvprv%2Citag/sig/AOq0QJ8wRAIgErABhAEaoKHUDu9dDbpxE_8gR4b8WWAi61fnu8UKnuICIEYrEKcHvqHdO4V3R7cvSGwi_HGH34IlQsKbziOfMBov/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgJxHmH0Sxo3cY_pW_ZzQ3hW9-7oz6K_pZWcUdrDDQ2sQCIQDJYNINQwLgKelgbO3CZYx7sMxdUAFpWdokmRBQ77vwvw%3D%3D"), abr_streaming_url: None, abr_ustreamer_config: None, + po_token: None, preview_frames: [ Frameset( url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwENSDfyq4qpAwVwAcABBqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLAXobPyrylgm8IEvjlZzqYTiPe1Ow", diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap index 72669c4..7914ab5 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap @@ -62,6 +62,7 @@ VideoPlayer( bitrate: 538143, average_bitrate: 383195, size: Some(7808990), + last_modified: Some(1580005649163759), index_range: Some(Range( start: 740, end: 1155, @@ -79,6 +80,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.4D401E\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -88,6 +90,7 @@ VideoPlayer( bitrate: 4190323, average_bitrate: 3208919, size: Some(65400181), + last_modified: Some(1580005649161486), index_range: Some(Range( start: 740, end: 1155, @@ -105,6 +108,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.64002A\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -116,6 +120,7 @@ VideoPlayer( bitrate: 49724, average_bitrate: 48818, size: 995840, + last_modified: Some(1580005582214385), index_range: Some(Range( start: 641, end: 876, @@ -131,6 +136,7 @@ VideoPlayer( channels: Some(2), loudness_db: Some(5.2159004), track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -140,6 +146,7 @@ VideoPlayer( bitrate: 130268, average_bitrate: 129508, size: 2640283, + last_modified: Some(1580005579712232), index_range: Some(Range( start: 632, end: 867, @@ -155,6 +162,7 @@ VideoPlayer( channels: Some(2), loudness_db: Some(5.2159004), track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -173,6 +181,7 @@ VideoPlayer( dash_manifest_url: None, abr_streaming_url: Some("https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=IOS&ei=q1jpYq-xHs7NgQev0bfwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=5&id=o-ANNg3iPHI56jhLSlPQk4pi4mdub5iAby0hmJBVrtiJgY&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&keepalive=yes&lsig=AG3C_xAwRgIhAK6T5ehnFBsc0FOurPHH1ME_vGcVysI-g5jrtEsvX64sAiEArY-iAvQCsc4R8yg8dvMdpnuHPIcPMCnRgyh8E527HF0%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&pl=37&rbqsm=fr&requiressl=yes&sabr=1&sig=AOq0QJ8wRgIhAJCJpb5gE12jQc2qOUy-Y61vEHeiAP_F78weNCzj8VklAiEAwR2PK52CmwsVHfRVk75OOYOxwYKNW2g1eDBw3COBP9w%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Csvpuc%2Csabr&svpuc=1"), abr_ustreamer_config: Some("CpYECp0DCAAQgAUY6AIl-n6qPi0AAIA_NT0Klz9oAXI6ChJtZnMyX2NtZnNfdjNfMV8wNDMSIAoeCgxkZXZpY2VfbW9kZWwSDgoMCgppcGhvbmUxNCw1GAAgAXIWChJtZnMyX2NtZnNfdjNfMV8wNDMYAHjoAoABAagBAbUB9ijcP6AC6AK4AgDaAmEQsOoBGKhGIKCcASjYNjCYdXCIJ4AB9AO4AQHgAQP4AQGQAgGYAgygAgGoAgGwAgG4AgHAAgHIAgHQAgLgAgHoAgLwAgGAAwKIA4gnmAMBqAMIwAMByAMB2AMB-LWR5QwB-gIvCAwQGBgyIDItAABwQjUAAIxCQAFIAVAKWAplAACAQGjAcM0BAACAP_ABAegC0AWCAwCQAwGgAwGoAwGwAwPQAwHYAwHgA5BOuAQBygRWChUI4NQDEJh1GOgHJQAAAAAoADAAQAEQ4NQDGNAPKjYKCnRiX2Nvc3RfNTAgDykAAAAAAAAAAEgBUAFdZmbmPmXNzEw-bZqZGT91mpkZP3jAqQeSAQDoBAHwBAH4BAGQBQGgBQEYASABMgwIqwIQjrrY25ug5wIyDAiqAhCOutjbm6DnAjIMCIcBEI662NuboOcCMgwIhgEQ78vY25ug5wIyDAiFARCOutjbm6DnAjIMCKABEI662NuboOcCMgwIiwEQ8aniu5ug5wIyDAiMARDozcm6m6DnAjoAEkwA8tjvwTBFAiAyo6mtW-M8Y8noBO_6Y3RmNo5U2LntCwiKcI2yye_zRgIhAK27d1UbqzhtkQhminIBAajD70ONeAQNwjt7VRpJRDimGgJlaQ=="), + po_token: None, preview_frames: [ Frameset( url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLCsCT8Lprh2S0ptmCRsWH7VtDl3YQ", diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tv.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tv.snap index 32ba960..bada27e 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tv.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tv.snap @@ -29,6 +29,7 @@ VideoPlayer( bitrate: 503574, average_bitrate: 503367, size: Some(10262148), + last_modified: Some(1700885551970466), index_range: None, init_range: None, duration_ms: Some(163096), @@ -40,6 +41,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -51,6 +53,7 @@ VideoPlayer( bitrate: 114816, average_bitrate: 111551, size: Some(2273274), + last_modified: Some(1705967288821438), index_range: Some(Range( start: 738, end: 1165, @@ -68,6 +71,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.4d400c\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -77,6 +81,7 @@ VideoPlayer( bitrate: 70630, average_bitrate: 56524, size: Some(1151892), + last_modified: Some(1705966620402771), index_range: Some(Range( start: 218, end: 767, @@ -94,6 +99,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -103,6 +109,7 @@ VideoPlayer( bitrate: 257417, average_bitrate: 246656, size: Some(5026513), + last_modified: Some(1705967298859029), index_range: Some(Range( start: 739, end: 1166, @@ -120,6 +127,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.4d4015\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -129,6 +137,7 @@ VideoPlayer( bitrate: 149589, average_bitrate: 124706, size: Some(2541351), + last_modified: Some(1705966614837727), index_range: Some(Range( start: 219, end: 768, @@ -146,6 +155,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -155,6 +165,7 @@ VideoPlayer( bitrate: 537902, average_bitrate: 383290, size: Some(7810925), + last_modified: Some(1705967286812435), index_range: Some(Range( start: 740, end: 1167, @@ -172,6 +183,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.4d401e\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -181,6 +193,7 @@ VideoPlayer( bitrate: 248858, average_bitrate: 205556, size: Some(4188954), + last_modified: Some(1705966624121874), index_range: Some(Range( start: 220, end: 770, @@ -198,6 +211,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -207,6 +221,7 @@ VideoPlayer( bitrate: 978945, average_bitrate: 722499, size: Some(14723538), + last_modified: Some(1705967282545273), index_range: Some(Range( start: 740, end: 1167, @@ -224,6 +239,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.4d401f\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -233,6 +249,7 @@ VideoPlayer( bitrate: 467884, average_bitrate: 382209, size: Some(7788899), + last_modified: Some(1705966622098793), index_range: Some(Range( start: 220, end: 770, @@ -250,6 +267,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -259,6 +277,7 @@ VideoPlayer( bitrate: 1560439, average_bitrate: 1207947, size: Some(24616305), + last_modified: Some(1705967307531372), index_range: Some(Range( start: 739, end: 1166, @@ -276,6 +295,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.4d401f\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -285,6 +305,7 @@ VideoPlayer( bitrate: 929607, average_bitrate: 722521, size: Some(14723992), + last_modified: Some(1705966613897741), index_range: Some(Range( start: 220, end: 770, @@ -302,6 +323,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -311,6 +333,7 @@ VideoPlayer( bitrate: 2188961, average_bitrate: 1694973, size: Some(34544823), + last_modified: Some(1705967092637061), index_range: Some(Range( start: 739, end: 1166, @@ -328,6 +351,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.4d4020\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -337,6 +361,7 @@ VideoPlayer( bitrate: 2250391, average_bitrate: 1482051, size: Some(30205331), + last_modified: Some(1705966545733919), index_range: Some(Range( start: 219, end: 786, @@ -354,6 +379,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -363,6 +389,7 @@ VideoPlayer( bitrate: 3926810, average_bitrate: 3044926, size: Some(62057888), + last_modified: Some(1705967093743693), index_range: Some(Range( start: 740, end: 1167, @@ -380,6 +407,7 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.64002a\"", format: mp4, codec: avc1, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -389,6 +417,7 @@ VideoPlayer( bitrate: 3473307, average_bitrate: 2713348, size: Some(55300085), + last_modified: Some(1705966651743358), index_range: Some(Range( start: 219, end: 792, @@ -406,6 +435,7 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -417,6 +447,7 @@ VideoPlayer( bitrate: 53073, average_bitrate: 45860, size: 934750, + last_modified: Some(1714877357172339), index_range: Some(Range( start: 266, end: 551, @@ -432,6 +463,7 @@ VideoPlayer( channels: Some(2), loudness_db: Some(5.21), track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -441,6 +473,7 @@ VideoPlayer( bitrate: 71197, average_bitrate: 61109, size: 1245582, + last_modified: Some(1714877466693058), index_range: Some(Range( start: 266, end: 551, @@ -456,6 +489,7 @@ VideoPlayer( channels: Some(2), loudness_db: Some(5.21), track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -465,6 +499,7 @@ VideoPlayer( bitrate: 130268, average_bitrate: 129508, size: 2640283, + last_modified: Some(1705966477945761), index_range: Some(Range( start: 632, end: 867, @@ -480,6 +515,7 @@ VideoPlayer( channels: Some(2), loudness_db: Some(5.2200003), track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -489,6 +525,7 @@ VideoPlayer( bitrate: 140833, average_bitrate: 121691, size: 2480393, + last_modified: Some(1714877359450110), index_range: Some(Range( start: 266, end: 551, @@ -504,6 +541,7 @@ VideoPlayer( channels: Some(2), loudness_db: Some(5.21), track: None, + xtags: None, drm_track_type: None, drm_systems: [], ), @@ -522,6 +560,7 @@ VideoPlayer( dash_manifest_url: None, abr_streaming_url: Some("https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=TVHTML5&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&keepalive=yes&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1722295996&mv=m&mvi=5&n=eBXmY26Y0c3VPyt&ns=Kl83P0QZk1oI9742KUD7ly8Q&pl=26&requiressl=yes&rqh=1&sabr=1&sig=AJfQdSswRAIgJRK55pIkQ3Pak9jZ4fHPDsxXv0YgkxKE-FFdIN12ph8CIFHlFEvAoUOoX4Fd1RmyCJqgLZhDkSLwD6s-xVW25kYL&smc=1&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Cxpc%2Csvpuc%2Cns%2Csabr%2Crqh&svpuc=1&xpc=EgVo2aDSNQ%3D%3D"), abr_ustreamer_config: Some("CswJCpcHCAAlAACAPy1SuF4_NQAAwD9YAWABaAFyFgoSbWZzMl9jbWZzX3YzXzJfMTA5GAB4j06gAQGoAQCQAgG4AgDIAgHaAroBELDqARioRiCgnAEoiCcwmHVwiCeAAfQDuAEB4AEDkAIBmAIMoAIBwAIB0AIC4AIB6AIEgAMCiAOIJ5gDAagDA8ADAcgDAdADAfgDAYAEAYgEAZAEAZgEAaAEAagEAcgEAdAEAdgEAeAEAOgEAfgEB4AFfYgFAbAFAbgFAcAFAcgFAdAFAdgFAeAF0A_oBQH4BdAPgAYBuAYBwAYB0AYB2AYB6AYB8AYB-AYBkAcBqAcB2AcB-LWR5QwB-gKeAi0AAIJCNQAAlkJIAWUAAIBAaMBwqAHQhgOwAeADuAEBzQEAAIA_8AEB_QEAAIA_hQKamRk-jQIAAIA_lQIAAAJCmAIBtQIAAIA_wALgA9ICEbD__________wEePEZaXF1e2gIFMjA6MDDgAnjoAugC9QIK16M7_QLNzMw9gAMBkAMBnQMK1yM9oAMBuAMByAMB2AMB5QNiSkRA7QMyyvM-8AMB_QNmZoY_hQQAAIBAmAQB1QQAACBB6ATwEPAEAb0Fo0Afu8UF308tP8gFAeAFAZgGAaAGAagGAbUGvTeGNb0GMzODQJAHAcAHAcgHAdUHAICdQ-UHAIAJRKEIAAAAAAAA8L-pCAAAAAAAAPC_sAjwAbgIAdgI8AHoCAGCAwCQAwGoAwGwAwPQAwHYAwHgA5BOuAQBygQcChMIwKkHEJh1GOgHJQAAAAAoADAAEODUAxjQD9IEDQoICLAJELAJIAEgiCfaBAsKBgjwLhDwLiCIJ-gEAfgEAYAFAYgFAZAFAagFAbAFAdAFAdgFAegFAfAFAYgGAZgGAagGgIACwAYByAYB0gYUCOgHEGQaDQiIJxUAAAA_Hc3MTD-CBwoVAACAPxhkIJBOiAcBoAcBsAcBuAcBwAcB-AcBgAgBoAgBsAgBuAgB0ggGCAEQARgBmAkBqQkAAAAAAADwv7EJAAAAAAAA8L_ICQHaCSRFRzRmTDl1Sm9tL2NWdklmNjg4bnB6c2t4SVQrMXl0N09POHXgCQGwCgHYCgHwCgGICwGYCwG4CwHICwHQCwHYCwHqCwSLBowG8AsB-AsBkAwBoAwBqAyQAbAMAbgMAcAMAdAMAeAMAegMAYANAaANAdANAeANAYgOAZAOAbAOAYinocoLARgBIAEyDAirAhDNgPzUlvKDAzIMCK8CEP64moKV8oMDMgwIiAEQ7Mj0upfygwMyDAj3ARCNxJTwlPKDAzIMCKoCEIW7uNSW8oMDMgwIrgIQn5LUz5TygwMyDAiHARD5xP-ul_KDAzIMCPQBEOmKifSU8oMDMgwIhgEQk_6DsZfygwMyDAjzARCSyIT1lPKDAzIMCIUBEJWg47aX8oMDMgwI8gEQ3_PN8JTygwMyDAigARC-zf6xl_KDAzIMCJYCENPIofOU8oMDMgwIjAEQodeqr5TygwMyDAj5ARDzzNT9v_WFAzIMCPoBEMKb8bHA9YUDMgwI-wEQ_s_f_r_1hQM6AEgAUiYaAmVuKAAyGFVDYnh4RWktSW1QbGJMeDVGLWZIZXRFZzgAQABYAJDL048OARJNAKEYC4YwRgIhAIj4Ug4dw_gq15NXvgcfXpI1Fm_fhmwl-4ad-rX3Ffg_AiEAkZDsUgoAGLOXIvWZlNyuyfu8HLWt-snFl3gkTiPo2acaAmVp"), + po_token: None, preview_frames: [ Frameset( url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLCsCT8Lprh2S0ptmCRsWH7VtDl3YQ", From a389648a4b01135c95535347b541a197794aa31d Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 3 Apr 2025 12:57:11 +0200 Subject: [PATCH 09/20] docs: abr streams now returned with 10% probability --- notes/AB_Tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/AB_Tests.md b/notes/AB_Tests.md index 8f1edf9..127721a 100644 --- a/notes/AB_Tests.md +++ b/notes/AB_Tests.md @@ -1107,4 +1107,4 @@ YouTube playlists may use a commandExecutorCommand which holds a list of command - **Encountered on:** 16.03.2025 - **Impact:** 🔴 High - **Endpoint:** player (YT) -- **Status:** Common (5%) +- **Status:** Common (10%) From e759738c00c604c8b6ef61d718425f0e86ebd4aa Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 4 Apr 2025 15:00:38 +0200 Subject: [PATCH 10/20] feat: add allow_abr_only option --- src/client/mod.rs | 35 +++++++++++++++++++++++++++++++++++ src/client/player.rs | 19 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/client/mod.rs b/src/client/mod.rs index 06386bc..4f0d792 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -400,6 +400,7 @@ struct RustyPipeOpts { utc_offset_minutes: i16, report: bool, strict: bool, + allow_abr_only: bool, auth: Option, visitor_data: Option, } @@ -556,6 +557,7 @@ impl Default for RustyPipeOpts { utc_offset_minutes: 0, report: false, strict: false, + allow_abr_only: false, auth: None, visitor_data: None, } @@ -962,6 +964,22 @@ impl RustyPipeBuilder { self } + /// Allow fetching player data containing only the ABR (Adaptive bitrate) stream URL. + /// + /// YouTube may return only the ABR stream URL and no URLs for individual streams + /// when using desktop client. To access the streams, your application needs to support + /// the ABR streaming protocol. + /// + /// You also need to set the `allow_abr_only` option for the [`crate::param::StreamFilter`] + /// to select streams without an URL. + /// + /// **Info**: you can set this option for individual queries, too + #[must_use] + pub fn allow_abr_only(mut self) -> Self { + self.default_opts.allow_abr_only = true; + self + } + /// Enable authentication for all requests /// /// Depending on the client type RustyPipe uses either the authentication cookie or the @@ -1780,6 +1798,20 @@ impl RustyPipeQuery { self } + /// Allow fetching player data containing only the ABR (Adaptive bitrate) stream URL. + /// + /// YouTube may return only the ABR stream URL and no URLs for individual streams + /// when using desktop client. To access the streams, your application needs to support + /// the ABR streaming protocol. + /// + /// You also need to set the `allow_abr_only` option for the [`crate::param::StreamFilter`] + /// to select streams without an URL. + #[must_use] + pub fn allow_abr_only(mut self) -> Self { + self.opts.allow_abr_only = true; + self + } + /// Enable authentication for this request /// /// Depending on the client type RustyPipe uses either the authentication cookie or the @@ -2356,6 +2388,7 @@ impl RustyPipeQuery { artist: ctx_src.artist.clone(), authenticated: self.opts.auth.unwrap_or_default(), session_po_token: ctx_src.session_po_token.clone(), + allow_abr_only: self.opts.allow_abr_only, }; let request = self @@ -2647,6 +2680,7 @@ struct MapRespCtx<'a> { artist: Option, authenticated: bool, session_po_token: Option, + allow_abr_only: bool, } /// Options to give to the mapper when making requests; @@ -2675,6 +2709,7 @@ impl<'a> MapRespCtx<'a> { artist: None, authenticated: false, session_po_token: None, + allow_abr_only: false, } } } diff --git a/src/client/player.rs b/src/client/player.rs index 70de86b..a7eb325 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -388,6 +388,24 @@ impl MapResponse for response::Player { ))); } + // Sometimes YouTube Desktop does not output any URLs for adaptive streams. + // Since this is currently rare, it is best to retry the request in this case + // unless ABR streaming is explicitly requested. + if !ctx.allow_abr_only + && !is_live + && !streaming_data.adaptive_formats.c.is_empty() + && streaming_data + .adaptive_formats + .c + .iter() + .all(|f| f.url.is_none() && f.signature_cipher.is_none()) + { + return Err(ExtractionError::Unavailable { + reason: UnavailabilityReason::TryAgain, + msg: "no adaptive stream URLs".to_owned(), + }); + } + let video_info = VideoPlayerDetails { id: video_details.video_id, name: video_details.title, @@ -1013,6 +1031,7 @@ mod tests { artist: None, authenticated: false, session_po_token: None, + allow_abr_only: false, }) .unwrap(); From 43b4f71a1b366e314964a3710fdf743e9cb846ca Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 6 Apr 2025 23:06:57 +0200 Subject: [PATCH 11/20] docs: abr streams now returned with 2% probability --- codegen/src/abtest.rs | 6 +++++- downloader/src/abr2.rs | 4 ++-- notes/AB_Tests.md | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/codegen/src/abtest.rs b/codegen/src/abtest.rs index 8c41913..8c5bd86 100644 --- a/codegen/src/abtest.rs +++ b/codegen/src/abtest.rs @@ -484,6 +484,10 @@ pub async fn command_executor_command(rp: &RustyPipeQuery) -> Result { pub async fn abr_stream_only(rp: &RustyPipeQuery) -> Result { let id = "pxY4OXVyMe4"; - let res = rp.player_from_client(id, ClientType::Desktop).await?; + let res = rp + .clone() + .allow_abr_only() + .player_from_client(id, ClientType::Desktop) + .await?; Ok(res.video_only_streams.iter().all(|s| s.url.is_none())) } diff --git a/downloader/src/abr2.rs b/downloader/src/abr2.rs index b543632..e8a7ba1 100644 --- a/downloader/src/abr2.rs +++ b/downloader/src/abr2.rs @@ -653,7 +653,7 @@ mod tests { #[tokio::test] #[tracing_test::traced_test] async fn experiment() { - let rp = RustyPipe::new(); + let rp = RustyPipe::builder().allow_abr_only().build().unwrap(); let player = rp.query().player("pxY4OXVyMe4").await.unwrap(); let stream_filter = StreamFilter::new() .allow_abr_only() @@ -697,7 +697,7 @@ mod tests { #[tokio::test] #[tracing_test::traced_test] async fn first_header() { - let rp = RustyPipe::new(); + let rp = RustyPipe::builder().allow_abr_only().build().unwrap(); let player = rp.query().player("pxY4OXVyMe4").await.unwrap(); let stream_filter = StreamFilter::new() .allow_abr_only() diff --git a/notes/AB_Tests.md b/notes/AB_Tests.md index 127721a..59b32bf 100644 --- a/notes/AB_Tests.md +++ b/notes/AB_Tests.md @@ -1107,4 +1107,4 @@ YouTube playlists may use a commandExecutorCommand which holds a list of command - **Encountered on:** 16.03.2025 - **Impact:** 🔴 High - **Endpoint:** player (YT) -- **Status:** Common (10%) +- **Status:** Experimental (2%) From 86c5bf8ef5896ee8294303e7dc26e64f8629081d Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Fri, 11 Apr 2025 11:52:35 +0200 Subject: [PATCH 12/20] docs: abr streams discontinued --- notes/AB_Tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/AB_Tests.md b/notes/AB_Tests.md index 59b32bf..e3193d9 100644 --- a/notes/AB_Tests.md +++ b/notes/AB_Tests.md @@ -1107,4 +1107,4 @@ YouTube playlists may use a commandExecutorCommand which holds a list of command - **Encountered on:** 16.03.2025 - **Impact:** 🔴 High - **Endpoint:** player (YT) -- **Status:** Experimental (2%) +- **Status:** Discontinued (0%) From 5b183f2b74f85caa9d2251b1aa2e05903d4e0c29 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Tue, 15 Apr 2025 19:14:29 +0200 Subject: [PATCH 13/20] docs: abr streams with 8% probability --- notes/AB_Tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/AB_Tests.md b/notes/AB_Tests.md index e3193d9..79733e8 100644 --- a/notes/AB_Tests.md +++ b/notes/AB_Tests.md @@ -1107,4 +1107,4 @@ YouTube playlists may use a commandExecutorCommand which holds a list of command - **Encountered on:** 16.03.2025 - **Impact:** 🔴 High - **Endpoint:** player (YT) -- **Status:** Discontinued (0%) +- **Status:** Common (8%) From 043dc006baba78765a74e13ab459b183a1f4518b Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 26 Apr 2025 00:36:18 +0200 Subject: [PATCH 14/20] docs: abr streams with 25% probability --- notes/AB_Tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/AB_Tests.md b/notes/AB_Tests.md index 79733e8..5c20ad6 100644 --- a/notes/AB_Tests.md +++ b/notes/AB_Tests.md @@ -1107,4 +1107,4 @@ YouTube playlists may use a commandExecutorCommand which holds a list of command - **Encountered on:** 16.03.2025 - **Impact:** 🔴 High - **Endpoint:** player (YT) -- **Status:** Common (8%) +- **Status:** Common (25%) From 04a89c815990f26aef9b3d576abfd79e69d321ec Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 28 Apr 2025 14:54:06 +0200 Subject: [PATCH 15/20] docs: abr streams with 12% probability --- notes/AB_Tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/AB_Tests.md b/notes/AB_Tests.md index 5c20ad6..f4ac52d 100644 --- a/notes/AB_Tests.md +++ b/notes/AB_Tests.md @@ -1107,4 +1107,4 @@ YouTube playlists may use a commandExecutorCommand which holds a list of command - **Encountered on:** 16.03.2025 - **Impact:** 🔴 High - **Endpoint:** player (YT) -- **Status:** Common (25%) +- **Status:** Common (12%) From ad75c7172787c814bd06f85ec90069a73835e127 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Tue, 29 Apr 2025 12:46:53 +0200 Subject: [PATCH 16/20] docs: abr streams stabilized --- notes/AB_Tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/AB_Tests.md b/notes/AB_Tests.md index f4ac52d..dd341fa 100644 --- a/notes/AB_Tests.md +++ b/notes/AB_Tests.md @@ -1107,4 +1107,4 @@ YouTube playlists may use a commandExecutorCommand which holds a list of command - **Encountered on:** 16.03.2025 - **Impact:** 🔴 High - **Endpoint:** player (YT) -- **Status:** Common (12%) +- **Status:** Stabilized From 48bb1b95d561d763bfc19cc23773e292d51a5d57 Mon Sep 17 00:00:00 2001 From: ta3pks Date: Tue, 17 Jun 2025 13:29:52 +0200 Subject: [PATCH 17/20] remove unwrap trying to fetch visitor data (#60) Co-authored-by: nikos efthias Reviewed-on: https://codeberg.org/ThetaDev/rustypipe/pulls/60 Co-authored-by: ta3pks Co-committed-by: ta3pks --- src/util/visitor_data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/visitor_data.rs b/src/util/visitor_data.rs index 8bc64c7..2de2cfe 100644 --- a/src/util/visitor_data.rs +++ b/src/util/visitor_data.rs @@ -110,7 +110,7 @@ impl VisitorDataCache { /// Fetch a new visitor data ID and store it in the cache pub async fn new_visitor_data(&self) -> Result { - let vd = self.fetch_visitor_data().await.unwrap(); + let vd = self.fetch_visitor_data().await?; self.inner .req_counter From 51c0d54ddb97f4a913ab6d2db6c70351f5aca7de Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Wed, 18 Jun 2025 13:24:56 +0200 Subject: [PATCH 18/20] WIP --- .gitignore | 4 + abr-proto/build.rs | 1 + .../video_streaming/client_abr_state.proto | 3 +- .../reload_player_response.proto | 10 + codegen/src/abtest.rs | 1 - downloader/Cargo.toml | 6 +- downloader/src/abr2.rs | 291 ++++-- downloader/src/abr3.rs | 968 ++++++++++++++++++ downloader/src/abr4.rs | 531 ++++++++++ downloader/src/abr_model.rs | 199 ++++ downloader/src/bin/abrmap.rs | 52 + downloader/src/bin/abrstream.rs | 97 ++ downloader/src/error.rs | 2 + downloader/src/lib.rs | 5 +- downloader/src/range_predict.rs | 110 ++ src/client/mod.rs | 35 - src/client/player.rs | 131 +-- src/client/response/player.rs | 4 +- src/model/mod.rs | 5 + src/model/ordering.rs | 1 + src/param/stream_filter.rs | 17 +- src/util/mod.rs | 14 +- src/util/protobuf.rs | 89 +- 23 files changed, 2370 insertions(+), 206 deletions(-) create mode 100644 abr-proto/proto/video_streaming/reload_player_response.proto create mode 100644 downloader/src/abr3.rs create mode 100644 downloader/src/abr4.rs create mode 100644 downloader/src/bin/abrmap.rs create mode 100644 downloader/src/bin/abrstream.rs create mode 100644 downloader/src/range_predict.rs diff --git a/.gitignore b/.gitignore index 3e0f26a..ad20047 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ rustypipe_reports rustypipe_cache*.json bg_snapshot.bin + +*.webm +*.mp4 +*.m4a diff --git a/abr-proto/build.rs b/abr-proto/build.rs index 93174f1..e8f4db2 100644 --- a/abr-proto/build.rs +++ b/abr-proto/build.rs @@ -35,6 +35,7 @@ fn compile() { vsdir.join("playback_cookie.proto"), vsdir.join("playback_start_policy.proto"), vsdir.join("proxy_status.proto"), + vsdir.join("reload_player_response.proto"), vsdir.join("request_cancellation_policy.proto"), vsdir.join("sabr_error.proto"), vsdir.join("sabr_redirect.proto"), diff --git a/abr-proto/proto/video_streaming/client_abr_state.proto b/abr-proto/proto/video_streaming/client_abr_state.proto index 3bd3cfb..5b05cb3 100644 --- a/abr-proto/proto/video_streaming/client_abr_state.proto +++ b/abr-proto/proto/video_streaming/client_abr_state.proto @@ -2,7 +2,7 @@ syntax = "proto2"; package video_streaming; message ClientAbrState { - optional int32 time_since_last_manual_format_selection_ms = 13; + optional int64 time_since_last_manual_format_selection_ms = 13; optional int32 last_manual_direction = 14; optional int32 last_manual_selected_resolution = 16; optional int32 detailed_network_type = 17; @@ -40,4 +40,5 @@ message ClientAbrState { optional int32 sabr_force_proxima = 66; optional int32 Tqb = 67; optional int64 sabr_force_max_network_interruption_duration_ms = 68; + optional string audio_track_id = 69; } diff --git a/abr-proto/proto/video_streaming/reload_player_response.proto b/abr-proto/proto/video_streaming/reload_player_response.proto new file mode 100644 index 0000000..27937c7 --- /dev/null +++ b/abr-proto/proto/video_streaming/reload_player_response.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; +package video_streaming; + +message ReloadPlaybackParams { + optional string token = 1; +} + +message ReloadPlayerResponse { + optional ReloadPlaybackParams reload_playback_params = 1; +} diff --git a/codegen/src/abtest.rs b/codegen/src/abtest.rs index 8c5bd86..a442ed7 100644 --- a/codegen/src/abtest.rs +++ b/codegen/src/abtest.rs @@ -486,7 +486,6 @@ pub async fn abr_stream_only(rp: &RustyPipeQuery) -> Result { let id = "pxY4OXVyMe4"; let res = rp .clone() - .allow_abr_only() .player_from_client(id, ClientType::Desktop) .await?; Ok(res.video_only_streams.iter().all(|s| s.url.is_none())) diff --git a/downloader/Cargo.toml b/downloader/Cargo.toml index 67c60eb..bd0b0f7 100644 --- a/downloader/Cargo.toml +++ b/downloader/Cargo.toml @@ -41,7 +41,7 @@ thiserror.workspace = true futures-util.workspace = true reqwest = { workspace = true, features = ["stream"] } rand.workspace = true -tokio = { workspace = true, features = ["macros", "fs", "process"] } +tokio = { workspace = true, features = ["macros", "fs", "process", "rt-multi-thread"] } indicatif = { workspace = true, optional = true } filenamify.workspace = true tracing.workspace = true @@ -60,6 +60,10 @@ byteorder = "1.0.0" async-stream = "0.3.6" pin-project-lite = "0.2.11" +serde.workspace = true +serde_json.workspace = true +tracing-subscriber.workspace = true + [dev-dependencies] path_macro.workspace = true rstest.workspace = true diff --git a/downloader/src/abr2.rs b/downloader/src/abr2.rs index e8a7ba1..86dfbab 100644 --- a/downloader/src/abr2.rs +++ b/downloader/src/abr2.rs @@ -1,16 +1,11 @@ -#![allow(unused)] +#![allow(missing_docs, unused)] -use std::{ - collections::{HashMap, HashSet}, - marker::PhantomData, - pin::Pin, - task::{ready, Poll}, -}; +use std::collections::{HashMap, HashSet}; use async_stream::try_stream; use bytes::{Buf, Bytes}; use data_encoding::BASE64URL; -use futures_util::{stream::BoxStream, FutureExt, Stream, StreamExt, TryStreamExt}; +use futures_util::{Stream, StreamExt, TryStreamExt}; use protobuf::{Message, MessageField}; use reqwest::Client; use rustypipe::model::{AudioStream, VideoStream}; @@ -22,88 +17,117 @@ use rustypipe_abr_proto::{ media_header::MediaHeader, next_request_policy::NextRequestPolicy, playback_cookie::PlaybackCookie, + reload_player_response::ReloadPlayerResponse, sabr_error::SabrError, sabr_redirect::SabrRedirect, stream_protection_status::StreamProtectionStatus, streamer_context::{streamer_context::ClientInfo, StreamerContext}, - time_range::TimeRange, video_playback_abr_request::VideoPlaybackAbrRequest, }; +use serde::{Deserialize, Serialize}; use crate::{abr_model::PartType, error::AbrError, util::BytesBuffer}; pub struct AbrStream { - http: Client, - url: String, - ustreamer_config: Vec, - po_token: Option>, - abr_state: ClientAbrState, - audio_format_ids: Vec, - video_format_ids: Vec, - playback_cookie: Option, - backoff_time_ms: i32, - formats_by_key: HashMap, - header_id_to_format_key_map: HashMap, - previous_sequences: HashMap>, - buffer: BytesBuffer, - last_header: Option, + pub http: Client, + pub url: String, + pub ustreamer_config: Vec, + pub po_token: Option>, + pub abr_state: ClientAbrState, + pub audio_format_ids: Vec, + pub video_format_ids: Vec, + pub playback_cookie: Option, + pub backoff_time_ms: i32, + pub formats_by_key: HashMap, + pub header_id_to_format_key_map: HashMap, + pub previous_sequences: HashMap>, + pub buffer: BytesBuffer, + pub last_header: Option, } #[derive(Default, Clone)] pub struct AbrStreamOptions<'a> { - http: Client, - url: String, - ustreamer_config: &'a str, - po_token: Option<&'a str>, - audio_streams: &'a [&'a AudioStream], - video_streams: &'a [&'a VideoStream], - abr_state: ClientAbrState, + pub http: Client, + pub url: String, + pub ustreamer_config: &'a str, + pub po_token: Option<&'a str>, + pub audio_streams: &'a [&'a AudioStream], + pub video_streams: &'a [&'a VideoStream], + pub player_time: i64, + pub abr_state: ClientAbrState, } -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct AbrStreamItem { - format: FormatInfo, - data: Bytes, + pub format: FormatInfo, + pub offset: i32, + pub seq: i64, + pub start_ms: i32, + pub data: Bytes, } #[derive(Debug, Clone)] pub struct FormatInfo { - itag: i32, - last_modified: u64, - xtags: Option, - mime_type: Option, + pub itag: i32, + pub last_modified: u64, + pub xtags: Option, + pub mime_type: Option, } #[derive(Debug)] -struct InitializedFormat { - format_id: FormatId, - duration_ms: Option, - mime_type: Option, - sequence_count: Option, - sequence_list: Vec, - state: BufferedRange, +pub struct InitializedFormat { + pub format_id: FormatId, + pub duration_ms: Option, + pub mime_type: Option, + pub sequence_count: Option, + pub sequence_list: Vec, + pub state: BufferedRange, } -#[derive(Debug)] -struct Sequence { - itag: Option, - format_id: FormatId, - is_init_segment: bool, - duration_ms: Option, - start_ms: Option, - start_data_range: Option, - sequence_number: Option, - content_length: Option, - time_range: Option, +#[derive(Debug, Clone, Serialize)] +pub struct Sequence { + pub itag: Option, + #[serde(skip)] + pub format_id: FormatId, + pub is_init_segment: bool, + pub duration_ms: Option, + pub start_ms: Option, + pub start_data_range: Option, + pub sequence_number: Option, + pub content_length: Option, + pub time_range: Option, + #[serde(skip)] + pub bytes_written: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TimeRange { + pub start: i64, + pub duration: i64, + pub timescale: i32, } #[derive(Debug, Clone)] -struct UmpHeader { +pub struct UmpHeader { part_type: PartType, size: u64, header_id: Option, } +impl std::fmt::Debug for AbrStreamItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "AbrStreamItem {{#{} seq{} {}ms {}-{}}}", + self.format.itag, + self.seq, + self.start_ms, + self.offset, + self.offset as usize + self.data.len() + ) + } +} + impl From<&InitializedFormat> for FormatInfo { fn from(value: &InitializedFormat) -> Self { Self { @@ -138,7 +162,7 @@ impl AbrStream { abr_state.set_time_since_last_manual_format_selection_ms(0); } if !abr_state.has_player_time_ms() { - abr_state.set_player_time_ms(0); + abr_state.set_player_time_ms(options.player_time); } if !abr_state.has_visibility() { abr_state.set_visibility(0); @@ -146,6 +170,12 @@ impl AbrStream { if !abr_state.has_enabled_track_types_bitfield() { abr_state.set_enabled_track_types_bitfield(i32::from(options.video_streams.is_empty())); } + if !abr_state.has_audio_track_id() { + if let Some(audio_track) = options.audio_streams.first().and_then(|s| s.track.as_ref()) + { + abr_state.set_audio_track_id(audio_track.id.to_owned()); + } + } if let Some(v) = options.video_streams.first() { if !abr_state.has_last_manual_selected_resolution() { abr_state.set_last_manual_selected_resolution(v.height as i32); @@ -154,6 +184,11 @@ impl AbrStream { abr_state.set_sticky_resolution(v.height as i32); } } + if !abr_state.has_drc_enabled() { + if let Some(audio) = options.audio_streams.first() { + abr_state.set_drc_enabled(audio.is_drc); + } + } let audio_format_ids = options .audio_streams @@ -249,7 +284,7 @@ impl AbrStream { (n, offset + byte_length) } - fn parse_ump_header(&mut self) -> Option { + pub fn parse_ump_header(&mut self) -> Option { let (part_type, o1) = self.read_varint(0); if o1 == 0 { return None; @@ -273,14 +308,18 @@ impl AbrStream { } } - fn process_ump_part(&mut self) -> Result<(Option, bool), AbrError> { + pub fn process_ump_part(&mut self) -> Result<(Option, bool), AbrError> { if let Some(last_header) = self.last_header.clone() { let hsize = last_header.size as usize; if last_header.part_type == PartType::MEDIA { // Media data may be processed if the part is only partially downloaded - self.process_media_data().map(|x| (x, true)) + self.process_media_data().map(|x| { + let eod = x.is_none(); + (x, eod) + }) } else if self.buffer.remaining() < hsize { // Wait for the entire part to be downloaded + tracing::info!("waiting for entire part"); Ok((None, true)) } else { let part_data = self.buffer.copy_to_bytes(hsize); @@ -290,6 +329,7 @@ impl AbrStream { PartType::NEXT_REQUEST_POLICY => { self.process_next_request_policy(&part_data)? } + PartType::RELOAD_PLAYER_RESPONSE => self.process_reload_player(&part_data)?, PartType::FORMAT_INITIALIZATION_METADATA => { self.process_format_initialization(&part_data)? } @@ -359,6 +399,7 @@ impl AbrStream { .iter() .all(|x| x.sequence_number != Some(mh.sequence_number())) { + let tr = mh.time_range.as_ref(); let seq = Sequence { itag: mh.itag, format_id: format_id.clone(), @@ -368,16 +409,36 @@ impl AbrStream { start_data_range: mh.start_data_range, sequence_number: mh.sequence_number, content_length: mh.content_length, - time_range: mh.time_range.as_ref().cloned(), + time_range: tr.and_then(|tr| { + tr.start.zip(tr.duration).zip(tr.timescale).map( + |((start, duration), timescale)| TimeRange { + start, + duration, + timescale, + }, + ) + }), + bytes_written: 0, }; - dbg!(&seq); - if mh.itag == Some(251) { - tracing::debug!( - "audio #{}: {}", - seq.sequence_number.unwrap_or_default(), - seq.content_length.unwrap_or_default() - ); - } + + // TODO: testing + let sname = if mh.itag == Some(251) { + "audio" + } else { + "video" + }; + tracing::debug!( + "{sname} #{}: content_length={} start_data_range={} time_start={} time_duration={}", + seq.sequence_number.unwrap_or_default(), + seq.content_length.unwrap_or_default(), + seq.start_data_range.unwrap_or_default(), + seq.time_range.as_ref().map(|t| t.start).unwrap_or_default(), + seq.time_range + .as_ref() + .map(|t| t.duration) + .unwrap_or_default(), + ); + current_format.sequence_list.push(seq); if mh.has_sequence_number() { @@ -393,7 +454,7 @@ impl AbrStream { Ok(()) } - fn process_media_data(&mut self) -> Result, AbrError> { + pub fn process_media_data(&mut self) -> Result, AbrError> { if let Some(last_header) = &mut self.last_header { if let Some(header_id) = last_header.header_id.or_else(|| { let x = self.buffer.try_get_u8().ok(); @@ -423,7 +484,22 @@ impl AbrStream { if last_header.size == 0 { self.last_header = None; } - Ok(Some(AbrStreamItem { format, data })) + let seq = self + .formats_by_key + .get_mut(format_key) + .unwrap() + .sequence_list + .last_mut() + .ok_or(AbrError::Invalid("no current sequence".into()))?; + let offset = seq.start_data_range.unwrap_or_default() + seq.bytes_written; + seq.bytes_written += data.len() as i32; + Ok(Some(AbrStreamItem { + format, + offset, + seq: seq.sequence_number.unwrap_or_default(), + start_ms: seq.start_ms.unwrap_or_default(), + data, + })) } else { Ok(None) } @@ -439,11 +515,18 @@ impl AbrStream { fn process_next_request_policy(&mut self, data: &[u8]) -> Result<(), AbrError> { let mut policy = NextRequestPolicy::parse_from_bytes(data)?; + tracing::debug!("NextRequestPolicy {policy:?}"); self.playback_cookie = policy.playback_cookie.take(); self.backoff_time_ms = policy.backoff_time_ms(); Ok(()) } + fn process_reload_player(&mut self, data: &[u8]) -> Result<(), AbrError> { + let mut reload = ReloadPlayerResponse::parse_from_bytes(data)?; + tracing::debug!("ReloadPlayerResponse {reload:?}"); + Ok(()) + } + fn process_format_initialization(&mut self, data: &[u8]) -> Result<(), AbrError> { let init = FormatInitializationMetadata::parse_from_bytes(data)?; let format = InitializedFormat::try_from(init)?; @@ -472,7 +555,22 @@ impl AbrStream { .or(self.formats_by_key.values().next()) } - async fn fetch_stream_data(&self) -> Result { + /// A stream is done if all sequences from all formats were received or the last request did not contain any sequences + fn is_done(&self) -> bool { + self.formats_by_key.values().all(|f| { + f.sequence_list.is_empty() + || f.sequence_count + .zip(f.sequence_list.last()) + .map(|(sc, last)| last.sequence_number == Some(sc.into())) + .unwrap_or_default() + }) + } + + pub async fn fetch_stream_data(&self) -> Result { + tracing::debug!( + "fetching stream data: {}ms", + self.abr_state.player_time_ms() + ); let body = VideoPlaybackAbrRequest { client_abr_state: MessageField::some(self.abr_state.clone()), selected_format_ids: self @@ -514,7 +612,7 @@ impl AbrStream { Ok(resp) } - fn stream(mut self) -> impl Stream> { + pub fn stream(mut self) -> impl Stream> { let stream = try_stream! { let mut ri = 1; loop { @@ -549,25 +647,23 @@ impl AbrStream { } tracing::debug!("request #{ri} fetched {page_len} bytes"); - let main_format = self.main_format().ok_or(AbrError::Invalid("no main format".into()))?; + let main_format = self.main_format().ok_or(AbrError::Invalid(format!("no main format: {:?}", self.formats_by_key.values()).into()))?; let player_time = self.abr_state.player_time_ms() + main_format.sequence_list.iter().fold(0, |acc, seq| { acc + i64::from(seq.duration_ms.unwrap_or_default()) }); - let is_last = main_format - .sequence_count - .zip(main_format.sequence_list.last()) - .map(|(sc, last)| last.sequence_number == Some(sc.into())) - .unwrap_or_default(); - tracing::debug!("player time: {player_time}; sequence_count={}, last={}", main_format.sequence_count.unwrap_or_default(), main_format - .sequence_list - .last() - .and_then(|x| x.sequence_number).unwrap_or_default()); - self.abr_state.set_player_time_ms(player_time); - if is_last { + + tracing::debug!( + "player time: {player_time}; sequence_count={}, last={:?}", + main_format.sequence_count.unwrap_or_default(), + main_format.sequence_list.last().and_then(|x| x.sequence_number), + ); + if self.is_done() { break; } + self.abr_state.set_player_time_ms(player_time); + if self.backoff_time_ms > 0 { tracing::debug!("backoff: {} ms", self.backoff_time_ms); tokio::time::sleep(std::time::Duration::from_millis( @@ -578,6 +674,12 @@ impl AbrStream { ri += 1; } + + // DEBUG: + // for f in self.formats_by_key.values() { + // let mut of = File::create(format!("/home/thetadev/Documents/Programmieren/Rust/rustypipe/tmp/abrmap/{}_{}.json", std::env::args().into_iter().nth(1).unwrap(), f.format_id.itag())).unwrap(); + // serde_json::to_writer(of, &f.sequence_list2).unwrap(); + // } }; Box::pin(stream) } @@ -653,15 +755,15 @@ mod tests { #[tokio::test] #[tracing_test::traced_test] async fn experiment() { - let rp = RustyPipe::builder().allow_abr_only().build().unwrap(); - let player = rp.query().player("pxY4OXVyMe4").await.unwrap(); + let rp = RustyPipe::builder().build().unwrap(); + let player = rp.query().player("ZeerrnuLi5E").await.unwrap(); let stream_filter = StreamFilter::new() .allow_abr_only() - // .video_max_res(360) + .video_max_res(360) .video_formats([VideoFormat::Webm]) .audio_codecs([AudioCodec::Opus]); let (video, audio) = player.select_video_audio_stream(&stream_filter); - // let video = video.unwrap(); + let video = video.unwrap(); let audio = audio.unwrap(); let mut abr = AbrStream::new(AbrStreamOptions { @@ -671,6 +773,7 @@ mod tests { audio_streams: &[audio], // video_streams: &[video], video_streams: &[], + player_time: 0, ..Default::default() }) .unwrap(); @@ -697,15 +800,15 @@ mod tests { #[tokio::test] #[tracing_test::traced_test] async fn first_header() { - let rp = RustyPipe::builder().allow_abr_only().build().unwrap(); - let player = rp.query().player("pxY4OXVyMe4").await.unwrap(); + let rp = RustyPipe::builder().build().unwrap(); + let player = rp.query().player("8AeOP8u89iE").await.unwrap(); let stream_filter = StreamFilter::new() .allow_abr_only() // .video_max_res(360) .video_formats([VideoFormat::Webm]) .audio_codecs([AudioCodec::Opus]); let (video, audio) = player.select_video_audio_stream(&stream_filter); - // let video = video.unwrap(); + let video = video.unwrap(); let audio = audio.unwrap(); let mut abr = AbrStream::new(AbrStreamOptions { @@ -713,8 +816,8 @@ mod tests { ustreamer_config: player.abr_ustreamer_config.as_deref().unwrap(), po_token: player.po_token.as_deref(), audio_streams: &[audio], - // video_streams: &[video], - video_streams: &[], + video_streams: &[video], + // video_streams: &[], ..Default::default() }) .unwrap(); diff --git a/downloader/src/abr3.rs b/downloader/src/abr3.rs new file mode 100644 index 0000000..6afcfdd --- /dev/null +++ b/downloader/src/abr3.rs @@ -0,0 +1,968 @@ +#![allow(missing_docs)] + +use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, RwLock}, + time::Instant, +}; + +use async_stream::try_stream; +use bytes::{Buf, Bytes}; +use data_encoding::BASE64URL; +use futures_util::{Stream, StreamExt}; +use protobuf::{Message, MessageField}; +use reqwest::Client; +use tokio::sync::broadcast; + +use rustypipe::model::{AudioStream, VideoStream}; +use rustypipe_abr_proto::{ + buffered_range::BufferedRange, + client_abr_state::ClientAbrState, + common::FormatId, + format_initialization_metadata::FormatInitializationMetadata, + media_header::MediaHeader, + next_request_policy::NextRequestPolicy, + playback_cookie::PlaybackCookie, + reload_player_response::ReloadPlayerResponse, + sabr_error::SabrError, + sabr_redirect::SabrRedirect, + stream_protection_status::StreamProtectionStatus, + streamer_context::{streamer_context::ClientInfo, StreamerContext}, + time_range::TimeRange, + video_playback_abr_request::VideoPlaybackAbrRequest, +}; + +use crate::{abr_model::PartType, error::AbrError, util::BytesBuffer}; + +#[derive(Clone)] +pub struct AbrStream { + inner: Arc, +} + +struct AbrStreamInner { + http: Client, + ustreamer_config: Vec, + cancel_tx: broadcast::Sender<()>, + vars: RwLock, +} + +// Mutable data shared with multiple streamers +struct AbrStreamVars { + url: String, + po_token: Option>, + audio_format_ids: Vec, + video_format_ids: Vec, + abr_state: ClientAbrState, + last_seek: Instant, + last_format_selection: Instant, + formats_by_key: HashMap, + playback_cookie: Option, +} + +// Data associated with a single stream (owned by the streamer) +pub struct AbrStreamConsumer { + stream: AbrStream, + header_id_to_format_key_map: HashMap, + sequence_lists: HashMap>, + previous_sequences: HashMap>, + backoff_time_ms: i32, + buffer: BytesBuffer, + last_header: Option, +} + +#[derive(Default, Clone)] +pub struct AbrStreamOptions<'a> { + pub http: Client, + pub url: String, + pub ustreamer_config: &'a str, + pub po_token: Option<&'a str>, + pub audio_streams: &'a [&'a AudioStream], + pub video_streams: &'a [&'a VideoStream], + pub player_time_ms: i64, + pub abr_state: ClientAbrState, +} + +#[derive(Clone)] +pub struct AbrStreamItem { + pub format: FormatInfo, + pub offset: i32, + pub seq: i64, + pub start_ms: i32, + pub data: Bytes, +} + +#[derive(Debug, Clone)] +pub struct FormatInfo { + pub itag: i32, + pub last_modified: u64, + pub xtags: Option, +} + +#[derive(Debug, Clone)] +struct InitializedFormat { + format_id: FormatId, + duration_ms: Option, + mime_type: Option, + sequence_count: Option, + state: BufferedRange, +} + +#[derive(Debug, Clone)] +struct Sequence { + itag: Option, + format_id: FormatId, + is_init_segment: bool, + duration_ms: Option, + start_ms: Option, + start_data_range: Option, + sequence_number: Option, + content_length: Option, + time_range: Option, + bytes_written: i32, +} + +#[derive(Debug, Clone)] +struct UmpHeader { + part_type: PartType, + size: u64, + header_id: Option, +} + +impl std::fmt::Debug for AbrStreamItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "AbrStreamItem {{#{} seq{} {}ms {}-{}}}", + self.format.itag, + self.seq, + self.start_ms, + self.offset, + self.offset as usize + self.data.len() + ) + } +} + +impl From<&InitializedFormat> for FormatInfo { + fn from(value: &InitializedFormat) -> Self { + Self { + itag: value.format_id.itag(), + last_modified: value.format_id.last_modified(), + xtags: value.format_id.xtags.clone(), + } + } +} + +impl AbrStream { + pub fn new(options: AbrStreamOptions) -> Result { + let ustreamer_config = BASE64URL + .decode(options.ustreamer_config.as_bytes()) + .map_err(|e| { + AbrError::Invalid(format!("could not parse ustreamer_config: {e}").into()) + })?; + let po_token = match options.po_token { + Some(pot) => Some(parse_po_token(pot)?), + None => None, + }; + + let mut abr_state = options.abr_state; + if !abr_state.has_last_manual_direction() { + abr_state.set_last_manual_direction(0); + } + if !abr_state.has_time_since_last_manual_format_selection_ms() { + abr_state.set_time_since_last_manual_format_selection_ms(0); + } + if !abr_state.has_player_time_ms() { + abr_state.set_player_time_ms(options.player_time_ms); + } + if !abr_state.has_visibility() { + abr_state.set_visibility(0); + } + if !abr_state.has_enabled_track_types_bitfield() { + abr_state.set_enabled_track_types_bitfield(i32::from(options.video_streams.is_empty())); + } + if !abr_state.has_audio_track_id() { + if let Some(audio_track) = options.audio_streams.first().and_then(|s| s.track.as_ref()) + { + abr_state.set_audio_track_id(audio_track.id.to_owned()); + } + } + if let Some(v) = options.video_streams.first() { + if !abr_state.has_last_manual_selected_resolution() { + abr_state.set_last_manual_selected_resolution(v.height as i32); + } + if !abr_state.has_sticky_resolution() { + abr_state.set_sticky_resolution(v.height as i32); + } + } + if !abr_state.has_drc_enabled() { + if let Some(audio) = options.audio_streams.first() { + abr_state.set_drc_enabled(audio.is_drc); + } + } + + let audio_format_ids = options + .audio_streams + .iter() + .map(|s| FormatId { + itag: Some(s.itag as i32), + last_modified: s.last_modified, + xtags: s.xtags.clone(), + ..Default::default() + }) + .collect::>(); + let video_format_ids = options + .video_streams + .iter() + .map(|s| FormatId { + itag: Some(s.itag as i32), + last_modified: s.last_modified, + xtags: s.xtags.clone(), + ..Default::default() + }) + .collect::>(); + + let (cancel_tx, _) = broadcast::channel(1); + + Ok(Self { + inner: AbrStreamInner { + http: options.http, + ustreamer_config, + cancel_tx, + vars: RwLock::new(AbrStreamVars { + url: options.url, + po_token, + abr_state, + audio_format_ids, + video_format_ids, + last_seek: Instant::now(), + last_format_selection: Instant::now(), + formats_by_key: HashMap::new(), + playback_cookie: None, + }), + } + .into(), + }) + } + + fn consumer(&self) -> AbrStreamConsumer { + AbrStreamConsumer { + stream: self.clone(), + header_id_to_format_key_map: HashMap::new(), + sequence_lists: HashMap::new(), + previous_sequences: HashMap::new(), + backoff_time_ms: 0, + buffer: BytesBuffer::new(), + last_header: None, + } + } + + pub fn cancel_current(&self) { + let _ = self.inner.cancel_tx.send(()); + } + + pub fn set_po_token(&self, po_token: &str) -> Result<(), AbrError> { + let pot = parse_po_token(po_token)?; + let mut vars = self.inner.vars.write().unwrap(); + vars.po_token = Some(pot); + Ok(()) + } + + pub fn set_audio_format_ids(&self, audio_streams: &[&AudioStream]) { + let audio_format_ids = audio_streams + .iter() + .map(|s| FormatId { + itag: Some(s.itag as i32), + last_modified: s.last_modified, + xtags: s.xtags.clone(), + ..Default::default() + }) + .collect::>(); + let mut vars = self.inner.vars.write().unwrap(); + vars.audio_format_ids = audio_format_ids; + + if let Some(audio) = audio_streams.first() { + vars.abr_state.set_drc_enabled(audio.is_drc); + vars.abr_state.audio_track_id = audio.track.as_ref().map(|t| t.id.to_owned()); + } + + vars.last_format_selection = Instant::now(); + } + + pub fn set_video_format_ids(&self, video_streams: &[&VideoStream]) { + let video_format_ids = video_streams + .iter() + .map(|s| FormatId { + itag: Some(s.itag as i32), + last_modified: s.last_modified, + xtags: s.xtags.clone(), + ..Default::default() + }) + .collect::>(); + let mut vars = self.inner.vars.write().unwrap(); + vars.video_format_ids = video_format_ids; + + if let Some(v) = video_streams.first() { + vars.abr_state + .set_last_manual_selected_resolution(v.height as i32); + vars.abr_state.set_sticky_resolution(v.height as i32); + } + if vars.abr_state.has_enabled_track_types_bitfield() { + vars.abr_state + .set_enabled_track_types_bitfield(i32::from(video_streams.is_empty())); + } + + vars.last_format_selection = Instant::now(); + } + + pub fn set_player_time_ms(&self, v: i64) { + { + let mut vars = self.inner.vars.write().unwrap(); + vars.abr_state.set_player_time_ms(v); + vars.last_seek = Instant::now(); + } + self.cancel_current(); + } + + pub fn stream(&self) -> impl Stream> { + let mut c = self.consumer(); + let mut rx = self.inner.cancel_tx.subscribe(); + + let stream = try_stream! { + let mut ri = 1; + + 'main: loop { + 'fetch: { + let resp = tokio::select! { + _ = rx.recv() => { + tracing::debug!("interrupted during fetch"); + c.buffer = BytesBuffer::new(); + c.previous_sequences = HashMap::new(); + c.last_header = None; + break 'fetch; + } + resp = c.fetch_stream_data() => resp + }?; + + let mut data_stream = resp.bytes_stream(); + let mut page_len = 0; + // c.sequence_lists.clear(); + + 'chunks: loop { + let chunk = tokio::select! { + _ = rx.recv() => { + tracing::debug!("interrupted during chunk"); + break 'fetch; + } + chunk = data_stream.next() => chunk + }; + + if let Some(chunk) = chunk { + let chunk = chunk?; + let clen = chunk.len(); + page_len += clen; + tracing::debug!("chunk {clen}"); + c.buffer.push(chunk); + + 'chunk: loop { + if c.last_header.is_none() { + c.last_header = c.parse_ump_header(); + } + tracing::debug!("loop {:?}", c.last_header); + if c.last_header.is_none() { + break 'chunk; + } + let (x, y) = c.process_ump_part()?; + if let Some(item) = x { + yield item; + } + if y { + break 'chunk; + } + } + } else { + break 'chunks; + } + } + + tracing::debug!("request #{ri} fetched {page_len} bytes"); + + { + let main_format = c.main_format()?; + let mut vars = c.stream.inner.vars.write().unwrap(); + let mut player_time = vars.abr_state.player_time_ms(); + let main_format_sl = c.sequence_lists.get(&get_format_key(&main_format.format_id)); + if let Some(main_format_sl) =main_format_sl { + player_time += main_format_sl.iter().fold(0, |acc, seq| { + acc + i64::from(seq.duration_ms.unwrap_or_default()) + }); + vars.abr_state.set_player_time_ms(player_time); + } + + tracing::debug!( + "player time: {player_time}; sequence_count={}, last={:?}", + main_format.sequence_count.unwrap_or_default(), + main_format_sl.and_then(|x| x.last()).and_then(|x| x.sequence_number), + ); + } + if c.is_done() { + break 'main; + } + } + + if c.backoff_time_ms > 0 { + tracing::debug!("backoff: {} ms", c.backoff_time_ms); + tokio::time::sleep(std::time::Duration::from_millis( + c.backoff_time_ms.try_into().unwrap_or_default(), + )).await; + c.backoff_time_ms = 0; + } + + ri += 1; + } + }; + Box::pin(stream) + } +} + +impl AbrStreamConsumer { + fn read_varint(&mut self, offset: usize) -> (u64, usize) { + // Determine the length of the val + let buffer_len = self.buffer.remaining().saturating_sub(offset); + if buffer_len == 0 { + return (0, 0); + } + + let fb = self.buffer[offset]; + let byte_length = if fb < 128 { + 1 + } else if fb < 192 { + 2 + } else if fb < 224 { + 3 + } else if fb < 240 { + 4 + } else { + 5 + }; + + if buffer_len < byte_length { + return (0, 0); + } + + let n = match byte_length { + 1 => self.buffer[offset].into(), + 2 => { + let b1 = u64::from(self.buffer[offset]); + let b2 = u64::from(self.buffer[offset + 1]); + (b1 & 0x3f) + 64 * b2 + } + 3 => { + let b1 = u64::from(self.buffer[offset]); + let b2 = u64::from(self.buffer[offset + 1]); + let b3 = u64::from(self.buffer[offset + 2]); + (b1 & 0x1f) + 32 * (b2 + 256 * b3) + } + 4 => { + let b1 = u64::from(self.buffer[offset]); + let b2 = u64::from(self.buffer[offset + 1]); + let b3 = u64::from(self.buffer[offset + 2]); + let b4 = u64::from(self.buffer[offset + 3]); + (b1 & 0x0f) + 16 * (b2 + 256 * (b3 + 256 * b4)) + } + _ => u32::from_be_bytes([ + self.buffer[offset + 1], + self.buffer[offset + 2], + self.buffer[offset + 3], + self.buffer[offset + 4], + ]) + .into(), + }; + (n, offset + byte_length) + } + + fn parse_ump_header(&mut self) -> Option { + let (part_type, o1) = self.read_varint(0); + if o1 == 0 { + return None; + } + let (part_size, o2) = self.read_varint(o1); + if o2 == 0 { + return None; + } + self.buffer.advance(o2); + + Some(UmpHeader { + part_type: part_type.into(), + size: part_size, + header_id: None, + }) + } + + fn process_ump_part(&mut self) -> Result<(Option, bool), AbrError> { + if let Some(last_header) = self.last_header.clone() { + let hsize = last_header.size as usize; + if last_header.part_type == PartType::MEDIA { + // Media data may be processed if the part is only partially downloaded + self.process_media_data().map(|x| { + let eod = x.is_none(); + (x, eod) + }) + } else if self.buffer.remaining() < hsize { + // Wait for the entire part to be downloaded + tracing::info!("waiting for entire part"); + Ok((None, true)) + } else { + let part_data = self.buffer.copy_to_bytes(hsize); + match last_header.part_type { + PartType::MEDIA_HEADER => self.process_media_header(&part_data)?, + PartType::MEDIA_END => self.process_media_end(&part_data), + PartType::NEXT_REQUEST_POLICY => { + self.process_next_request_policy(&part_data)? + } + PartType::RELOAD_PLAYER_RESPONSE => self.process_reload_player(&part_data)?, + PartType::FORMAT_INITIALIZATION_METADATA => { + self.process_format_initialization(&part_data)? + } + PartType::SABR_ERROR => { + return Err(AbrError::Sabr(SabrError::parse_from_bytes(&part_data)?)); + } + PartType::SABR_REDIRECT => self.process_sabr_redirect(&part_data)?, + PartType::STREAM_PROTECTION_STATUS => { + let prot = StreamProtectionStatus::parse_from_bytes(&part_data)?; + match prot.status() { + 1 => tracing::debug!("[StreamProtectionStatus]: Good"), + 2 => tracing::warn!("[StreamProtectionStatus]: Attestation pending"), + 3 => return Err(AbrError::Attestation), + _ => {} + } + } + _ => {} + }; + self.last_header = None; + Ok((None, false)) + } + } else { + Ok((None, true)) + } + } + + fn process_media_header(&mut self, data: &[u8]) -> Result<(), AbrError> { + let mh = MediaHeader::parse_from_bytes(data)?; + + let format_id = mh + .format_id + .as_ref() + .ok_or(AbrError::Invalid("media header: no format_id".into()))?; + let header_id = mh + .header_id + .and_then(|hid| u8::try_from(hid).ok()) + .ok_or(AbrError::Invalid("media header: no header_id".into()))?; + + let format_key = get_format_key(format_id); + + let mut vars = self.stream.inner.vars.write().unwrap(); + if !vars.formats_by_key.contains_key(&format_key) { + vars.formats_by_key + .insert(format_key.to_owned(), InitializedFormat::try_from(&mh)?); + } + + let current_format = vars.formats_by_key.get_mut(&format_key).unwrap(); + + // This is a hacky workaround to prevent duplicate sequences from being added. This should be fixed in the future + // (preferably by figuring out how to make the server not send duplicates). + if let Some(sequence_number) = mh.sequence_number { + let pseq = self + .previous_sequences + .entry(format_key.to_owned()) + .or_default(); + if !pseq.insert(sequence_number) { + tracing::warn!("duplicate sequence {sequence_number}"); + return Ok(()); + } + } + + // Save the header's ID so we can identify its stream data later. + self.header_id_to_format_key_map + .entry(header_id) + .or_insert(format_key.to_owned()); + + let sequence_list = self.sequence_lists.entry(format_key).or_default(); + if sequence_list + .iter() + .all(|x| x.sequence_number != Some(mh.sequence_number())) + { + if mh.has_sequence_number() { + current_format.state.set_duration_ms( + current_format.state.duration_ms() + i64::from(mh.duration_ms()), + ); + current_format + .state + .set_end_segment_index(current_format.state.end_segment_index() + 1); + } + + let seq = Sequence { + itag: mh.itag, + format_id: format_id.clone(), + is_init_segment: mh.is_init_seg(), + duration_ms: mh.duration_ms, + start_ms: mh.start_ms, + start_data_range: mh.start_data_range, + sequence_number: mh.sequence_number, + content_length: mh.content_length, + time_range: mh.time_range.into_option(), + bytes_written: 0, + }; + + // TODO: testing + let sname = if mh.itag == Some(251) { + "audio" + } else { + "video" + }; + tracing::debug!( + "{sname} #{}: content_length={} start_data_range={} time_start={} time_duration={}", + seq.sequence_number.unwrap_or_default(), + seq.content_length.unwrap_or_default(), + seq.start_data_range.unwrap_or_default(), + seq.time_range + .as_ref() + .and_then(|t| t.start) + .unwrap_or_default(), + seq.time_range + .as_ref() + .and_then(|t| t.duration) + .unwrap_or_default(), + ); + + sequence_list.push(seq); + } + + Ok(()) + } + + fn process_media_data(&mut self) -> Result, AbrError> { + if let Some(last_header) = &mut self.last_header { + if let Some(header_id) = last_header.header_id.or_else(|| { + let x = self.buffer.try_get_u8().ok(); + if x.is_some() { + last_header.header_id = x; + last_header.size -= 1; + } + x + }) { + let to_copy = self.buffer.remaining().min(last_header.size as usize); + if to_copy == 0 { + return Ok(None); + } + let vars = self.stream.inner.vars.read().unwrap(); + let format_key = self + .header_id_to_format_key_map + .get(&header_id) + .ok_or_else(|| { + AbrError::Invalid(format!("media: unknown header id {header_id}").into()) + })?; + let current_format = vars + .formats_by_key + .get(format_key) + .ok_or(AbrError::Invalid("no current format".into()))?; + let format = FormatInfo::from(current_format); + let data = self.buffer.copy_to_bytes(to_copy); + last_header.size -= data.len() as u64; + if last_header.size == 0 { + self.last_header = None; + } + let seq = self + .sequence_lists + .get_mut(format_key) + .and_then(|x| x.last_mut()) + .ok_or(AbrError::Invalid("no current sequence".into()))?; + let offset = seq.start_data_range.unwrap_or_default() + seq.bytes_written; + seq.bytes_written += data.len() as i32; + Ok(Some(AbrStreamItem { + format, + offset, + seq: seq.sequence_number.unwrap_or_default(), + start_ms: seq.start_ms.unwrap_or_default(), + data, + })) + } else { + Ok(None) + } + } else { + Ok(None) + } + } + + fn process_media_end(&mut self, data: &[u8]) { + let header_id = data[0]; + self.header_id_to_format_key_map.remove(&header_id); + } + + fn process_next_request_policy(&mut self, data: &[u8]) -> Result<(), AbrError> { + let mut policy = NextRequestPolicy::parse_from_bytes(data)?; + tracing::debug!("NextRequestPolicy {policy:?}"); + self.backoff_time_ms = policy.backoff_time_ms(); + let mut vars = self.stream.inner.vars.write().unwrap(); + vars.playback_cookie = policy.playback_cookie.take(); + Ok(()) + } + + fn process_reload_player(&mut self, data: &[u8]) -> Result<(), AbrError> { + let reload = ReloadPlayerResponse::parse_from_bytes(data)?; + tracing::debug!("ReloadPlayerResponse {reload:?}"); + Ok(()) + } + + fn process_format_initialization(&mut self, data: &[u8]) -> Result<(), AbrError> { + let init = FormatInitializationMetadata::parse_from_bytes(data)?; + let format = InitializedFormat::try_from(init)?; + let format_key = get_format_key(&format.format_id); + let mut vars = self.stream.inner.vars.write().unwrap(); + vars.formats_by_key.insert(format_key, format); + Ok(()) + } + + fn process_sabr_redirect(&mut self, data: &[u8]) -> Result<(), AbrError> { + let redir = SabrRedirect::parse_from_bytes(data)?; + let url = redir + .url + .ok_or(AbrError::Invalid("sabr redirect: no URL".into()))?; + + { + let mut vars = self.stream.inner.vars.write().unwrap(); + vars.url = url; + } + + Ok(()) + } + + fn main_format(&self) -> Result { + let vars = self.stream.inner.vars.read().unwrap(); + let fid = if vars.video_format_ids.is_empty() { + vars.audio_format_ids.first() + } else { + vars.video_format_ids.first() + }; + + fid.and_then(|fid| { + vars.formats_by_key.values().find(|fmt| { + fmt.format_id.itag == fid.itag && fmt.format_id.last_modified == fid.last_modified + }) + }) + .or_else(|| { + vars.formats_by_key.values().find(|fmt| { + fmt.mime_type + .as_deref() + .map(|t| t.contains("video")) + .unwrap_or_default() + }) + }) + .or_else(|| vars.formats_by_key.values().next()) + .cloned() + .ok_or_else(|| { + AbrError::Invalid(format!("no main format: {:?}", vars.formats_by_key.values()).into()) + }) + } + + /// A stream is done if all sequences from all formats were received or the last request did not contain any sequences + fn is_done(&self) -> bool { + let vars = self.stream.inner.vars.read().unwrap(); + self.sequence_lists.iter().all(|(key, sl)| { + sl.is_empty() + || vars + .formats_by_key + .get(key) + .and_then(|f| { + f.sequence_count + .zip(sl.last()) + .map(|(sc, last)| last.sequence_number == Some(sc.into())) + }) + .unwrap_or_default() + }) + } + + async fn fetch_stream_data(&self) -> Result { + let (url, body) = { + let vars = self.stream.inner.vars.read().unwrap(); + let mut abr_state = vars.abr_state.clone(); + abr_state.set_time_since_last_seek(vars.last_seek.elapsed().as_millis() as i64); + abr_state.set_time_since_last_action_ms( + vars.last_format_selection.elapsed().as_millis() as i64, + ); + + tracing::debug!( + "fetching stream data: {}ms", + vars.abr_state.player_time_ms() + ); + ( + vars.url.to_owned(), + VideoPlaybackAbrRequest { + client_abr_state: MessageField::some(abr_state), + selected_format_ids: vars + .formats_by_key + .values() + .map(|f| f.format_id.clone()) + .collect(), + buffered_ranges: vars + .formats_by_key + .values() + .map(|f| f.state.clone()) + .collect(), + video_playback_ustreamer_config: Some( + self.stream.inner.ustreamer_config.clone(), + ), + selected_audio_format_ids: vars.audio_format_ids.clone(), + selected_video_format_ids: vars.video_format_ids.clone(), + streamer_context: MessageField::some(StreamerContext { + client_info: MessageField::some(ClientInfo { + client_name: Some(1), + client_version: Some("2.20250314.01.00".to_owned()), + ..Default::default() + }), + po_token: vars.po_token.clone(), + playback_cookie: vars + .playback_cookie + .as_ref() + .and_then(|c| c.write_to_bytes().ok()), + ..Default::default() + }), + ..Default::default() + }, + ) + }; + + let resp = self + .stream + .inner + .http + .post(url) + .body(body.write_to_bytes()?) + .send() + .await? + .error_for_status()?; + Ok(resp) + } +} + +fn get_format_key(format_id: &FormatId) -> String { + format!("{};{};", format_id.itag(), format_id.last_modified()) +} + +fn parse_po_token(po_token: &str) -> Result, AbrError> { + BASE64URL + .decode(po_token.as_bytes()) + .map_err(|e| AbrError::Invalid(format!("could not parse po_token: {e}").into())) +} + +impl TryFrom<&MediaHeader> for InitializedFormat { + type Error = AbrError; + + fn try_from(value: &MediaHeader) -> Result { + let format_id = value + .format_id + .as_ref() + .ok_or(AbrError::Invalid("media header: no format_id".into()))?; + Ok(InitializedFormat { + format_id: format_id.clone(), + duration_ms: value.duration_ms, + mime_type: None, + sequence_count: None, + state: BufferedRange { + format_id: MessageField::some(format_id.clone()), + start_time_ms: Some(0), + duration_ms: Some(0), + start_segment_index: Some(1), + end_segment_index: Some(0), + ..Default::default() + }, + }) + } +} + +impl TryFrom for InitializedFormat { + type Error = AbrError; + + fn try_from(value: FormatInitializationMetadata) -> Result { + let format_id = *value.format_id.0.ok_or(AbrError::Invalid( + "format initialization: no format_id".into(), + ))?; + Ok(InitializedFormat { + format_id: format_id.clone(), + duration_ms: value.duration_ms, + mime_type: value.mime_type.clone(), + sequence_count: value.sequence_count, + state: BufferedRange { + format_id: MessageField::some(format_id), + start_time_ms: Some(0), + duration_ms: Some(0), + start_segment_index: Some(1), + end_segment_index: Some(0), + ..Default::default() + }, + }) + } +} + +#[cfg(test)] +mod tests { + use std::io::Write; + + use futures_util::TryStreamExt; + use rustypipe::{ + client::RustyPipe, + model::{AudioCodec, VideoFormat}, + param::StreamFilter, + }; + + use super::*; + + #[tokio::test] + #[tracing_test::traced_test] + async fn experiment() { + let rp = RustyPipe::builder().build().unwrap(); + let player = rp.query().player("ZeerrnuLi5E").await.unwrap(); + let stream_filter = StreamFilter::new() + .allow_abr_only() + .video_max_res(360) + .video_formats([VideoFormat::Webm]) + .audio_codecs([AudioCodec::Opus]); + let (_, audio) = player.select_video_audio_stream(&stream_filter); + // let video = video.unwrap(); + let audio = audio.unwrap(); + + let abr = AbrStream::new(AbrStreamOptions { + url: player.abr_streaming_url.clone().unwrap(), + ustreamer_config: player.abr_ustreamer_config.as_deref().unwrap(), + po_token: player.po_token.as_deref(), + audio_streams: &[audio], + // video_streams: &[video], + video_streams: &[], + + ..Default::default() + }) + .unwrap(); + + { + let mut stream = abr.stream(); + // let mut files = HashMap::new(); + let mut i = 0; + + while let Some(item) = stream.try_next().await.unwrap() { + dbg!(&item); + // let f = files.entry(item.format.itag).or_insert_with(|| { + // std::fs::File::create(format!("test_{}.webm", item.format.itag)).unwrap() + // }); + // f.write_all(&item.data).unwrap(); + if i == 2 { + abr.set_player_time_ms(60000); + } + i += 1; + } + } + + // let audio_md = std::fs::metadata(format!("test_{}.webm", audio.itag)).unwrap(); + // assert_eq!(audio_md.len(), audio.size); + // let video_md = std::fs::metadata(format!("test_{}.webm", video.itag)).unwrap(); + // assert_eq!(video_md.len(), video.size.unwrap()); + } +} diff --git a/downloader/src/abr4.rs b/downloader/src/abr4.rs new file mode 100644 index 0000000..f7ffb6d --- /dev/null +++ b/downloader/src/abr4.rs @@ -0,0 +1,531 @@ +#![allow(missing_docs)] + +use std::{ + collections::{HashMap, HashSet}, + time::Instant, +}; + +use async_stream::try_stream; +use bytes::Bytes; +use data_encoding::BASE64URL; +use futures_util::Stream; +use protobuf::{Message, MessageField}; +use reqwest::Client; +use tokio::sync::mpsc; + +use rustypipe::model::{traits::YtStream, AudioStream, VideoStream}; +use rustypipe_abr_proto::{ + buffered_range::BufferedRange, + client_abr_state::ClientAbrState, + common::FormatId, + format_initialization_metadata::FormatInitializationMetadata, + media_header::MediaHeader, + playback_cookie::PlaybackCookie, + streamer_context::{streamer_context::ClientInfo, StreamerContext}, + video_playback_abr_request::VideoPlaybackAbrRequest, +}; + +use crate::{ + abr_model::{MediaData, UmpHeader, UmpPart}, + error::AbrError, + util::BytesBuffer, +}; + +/* +Features: +- create a stream with start pos in seconds or bytes +- stream: receive packets with media ident + byte range +- evtl: ability to control stream (update position/quality) +*/ + +pub struct AbrStream { + http: Client, + url: String, + ustreamer_config: Vec, + po_token: Option>, + audio_stream: AudioStream, + video_stream: Option, + playback_cookie: Option, + backoff_time_ms: i32, + rx: mpsc::Receiver, + formats_by_key: HashMap, + header_id_to_format_key_map: HashMap, + // previous_sequences: HashMap>, + buffer: BytesBuffer, + last_header: Option, + time_last_format_selection: Instant, + time_last_seek: Instant, +} + +#[derive(Clone)] +pub struct AbrStreamController { + tx: mpsc::Sender, +} + +#[derive(Clone)] +pub struct AbrStreamOptions<'a> { + pub http: Client, + pub url: String, + pub ustreamer_config: &'a str, + pub po_token: Option<&'a str>, + pub audio_stream: AudioStream, + pub video_stream: Option, + // pub player_time: i64, +} + +pub enum AbrStreamUpdate { + SetTime(i64), + // SetByteOffset(i64), + // SetAudioStream(AudioStream), + // SetVideoStream(VideoStream), +} + +pub struct AbrStreamItem { + pub format: FormatInfo, + pub offset: i32, + pub seq: i64, + pub start_ms: i32, + pub data: Bytes, +} + +#[derive(Debug, Clone)] +pub struct FormatInfo { + pub itag: i32, + pub last_modified: u64, + pub xtags: Option, + pub mime_type: Option, +} + +#[derive(Debug)] +pub struct InitializedFormat { + pub format_id: FormatId, + pub duration_ms: Option, + pub mime_type: Option, + pub sequence_count: Option, + pub sequence_list: Vec, + pub state: BufferedRange, +} + +#[derive(Debug, Clone)] +pub struct Sequence { + pub itag: Option, + pub format_id: FormatId, + pub is_init_segment: bool, + pub duration_ms: Option, + pub start_ms: Option, + pub start_data_range: Option, + pub sequence_number: Option, + pub content_length: Option, + pub bytes_written: i32, +} + +impl std::fmt::Debug for AbrStreamItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "AbrStreamItem {{#{} seq{} {}ms {}-{}}}", + self.format.itag, + self.seq, + self.start_ms, + self.offset, + self.offset as usize + self.data.len() + ) + } +} + +impl From<&InitializedFormat> for FormatInfo { + fn from(value: &InitializedFormat) -> Self { + Self { + itag: value.format_id.itag(), + last_modified: value.format_id.last_modified(), + xtags: value.format_id.xtags.clone(), + mime_type: value.mime_type.clone(), + } + } +} + +impl AbrStreamController { + pub async fn send(&mut self, update: AbrStreamUpdate) { + _ = self.tx.send(update).await; + } +} + +impl AbrStream { + pub fn new(options: AbrStreamOptions) -> Result<(Self, AbrStreamController), AbrError> { + let ustreamer_config = BASE64URL + .decode(options.ustreamer_config.as_bytes()) + .map_err(|e| { + AbrError::Invalid(format!("could not parse ustreamer_config: {e}").into()) + })?; + let po_token = + match options.po_token { + Some(pot) => Some(BASE64URL.decode(pot.as_bytes()).map_err(|e| { + AbrError::Invalid(format!("could not parse po_token: {e}").into()) + })?), + None => None, + }; + + let (tx, rx) = mpsc::channel(1); + let now = Instant::now(); + + Ok(( + Self { + http: options.http, + url: options.url, + ustreamer_config, + po_token, + audio_stream: options.audio_stream, + video_stream: options.video_stream, + playback_cookie: None, + backoff_time_ms: 0, + rx, + formats_by_key: HashMap::new(), + header_id_to_format_key_map: HashMap::new(), + // previous_sequences: HashMap::new(), + buffer: BytesBuffer::new(), + last_header: None, + time_last_seek: now, + time_last_format_selection: now, + }, + AbrStreamController { tx }, + )) + } + + pub async fn fetch_stream_data( + &self, + player_time: i64, + buffered_ranges: Vec, + ) -> Result { + tracing::debug!("fetching stream data: {player_time} ms"); + + let resolution = self.video_stream.as_ref().map(|v| v.height as i32); + + let since_last_format_selection = + self.time_last_format_selection.elapsed().as_millis() as i64; + let since_last_seek = self.time_last_seek.elapsed().as_millis() as i64; + + let mut selected_format_ids = Vec::new(); + if let Some(v) = &self.video_stream { + if buffered_ranges + .iter() + .any(|r| stream_format_eq(v, &r.format_id)) + { + selected_format_ids.push(stream_to_format_id(v)); + } + } + if buffered_ranges + .iter() + .any(|r| stream_format_eq(&self.audio_stream, &r.format_id)) + { + selected_format_ids.push(stream_to_format_id(&self.audio_stream)); + } + + let body = VideoPlaybackAbrRequest { + client_abr_state: MessageField::some(ClientAbrState { + time_since_last_manual_format_selection_ms: Some(since_last_format_selection), + last_manual_selected_resolution: resolution, + client_viewport_width: Some(1920), + client_viewport_height: Some(1080), + sticky_resolution: resolution, + player_time_ms: Some(player_time), + time_since_last_seek: Some(since_last_seek), + visibility: Some(0), + time_since_last_action_ms: Some(since_last_format_selection.min(since_last_seek)), + enabled_track_types_bitfield: Some(i32::from(self.video_stream.is_none())), + drc_enabled: Some(self.audio_stream.is_drc), + audio_track_id: self.audio_stream.track.as_ref().map(|t| t.id.to_owned()), + ..Default::default() + }), + selected_format_ids, + buffered_ranges, + video_playback_ustreamer_config: Some(self.ustreamer_config.clone()), + selected_audio_format_ids: vec![stream_to_format_id(&self.audio_stream)], + selected_video_format_ids: self + .video_stream + .as_ref() + .map(|v| vec![stream_to_format_id(v)]) + .unwrap_or_default(), + streamer_context: MessageField::some(StreamerContext { + client_info: MessageField::some(ClientInfo { + client_name: Some(1), + client_version: Some("2.20250314.01.00".to_owned()), + ..Default::default() + }), + po_token: self.po_token.clone(), + playback_cookie: self + .playback_cookie + .as_ref() + .and_then(|c| c.write_to_bytes().ok()), + ..Default::default() + }), + ..Default::default() + }; + + let resp = self + .http + .post(&self.url) + .body(body.write_to_bytes()?) + .send() + .await? + .error_for_status()?; + Ok(resp) + } + + fn clear_sequence_list(&mut self) { + for f in self.formats_by_key.values_mut() { + f.sequence_list.clear(); + } + } + + fn process_ump_part(&mut self, part: UmpPart) -> Result, AbrError> { + match part { + UmpPart::MediaHeader(media_header) => { + self.process_media_header(media_header)?; + } + UmpPart::MediaData(media_data) => { + return self.process_media_data(media_data); + } + UmpPart::MediaEnd(header_id) => { + self.header_id_to_format_key_map.remove(&header_id); + } + UmpPart::FormatInit(init) => { + let format = InitializedFormat::try_from(init)?; + let format_key = get_format_key(&format.format_id); + self.formats_by_key.insert(format_key, format); + } + UmpPart::NextRequestPolicy(mut policy) => { + tracing::debug!("NextRequestPolicy {policy:?}"); + self.playback_cookie = policy.playback_cookie.take(); + self.backoff_time_ms = policy.backoff_time_ms(); + } + UmpPart::SabrRedirect(sabr_redirect) => { + self.url = sabr_redirect + .url + .ok_or(AbrError::Invalid("sabr redirect: no URL".into()))?; + tracing::debug!("SABR redirect to {}", self.url); + } + } + Ok(None) + } + + fn process_media_header(&mut self, mh: MediaHeader) -> Result<(), AbrError> { + let format_id = mh + .format_id + .as_ref() + .ok_or(AbrError::Invalid("media header: no format_id".into()))?; + let header_id = mh + .header_id + .and_then(|hid| u8::try_from(hid).ok()) + .ok_or(AbrError::Invalid("media header: no header_id".into()))?; + + let format_key = get_format_key(format_id); + + if !self.formats_by_key.contains_key(&format_key) { + self.formats_by_key + .insert(format_key.to_owned(), InitializedFormat::try_from(&mh)?); + } + let current_format = self.formats_by_key.get_mut(&format_key).unwrap(); + + // This is a hacky workaround to prevent duplicate sequences from being added. This should be fixed in the future + // (preferably by figuring out how to make the server not send duplicates). + /* + if let Some(sequence_number) = mh.sequence_number { + let pseq = self + .previous_sequences + .entry(format_key.to_owned()) + .or_default(); + if !pseq.insert(sequence_number) { + tracing::warn!("duplicate sequence {sequence_number}"); + return Ok(()); + } + } + */ + + // Save the header's ID so we can identify its stream data later. + self.header_id_to_format_key_map + .entry(header_id) + .or_insert(format_key); + + if current_format + .sequence_list + .iter() + .all(|x| x.sequence_number != Some(mh.sequence_number())) + { + let seq = Sequence { + itag: mh.itag, + format_id: format_id.clone(), + is_init_segment: mh.is_init_seg(), + duration_ms: mh.duration_ms, + start_ms: mh.start_ms, + start_data_range: mh.start_data_range, + sequence_number: mh.sequence_number, + content_length: mh.content_length, + bytes_written: 0, + }; + + // TODO: testing + let sname = if mh.itag == Some(251) { + "audio" + } else { + "video" + }; + tracing::debug!( + "{sname} #{}: content_length={} start_data_range={} time_start={} time_duration={}", + seq.sequence_number.unwrap_or_default(), + seq.content_length.unwrap_or_default(), + seq.start_data_range.unwrap_or_default(), + seq.start_ms.unwrap_or_default(), + seq.duration_ms.unwrap_or_default(), + ); + + current_format.sequence_list.push(seq); + + if mh.has_sequence_number() { + current_format.state.set_duration_ms( + current_format.state.duration_ms() + i64::from(mh.duration_ms()), + ); + current_format + .state + .set_end_segment_index(current_format.state.end_segment_index() + 1); + } + } + + Ok(()) + } + + pub fn process_media_data( + &mut self, + data: MediaData, + ) -> Result, AbrError> { + let format_key = self + .header_id_to_format_key_map + .get(&data.header_id) + .ok_or_else(|| { + AbrError::Invalid(format!("media: unknown header id {}", data.header_id).into()) + })?; + let current_format = self + .formats_by_key + .get(format_key) + .ok_or(AbrError::Invalid("no current format".into()))?; + let format = FormatInfo::from(current_format); + if !data.partial { + self.last_header = None; + } + + let seq = self + .formats_by_key + .get_mut(format_key) + .unwrap() + .sequence_list + .last_mut() + .ok_or(AbrError::Invalid("no current sequence".into()))?; + let offset = seq.start_data_range.unwrap_or_default() + seq.bytes_written; + seq.bytes_written += data.data.len() as i32; + Ok(Some(AbrStreamItem { + format, + offset, + seq: seq.sequence_number.unwrap_or_default(), + start_ms: seq.start_ms.unwrap_or_default(), + data: data.data, + })) + } + + /// A stream is done if all sequences from all formats were received or the last request did not contain any sequences + fn is_done(&self) -> bool { + self.formats_by_key.values().all(|f| { + f.sequence_list.is_empty() + || f.sequence_count + .zip(f.sequence_list.last()) + .map(|(sc, last)| last.sequence_number == Some(sc.into())) + .unwrap_or_default() + }) + } + + async fn stream_step(&mut self) -> Result, AbrError> { + // let resp = self.fetch_stream_data(player_time, buffered_ranges); + + todo!() + } + + /* + pub fn stream(mut self) -> impl Stream> { + let stream = try_stream! { + let mut ri = 1; + loop { + let resp = self.fetch_stream_data().await?; + } + }; + Box::pin(stream) + } + */ +} + +fn get_format_key(format_id: &FormatId) -> String { + format!("{};{};", format_id.itag(), format_id.last_modified()) +} + +fn stream_to_format_id(stream: &S) -> FormatId { + FormatId { + itag: Some(stream.itag() as i32), + last_modified: stream.last_modified(), + xtags: stream.xtags().map(|s| s.to_owned()), + ..Default::default() + } +} + +fn stream_format_eq(stream: &S, format: &FormatId) -> bool { + stream.itag() as i32 == format.itag() + && stream.last_modified() == format.last_modified + && stream.xtags() == format.xtags.as_deref() +} + +impl TryFrom<&MediaHeader> for InitializedFormat { + type Error = AbrError; + + fn try_from(value: &MediaHeader) -> Result { + let format_id = value + .format_id + .as_ref() + .ok_or(AbrError::Invalid("media header: no format_id".into()))?; + Ok(InitializedFormat { + format_id: format_id.clone(), + duration_ms: value.duration_ms, + mime_type: None, + sequence_count: None, + sequence_list: Vec::new(), + state: BufferedRange { + format_id: MessageField::some(format_id.clone()), + start_time_ms: Some(0), + duration_ms: Some(0), + start_segment_index: Some(1), + end_segment_index: Some(0), + ..Default::default() + }, + }) + } +} + +impl TryFrom for InitializedFormat { + type Error = AbrError; + + fn try_from(value: FormatInitializationMetadata) -> Result { + let format_id = *value.format_id.0.ok_or(AbrError::Invalid( + "format initialization: no format_id".into(), + ))?; + Ok(InitializedFormat { + format_id: format_id.clone(), + duration_ms: value.duration_ms, + mime_type: value.mime_type.clone(), + sequence_count: value.sequence_count, + sequence_list: Vec::new(), + state: BufferedRange { + format_id: MessageField::some(format_id), + start_time_ms: Some(0), + duration_ms: Some(0), + start_segment_index: Some(1), + end_segment_index: Some(0), + ..Default::default() + }, + }) + } +} diff --git a/downloader/src/abr_model.rs b/downloader/src/abr_model.rs index 35df60e..eb6b16f 100644 --- a/downloader/src/abr_model.rs +++ b/downloader/src/abr_model.rs @@ -1,3 +1,13 @@ +use bytes::{Buf, Bytes}; +use protobuf::Message; +use rustypipe_abr_proto::{ + format_initialization_metadata::FormatInitializationMetadata, media_header::MediaHeader, + next_request_policy::NextRequestPolicy, sabr_error::SabrError, sabr_redirect::SabrRedirect, + stream_protection_status::StreamProtectionStatus, +}; + +use crate::{error::AbrError, util::BytesBuffer}; + #[allow(non_camel_case_types, clippy::upper_case_acronyms)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PartType { @@ -85,3 +95,192 @@ impl From for PartType { } } } + +/// Header of a UMP part +#[derive(Debug, Clone)] +pub struct UmpHeader { + /// UMP part type + pub part_type: PartType, + /// UMP part size in bytes + pub size: usize, + /// Media stream header id (used to distinguish MEDIA parts from different streams) + pub header_id: Option, +} + +pub enum UmpPart { + MediaHeader(MediaHeader), + MediaData(MediaData), + MediaEnd(u8), + FormatInit(FormatInitializationMetadata), + NextRequestPolicy(NextRequestPolicy), + SabrRedirect(SabrRedirect), +} + +pub struct MediaData { + pub header_id: u8, + pub data: Bytes, + pub partial: bool, +} + +/// Read a variable-length integer from the given buffer without advancing it +/// +/// Returns the integer value and the next offset +pub fn read_varint(buf: &BytesBuffer, offset: usize) -> (u64, usize) { + // Determine the length of the val + let buffer_len = buf.remaining().saturating_sub(offset); + if buffer_len == 0 { + return (0, 0); + } + + let fb = buf[offset]; + let byte_length = if fb < 128 { + 1 + } else if fb < 192 { + 2 + } else if fb < 224 { + 3 + } else if fb < 240 { + 4 + } else { + 5 + }; + + if buffer_len < byte_length { + return (0, 0); + } + + let n = match byte_length { + 1 => buf[offset].into(), + 2 => { + let b1 = u64::from(buf[offset]); + let b2 = u64::from(buf[offset + 1]); + (b1 & 0x3f) + 64 * b2 + } + 3 => { + let b1 = u64::from(buf[offset]); + let b2 = u64::from(buf[offset + 1]); + let b3 = u64::from(buf[offset + 2]); + (b1 & 0x1f) + 32 * (b2 + 256 * b3) + } + 4 => { + let b1 = u64::from(buf[offset]); + let b2 = u64::from(buf[offset + 1]); + let b3 = u64::from(buf[offset + 2]); + let b4 = u64::from(buf[offset + 3]); + (b1 & 0x0f) + 16 * (b2 + 256 * (b3 + 256 * b4)) + } + _ => u32::from_be_bytes([ + buf[offset + 1], + buf[offset + 2], + buf[offset + 3], + buf[offset + 4], + ]) + .into(), + }; + (n, offset + byte_length) +} + +/// Try to parse the next UMP header from the given buffer. Only advances the buffer +/// if a complete header can be read. +pub fn parse_ump_header(buf: &mut BytesBuffer) -> Option { + let (part_type, o1) = read_varint(buf, 0); + let part_type = PartType::from(part_type); + if o1 == 0 { + return None; + } + let (mut part_size, mut o2) = read_varint(buf, o1); + if o2 == 0 { + return None; + } + + let mut header_id = None; + if part_type == PartType::MEDIA && part_size > 0 { + header_id = buf.get(o2); + if header_id.is_none() { + return None; + } + o2 += 1; + part_size -= 1; + } + + buf.advance(o2); + + Some(UmpHeader { + part_type, + size: part_size as usize, + header_id, + }) +} + +/// Try to parse the next UMP part from the given buffer. Only advances the buffer +/// if a complete part can be read. +/// +/// MediaData parts may be returned partially. +pub fn parse_ump_part( + buf: &mut BytesBuffer, + last_header: &mut UmpHeader, +) -> Result, AbrError> { + if last_header.part_type == PartType::MEDIA { + let header_id = last_header + .header_id + .ok_or(AbrError::Invalid("no header_id for media part".into()))?; + let to_copy = buf.remaining().min(last_header.size); + let partial = to_copy < last_header.size; + if to_copy == 0 { + return Ok(None); + } + let data = buf.copy_to_bytes(to_copy); + last_header.size -= data.len(); + + Ok(Some(UmpPart::MediaData(MediaData { + header_id, + data, + partial, + }))) + } else if buf.remaining() < last_header.size { + tracing::debug!("waiting for entire part"); + Ok(None) + } else { + let part_data = buf.copy_to_bytes(last_header.size); + match last_header.part_type { + PartType::MEDIA_HEADER => Ok(Some(UmpPart::MediaHeader( + MediaHeader::parse_from_bytes(&part_data)?, + ))), + PartType::MEDIA_END => Ok(Some(UmpPart::MediaEnd(part_data[0]))), + PartType::FORMAT_INITIALIZATION_METADATA => Ok(Some(UmpPart::FormatInit( + FormatInitializationMetadata::parse_from_bytes(&part_data)?, + ))), + PartType::NEXT_REQUEST_POLICY => Ok(Some(UmpPart::NextRequestPolicy( + NextRequestPolicy::parse_from_bytes(&part_data)?, + ))), + PartType::SABR_REDIRECT => Ok(Some(UmpPart::SabrRedirect( + SabrRedirect::parse_from_bytes(&part_data)?, + ))), + PartType::SABR_ERROR => Err(AbrError::Sabr(SabrError::parse_from_bytes(&part_data)?)), + PartType::RELOAD_PLAYER_RESPONSE => Err(AbrError::ReloadPlayer), + PartType::STREAM_PROTECTION_STATUS => { + let prot = StreamProtectionStatus::parse_from_bytes(&part_data)?; + match prot.status() { + 1 => tracing::debug!("[StreamProtectionStatus]: Good"), + 2 => tracing::warn!("[StreamProtectionStatus]: Attestation pending"), + 3 => return Err(AbrError::Attestation), + _ => {} + }; + Ok(None) + } + _ => { + tracing::debug!("skipping part {:?}", last_header.part_type); + Ok(None) + } + } + } +} + +impl UmpPart { + pub fn partial(&self) -> bool { + match self { + UmpPart::MediaData(media_data) => media_data.partial, + _ => false, + } + } +} diff --git a/downloader/src/bin/abrmap.rs b/downloader/src/bin/abrmap.rs new file mode 100644 index 0000000..02d9047 --- /dev/null +++ b/downloader/src/bin/abrmap.rs @@ -0,0 +1,52 @@ +use futures_util::TryStreamExt; +use rustypipe::{ + client::RustyPipe, + model::{AudioCodec, VideoFormat}, + param::StreamFilter, +}; +use rustypipe_downloader::abr2::{AbrStream, AbrStreamOptions}; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + let id = std::env::args().into_iter().nth(1).unwrap(); + + let rp = RustyPipe::builder().build().unwrap(); + let player = rp + .query() + .visitor_data("Cgt0SlJzQllxNFhuMCjZ1Z3BBjIKCgJERRIEEgAgXg%3D%3D") + .player_from_client(id, rustypipe::client::ClientType::Desktop) + .await + .unwrap(); + assert_eq!(player.client_type, rustypipe::client::ClientType::Desktop); + + let stream_filter = StreamFilter::new() + .allow_abr_only() + .video_max_res(360) + .video_formats([VideoFormat::Webm]) + .audio_codecs([AudioCodec::Opus]); + let (video, audio) = player.select_video_audio_stream(&stream_filter); + let video = video.unwrap(); + let audio = audio.unwrap(); + + dbg!(&audio); + + let abr = AbrStream::new(AbrStreamOptions { + url: player.abr_streaming_url.clone().unwrap(), + ustreamer_config: player.abr_ustreamer_config.as_deref().unwrap(), + po_token: player.po_token.as_deref(), + audio_streams: &[audio], + video_streams: &[video], + // video_streams: &[], + ..Default::default() + }) + .unwrap(); + + { + let mut stream = abr.stream(); + while let Some(itm) = stream.try_next().await.unwrap() { + println!("got {} bytes", itm.data.len()); + } + } +} diff --git a/downloader/src/bin/abrstream.rs b/downloader/src/bin/abrstream.rs new file mode 100644 index 0000000..f90efe6 --- /dev/null +++ b/downloader/src/bin/abrstream.rs @@ -0,0 +1,97 @@ +use std::{fs::File, io::BufReader}; + +use bytes::Buf; +use rustypipe::{ + model::{AudioCodec, VideoFormat, VideoPlayer}, + param::StreamFilter, +}; +use rustypipe_downloader::abr2::{AbrStream, AbrStreamOptions}; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + let mut args = std::env::args().skip(1); + let player_fname = args.next().expect("player file"); + let time_ms = args.next().expect("time in ms").parse::().unwrap(); + + /* + let rp = RustyPipe::builder().build().unwrap(); + let player = rp.query().player(vid).await.unwrap(); + */ + let player: VideoPlayer = { + let pfile = BufReader::new(File::open(player_fname).unwrap()); + serde_json::from_reader(pfile).unwrap() + }; + + let stream_filter = StreamFilter::new() + .allow_abr_only() + .video_max_res(360) + .video_formats([VideoFormat::Webm]) + .audio_codecs([AudioCodec::Opus]); + let (_, audio) = player.select_video_audio_stream(&stream_filter); + // let video = video.unwrap(); + let audio = audio.unwrap(); + + let mut abr = AbrStream::new(AbrStreamOptions { + url: player.abr_streaming_url.clone().unwrap(), + ustreamer_config: player.abr_ustreamer_config.as_deref().unwrap(), + po_token: player.po_token.as_deref(), + audio_streams: &[audio], + // video_streams: &[video], + video_streams: &[], + player_time: time_ms, + ..Default::default() + }) + .unwrap(); + + let fid = rustypipe_abr_proto::common::FormatId { + itag: Some(251), + last_modified: Some(1714693509531582), + ..Default::default() + }; + abr.formats_by_key.insert( + "251;1714693509531582;".to_owned(), + rustypipe_downloader::abr2::InitializedFormat { + format_id: fid.clone(), + duration_ms: Some(229301), + mime_type: Some("audio/webm; codecs=\"opus\"".to_owned()), + sequence_count: Some(23), + sequence_list: Vec::new(), + state: rustypipe_abr_proto::buffered_range::BufferedRange { + format_id: Some(fid).into(), + start_time_ms: Some(0), + duration_ms: Some(1), + start_segment_index: Some(1), + end_segment_index: Some(1), + ..Default::default() + }, + }, + ); + + let resp = abr.fetch_stream_data().await.unwrap(); + let data = resp.bytes().await.unwrap(); + tracing::debug!("fetched {} bytes", data.len()); + + abr.buffer.push(data); + + loop { + if abr.last_header.is_none() { + abr.last_header = abr.parse_ump_header(); + } + tracing::debug!("loop {:?}", abr.last_header); + if abr.last_header.is_none() { + break; + } + let (x, y) = abr.process_ump_part().unwrap(); + if let Some(item) = x { + dbg!(&item); + } + if y { + break; + } + } + + tracing::info!("buffer remaining: {}", abr.buffer.remaining()); + // dbg!(&abr.formats_by_key); +} diff --git a/downloader/src/error.rs b/downloader/src/error.rs index cf56f25..496819b 100644 --- a/downloader/src/error.rs +++ b/downloader/src/error.rs @@ -74,4 +74,6 @@ pub enum AbrError { Sabr(SabrError), #[error("attestation required")] Attestation, + #[error("reload player")] + ReloadPlayer, } diff --git a/downloader/src/lib.rs b/downloader/src/lib.rs index f78df7d..279995d 100644 --- a/downloader/src/lib.rs +++ b/downloader/src/lib.rs @@ -3,9 +3,12 @@ #![warn(missing_docs, clippy::todo, clippy::dbg_macro)] mod abr; -mod abr2; +pub mod abr2; +pub mod abr3; +pub mod abr4; mod abr_model; mod error; +mod range_predict; mod streamtest; mod util; diff --git a/downloader/src/range_predict.rs b/downloader/src/range_predict.rs new file mode 100644 index 0000000..a071fd1 --- /dev/null +++ b/downloader/src/range_predict.rs @@ -0,0 +1,110 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use crate::abr2::TimeRange; + +struct Sx { + stream_len: i64, + header_len: i64, + duration: i64, + positions: BTreeMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Sequence { + pub itag: i32, + pub is_init_segment: bool, + pub duration_ms: Option, + pub start_ms: Option, + pub start_data_range: i32, + pub sequence_number: Option, + pub content_length: i64, + pub time_range: Option, +} + +impl Sx { + fn new(stream_len: i64, header_len: i64, duration: i64) -> Self { + let mut positions = BTreeMap::new(); + positions.insert(header_len, 0); + positions.insert(stream_len - 1, duration); + + Self { + stream_len, + header_len, + duration, + positions, + } + } + + fn pos_predict(&self, pos: i64) -> i64 { + if pos < self.header_len { + return 0; + } + if pos >= self.stream_len { + panic!("out of range"); + } + + let blow = *self.positions.keys().rev().find(|x| **x <= pos).unwrap(); + let bhigh = *self.positions.keys().find(|x| **x >= pos).unwrap(); + let brange = bhigh - blow; + let bperc = pos as f64 / brange as f64; + + let tlow = self.positions[&blow]; + let thigh = self.positions[&bhigh]; + let trange = thigh - tlow; + + let tpoint = (trange as f64 * bperc) as i64 + tlow; + (tpoint - 5000).max(tlow) + } +} + +#[cfg(test)] +mod tests { + use std::{fs::File, io::BufReader}; + + use super::{Sequence, Sx}; + + fn fetch_seg(sequences: &[Sequence], ts: i64) -> &Sequence { + if ts <= 10000 { + &sequences[0] + } else { + sequences + .iter() + .rev() + .find(|s| s.time_range.as_ref().is_some_and(|t| t.start <= ts)) + .unwrap() + } + } + + #[test] + fn t_range_predict() { + let f = File::open( + "/home/thetadev/Documents/Programmieren/Rust/rustypipe/tmp/abrmap/HSkTpseMd-A_251_nodrc.json", + ) + .unwrap(); + let sequences: Vec = serde_json::from_reader(BufReader::new(f)).unwrap(); + let last = sequences.last().unwrap(); + let stream_len = i64::from(last.start_data_range) + last.content_length; + let header_len = sequences[0].content_length; + let ltr = last.time_range.as_ref().unwrap(); + let duration = ltr.start + ltr.duration; + + let sx = Sx::new(stream_len, header_len, duration); + + let max_sl = sequences.iter().map(|s| s.content_length).max().unwrap(); + + for offset in (0..stream_len).step_by(1000) { + let pred = sx.pos_predict(offset); + let s = fetch_seg(&sequences, pred); + let to_skip = offset - i64::from(s.start_data_range); + if to_skip < 0 { + println!("after target ({to_skip}): offset={offset} pred={pred} s={s:?}"); + } else if to_skip <= max_sl { + println!("on target ({to_skip})"); + } else { + println!("before target ({to_skip})"); + } + } + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs index 4f0d792..06386bc 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -400,7 +400,6 @@ struct RustyPipeOpts { utc_offset_minutes: i16, report: bool, strict: bool, - allow_abr_only: bool, auth: Option, visitor_data: Option, } @@ -557,7 +556,6 @@ impl Default for RustyPipeOpts { utc_offset_minutes: 0, report: false, strict: false, - allow_abr_only: false, auth: None, visitor_data: None, } @@ -964,22 +962,6 @@ impl RustyPipeBuilder { self } - /// Allow fetching player data containing only the ABR (Adaptive bitrate) stream URL. - /// - /// YouTube may return only the ABR stream URL and no URLs for individual streams - /// when using desktop client. To access the streams, your application needs to support - /// the ABR streaming protocol. - /// - /// You also need to set the `allow_abr_only` option for the [`crate::param::StreamFilter`] - /// to select streams without an URL. - /// - /// **Info**: you can set this option for individual queries, too - #[must_use] - pub fn allow_abr_only(mut self) -> Self { - self.default_opts.allow_abr_only = true; - self - } - /// Enable authentication for all requests /// /// Depending on the client type RustyPipe uses either the authentication cookie or the @@ -1798,20 +1780,6 @@ impl RustyPipeQuery { self } - /// Allow fetching player data containing only the ABR (Adaptive bitrate) stream URL. - /// - /// YouTube may return only the ABR stream URL and no URLs for individual streams - /// when using desktop client. To access the streams, your application needs to support - /// the ABR streaming protocol. - /// - /// You also need to set the `allow_abr_only` option for the [`crate::param::StreamFilter`] - /// to select streams without an URL. - #[must_use] - pub fn allow_abr_only(mut self) -> Self { - self.opts.allow_abr_only = true; - self - } - /// Enable authentication for this request /// /// Depending on the client type RustyPipe uses either the authentication cookie or the @@ -2388,7 +2356,6 @@ impl RustyPipeQuery { artist: ctx_src.artist.clone(), authenticated: self.opts.auth.unwrap_or_default(), session_po_token: ctx_src.session_po_token.clone(), - allow_abr_only: self.opts.allow_abr_only, }; let request = self @@ -2680,7 +2647,6 @@ struct MapRespCtx<'a> { artist: Option, authenticated: bool, session_po_token: Option, - allow_abr_only: bool, } /// Options to give to the mapper when making requests; @@ -2709,7 +2675,6 @@ impl<'a> MapRespCtx<'a> { artist: None, authenticated: false, session_po_token: None, - allow_abr_only: false, } } } diff --git a/src/client/player.rs b/src/client/player.rs index a7eb325..da932f1 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -50,15 +50,33 @@ struct QPlayer<'a> { #[serde(rename_all = "camelCase")] struct QPlaybackContext<'a> { content_playback_context: QContentPlaybackContext<'a>, + device_playback_capabilities: QDevicePlaybackCapabilities, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] struct QContentPlaybackContext<'a> { - /// Signature timestamp extracted from player.js - signature_timestamp: &'a str, + auto_captions_default_on: bool, + autonav_state: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + current_url: Option, + html5_preference: &'a str, + lact_milliseconds: i8, /// Referer URL from website referer: String, + /// Signature timestamp extracted from player.js + signature_timestamp: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + splay: Option, + #[serde(skip_serializing_if = "Option::is_none")] + vis: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct QDevicePlaybackCapabilities { + supports_vp9_encoding: bool, + support_xhr: bool, } #[derive(Debug, Serialize)] @@ -210,8 +228,27 @@ impl RustyPipeQuery { let playback_context = deobf.as_ref().map(|deobf| QPlaybackContext { content_playback_context: QContentPlaybackContext { - signature_timestamp: &deobf.sts, + auto_captions_default_on: false, + autonav_state: "STATE_OFF", + current_url: if client_type == ClientType::Tv { + None + } else { + Some(format!("/watch?v={video_id}")) + }, + html5_preference: "HTML5_PREF_WANTS", + lact_milliseconds: if client_type == ClientType::Tv { + 10 + } else { + -1 + }, referer: format!("https://www.youtube.com/watch?v={video_id}"), + signature_timestamp: &deobf.sts, + splay: Some(false).filter(|_| client_type != ClientType::Tv), + vis: Some(0).filter(|_| client_type != ClientType::Tv), + }, + device_playback_capabilities: QDevicePlaybackCapabilities { + supports_vp9_encoding: true, + support_xhr: true, }, }); @@ -388,24 +425,6 @@ impl MapResponse for response::Player { ))); } - // Sometimes YouTube Desktop does not output any URLs for adaptive streams. - // Since this is currently rare, it is best to retry the request in this case - // unless ABR streaming is explicitly requested. - if !ctx.allow_abr_only - && !is_live - && !streaming_data.adaptive_formats.c.is_empty() - && streaming_data - .adaptive_formats - .c - .iter() - .all(|f| f.url.is_none() && f.signature_cipher.is_none()) - { - return Err(ExtractionError::Unavailable { - reason: UnavailabilityReason::TryAgain, - msg: "no adaptive stream URLs".to_owned(), - }); - } - let video_info = VideoPlayerDetails { id: video_details.video_id, name: video_details.title, @@ -718,7 +737,7 @@ impl<'a> StreamsMapper<'a> { &mut self, url: &Option, signature_cipher: &Option, - ) -> Result { + ) -> Result, ExtractionError> { let (url_base, mut url_params) = match url { Some(url) => util::url_to_params(url).map_err(|_| { @@ -733,7 +752,7 @@ impl<'a> StreamsMapper<'a> { ) }) } - None => return Ok(UrlMapRes::default()), + None => return Ok(None), }, }?; @@ -745,10 +764,7 @@ impl<'a> StreamsMapper<'a> { let url = Url::parse_with_params(url_base.as_str(), url_params.iter()) .map_err(|_| ExtractionError::InvalidData("could not combine URL".into()))?; - Ok(UrlMapRes { - url: Some(url.to_string()), - xtags: url_params.get("xtags").cloned(), - }) + Ok(Some(url.to_string())) } fn map_url_abr(&mut self, url: &str) -> Result { @@ -776,10 +792,9 @@ impl<'a> StreamsMapper<'a> { format!("invalid video format. itag: {}", f.itag).into(), )); }; - let map_res = self.map_url(&f.url, &f.signature_cipher)?; Ok(VideoStream { - url: map_res.url, + url: self.map_url(&f.url, &f.signature_cipher)?, itag: f.itag, bitrate: f.bitrate, average_bitrate: f.average_bitrate.unwrap_or(f.bitrate), @@ -799,7 +814,7 @@ impl<'a> StreamsMapper<'a> { format, codec: get_video_codec(codecs), mime: f.mime_type, - xtags: map_res.xtags, + xtags: f.xtags, drm_track_type: f.drm_track_type.map(|t| t.into()), drm_systems: f.drm_families.into_iter().map(|t| t.into()).collect(), }) @@ -818,10 +833,9 @@ impl<'a> StreamsMapper<'a> { let format = get_audio_format(mtype).ok_or_else(|| { ExtractionError::InvalidData(format!("invalid audio format. itag: {}", f.itag).into()) })?; - let map_res = self.map_url(&f.url, &f.signature_cipher)?; Ok(AudioStream { - url: map_res.url, + url: self.map_url(&f.url, &f.signature_cipher)?, itag: f.itag, bitrate: f.bitrate, average_bitrate: f.average_bitrate.unwrap_or(f.bitrate), @@ -839,10 +853,11 @@ impl<'a> StreamsMapper<'a> { mime: f.mime_type, channels: f.audio_channels, loudness_db: f.loudness_db, + is_drc: f.is_drc, track: f .audio_track - .map(|t| self.map_audio_track(t, map_res.xtags.as_deref())), - xtags: map_res.xtags, + .map(|t| self.map_audio_track(t, f.xtags.as_deref())), + xtags: f.xtags, drm_track_type: f.drm_track_type.map(|t| t.into()), drm_systems: f.drm_families.into_iter().map(|t| t.into()).collect(), }) @@ -857,24 +872,30 @@ impl<'a> StreamsMapper<'a> { let mut track_type = None; if let Some(xtags) = xtags { - xtags - .split(':') - .filter_map(|param| param.split_once('=')) - .for_each(|(k, v)| match k { - "lang" => { - lang = Some(v.to_owned()); + let xtags_data = urlencoding::decode(xtags) + .ok() + .and_then(|dec| util::b64_decode(dec.as_bytes()).ok()) + .and_then(util::kv_from_pb); + + match xtags_data { + Some(mut xtags_data) => { + if let Some(acont) = xtags_data.get("acont") { + match serde_plain::from_str(acont) { + Ok(v) => { + track_type = Some(v); + } + Err(_) => { + self.warnings + .push(format!("could not parse audio track type `{acont}`")); + } + } } - "acont" => match serde_plain::from_str(v) { - Ok(v) => { - track_type = Some(v); - } - Err(_) => { - self.warnings - .push(format!("could not parse audio track type `{v}`")); - } - }, - _ => {} - }); + lang = xtags_data.remove("lang"); + } + None => self + .warnings + .push(format!("could not parse xtags `{xtags}`")), + } } AudioTrack { @@ -887,12 +908,6 @@ impl<'a> StreamsMapper<'a> { } } -#[derive(Default)] -struct UrlMapRes { - url: Option, - xtags: Option, -} - fn parse_mime(mime: &str) -> Option<(&str, Vec<&str>)> { static PATTERN: Lazy = Lazy::new(|| Regex::new(r#"(\w+/\w+);\scodecs="([a-zA-Z-0-9.,\s]*)""#).unwrap()); @@ -1031,7 +1046,6 @@ mod tests { artist: None, authenticated: false, session_po_token: None, - allow_abr_only: false, }) .unwrap(); @@ -1052,7 +1066,6 @@ mod tests { let url = mapper .map_url(&None, &Some(signature_cipher.to_owned())) .unwrap() - .url .unwrap(); assert_eq!(url, "https://rr5---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=3781277&dur=229.301&ei=vb7nYvH5BMK8gAfBj7ToBQ&expire=1659376413&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AB_BABwrXZJN428ZwDxq5ScPn2AbcGODnRlTVhCQ3mj2&initcwndbps=1588750&ip=2003%3Ade%3Aaf06%3A6300%3Ac750%3A1b77%3Ac74a%3A80e3&itag=251&keepalive=yes&lmt=1655510291473933&lsig=AG3C_xAwRQIgCKCGJ1iu4wlaGXy3jcJyU3inh9dr1FIfqYOZEG_MdmACIQCbungkQYFk7EhD6K2YvLaHFMjKOFWjw001_tLb0lPDtg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=hH&mime=audio%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5ednsl&ms=au%2Conr&mt=1659354538&mv=m&mvi=5&n=XzXGSfGusw6OCQ&ns=b_Mq_qlTFcSGlG9RpwpM9xQH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAPIsKd7-xi4xVHEC9gb__dU4hzfzsHEj9ytd3nt0gEceAiACJWBcw-wFEq9qir35bwKHJZxtQ9mOL7SKiVkLQNDa6A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-Khi831z8dTejFIRCvCEwx_6romtM&txp=4532434&vprv=1"); diff --git a/src/client/response/player.rs b/src/client/response/player.rs index 2cb460f..491779d 100644 --- a/src/client/response/player.rs +++ b/src/client/response/player.rs @@ -114,8 +114,8 @@ pub(crate) struct Format { pub last_modified: Option, #[serde_as(as = "Option")] pub content_length: Option, - pub bitrate: u32, + pub xtags: Option, pub width: Option, pub height: Option, @@ -143,6 +143,8 @@ pub(crate) struct Format { pub audio_sample_rate: Option, pub audio_channels: Option, pub loudness_db: Option, + #[serde(default)] + pub is_drc: bool, pub audio_track: Option, pub signature_cipher: Option, diff --git a/src/model/mod.rs b/src/model/mod.rs index f97090c..b6f7ef8 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -288,6 +288,11 @@ pub struct AudioStream { /// /// The loudness parameter is not available when using the Android client. pub loudness_db: Option, + /// True if the audio stream uses dynamic range compression (Stable Volume option) + /// + /// + /// + pub is_drc: bool, /// Audio track information /// /// Videos can have multiple audio tracks (different languages). diff --git a/src/model/ordering.rs b/src/model/ordering.rs index 445dcad..8fe8718 100644 --- a/src/model/ordering.rs +++ b/src/model/ordering.rs @@ -36,6 +36,7 @@ impl QualityOrd for AudioStream { .as_ref() .map(|t| track_type_rating(t.track_type)), ) + .then_with(|| other.is_drc.cmp(&self.is_drc)) .then_with(|| self.channels.cmp(&other.channels)) .then_with(|| cmp_bitrate(self).cmp(&cmp_bitrate(other))) } diff --git a/src/param/stream_filter.rs b/src/param/stream_filter.rs index 537c490..5143b1f 100644 --- a/src/param/stream_filter.rs +++ b/src/param/stream_filter.rs @@ -17,6 +17,7 @@ pub struct StreamFilter { audio_languages: Vec, audio_autodub: bool, audio_descriptive: bool, + audio_drc: bool, video_max_res: Option, video_max_fps: Option, video_formats: Option>, @@ -28,7 +29,7 @@ pub struct StreamFilter { drm_system: Option, } -const N_RES_AUDIO: usize = 4; +const N_RES_AUDIO: usize = 5; const N_RES_VIDEO: usize = 5; type AudioRes = Option<[i64; N_RES_AUDIO]>; type VideoRes = Option<[i64; N_RES_VIDEO]>; @@ -110,6 +111,16 @@ impl StreamFilter { self } + /// Prefer audio streams that use dynamic range compression (Stable volume). + /// + /// + /// + #[must_use] + pub fn audio_drc(mut self) -> Self { + self.audio_drc = true; + self + } + /// Set the maximum video resolution. Resolution is determined by the /// pixel count of the shorter edge (e.g. 1080p). /// @@ -271,6 +282,8 @@ impl StreamFilter { None => 0, }; + let drc = i64::from(self.audio_drc == stream.is_drc); + let channels = stream.channels.unwrap_or_default(); if let Some(max_channels) = self.audio_max_channels { if channels > max_channels { @@ -280,7 +293,7 @@ impl StreamFilter { self.check_drm(stream.drm_track_type, &stream.drm_systems)?; - Some([language, track_type, channels.into(), bitrate]) + Some([language, track_type, drc, channels.into(), bitrate]) } fn apply_video(&self, stream: &VideoStream, video_only: bool) -> VideoRes { diff --git a/src/util/mod.rs b/src/util/mod.rs index 368d114..9e54eec 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -6,7 +6,7 @@ pub mod dictionary; pub mod timeago; pub use date::{now_sec, shift_months, shift_weeks_monday, shift_years}; -pub use protobuf::{string_from_pb, ProtoBuilder}; +pub use protobuf::{kv_from_pb, string_from_pb, ProtoBuilder}; pub use visitor_data::VisitorDataCache; use std::{ @@ -486,7 +486,17 @@ pub fn b64_encode>(input: T) -> String { } pub fn b64_decode>(input: T) -> Result, data_encoding::DecodeError> { - data_encoding::BASE64URL.decode(input.as_ref()) + // Remove trailing padding + let mut x = input.as_ref(); + while !x.is_empty() { + let li = x.len() - 1; + if x[li] == b'=' { + x = &x[0..li]; + } else { + break; + } + } + data_encoding::BASE64URL_NOPAD.decode(x) } /// Get the country from its English name diff --git a/src/util/protobuf.rs b/src/util/protobuf.rs index 84492f6..b56dae9 100644 --- a/src/util/protobuf.rs +++ b/src/util/protobuf.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + /// [`ProtoBuilder`] is used to construct protobuf messages using a builder pattern #[derive(Debug, Default)] pub struct ProtoBuilder { @@ -100,7 +102,7 @@ fn parse_varint>(pb: &mut P) -> Option { } } -fn parse_field>(pb: &mut P) -> Option<(u32, u8)> { +fn parse_field(pb: &mut impl Iterator) -> Option<(u32, u8)> { parse_varint(pb).map(|v| { let f = (v >> 3) as u32; let w = (v & 0x07) as u8; @@ -108,13 +110,12 @@ fn parse_field>(pb: &mut P) -> Option<(u32, u8)> { }) } -pub fn string_from_pb>(pb: P, field: u32) -> Option { - let mut pb = pb.into_iter(); - while let Some((this_field, wire)) = parse_field(&mut pb) { +fn next_string_from_pb(pb: &mut impl Iterator, filter: &[u32]) -> Option<(u32, String)> { + while let Some((this_field, wire)) = parse_field(pb) { let to_skip = match wire { // varint 0 => { - parse_varint(&mut pb); + parse_varint(pb); 0 } // fixed 64bit @@ -123,15 +124,16 @@ pub fn string_from_pb>(pb: P, field: u32) -> Option 4, // string 2 => { - let len = parse_varint(&mut pb)?; - if this_field == field { + let len = parse_varint(pb)?; + if filter.contains(&this_field) { let mut buf = Vec::new(); for _ in 0..len { buf.push(pb.next()?); } - return String::from_utf8(buf).ok(); + return Some((this_field, String::from_utf8(buf).ok()?)); + } else { + len } - len } _ => return None, }; @@ -142,6 +144,64 @@ pub fn string_from_pb>(pb: P, field: u32) -> Option>(pb: P, field: u32) -> Option { + let mut pb = pb.into_iter(); + next_string_from_pb(&mut pb, &[field]).map(|x| x.1) +} + +/// Parse a protobuf-encoded key/value map +/// +/// K/V pairs are embedded structs of field 1 +/// K/V struct: {key = 1; value = 2} +pub fn kv_from_pb>(pb: P) -> Option> { + let mut res = HashMap::new(); + let pb = &mut (pb.into_iter()); + while let Some((this_field, wire)) = parse_field(pb) { + let to_skip = match wire { + // varint + 0 => { + parse_varint(pb); + 0 + } + // fixed 64bit + 1 => 8, + // fixed 32bit + 5 => 4, + // embed + 2 => { + let len = parse_varint(pb)?; + if this_field == 1 { + let mut tsh = pb.take(len as usize); + let (f1, v1) = next_string_from_pb(&mut tsh, &[1, 2])?; + let (f2, v2) = next_string_from_pb(&mut tsh, &[1, 2])?; + let rem = tsh.size_hint().1.unwrap_or_default(); + if rem > 0 { + tsh.nth(rem - 1); + } + + let (k, v) = if f1 == 1 && f2 == 2 { + (v1, v2) + } else if f2 == 1 && f1 == 2 { + (v2, v1) + } else { + return None; + }; + res.insert(k, v); + 0 + } else { + len + } + } + _ => return None, + }; + for _ in 0..to_skip { + pb.next(); + } + } + Some(res) +} + #[cfg(test)] mod tests { use crate::util; @@ -170,4 +230,15 @@ mod tests { let res = string_from_pb(p_bytes, 3).unwrap(); assert_eq!(res, "UC9vrvNSL3xcWGSkV86REBSg"); } + + #[test] + fn parse_proto_kv() { + let p = "ChEKBWFjb250EghvcmlnaW5hbAoKCgRsYW5nEgJlbg"; + let p_bytes = util::b64_decode(urlencoding::decode(p).unwrap().as_bytes()).unwrap(); + + let res = kv_from_pb(p_bytes).unwrap(); + assert_eq!(res.len(), 2); + assert_eq!(res["acont"], "original"); + assert_eq!(res["lang"], "en"); + } } From d8d6830dfba51078c7a4d855ae61d20378a1077b Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 30 Jun 2025 00:01:55 +0200 Subject: [PATCH 19/20] wip: add sabr_context_update --- abr-proto/build.rs | 3 + .../proto/video_streaming/media_header.proto | 9 +- .../sabr_context_sending_policy.proto | 5 + .../video_streaming/sabr_context_update.proto | 21 +++ .../proto/video_streaming/sabr_seek.proto | 18 +++ .../video_streaming/streamer_context.proto | 8 +- downloader/src/abr2.rs | 134 ++++++++++++++++-- downloader/src/bin/abrmap.rs | 1 - src/client/player.rs | 11 ++ 9 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 abr-proto/proto/video_streaming/sabr_context_sending_policy.proto create mode 100644 abr-proto/proto/video_streaming/sabr_context_update.proto create mode 100644 abr-proto/proto/video_streaming/sabr_seek.proto diff --git a/abr-proto/build.rs b/abr-proto/build.rs index e8f4db2..4b79762 100644 --- a/abr-proto/build.rs +++ b/abr-proto/build.rs @@ -43,6 +43,9 @@ fn compile() { vsdir.join("streamer_context.proto"), vsdir.join("time_range.proto"), vsdir.join("video_playback_abr_request.proto"), + vsdir.join("sabr_context_update.proto"), + vsdir.join("sabr_context_sending_policy.proto"), + vsdir.join("sabr_seek.proto"), ]; let slices = files.iter().map(Deref::deref).collect::>(); diff --git a/abr-proto/proto/video_streaming/media_header.proto b/abr-proto/proto/video_streaming/media_header.proto index 58aa007..0c4dad0 100644 --- a/abr-proto/proto/video_streaming/media_header.proto +++ b/abr-proto/proto/video_streaming/media_header.proto @@ -5,7 +5,7 @@ import "misc/common.proto"; import "video_streaming/time_range.proto"; message MediaHeader { - optional uint32 header_id = 1; + optional uint32 header_id = 1; optional string video_id = 2; optional int32 itag = 3; optional uint64 lmt = 4; @@ -19,12 +19,11 @@ message MediaHeader { optional int32 duration_ms = 12; optional .misc.FormatId format_id = 13; optional int64 content_length = 14; - optional TimeRange time_range = 15; + optional TimeRange time_range = 15; enum Compression { - VAL0 = 0; - VAL1 = 1; + UNKNOWN = 0; + NONE = 1; GZIP = 2; } - } diff --git a/abr-proto/proto/video_streaming/sabr_context_sending_policy.proto b/abr-proto/proto/video_streaming/sabr_context_sending_policy.proto new file mode 100644 index 0000000..8d8762b --- /dev/null +++ b/abr-proto/proto/video_streaming/sabr_context_sending_policy.proto @@ -0,0 +1,5 @@ +message SabrContextSendingPolicy { + repeated int32 start_policy = 1; + repeated int32 stop_policy = 2; + repeated int32 discard_policy = 3; +} diff --git a/abr-proto/proto/video_streaming/sabr_context_update.proto b/abr-proto/proto/video_streaming/sabr_context_update.proto new file mode 100644 index 0000000..d48e485 --- /dev/null +++ b/abr-proto/proto/video_streaming/sabr_context_update.proto @@ -0,0 +1,21 @@ +message SabrContextUpdate { + enum SabrContextScope { + SABR_CONTEXT_SCOPE_UNKNOWN = 0; + SABR_CONTEXT_SCOPE_PLAYBACK = 1; + SABR_CONTEXT_SCOPE_REQUEST = 2; + SABR_CONTEXT_SCOPE_WATCH_ENDPOINT = 3; + SABR_CONTEXT_SCOPE_CONTENT_ADS = 4; + } + + enum SabrContextWritePolicy { + SABR_CONTEXT_WRITE_POLICY_UNSPECIFIED = 0; + SABR_CONTEXT_WRITE_POLICY_OVERWRITE = 1; + SABR_CONTEXT_WRITE_POLICY_KEEP_EXISTING = 2; + } + + optional int32 type = 1; + optional SabrContextScope scope = 2; + optional bytes value = 3; + optional bool send_by_default = 4; + optional SabrContextWritePolicy write_policy = 5; +} diff --git a/abr-proto/proto/video_streaming/sabr_seek.proto b/abr-proto/proto/video_streaming/sabr_seek.proto new file mode 100644 index 0000000..30939f3 --- /dev/null +++ b/abr-proto/proto/video_streaming/sabr_seek.proto @@ -0,0 +1,18 @@ +message SabrSeek { + enum SeekSource { + SEEK_SOURCE_UNKNOWN = 0; + SEEK_SOURCE_SABR_PARTIAL_CHUNK = 9; + SEEK_SOURCE_SABR_SEEK_TO_HEAD = 10; + SEEK_SOURCE_SABR_LIVE_DVR_USER_SEEK = 11; + SEEK_SOURCE_SABR_SEEK_TO_DVR_LOWER_BOUND = 12; + SEEK_SOURCE_SABR_SEEK_TO_DVR_UPPER_BOUND = 13; + SEEK_SOURCE_SABR_ACCURATE_SEEK = 17; + SEEK_SOURCE_SABR_INGESTION_WALL_TIME_SEEK = 29; + SEEK_SOURCE_SABR_SEEK_TO_CLOSEST_KEYFRAME = 59; + SEEK_SOURCE_SABR_RELOAD_PLAYER_RESPONSE_TOKEN_SEEK = 106; + } + + optional int32 seek_time_ticks = 1; + optional int32 timescale = 2; + optional SeekSource seek_source = 3; +} diff --git a/abr-proto/proto/video_streaming/streamer_context.proto b/abr-proto/proto/video_streaming/streamer_context.proto index 3a766c3..c99a60d 100644 --- a/abr-proto/proto/video_streaming/streamer_context.proto +++ b/abr-proto/proto/video_streaming/streamer_context.proto @@ -24,7 +24,7 @@ message StreamerContext { optional float screen_density_float = 65; optional int64 utc_offset_minutes = 67; optional string time_zone = 80; - optional string chipset = 92; // e.g. "qcom;taro" + optional string chipset = 92; // e.g. "qcom;taro" optional GLDeviceInfo gl_device_info = 102; } @@ -40,7 +40,7 @@ message StreamerContext { optional int32 gl_es_version_minor = 3; } - message Fqa { + message SabrContext { optional int32 type = 1; optional bytes value = 2; } @@ -59,8 +59,8 @@ message StreamerContext { optional bytes po_token = 2; optional bytes playback_cookie = 3; optional bytes gp = 4; - repeated Fqa field5 = 5; - repeated int32 field6 = 6; + repeated SabrContext sabr_contexts = 5; + repeated int32 unsent_sabr_contexts = 6; optional string field7 = 7; optional Gqa field8 = 8; } diff --git a/downloader/src/abr2.rs b/downloader/src/abr2.rs index 86dfbab..72faf6d 100644 --- a/downloader/src/abr2.rs +++ b/downloader/src/abr2.rs @@ -18,10 +18,16 @@ use rustypipe_abr_proto::{ next_request_policy::NextRequestPolicy, playback_cookie::PlaybackCookie, reload_player_response::ReloadPlayerResponse, + sabr_context_sending_policy::SabrContextSendingPolicy, + sabr_context_update::{sabr_context_update::SabrContextWritePolicy, SabrContextUpdate}, sabr_error::SabrError, sabr_redirect::SabrRedirect, + sabr_seek::SabrSeek, stream_protection_status::StreamProtectionStatus, - streamer_context::{streamer_context::ClientInfo, StreamerContext}, + streamer_context::{ + streamer_context::{ClientInfo, SabrContext}, + StreamerContext, + }, video_playback_abr_request::VideoPlaybackAbrRequest, }; use serde::{Deserialize, Serialize}; @@ -43,6 +49,8 @@ pub struct AbrStream { pub previous_sequences: HashMap>, pub buffer: BytesBuffer, pub last_header: Option, + pub sabr_context_updates: HashMap, + pub sabr_contexts_to_send: HashSet, } #[derive(Default, Clone)] @@ -226,6 +234,8 @@ impl AbrStream { previous_sequences: HashMap::new(), buffer: BytesBuffer::new(), last_header: None, + sabr_context_updates: HashMap::new(), + sabr_contexts_to_send: HashSet::new(), }) } @@ -346,6 +356,13 @@ impl AbrStream { _ => {} } } + PartType::SABR_SEEK => self.process_sabr_seek(&part_data)?, + PartType::SABR_CONTEXT_UPDATE => { + self.process_sabr_context_update(&part_data)? + } + PartType::SABR_CONTEXT_SENDING_POLICY => { + self.process_sabr_context_sending_policy(&part_data)? + } _ => {} }; self.last_header = None; @@ -543,6 +560,76 @@ impl AbrStream { Ok(()) } + fn process_sabr_seek(&mut self, data: &[u8]) -> Result<(), AbrError> { + let seek = SabrSeek::parse_from_bytes(data)?; + if let Some((seek_time_ticks, timescale)) = seek.seek_time_ticks.zip(seek.timescale) { + let seek_to = i64::from(seek_time_ticks) * 1000 / i64::from(timescale); + tracing::debug!("Seeking to {seek_to}ms"); + self.abr_state.set_player_time_ms(seek_to); + + /* + TODO: Clear latest segment of each initialized format + as we expect them to no longer be in order. + */ + } + Ok(()) + } + + fn process_sabr_context_update(&mut self, data: &[u8]) -> Result<(), AbrError> { + let sabr_ctx_update = SabrContextUpdate::parse_from_bytes(data)?; + if let Some((typ, value)) = sabr_ctx_update + .type_ + .as_ref() + .zip(sabr_ctx_update.value.as_ref()) + { + if sabr_ctx_update.write_policy() + == SabrContextWritePolicy::SABR_CONTEXT_WRITE_POLICY_KEEP_EXISTING + && self.sabr_context_updates.contains_key(typ) + { + tracing::debug!("Received a SABR Context Update with write_policy=KEEP_EXISTING matching an existing SABR Context Update. Ignoring update") + } else { + tracing::warn!("Received a SABR Context Update. YouTube is likely trying to force ads on the client."); + tracing::debug!("Registered SabrContextUpdate {sabr_ctx_update:?}"); + if sabr_ctx_update.send_by_default() { + self.sabr_contexts_to_send.insert(*typ); + } + self.sabr_context_updates.insert(*typ, sabr_ctx_update); + } + } else { + tracing::warn!("Received an invalid SabrContextUpdate, ignoring") + } + Ok(()) + } + + fn process_sabr_context_sending_policy(&mut self, data: &[u8]) -> Result<(), AbrError> { + let policy = SabrContextSendingPolicy::parse_from_bytes(data)?; + for start_type in policy.start_policy { + if !self.sabr_context_updates.contains_key(&start_type) { + tracing::debug!( + "Server requested to enable SABR Context Update for type {start_type}" + ); + self.sabr_contexts_to_send.insert(start_type); + } + } + for stop_type in policy.stop_policy { + if self.sabr_context_updates.contains_key(&stop_type) { + tracing::debug!( + "Server requested to disable SABR Context Update for type {stop_type}" + ); + self.sabr_contexts_to_send.remove(&stop_type); + } + } + for discard_type in policy.discard_policy { + if self.sabr_context_updates.contains_key(&discard_type) { + tracing::debug!( + "Server requested to discard SABR Context Update for type {discard_type}" + ); + self.sabr_context_updates.remove(&discard_type); + } + } + Ok(()) + } + fn main_format(&self) -> Option<&InitializedFormat> { self.formats_by_key .values() @@ -597,6 +684,22 @@ impl AbrStream { .playback_cookie .as_ref() .and_then(|c| c.write_to_bytes().ok()), + sabr_contexts: self + .sabr_context_updates + .values() + .filter(|v| self.sabr_contexts_to_send.contains(&v.type_())) + .map(|v| SabrContext { + type_: v.type_, + value: v.value.clone(), + ..Default::default() + }) + .collect(), + unsent_sabr_contexts: self + .sabr_contexts_to_send + .iter() + .copied() + .filter(|v| !self.sabr_context_updates.contains_key(v)) + .collect(), ..Default::default() }), ..Default::default() @@ -647,23 +750,24 @@ impl AbrStream { } tracing::debug!("request #{ri} fetched {page_len} bytes"); - let main_format = self.main_format().ok_or(AbrError::Invalid(format!("no main format: {:?}", self.formats_by_key.values()).into()))?; - let player_time = self.abr_state.player_time_ms() - + main_format.sequence_list.iter().fold(0, |acc, seq| { - acc + i64::from(seq.duration_ms.unwrap_or_default()) - }); + if let Some(main_format) = self.main_format() { + let player_time = self.abr_state.player_time_ms() + + main_format.sequence_list.iter().fold(0, |acc, seq| { + acc + i64::from(seq.duration_ms.unwrap_or_default()) + }); - tracing::debug!( - "player time: {player_time}; sequence_count={}, last={:?}", - main_format.sequence_count.unwrap_or_default(), - main_format.sequence_list.last().and_then(|x| x.sequence_number), - ); - if self.is_done() { - break; + tracing::debug!( + "player time: {player_time}; sequence_count={}, last={:?}", + main_format.sequence_count.unwrap_or_default(), + main_format.sequence_list.last().and_then(|x| x.sequence_number), + ); + if self.is_done() { + break; + } + + self.abr_state.set_player_time_ms(player_time); } - self.abr_state.set_player_time_ms(player_time); - if self.backoff_time_ms > 0 { tracing::debug!("backoff: {} ms", self.backoff_time_ms); tokio::time::sleep(std::time::Duration::from_millis( diff --git a/downloader/src/bin/abrmap.rs b/downloader/src/bin/abrmap.rs index 02d9047..c233d71 100644 --- a/downloader/src/bin/abrmap.rs +++ b/downloader/src/bin/abrmap.rs @@ -15,7 +15,6 @@ async fn main() { let rp = RustyPipe::builder().build().unwrap(); let player = rp .query() - .visitor_data("Cgt0SlJzQllxNFhuMCjZ1Z3BBjIKCgJERRIEEgAgXg%3D%3D") .player_from_client(id, rustypipe::client::ClientType::Desktop) .await .unwrap(); diff --git a/src/client/player.rs b/src/client/player.rs index da932f1..7734bc4 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -44,6 +44,8 @@ struct QPlayer<'a> { /// Botguard data #[serde(skip_serializing_if = "Option::is_none")] service_integrity_dimensions: Option, + #[serde(skip_serializing_if = "str::is_empty")] + params: &'a str, } #[derive(Debug, Serialize)] @@ -70,6 +72,8 @@ struct QContentPlaybackContext<'a> { splay: Option, #[serde(skip_serializing_if = "Option::is_none")] vis: Option, + // #[serde(default, skip_serializing_if = "<&bool>::not")] + // is_inline_playback_no_ad: bool, } #[derive(Debug, Serialize)] @@ -245,6 +249,7 @@ impl RustyPipeQuery { signature_timestamp: &deobf.sts, splay: Some(false).filter(|_| client_type != ClientType::Tv), vis: Some(0).filter(|_| client_type != ClientType::Tv), + // is_inline_playback_no_ad: client_type == ClientType::Desktop, }, device_playback_capabilities: QDevicePlaybackCapabilities { supports_vp9_encoding: true, @@ -258,6 +263,12 @@ impl RustyPipeQuery { content_check_ok: true, racy_check_ok: true, service_integrity_dimensions: player_po.content_po_token, + // params: if client_type == ClientType::Desktop { + // "YAHIAQGiBhUBdpLKYASyFW9m4ER1ZCQj7pdlkO8%3D" + // } else { + // "" + // }, + params: "", }; self.execute_request_ctx::( From 01c6a6062462e5222ea4716f2b1d08891c744976 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sun, 6 Jul 2025 17:28:32 +0200 Subject: [PATCH 20/20] WIP --- downloader/src/abr2.rs | 10 +- downloader/src/abr4.rs | 244 ++++++++++++++++++++++++++----- downloader/src/abr_model.rs | 210 +++++++++++++++----------- downloader/src/bin/abrstream2.rs | 97 ++++++++++++ src/client/player.rs | 15 +- 5 files changed, 440 insertions(+), 136 deletions(-) create mode 100644 downloader/src/bin/abrstream2.rs diff --git a/downloader/src/abr2.rs b/downloader/src/abr2.rs index 72faf6d..25b2962 100644 --- a/downloader/src/abr2.rs +++ b/downloader/src/abr2.rs @@ -458,13 +458,13 @@ impl AbrStream { current_format.sequence_list.push(seq); - if mh.has_sequence_number() { - current_format.state.set_duration_ms( - current_format.state.duration_ms() + i64::from(mh.duration_ms()), - ); + if let Some(sequence_number) = mh.sequence_number { current_format .state - .set_end_segment_index(current_format.state.end_segment_index() + 1); + .set_duration_ms(i64::from(mh.start_ms()) + i64::from(mh.duration_ms())); + current_format + .state + .set_end_segment_index(sequence_number as i32); } } diff --git a/downloader/src/abr4.rs b/downloader/src/abr4.rs index f7ffb6d..dd683f1 100644 --- a/downloader/src/abr4.rs +++ b/downloader/src/abr4.rs @@ -8,7 +8,8 @@ use std::{ use async_stream::try_stream; use bytes::Bytes; use data_encoding::BASE64URL; -use futures_util::Stream; +use futures_util::{Stream, StreamExt}; +use pin_project_lite::pin_project; use protobuf::{Message, MessageField}; use reqwest::Client; use tokio::sync::mpsc; @@ -21,12 +22,17 @@ use rustypipe_abr_proto::{ format_initialization_metadata::FormatInitializationMetadata, media_header::MediaHeader, playback_cookie::PlaybackCookie, - streamer_context::{streamer_context::ClientInfo, StreamerContext}, + sabr_context_sending_policy::SabrContextSendingPolicy, + sabr_context_update::{sabr_context_update::SabrContextWritePolicy, SabrContextUpdate}, + streamer_context::{ + streamer_context::{ClientInfo, SabrContext}, + StreamerContext, + }, video_playback_abr_request::VideoPlaybackAbrRequest, }; use crate::{ - abr_model::{MediaData, UmpHeader, UmpPart}, + abr_model::{AbrFlowRes, MediaData, UmpHeader, UmpPart}, error::AbrError, util::BytesBuffer, }; @@ -38,23 +44,29 @@ Features: - evtl: ability to control stream (update position/quality) */ -pub struct AbrStream { - http: Client, - url: String, - ustreamer_config: Vec, - po_token: Option>, - audio_stream: AudioStream, - video_stream: Option, - playback_cookie: Option, - backoff_time_ms: i32, - rx: mpsc::Receiver, - formats_by_key: HashMap, - header_id_to_format_key_map: HashMap, - // previous_sequences: HashMap>, - buffer: BytesBuffer, - last_header: Option, - time_last_format_selection: Instant, - time_last_seek: Instant, +pin_project! { + pub struct AbrStream { + http: Client, + url: String, + ustreamer_config: Vec, + po_token: Option>, + audio_stream: AudioStream, + video_stream: Option, + playback_cookie: Option, + player_time_ms: i64, + backoff_time_ms: i32, + tx: mpsc::Sender, + rx: mpsc::Receiver, + formats_by_key: HashMap, + header_id_to_format_key_map: HashMap, + // previous_sequences: HashMap>, + buffer: BytesBuffer, + last_header: Option, + sabr_context_updates: HashMap, + sabr_contexts_to_send: HashSet, + time_last_format_selection: Instant, + time_last_seek: Instant, + } } #[derive(Clone)] @@ -74,7 +86,7 @@ pub struct AbrStreamOptions<'a> { } pub enum AbrStreamUpdate { - SetTime(i64), + SeekTo(i64), // SetByteOffset(i64), // SetAudioStream(AudioStream), // SetVideoStream(VideoStream), @@ -177,7 +189,9 @@ impl AbrStream { audio_stream: options.audio_stream, video_stream: options.video_stream, playback_cookie: None, + player_time_ms: 0, backoff_time_ms: 0, + tx: tx.clone(), rx, formats_by_key: HashMap::new(), header_id_to_format_key_map: HashMap::new(), @@ -186,6 +200,8 @@ impl AbrStream { last_header: None, time_last_seek: now, time_last_format_selection: now, + sabr_context_updates: HashMap::new(), + sabr_contexts_to_send: HashSet::new(), }, AbrStreamController { tx }, )) @@ -256,6 +272,22 @@ impl AbrStream { .playback_cookie .as_ref() .and_then(|c| c.write_to_bytes().ok()), + sabr_contexts: self + .sabr_context_updates + .values() + .filter(|v| self.sabr_contexts_to_send.contains(&v.type_())) + .map(|v| SabrContext { + type_: v.type_, + value: v.value.clone(), + ..Default::default() + }) + .collect(), + unsent_sabr_contexts: self + .sabr_contexts_to_send + .iter() + .copied() + .filter(|v| !self.sabr_context_updates.contains_key(v)) + .collect(), ..Default::default() }), ..Default::default() @@ -304,6 +336,20 @@ impl AbrStream { .ok_or(AbrError::Invalid("sabr redirect: no URL".into()))?; tracing::debug!("SABR redirect to {}", self.url); } + UmpPart::SabrSeek(seek) => { + if let Some((seek_time_ticks, timescale)) = seek.seek_time_ticks.zip(seek.timescale) + { + let seek_to = i64::from(seek_time_ticks) * 1000 / i64::from(timescale); + tracing::debug!("SabrSeek to {seek_to}ms"); + self.seek_to(seek_to); + } + } + UmpPart::SabrContextUpdate(sabr_ctx_update) => { + self.process_sabr_context_update(sabr_ctx_update)?; + } + UmpPart::SabrContextSendingPolicy(policy) => { + self.process_sabr_context_sending_policy(policy)?; + } } Ok(None) } @@ -380,13 +426,13 @@ impl AbrStream { current_format.sequence_list.push(seq); - if mh.has_sequence_number() { - current_format.state.set_duration_ms( - current_format.state.duration_ms() + i64::from(mh.duration_ms()), - ); + if let Some(sequence_number) = mh.sequence_number { + let t = i64::from(mh.start_ms()) + i64::from(mh.duration_ms()); + self.player_time_ms = t; + current_format.state.set_duration_ms(t); current_format .state - .set_end_segment_index(current_format.state.end_segment_index() + 1); + .set_end_segment_index(sequence_number as i32); } } @@ -430,6 +476,77 @@ impl AbrStream { })) } + fn process_sabr_context_update( + &mut self, + sabr_ctx_update: SabrContextUpdate, + ) -> Result<(), AbrError> { + if let Some((typ, _value)) = sabr_ctx_update + .type_ + .as_ref() + .zip(sabr_ctx_update.value.as_ref()) + { + if sabr_ctx_update.write_policy() + == SabrContextWritePolicy::SABR_CONTEXT_WRITE_POLICY_KEEP_EXISTING + && self.sabr_context_updates.contains_key(typ) + { + tracing::debug!("Received a SABR Context Update with write_policy=KEEP_EXISTING matching an existing SABR Context Update. Ignoring update") + } else { + tracing::warn!("Received a SABR Context Update. YouTube is likely trying to force ads on the client."); + tracing::debug!("Registered SabrContextUpdate {sabr_ctx_update:?}"); + if sabr_ctx_update.send_by_default() { + self.sabr_contexts_to_send.insert(*typ); + } + self.sabr_context_updates.insert(*typ, sabr_ctx_update); + } + } else { + tracing::warn!("Received an invalid SabrContextUpdate, ignoring") + } + Ok(()) + } + + fn process_sabr_context_sending_policy( + &mut self, + policy: SabrContextSendingPolicy, + ) -> Result<(), AbrError> { + for start_type in policy.start_policy { + if !self.sabr_context_updates.contains_key(&start_type) { + tracing::debug!( + "Server requested to enable SABR Context Update for type {start_type}" + ); + self.sabr_contexts_to_send.insert(start_type); + } + } + for stop_type in policy.stop_policy { + if self.sabr_context_updates.contains_key(&stop_type) { + tracing::debug!( + "Server requested to disable SABR Context Update for type {stop_type}" + ); + self.sabr_contexts_to_send.remove(&stop_type); + } + } + for discard_type in policy.discard_policy { + if self.sabr_context_updates.contains_key(&discard_type) { + tracing::debug!( + "Server requested to discard SABR Context Update for type {discard_type}" + ); + self.sabr_context_updates.remove(&discard_type); + } + } + Ok(()) + } + + fn main_format(&self) -> Option<&InitializedFormat> { + self.formats_by_key + .values() + .find(|fmt| { + fmt.mime_type + .as_deref() + .map(|t| t.contains("video")) + .unwrap_or_default() + }) + .or(self.formats_by_key.values().next()) + } + /// A stream is done if all sequences from all formats were received or the last request did not contain any sequences fn is_done(&self) -> bool { self.formats_by_key.values().all(|f| { @@ -441,23 +558,84 @@ impl AbrStream { }) } - async fn stream_step(&mut self) -> Result, AbrError> { - // let resp = self.fetch_stream_data(player_time, buffered_ranges); - - todo!() + pub fn seek_to(&mut self, seek_to: i64) { + self.player_time_ms = seek_to; + self.time_last_seek = Instant::now(); + self.formats_by_key.values_mut().for_each(|f| { + f.state.set_start_time_ms(0); + f.state.set_duration_ms(0); + f.state.set_start_segment_index(1); + f.state.set_end_segment_index(1); + }); + self.clear_sequence_list(); + // self.main_format().unwrap().sequence_list.last() } - /* pub fn stream(mut self) -> impl Stream> { let stream = try_stream! { let mut ri = 1; loop { - let resp = self.fetch_stream_data().await?; + let buffered_ranges = self + .formats_by_key + .values() + .map(|f| f.state.clone()) + .collect(); + let resp = self + .fetch_stream_data(self.player_time_ms, buffered_ranges) + .await?; + + let mut data_stream = resp.bytes_stream(); + let mut page_len = 0; + self.clear_sequence_list(); + + while let Some(chunk) = data_stream.next().await { + let chunk = chunk?; + let clen = chunk.len(); + page_len += clen; + tracing::debug!("chunk {clen}"); + self.buffer.push(chunk); + + loop { + if self.last_header.is_none() { + self.last_header = UmpHeader::parse(&mut self.buffer); + } + tracing::debug!("loop {:?}", self.last_header); + if let Some(last_header) = &mut self.last_header { + let pres = UmpPart::parse(&mut self.buffer, last_header); + match pres { + Ok(part) => if let Some(itm) = self.process_ump_part(part)? { + yield itm; + }, + Err(AbrFlowRes::Skipped) => {} + Err(AbrFlowRes::EmptyBuffer) => { + break; + } + Err(AbrFlowRes::Error(e)) => Err(e)?, + } + } else { + break; + } + } + } + tracing::debug!("request #{ri} fetched {page_len} bytes"); + + if let Some(main_format) = self.main_format() { + tracing::debug!( + "player time: {}; sequence_count={}, last={:?}", + self.player_time_ms, + main_format.sequence_count.unwrap_or_default(), + main_format.sequence_list.last().and_then(|x| x.sequence_number), + ); + } + if self.is_done() { + break; + } + + ri += 1; } }; Box::pin(stream) } - */ } fn get_format_key(format_id: &FormatId) -> String { diff --git a/downloader/src/abr_model.rs b/downloader/src/abr_model.rs index eb6b16f..d218e47 100644 --- a/downloader/src/abr_model.rs +++ b/downloader/src/abr_model.rs @@ -2,8 +2,9 @@ use bytes::{Buf, Bytes}; use protobuf::Message; use rustypipe_abr_proto::{ format_initialization_metadata::FormatInitializationMetadata, media_header::MediaHeader, - next_request_policy::NextRequestPolicy, sabr_error::SabrError, sabr_redirect::SabrRedirect, - stream_protection_status::StreamProtectionStatus, + next_request_policy::NextRequestPolicy, sabr_context_sending_policy::SabrContextSendingPolicy, + sabr_context_update::SabrContextUpdate, sabr_error::SabrError, sabr_redirect::SabrRedirect, + sabr_seek::SabrSeek, stream_protection_status::StreamProtectionStatus, }; use crate::{error::AbrError, util::BytesBuffer}; @@ -114,6 +115,9 @@ pub enum UmpPart { FormatInit(FormatInitializationMetadata), NextRequestPolicy(NextRequestPolicy), SabrRedirect(SabrRedirect), + SabrSeek(SabrSeek), + SabrContextUpdate(SabrContextUpdate), + SabrContextSendingPolicy(SabrContextSendingPolicy), } pub struct MediaData { @@ -180,103 +184,131 @@ pub fn read_varint(buf: &BytesBuffer, offset: usize) -> (u64, usize) { (n, offset + byte_length) } -/// Try to parse the next UMP header from the given buffer. Only advances the buffer -/// if a complete header can be read. -pub fn parse_ump_header(buf: &mut BytesBuffer) -> Option { - let (part_type, o1) = read_varint(buf, 0); - let part_type = PartType::from(part_type); - if o1 == 0 { - return None; - } - let (mut part_size, mut o2) = read_varint(buf, o1); - if o2 == 0 { - return None; - } - - let mut header_id = None; - if part_type == PartType::MEDIA && part_size > 0 { - header_id = buf.get(o2); - if header_id.is_none() { +impl UmpHeader { + /// Try to parse the next UMP header from the given buffer. Only advances the buffer + /// if a complete header can be read. + pub fn parse(buf: &mut BytesBuffer) -> Option { + let (part_type, o1) = read_varint(buf, 0); + let part_type = PartType::from(part_type); + if o1 == 0 { return None; } - o2 += 1; - part_size -= 1; + let (mut part_size, mut o2) = read_varint(buf, o1); + if o2 == 0 { + return None; + } + + let mut header_id = None; + if part_type == PartType::MEDIA && part_size > 0 { + header_id = buf.get(o2); + if header_id.is_none() { + return None; + } + o2 += 1; + part_size -= 1; + } + + buf.advance(o2); + + Some(Self { + part_type, + size: part_size as usize, + header_id, + }) } - - buf.advance(o2); - - Some(UmpHeader { - part_type, - size: part_size as usize, - header_id, - }) } -/// Try to parse the next UMP part from the given buffer. Only advances the buffer -/// if a complete part can be read. -/// -/// MediaData parts may be returned partially. -pub fn parse_ump_part( - buf: &mut BytesBuffer, - last_header: &mut UmpHeader, -) -> Result, AbrError> { - if last_header.part_type == PartType::MEDIA { - let header_id = last_header - .header_id - .ok_or(AbrError::Invalid("no header_id for media part".into()))?; - let to_copy = buf.remaining().min(last_header.size); - let partial = to_copy < last_header.size; - if to_copy == 0 { - return Ok(None); - } - let data = buf.copy_to_bytes(to_copy); - last_header.size -= data.len(); +pub enum AbrFlowRes { + Skipped, + EmptyBuffer, + Error(AbrError), +} - Ok(Some(UmpPart::MediaData(MediaData { - header_id, - data, - partial, - }))) - } else if buf.remaining() < last_header.size { - tracing::debug!("waiting for entire part"); - Ok(None) - } else { - let part_data = buf.copy_to_bytes(last_header.size); - match last_header.part_type { - PartType::MEDIA_HEADER => Ok(Some(UmpPart::MediaHeader( - MediaHeader::parse_from_bytes(&part_data)?, - ))), - PartType::MEDIA_END => Ok(Some(UmpPart::MediaEnd(part_data[0]))), - PartType::FORMAT_INITIALIZATION_METADATA => Ok(Some(UmpPart::FormatInit( - FormatInitializationMetadata::parse_from_bytes(&part_data)?, - ))), - PartType::NEXT_REQUEST_POLICY => Ok(Some(UmpPart::NextRequestPolicy( - NextRequestPolicy::parse_from_bytes(&part_data)?, - ))), - PartType::SABR_REDIRECT => Ok(Some(UmpPart::SabrRedirect( - SabrRedirect::parse_from_bytes(&part_data)?, - ))), - PartType::SABR_ERROR => Err(AbrError::Sabr(SabrError::parse_from_bytes(&part_data)?)), - PartType::RELOAD_PLAYER_RESPONSE => Err(AbrError::ReloadPlayer), - PartType::STREAM_PROTECTION_STATUS => { - let prot = StreamProtectionStatus::parse_from_bytes(&part_data)?; - match prot.status() { - 1 => tracing::debug!("[StreamProtectionStatus]: Good"), - 2 => tracing::warn!("[StreamProtectionStatus]: Attestation pending"), - 3 => return Err(AbrError::Attestation), - _ => {} - }; - Ok(None) - } - _ => { - tracing::debug!("skipping part {:?}", last_header.part_type); - Ok(None) - } - } +impl From for AbrFlowRes { + fn from(value: AbrError) -> Self { + Self::Error(value) + } +} + +impl From for AbrFlowRes { + fn from(value: protobuf::Error) -> Self { + Self::Error(value.into()) } } impl UmpPart { + /// Try to parse the next UMP part from the given buffer. Only advances the buffer + /// if a complete part can be read. + /// + /// MediaData parts may be returned partially. + pub fn parse(buf: &mut BytesBuffer, last_header: &mut UmpHeader) -> Result { + if last_header.part_type == PartType::MEDIA { + let header_id = last_header + .header_id + .ok_or(AbrError::Invalid("no header_id for media part".into()))?; + let to_copy = buf.remaining().min(last_header.size); + let partial = to_copy < last_header.size; + if to_copy == 0 { + return Err(AbrFlowRes::EmptyBuffer); + } + let data = buf.copy_to_bytes(to_copy); + last_header.size -= data.len(); + + Ok(UmpPart::MediaData(MediaData { + header_id, + data, + partial, + })) + } else if buf.remaining() < last_header.size { + tracing::debug!("waiting for entire part"); + Err(AbrFlowRes::EmptyBuffer) + } else { + let part_data = buf.copy_to_bytes(last_header.size); + match last_header.part_type { + PartType::MEDIA_HEADER => Ok(UmpPart::MediaHeader(MediaHeader::parse_from_bytes( + &part_data, + )?)), + PartType::MEDIA_END => Ok(UmpPart::MediaEnd(part_data[0])), + PartType::FORMAT_INITIALIZATION_METADATA => Ok(UmpPart::FormatInit( + FormatInitializationMetadata::parse_from_bytes(&part_data)?, + )), + PartType::NEXT_REQUEST_POLICY => Ok(UmpPart::NextRequestPolicy( + NextRequestPolicy::parse_from_bytes(&part_data)?, + )), + PartType::SABR_REDIRECT => Ok(UmpPart::SabrRedirect( + SabrRedirect::parse_from_bytes(&part_data)?, + )), + PartType::SABR_SEEK => { + Ok(UmpPart::SabrSeek(SabrSeek::parse_from_bytes(&part_data)?)) + } + PartType::SABR_CONTEXT_UPDATE => Ok(UmpPart::SabrContextUpdate( + SabrContextUpdate::parse_from_bytes(&part_data)?, + )), + PartType::SABR_CONTEXT_SENDING_POLICY => Ok(UmpPart::SabrContextSendingPolicy( + SabrContextSendingPolicy::parse_from_bytes(&part_data)?, + )), + PartType::SABR_ERROR => { + Err(AbrError::Sabr(SabrError::parse_from_bytes(&part_data)?).into()) + } + PartType::RELOAD_PLAYER_RESPONSE => Err(AbrError::ReloadPlayer.into()), + PartType::STREAM_PROTECTION_STATUS => { + let prot = StreamProtectionStatus::parse_from_bytes(&part_data)?; + match prot.status() { + 1 => tracing::debug!("[StreamProtectionStatus]: Good"), + 2 => tracing::warn!("[StreamProtectionStatus]: Attestation pending"), + 3 => return Err(AbrError::Attestation.into()), + _ => {} + }; + Err(AbrFlowRes::Skipped) + } + _ => { + tracing::debug!("skipping part {:?}", last_header.part_type); + Err(AbrFlowRes::Skipped) + } + } + } + } + pub fn partial(&self) -> bool { match self { UmpPart::MediaData(media_data) => media_data.partial, diff --git a/downloader/src/bin/abrstream2.rs b/downloader/src/bin/abrstream2.rs new file mode 100644 index 0000000..f90efe6 --- /dev/null +++ b/downloader/src/bin/abrstream2.rs @@ -0,0 +1,97 @@ +use std::{fs::File, io::BufReader}; + +use bytes::Buf; +use rustypipe::{ + model::{AudioCodec, VideoFormat, VideoPlayer}, + param::StreamFilter, +}; +use rustypipe_downloader::abr2::{AbrStream, AbrStreamOptions}; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + let mut args = std::env::args().skip(1); + let player_fname = args.next().expect("player file"); + let time_ms = args.next().expect("time in ms").parse::().unwrap(); + + /* + let rp = RustyPipe::builder().build().unwrap(); + let player = rp.query().player(vid).await.unwrap(); + */ + let player: VideoPlayer = { + let pfile = BufReader::new(File::open(player_fname).unwrap()); + serde_json::from_reader(pfile).unwrap() + }; + + let stream_filter = StreamFilter::new() + .allow_abr_only() + .video_max_res(360) + .video_formats([VideoFormat::Webm]) + .audio_codecs([AudioCodec::Opus]); + let (_, audio) = player.select_video_audio_stream(&stream_filter); + // let video = video.unwrap(); + let audio = audio.unwrap(); + + let mut abr = AbrStream::new(AbrStreamOptions { + url: player.abr_streaming_url.clone().unwrap(), + ustreamer_config: player.abr_ustreamer_config.as_deref().unwrap(), + po_token: player.po_token.as_deref(), + audio_streams: &[audio], + // video_streams: &[video], + video_streams: &[], + player_time: time_ms, + ..Default::default() + }) + .unwrap(); + + let fid = rustypipe_abr_proto::common::FormatId { + itag: Some(251), + last_modified: Some(1714693509531582), + ..Default::default() + }; + abr.formats_by_key.insert( + "251;1714693509531582;".to_owned(), + rustypipe_downloader::abr2::InitializedFormat { + format_id: fid.clone(), + duration_ms: Some(229301), + mime_type: Some("audio/webm; codecs=\"opus\"".to_owned()), + sequence_count: Some(23), + sequence_list: Vec::new(), + state: rustypipe_abr_proto::buffered_range::BufferedRange { + format_id: Some(fid).into(), + start_time_ms: Some(0), + duration_ms: Some(1), + start_segment_index: Some(1), + end_segment_index: Some(1), + ..Default::default() + }, + }, + ); + + let resp = abr.fetch_stream_data().await.unwrap(); + let data = resp.bytes().await.unwrap(); + tracing::debug!("fetched {} bytes", data.len()); + + abr.buffer.push(data); + + loop { + if abr.last_header.is_none() { + abr.last_header = abr.parse_ump_header(); + } + tracing::debug!("loop {:?}", abr.last_header); + if abr.last_header.is_none() { + break; + } + let (x, y) = abr.process_ump_part().unwrap(); + if let Some(item) = x { + dbg!(&item); + } + if y { + break; + } + } + + tracing::info!("buffer remaining: {}", abr.buffer.remaining()); + // dbg!(&abr.formats_by_key); +} diff --git a/src/client/player.rs b/src/client/player.rs index 7734bc4..391d7cf 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -72,8 +72,6 @@ struct QContentPlaybackContext<'a> { splay: Option, #[serde(skip_serializing_if = "Option::is_none")] vis: Option, - // #[serde(default, skip_serializing_if = "<&bool>::not")] - // is_inline_playback_no_ad: bool, } #[derive(Debug, Serialize)] @@ -249,7 +247,6 @@ impl RustyPipeQuery { signature_timestamp: &deobf.sts, splay: Some(false).filter(|_| client_type != ClientType::Tv), vis: Some(0).filter(|_| client_type != ClientType::Tv), - // is_inline_playback_no_ad: client_type == ClientType::Desktop, }, device_playback_capabilities: QDevicePlaybackCapabilities { supports_vp9_encoding: true, @@ -263,12 +260,12 @@ impl RustyPipeQuery { content_check_ok: true, racy_check_ok: true, service_integrity_dimensions: player_po.content_po_token, - // params: if client_type == ClientType::Desktop { - // "YAHIAQGiBhUBdpLKYASyFW9m4ER1ZCQj7pdlkO8%3D" - // } else { - // "" - // }, - params: "", + params: if client_type == ClientType::Desktop { + // Simulate video preview to circumvent anti-adblock delay + "YAHIAQE%3D" + } else { + "" + }, }; self.execute_request_ctx::(