1use std::env;
4use tokio::io;
5use tokio::fs;
6use tokio::fs::File;
7use tokio::io::{BufReader, BufWriter, AsyncWriteExt, AsyncSeekExt, AsyncReadExt};
8use std::path::{Path, PathBuf};
9use std::process::Command;
10use std::time::Duration;
11use tokio::time::Instant;
12use chrono::Utc;
13use std::sync::Arc;
14use std::collections::HashMap;
15use std::cmp::min;
16use std::ffi::OsStr;
17use std::num::NonZeroU32;
18use std::sync::LazyLock;
19use futures_util::TryFutureExt;
20use tracing::{trace, info, warn, error};
21use regex::Regex;
22use url::Url;
23use bytes::Bytes;
24use data_url::DataUrl;
25use reqwest::header::{RANGE, CONTENT_TYPE};
26use backon::{ExponentialBuilder, Retryable};
27use governor::{Quota, RateLimiter};
28use xot::{xmlname, Xot};
29use edit_distance::edit_distance;
30use crate::{MPD, Period, Representation, AdaptationSet, SegmentBase, DashMpdError};
31use crate::{parse, mux_audio_video, copy_video_to_container, copy_audio_to_container};
32use crate::{is_audio_adaptation, is_video_adaptation, is_subtitle_adaptation};
33use crate::{subtitle_type, content_protection_type, SubtitleType};
34use crate::check_conformity;
35#[cfg(not(feature = "libav"))]
36use crate::ffmpeg::concat_output_files;
37use crate::media::{temporary_outpath, AudioTrack};
38use crate::subtitles::{wvtt_extract, convert_ttml_srt, convert_vtt_srt};
39use crate::decryption::{
40 decrypt_mp4decrypt,
41 decrypt_shaka,
42 decrypt_shaka_container,
43 decrypt_mp4box,
44 decrypt_mp4box_container
45};
46#[allow(unused_imports)]
47use crate::media::video_containers_concatable;
48
49#[cfg(all(feature = "sandbox", target_os = "linux"))]
50use crate::sandbox::{restrict_thread};
51
52
53pub type HttpClient = reqwest::Client;
55type DirectRateLimiter = RateLimiter<governor::state::direct::NotKeyed,
56 governor::state::InMemoryState,
57 governor::clock::DefaultClock,
58 governor::middleware::NoOpMiddleware>;
59
60
61#[must_use]
64pub fn partial_process_output(output: &[u8]) -> String {
65 let cleaned = strip_ansi_escapes::strip(output);
66 let len = min(cleaned.len(), 4096);
67 #[allow(clippy::indexing_slicing)]
68 String::from_utf8_lossy(&cleaned[0..len]).to_string()
69}
70
71
72pub fn tmp_file_path(prefix: &str, extension: &OsStr) -> Result<PathBuf, DashMpdError> {
75 if let Some(ext) = extension.to_str() {
76 let fmt = format!(".{}", extension.to_string_lossy());
78 let suffix = if ext.starts_with('.') {
79 extension
80 } else {
81 OsStr::new(&fmt)
82 };
83 let file = tempfile::Builder::new()
84 .prefix(prefix)
85 .suffix(suffix)
86 .rand_bytes(7)
87 .disable_cleanup(env::var("DASHMPD_PERSIST_FILES").is_ok())
88 .tempfile()
89 .map_err(|e| DashMpdError::Io(e, String::from("creating temporary file")))?;
90 Ok(file.path().to_path_buf())
91 } else {
92 Err(DashMpdError::Other(String::from("converting filename extension")))
93 }
94}
95
96
97#[cfg(unix)]
101async fn ensure_permissions_readable(path: &Path) -> Result<(), DashMpdError> {
102 use std::fs::Permissions;
103 use std::os::unix::fs::PermissionsExt;
104
105 let perms = Permissions::from_mode(0o644);
106 fs::set_permissions(path, perms)
107 .map_err(|e| DashMpdError::Io(e, String::from("setting file permissions"))).await?;
108 Ok(())
109}
110
111#[cfg(not(unix))]
112async fn ensure_permissions_readable(path: &Path) -> Result<(), DashMpdError> {
113 let mut perms = fs::metadata(path).await
114 .map_err(|e| DashMpdError::Io(e, String::from("reading file permissions")))?
115 .permissions();
116 perms.set_readonly(false);
117 fs::set_permissions(path, perms)
118 .map_err(|e| DashMpdError::Io(e, String::from("setting file permissions"))).await?;
119 Ok(())
120}
121
122
123pub trait ProgressObserver: Send + Sync {
126 fn update(&self, percent: u32, bandwidth: u64, message: &str);
127}
128
129
130#[derive(PartialEq, Eq, Clone, Copy, Default)]
133pub enum QualityPreference { #[default] Lowest, Intermediate, Highest }
134
135
136pub struct DashDownloader {
156 pub mpd_url: String,
157 pub redirected_url: Url,
158 base_url: Option<String>,
159 referer: Option<String>,
160 auth_username: Option<String>,
161 auth_password: Option<String>,
162 auth_bearer_token: Option<String>,
163 pub output_path: Option<PathBuf>,
164 http_client: Option<HttpClient>,
165 quality_preference: QualityPreference,
166 language_preference_audio: Option<String>,
167 language_preference_subtitles: Option<String>,
168 role_preference: Vec<String>,
169 video_width_preference: Option<u64>,
170 video_height_preference: Option<u64>,
171 video_codec_preference: Vec<String>,
172 video_id_wanted: Option<String>,
173 fetch_video: bool,
174 fetch_audio: bool,
175 fetch_subtitles: bool,
176 keep_video: Option<PathBuf>,
177 keep_audio: Option<PathBuf>,
179 concatenate_periods: bool,
180 fragment_path: Option<PathBuf>,
181 pub decryption_keys: HashMap<String, String>,
182 xslt_stylesheets: Vec<PathBuf>,
183 minimum_period_duration: Option<Duration>,
184 content_type_checks: bool,
185 conformity_checks: bool,
186 use_index_range: bool,
187 fragment_retry_count: u32,
188 max_error_count: u32,
189 progress_observers: Vec<Arc<dyn ProgressObserver>>,
190 sleep_between_requests: u8,
191 allow_live_streams: bool,
192 force_duration: Option<f64>,
193 rate_limit: u64,
194 bw_limiter: Option<DirectRateLimiter>,
195 bw_estimator_started: Instant,
196 bw_estimator_bytes: usize,
197 pub sandbox: bool,
198 pub verbosity: u8,
199 record_metainformation: bool,
200 pub muxer_preference: HashMap<String, String>,
201 pub concat_preference: HashMap<String, String>,
202 pub decryptor_preference: String,
203 pub ffmpeg_location: String,
204 pub vlc_location: String,
205 pub mkvmerge_location: String,
206 pub mp4box_location: String,
207 pub mp4decrypt_location: String,
208 pub shaka_packager_location: String,
209}
210
211
212#[cfg(not(doctest))]
215impl DashDownloader {
234 #[must_use]
240 pub fn new(mpd_url: &str) -> DashDownloader {
241 DashDownloader {
242 mpd_url: String::from(mpd_url),
243 redirected_url: Url::parse(mpd_url).unwrap(),
244 base_url: None,
245 referer: None,
246 auth_username: None,
247 auth_password: None,
248 auth_bearer_token: None,
249 output_path: None,
250 http_client: None,
251 quality_preference: QualityPreference::Lowest,
252 language_preference_audio: None,
253 language_preference_subtitles: None,
254 role_preference: vec!["main".to_string(), "alternate".to_string()],
255 video_width_preference: None,
256 video_height_preference: None,
257 video_codec_preference: Vec::new(),
258 video_id_wanted: None,
259 fetch_video: true,
260 fetch_audio: true,
261 fetch_subtitles: false,
262 keep_video: None,
263 keep_audio: None,
264 concatenate_periods: true,
265 fragment_path: None,
266 decryption_keys: HashMap::new(),
267 xslt_stylesheets: Vec::new(),
268 minimum_period_duration: None,
269 content_type_checks: true,
270 conformity_checks: true,
271 use_index_range: true,
272 fragment_retry_count: 10,
273 max_error_count: 30,
274 progress_observers: Vec::new(),
275 sleep_between_requests: 0,
276 allow_live_streams: false,
277 force_duration: None,
278 rate_limit: 0,
279 bw_limiter: None,
280 bw_estimator_started: Instant::now(),
281 bw_estimator_bytes: 0,
282 sandbox: false,
283 verbosity: 0,
284 record_metainformation: true,
285 muxer_preference: HashMap::new(),
286 concat_preference: HashMap::new(),
287 decryptor_preference: String::from("mp4decrypt"),
288 ffmpeg_location: String::from("ffmpeg"),
289 vlc_location: if cfg!(target_os = "windows") {
290 String::from("c:/Program Files/VideoLAN/VLC/vlc.exe")
293 } else {
294 String::from("vlc")
295 },
296 mkvmerge_location: String::from("mkvmerge"),
297 mp4box_location: if cfg!(target_os = "windows") {
298 String::from("MP4Box.exe")
299 } else if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
300 String::from("MP4Box")
301 } else {
302 String::from("mp4box")
303 },
304 mp4decrypt_location: String::from("mp4decrypt"),
305 shaka_packager_location: String::from("shaka-packager"),
306 }
307 }
308
309 #[must_use]
312 pub fn with_base_url(mut self, base_url: String) -> DashDownloader {
313 self.base_url = Some(base_url);
314 self
315 }
316
317
318 #[must_use]
340 pub fn with_http_client(mut self, client: HttpClient) -> DashDownloader {
341 self.http_client = Some(client);
342 self
343 }
344
345 #[must_use]
349 pub fn with_referer(mut self, referer: String) -> DashDownloader {
350 self.referer = Some(referer);
351 self
352 }
353
354 #[must_use]
357 pub fn with_authentication(mut self, username: &str, password: &str) -> DashDownloader {
358 self.auth_username = Some(username.to_string());
359 self.auth_password = Some(password.to_string());
360 self
361 }
362
363 #[must_use]
366 pub fn with_auth_bearer(mut self, token: &str) -> DashDownloader {
367 self.auth_bearer_token = Some(token.to_string());
368 self
369 }
370
371 #[must_use]
374 pub fn add_progress_observer(mut self, observer: Arc<dyn ProgressObserver>) -> DashDownloader {
375 self.progress_observers.push(observer);
376 self
377 }
378
379 #[must_use]
382 pub fn best_quality(mut self) -> DashDownloader {
383 self.quality_preference = QualityPreference::Highest;
384 self
385 }
386
387 #[must_use]
390 pub fn intermediate_quality(mut self) -> DashDownloader {
391 self.quality_preference = QualityPreference::Intermediate;
392 self
393 }
394
395 #[must_use]
398 pub fn worst_quality(mut self) -> DashDownloader {
399 self.quality_preference = QualityPreference::Lowest;
400 self
401 }
402
403 #[must_use]
410 pub fn prefer_language(mut self, lang: String) -> DashDownloader {
411 self.language_preference_audio = Some(lang.clone());
412 self.language_preference_subtitles = Some(lang);
413 self
414 }
415
416 #[must_use]
421 pub fn prefer_audio_language(mut self, lang: String) -> DashDownloader {
422 self.language_preference_audio = Some(lang);
423 self
424 }
425
426 #[must_use]
431 pub fn prefer_subtitle_language(mut self, lang: String) -> DashDownloader {
432 self.language_preference_subtitles = Some(lang);
433 self
434 }
435
436
437 #[must_use]
447 pub fn prefer_roles(mut self, role_preference: Vec<String>) -> DashDownloader {
448 if role_preference.len() < u8::MAX as usize {
449 self.role_preference = role_preference;
450 } else {
451 warn!("Ignoring role_preference ordering due to excessive length");
452 }
453 self
454 }
455
456 #[must_use]
459 pub fn prefer_video_width(mut self, width: u64) -> DashDownloader {
460 self.video_width_preference = Some(width);
461 self
462 }
463
464 #[must_use]
467 pub fn prefer_video_height(mut self, height: u64) -> DashDownloader {
468 self.video_height_preference = Some(height);
469 self
470 }
471
472 #[must_use]
477 pub fn prefer_video_codecs(mut self, codec_preference: Vec<String>) -> DashDownloader {
478 if codec_preference.len() < u8::MAX as usize {
479 self.video_codec_preference = codec_preference;
480 } else {
481 warn!("Ignoring video codec_preference due to excessive length");
482 }
483 self
484 }
485
486 #[must_use]
492 pub fn want_video_id_substring(mut self, substring: String) -> DashDownloader {
493 self.video_id_wanted = Some(substring);
494 self
495 }
496
497 #[must_use]
499 pub fn video_only(mut self) -> DashDownloader {
500 self.fetch_audio = false;
501 self.fetch_video = true;
502 self
503 }
504
505 #[must_use]
507 pub fn audio_only(mut self) -> DashDownloader {
508 self.fetch_audio = true;
509 self.fetch_video = false;
510 self
511 }
512
513 #[must_use]
516 pub fn keep_video_as<P: Into<PathBuf>>(mut self, video_path: P) -> DashDownloader {
517 self.keep_video = Some(video_path.into());
518 self
519 }
520
521 #[must_use]
524 pub fn keep_audio_as<P: Into<PathBuf>>(mut self, audio_path: P) -> DashDownloader {
525 self.keep_audio = Some(audio_path.into());
526 self
527 }
528
529 #[must_use]
532 pub fn save_fragments_to<P: Into<PathBuf>>(mut self, fragment_path: P) -> DashDownloader {
533 self.fragment_path = Some(fragment_path.into());
534 self
535 }
536
537 #[must_use]
549 pub fn add_decryption_key(mut self, id: String, key: String) -> DashDownloader {
550 self.decryption_keys.insert(id, key);
551 self
552 }
553
554 #[must_use]
566 pub fn with_xslt_stylesheet<P: Into<PathBuf>>(mut self, stylesheet: P) -> DashDownloader {
567 self.xslt_stylesheets.push(stylesheet.into());
568 self
569 }
570
571 #[must_use]
574 pub fn minimum_period_duration(mut self, value: Duration) -> DashDownloader {
575 self.minimum_period_duration = Some(value);
576 self
577 }
578
579 #[must_use]
583 pub fn fetch_audio(mut self, value: bool) -> DashDownloader {
584 self.fetch_audio = value;
585 self
586 }
587
588 #[must_use]
592 pub fn fetch_video(mut self, value: bool) -> DashDownloader {
593 self.fetch_video = value;
594 self
595 }
596
597 #[must_use]
605 pub fn fetch_subtitles(mut self, value: bool) -> DashDownloader {
606 self.fetch_subtitles = value;
607 self
608 }
609
610 #[must_use]
614 pub fn concatenate_periods(mut self, value: bool) -> DashDownloader {
615 self.concatenate_periods = value;
616 self
617 }
618
619 #[must_use]
622 pub fn without_content_type_checks(mut self) -> DashDownloader {
623 self.content_type_checks = false;
624 self
625 }
626
627 #[must_use]
630 pub fn content_type_checks(mut self, value: bool) -> DashDownloader {
631 self.content_type_checks = value;
632 self
633 }
634
635 #[must_use]
638 pub fn conformity_checks(mut self, value: bool) -> DashDownloader {
639 self.conformity_checks = value;
640 self
641 }
642
643 #[must_use]
658 pub fn use_index_range(mut self, value: bool) -> DashDownloader {
659 self.use_index_range = value;
660 self
661 }
662
663 #[must_use]
667 pub fn fragment_retry_count(mut self, count: u32) -> DashDownloader {
668 self.fragment_retry_count = count;
669 self
670 }
671
672 #[must_use]
679 pub fn max_error_count(mut self, count: u32) -> DashDownloader {
680 self.max_error_count = count;
681 self
682 }
683
684 #[must_use]
686 pub fn sleep_between_requests(mut self, seconds: u8) -> DashDownloader {
687 self.sleep_between_requests = seconds;
688 self
689 }
690
691 #[must_use]
703 pub fn allow_live_streams(mut self, value: bool) -> DashDownloader {
704 self.allow_live_streams = value;
705 self
706 }
707
708 #[must_use]
714 pub fn force_duration(mut self, seconds: f64) -> DashDownloader {
715 if seconds < 0.0 {
716 warn!("Ignoring negative value for force_duration()");
717 } else {
718 self.force_duration = Some(seconds);
719 if self.verbosity > 1 {
720 info!("Setting forced duration to {seconds:.1} seconds");
721 }
722 }
723 self
724 }
725
726 #[must_use]
732 pub fn with_rate_limit(mut self, bps: u64) -> DashDownloader {
733 if bps < 10 * 1024 {
734 warn!("Limiting bandwidth below 10kB/s is unlikely to be stable");
735 }
736 if self.verbosity > 1 {
737 info!("Limiting bandwidth to {} kB/s", bps/1024);
738 }
739 self.rate_limit = bps;
740 let mut kps = 1 + bps / 1024;
746 if kps > u64::from(u32::MAX) {
747 warn!("Throttling bandwidth limit");
748 kps = u32::MAX.into();
749 }
750 if let Some(bw_limit) = NonZeroU32::new(kps as u32) {
751 if let Some(burst) = NonZeroU32::new(10 * 1024) {
752 let bw_quota = Quota::per_second(bw_limit)
753 .allow_burst(burst);
754 self.bw_limiter = Some(RateLimiter::direct(bw_quota));
755 }
756 }
757 self
758 }
759
760 #[must_use]
770 pub fn verbosity(mut self, level: u8) -> DashDownloader {
771 self.verbosity = level;
772 self
773 }
774
775 #[must_use]
785 pub fn sandbox(mut self, enable: bool) -> DashDownloader {
786 #[cfg(not(all(feature = "sandbox", target_os = "linux")))]
787 if enable {
788 warn!("Sandboxing only available on Linux with crate feature sandbox enabled");
789 }
790 if self.verbosity > 1 && enable {
791 info!("Enabling sandboxing support");
792 }
793 self.sandbox = enable;
794 self
795 }
796
797 #[must_use]
801 pub fn record_metainformation(mut self, record: bool) -> DashDownloader {
802 self.record_metainformation = record;
803 self
804 }
805
806 #[must_use]
828 pub fn with_muxer_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
829 self.muxer_preference.insert(container.to_string(), ordering.to_string());
830 self
831 }
832
833 #[must_use]
856 pub fn with_concat_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
857 self.concat_preference.insert(container.to_string(), ordering.to_string());
858 self
859 }
860
861 #[must_use]
870 pub fn with_decryptor_preference(mut self, decryption_tool: &str) -> DashDownloader {
871 self.decryptor_preference = decryption_tool.to_string();
872 self
873 }
874
875 #[must_use]
890 pub fn with_ffmpeg(mut self, ffmpeg_path: &str) -> DashDownloader {
891 self.ffmpeg_location = ffmpeg_path.to_string();
892 self
893 }
894
895 #[must_use]
910 pub fn with_vlc(mut self, vlc_path: &str) -> DashDownloader {
911 self.vlc_location = vlc_path.to_string();
912 self
913 }
914
915 #[must_use]
923 pub fn with_mkvmerge(mut self, path: &str) -> DashDownloader {
924 self.mkvmerge_location = path.to_string();
925 self
926 }
927
928 #[must_use]
936 pub fn with_mp4box(mut self, path: &str) -> DashDownloader {
937 self.mp4box_location = path.to_string();
938 self
939 }
940
941 #[must_use]
949 pub fn with_mp4decrypt(mut self, path: &str) -> DashDownloader {
950 self.mp4decrypt_location = path.to_string();
951 self
952 }
953
954 #[must_use]
962 pub fn with_shaka_packager(mut self, path: &str) -> DashDownloader {
963 self.shaka_packager_location = path.to_string();
964 self
965 }
966
967 pub async fn download_to<P: Into<PathBuf>>(mut self, out: P) -> Result<PathBuf, DashMpdError> {
977 self.output_path = Some(out.into());
978 if self.http_client.is_none() {
979 let client = reqwest::Client::builder()
980 .timeout(Duration::new(30, 0))
981 .cookie_store(true)
982 .build()
983 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
984 self.http_client = Some(client);
985 }
986 fetch_mpd(&mut self).await
987 }
988
989 pub async fn download(mut self) -> Result<PathBuf, DashMpdError> {
996 let cwd = env::current_dir()
997 .map_err(|e| DashMpdError::Io(e, String::from("obtaining current directory")))?;
998 let filename = generate_filename_from_url(&self.mpd_url);
999 let outpath = cwd.join(filename);
1000 self.output_path = Some(outpath);
1001 if self.http_client.is_none() {
1002 let client = reqwest::Client::builder()
1003 .timeout(Duration::new(30, 0))
1004 .cookie_store(true)
1005 .build()
1006 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
1007 self.http_client = Some(client);
1008 }
1009 fetch_mpd(&mut self).await
1010 }
1011}
1012
1013
1014fn mpd_is_dynamic(mpd: &MPD) -> bool {
1015 if let Some(mpdtype) = mpd.mpdtype.as_ref() {
1016 return mpdtype.eq("dynamic");
1017 }
1018 false
1019}
1020
1021fn parse_range(range: &str) -> Result<(u64, u64), DashMpdError> {
1024 let v: Vec<&str> = range.split_terminator('-').collect();
1025 if v.len() != 2 {
1026 return Err(DashMpdError::Parsing(format!("invalid range specifier: {range}")));
1027 }
1028 #[allow(clippy::indexing_slicing)]
1029 let start: u64 = v[0].parse()
1030 .map_err(|_| DashMpdError::Parsing(String::from("invalid start for range specifier")))?;
1031 #[allow(clippy::indexing_slicing)]
1032 let end: u64 = v[1].parse()
1033 .map_err(|_| DashMpdError::Parsing(String::from("invalid end for range specifier")))?;
1034 Ok((start, end))
1035}
1036
1037#[derive(Debug)]
1038struct MediaFragment {
1039 period: u8,
1040 url: Url,
1041 start_byte: Option<u64>,
1042 end_byte: Option<u64>,
1043 is_init: bool,
1044 timeout: Option<Duration>,
1045}
1046
1047#[derive(Debug)]
1048struct MediaFragmentBuilder {
1049 period: u8,
1050 url: Url,
1051 start_byte: Option<u64>,
1052 end_byte: Option<u64>,
1053 is_init: bool,
1054 timeout: Option<Duration>,
1055}
1056
1057impl MediaFragmentBuilder {
1058 pub fn new(period: u8, url: Url) -> MediaFragmentBuilder {
1059 MediaFragmentBuilder {
1060 period, url, start_byte: None, end_byte: None, is_init: false, timeout: None
1061 }
1062 }
1063
1064 pub fn with_range(mut self, start_byte: Option<u64>, end_byte: Option<u64>) -> MediaFragmentBuilder {
1065 self.start_byte = start_byte;
1066 self.end_byte = end_byte;
1067 self
1068 }
1069
1070 pub fn with_timeout(mut self, timeout: Duration) -> MediaFragmentBuilder {
1071 self.timeout = Some(timeout);
1072 self
1073 }
1074
1075 pub fn set_init(mut self) -> MediaFragmentBuilder {
1076 self.is_init = true;
1077 self
1078 }
1079
1080 pub fn build(self) -> MediaFragment {
1081 MediaFragment {
1082 period: self.period,
1083 url: self.url,
1084 start_byte: self.start_byte,
1085 end_byte: self.end_byte,
1086 is_init: self.is_init,
1087 timeout: self.timeout
1088 }
1089 }
1090}
1091
1092#[derive(Debug, Default)]
1096struct PeriodOutputs {
1097 fragments: Vec<MediaFragment>,
1098 diagnostics: Vec<String>,
1099 subtitle_formats: Vec<SubtitleType>,
1100 selected_audio_language: String,
1101 selected_subtitle_language: String,
1102}
1103
1104#[derive(Debug, Default)]
1105struct PeriodDownloads {
1106 audio_fragments: Vec<MediaFragment>,
1107 video_fragments: Vec<MediaFragment>,
1108 subtitle_fragments: Vec<MediaFragment>,
1109 subtitle_formats: Vec<SubtitleType>,
1110 period_counter: u8,
1111 id: Option<String>,
1112 selected_audio_language: String,
1113 selected_subtitle_language: String,
1114}
1115
1116fn period_fragment_count(pd: &PeriodDownloads) -> usize {
1117 pd.audio_fragments.len() +
1118 pd.video_fragments.len() +
1119 pd.subtitle_fragments.len()
1120}
1121
1122
1123
1124async fn throttle_download_rate(downloader: &DashDownloader, size: u32) -> Result<(), DashMpdError> {
1125 if downloader.rate_limit > 0 {
1126 if let Some(cells) = NonZeroU32::new(size) {
1127 if let Some(limiter) = downloader.bw_limiter.as_ref() {
1128 #[allow(clippy::redundant_pattern_matching)]
1129 if let Err(_) = limiter.until_n_ready(cells).await {
1130 return Err(DashMpdError::Other(
1131 "Bandwidth limit is too low".to_string()));
1132 }
1133 }
1134 }
1135 }
1136 Ok(())
1137}
1138
1139
1140fn generate_filename_from_url(url: &str) -> PathBuf {
1141 use sanitise_file_name::{sanitise_with_options, Options};
1142
1143 let mut path = url;
1144 if let Some(p) = path.strip_prefix("http://") {
1145 path = p;
1146 } else if let Some(p) = path.strip_prefix("https://") {
1147 path = p;
1148 } else if let Some(p) = path.strip_prefix("file://") {
1149 path = p;
1150 }
1151 if let Some(p) = path.strip_prefix("www.") {
1152 path = p;
1153 }
1154 if let Some(p) = path.strip_prefix("ftp.") {
1155 path = p;
1156 }
1157 if let Some(p) = path.strip_suffix(".mpd") {
1158 path = p;
1159 }
1160 let mut sanitize_opts = Options::DEFAULT;
1161 sanitize_opts.length_limit = 150;
1162 PathBuf::from(sanitise_with_options(path, &sanitize_opts) + ".mp4")
1167}
1168
1169fn output_path_for_period(base: &Path, period: u8) -> PathBuf {
1186 assert!(period > 0);
1187 if period == 1 {
1188 base.to_path_buf()
1189 } else {
1190 if let Some(stem) = base.file_stem() {
1191 if let Some(ext) = base.extension() {
1192 let fname = format!("{}-p{period}.{}", stem.to_string_lossy(), ext.to_string_lossy());
1193 return base.with_file_name(fname);
1194 }
1195 }
1196 let p = format!("dashmpd-p{period}");
1197 tmp_file_path(&p, base.extension().unwrap_or(OsStr::new("mp4")))
1198 .unwrap_or_else(|_| p.into())
1199 }
1200}
1201
1202fn is_absolute_url(s: &str) -> bool {
1203 s.starts_with("http://") ||
1204 s.starts_with("https://") ||
1205 s.starts_with("file://") ||
1206 s.starts_with("ftp://")
1207}
1208
1209fn merge_baseurls(current: &Url, new: &str) -> Result<Url, DashMpdError> {
1210 if is_absolute_url(new) {
1211 Url::parse(new)
1212 .map_err(|e| parse_error("parsing BaseURL", e))
1213 } else {
1214 let mut merged = current.join(new)
1227 .map_err(|e| parse_error("joining base with BaseURL", e))?;
1228 if merged.query().is_none() {
1229 merged.set_query(current.query());
1230 }
1231 Ok(merged)
1232 }
1233}
1234
1235fn content_type_audio_p(response: &reqwest::Response) -> bool {
1240 match response.headers().get("content-type") {
1241 Some(ct) => {
1242 let ctb = ct.as_bytes();
1243 ctb.starts_with(b"audio/") ||
1244 ctb.starts_with(b"video/") ||
1245 ctb.starts_with(b"application/octet-stream")
1246 },
1247 None => false,
1248 }
1249}
1250
1251fn content_type_video_p(response: &reqwest::Response) -> bool {
1253 match response.headers().get("content-type") {
1254 Some(ct) => {
1255 let ctb = ct.as_bytes();
1256 ctb.starts_with(b"video/") ||
1257 ctb.starts_with(b"application/octet-stream")
1258 },
1259 None => false,
1260 }
1261}
1262
1263
1264fn adaptation_lang_distance(a: &AdaptationSet, language_preference: &str) -> u8 {
1268 if let Some(lang) = &a.lang {
1269 if lang.eq(language_preference) {
1270 return 0;
1271 }
1272 edit_distance(lang, language_preference)
1274 .try_into()
1275 .unwrap_or(u8::MAX)
1276 } else {
1277 100
1278 }
1279}
1280
1281fn adaptation_roles(a: &AdaptationSet) -> Vec<String> {
1284 let mut roles = Vec::new();
1285 for r in &a.Role {
1286 if let Some(rv) = &r.value {
1287 roles.push(String::from(rv));
1288 }
1289 }
1290 for cc in &a.ContentComponent {
1291 for r in &cc.Role {
1292 if let Some(rv) = &r.value {
1293 roles.push(String::from(rv));
1294 }
1295 }
1296 }
1297 roles
1298}
1299
1300fn adaptation_role_distance(a: &AdaptationSet, role_preference: &[String]) -> u8 {
1302 adaptation_roles(a).iter()
1303 .map(|r| role_preference.binary_search(r).unwrap_or(u8::MAX.into()))
1304 .map(|u| u8::try_from(u).unwrap_or(u8::MAX))
1305 .min()
1306 .unwrap_or(u8::MAX)
1307}
1308
1309
1310fn select_preferred_adaptations<'a>(
1318 adaptations: Vec<&'a AdaptationSet>,
1319 downloader: &DashDownloader) -> Vec<&'a AdaptationSet>
1320{
1321 let mut preferred: Vec<&'a AdaptationSet>;
1322 if let Some(ref lang) = downloader.language_preference_audio {
1324 preferred = Vec::new();
1325 let distance: Vec<u8> = adaptations.iter()
1326 .map(|a| adaptation_lang_distance(a, lang))
1327 .collect();
1328 let min_distance = distance.iter().min().unwrap_or(&0);
1329 for (i, a) in adaptations.iter().enumerate() {
1330 if let Some(di) = distance.get(i) {
1331 if di == min_distance {
1332 preferred.push(a);
1333 }
1334 }
1335 }
1336 } else {
1337 preferred = adaptations;
1338 }
1339 let role_distance: Vec<u8> = preferred.iter()
1345 .map(|a| adaptation_role_distance(a, &downloader.role_preference))
1346 .collect();
1347 let role_distance_min = role_distance.iter().min().unwrap_or(&0);
1348 let mut best = Vec::new();
1349 for (i, a) in preferred.into_iter().enumerate() {
1350 if let Some(rdi) = role_distance.get(i) {
1351 if rdi == role_distance_min {
1352 best.push(a);
1353 }
1354 }
1355 }
1356 best
1357}
1358
1359
1360fn representation_filter_video_id<'a>(
1363 representations: Vec<&'a Representation>,
1364 downloader: &DashDownloader) -> Vec<&'a Representation>
1365{
1366 if let Some(wantid) = &downloader.video_id_wanted {
1367 representations.iter()
1368 .filter(|r| r.id.as_ref().is_some_and(|i| i.contains(wantid)))
1369 .copied()
1370 .collect()
1371 } else {
1372 representations
1373 }
1374}
1375
1376fn representation_filter_video_width<'a>(
1381 representations: Vec<&'a Representation>,
1382 downloader: &DashDownloader) -> Vec<&'a Representation>
1383{
1384 if let Some(want) = downloader.video_width_preference {
1385 let best = representations.iter()
1386 .min_by_key(|x| if let Some(w) = x.width { want.abs_diff(w) } else { u64::MAX });
1387 match best {
1388 Some(b) => representations.iter()
1389 .filter(|r| r.width == b.width)
1390 .copied()
1391 .collect::<Vec<&Representation>>(),
1392 None => representations,
1393 }
1394 } else {
1395 representations
1396 }
1397}
1398
1399fn representation_filter_video_height<'a>(
1404 representations: Vec<&'a Representation>,
1405 downloader: &DashDownloader) -> Vec<&'a Representation>
1406{
1407 if let Some(want) = downloader.video_height_preference {
1408 let best = representations.iter()
1409 .min_by_key(|x| if let Some(h) = x.height { want.abs_diff(h) } else { u64::MAX });
1410 match best {
1411 Some(b) => representations.iter()
1412 .filter(|r| r.height == b.height)
1413 .copied()
1414 .collect::<Vec<&Representation>>(),
1415 None => representations,
1416 }
1417 } else {
1418 representations
1419 }
1420}
1421
1422fn representation_filter_video_codec<'a>(
1429 representations: Vec<&'a Representation>,
1430 downloader: &DashDownloader) -> Vec<&'a Representation>
1431{
1432 if downloader.video_codec_preference.is_empty() {
1433 representations
1434 } else {
1435 let best = representations.iter()
1436 .min_by_key(|r|
1437 if let Some(codec) = &r.codecs {
1438 downloader.video_codec_preference.iter()
1439 .position(|prefc| codec.starts_with(prefc))
1440 .unwrap_or(usize::MAX)
1441 } else {
1442 usize::MAX
1443 });
1444 match best {
1445 Some(b) => if let Some(bcodec) = &b.codecs {
1446 let bcodec_start = match bcodec.find('.') {
1451 Some(idx) => &bcodec[..idx],
1452 None => bcodec,
1453 };
1454 representations.iter()
1455 .filter(|r| r.codecs.as_ref()
1456 .is_some_and(|rc| rc.starts_with(bcodec_start)))
1457 .copied()
1458 .collect()
1459 } else {
1460 representations
1461 },
1462 None => representations,
1463 }
1464 }
1465}
1466
1467fn representation_filter_video_quality<'a>(
1473 representations: Vec<&'a Representation>,
1474 downloader: &DashDownloader) -> Vec<&'a Representation>
1475{
1476 if representations.iter().all(|x| x.qualityRanking.is_some()) {
1477 match downloader.quality_preference {
1480 QualityPreference::Lowest => {
1481 let best = representations.iter()
1482 .max_by_key(|r| r.qualityRanking.unwrap_or(u8::MAX));
1483 match best {
1484 Some(b) => representations.iter()
1485 .filter(|r| r.qualityRanking.unwrap_or(u8::MAX) ==
1486 b.qualityRanking.unwrap_or(u8::MAX))
1487 .copied()
1488 .collect(),
1489 None => representations,
1490 }
1491 },
1492 QualityPreference::Highest => {
1493 let best = representations.iter()
1494 .min_by_key(|r| r.qualityRanking.unwrap_or(0));
1495 match best {
1496 Some(b) => representations.iter()
1497 .filter(|r| r.qualityRanking.unwrap_or(0) ==
1498 b.qualityRanking.unwrap_or(0))
1499 .copied()
1500 .collect(),
1501 None => representations,
1502 }
1503 },
1504 QualityPreference::Intermediate => {
1505 let count = representations.len();
1506 match count {
1507 0 | 1 => representations,
1508 _ => {
1509 let mut ranking: Vec<u8> = representations.iter()
1510 .map(|r| r.qualityRanking.unwrap_or(u8::MAX))
1511 .collect();
1512 ranking.sort_unstable();
1513 if let Some(want_ranking) = ranking.get(count / 2) {
1514 representations.iter()
1515 .filter(|r| r.qualityRanking.unwrap_or(u8::MAX) == *want_ranking)
1516 .copied()
1517 .collect()
1518 } else {
1519 representations
1520 }
1521 },
1522 }
1523 },
1524 }
1525 } else {
1526 let bw_large = 1_000_000_000;
1528 match downloader.quality_preference {
1529 QualityPreference::Lowest => {
1530 let best = representations.iter()
1531 .min_by_key(|r| r.bandwidth.unwrap_or(bw_large));
1532 match best {
1533 Some(b) => representations.iter()
1534 .filter(|r| r.bandwidth.unwrap_or(bw_large) ==
1535 b.bandwidth.unwrap_or(bw_large))
1536 .copied()
1537 .collect(),
1538 None => representations,
1539 }
1540 },
1541 QualityPreference::Highest => {
1542 let best = representations.iter()
1543 .max_by_key(|r| r.bandwidth.unwrap_or(0));
1544 match best {
1545 Some(b) => representations.iter()
1546 .filter(|r| r.bandwidth.unwrap_or(0) ==
1547 b.bandwidth.unwrap_or(0))
1548 .copied()
1549 .collect(),
1550 None => representations,
1551 }
1552 }
1553 QualityPreference::Intermediate => {
1554 let count = representations.len();
1555 match count {
1556 0 | 1 => representations,
1557 _ => {
1558 let mut ranking: Vec<u64> = representations.iter()
1559 .map(|r| r.bandwidth.unwrap_or(bw_large))
1560 .collect();
1561 ranking.sort_unstable();
1562 if let Some(want_ranking) = ranking.get(count / 2) {
1563 representations.iter()
1564 .filter(|r| r.bandwidth.unwrap_or(bw_large) == *want_ranking)
1565 .copied()
1566 .collect()
1567 } else {
1568 representations
1569 }
1570 },
1571 }
1572 },
1573 }
1574 }
1575}
1576
1577
1578fn select_preferred_representation<'a>(
1584 representations: &[&'a Representation],
1585 downloader: &DashDownloader) -> Option<&'a Representation>
1586{
1587 if representations.iter().all(|x| x.qualityRanking.is_some()) {
1588 match downloader.quality_preference {
1591 QualityPreference::Lowest =>
1592 representations.iter()
1593 .max_by_key(|r| r.qualityRanking.unwrap_or(u8::MAX))
1594 .copied(),
1595 QualityPreference::Highest =>
1596 representations.iter().min_by_key(|r| r.qualityRanking.unwrap_or(0))
1597 .copied(),
1598 QualityPreference::Intermediate => {
1599 let count = representations.len();
1600 match count {
1601 0 => None,
1602 1 => Some(representations[0]),
1603 _ => {
1604 let mut ranking: Vec<u8> = representations.iter()
1605 .map(|r| r.qualityRanking.unwrap_or(u8::MAX))
1606 .collect();
1607 ranking.sort_unstable();
1608 if let Some(want_ranking) = ranking.get(count / 2) {
1609 representations.iter()
1610 .find(|r| r.qualityRanking.unwrap_or(u8::MAX) == *want_ranking)
1611 .copied()
1612 } else {
1613 representations.first().copied()
1614 }
1615 },
1616 }
1617 },
1618 }
1619 } else {
1620 match downloader.quality_preference {
1622 QualityPreference::Lowest => representations.iter()
1623 .min_by_key(|r| r.bandwidth.unwrap_or(1_000_000_000))
1624 .copied(),
1625 QualityPreference::Highest => representations.iter()
1626 .max_by_key(|r| r.bandwidth.unwrap_or(0))
1627 .copied(),
1628 QualityPreference::Intermediate => {
1629 let count = representations.len();
1630 match count {
1631 0 => None,
1632 1 => Some(representations[0]),
1633 _ => {
1634 let mut ranking: Vec<u64> = representations.iter()
1635 .map(|r| r.bandwidth.unwrap_or(100_000_000))
1636 .collect();
1637 ranking.sort_unstable();
1638 if let Some(want_ranking) = ranking.get(count / 2) {
1639 representations.iter()
1640 .find(|r| r.bandwidth.unwrap_or(100_000_000) == *want_ranking)
1641 .copied()
1642 } else {
1643 representations.first().copied()
1644 }
1645 },
1646 }
1647 },
1648 }
1649 }
1650}
1651
1652
1653fn print_available_subtitles_representation(r: &Representation, a: &AdaptationSet) {
1655 let unspecified = "<unspecified>".to_string();
1656 let empty = "".to_string();
1657 let lang = r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unspecified));
1658 let codecs = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&empty));
1659 let typ = subtitle_type(&a);
1660 let stype = if !codecs.is_empty() {
1661 format!("{typ:?}/{codecs}")
1662 } else {
1663 format!("{typ:?}")
1664 };
1665 let role = a.Role.first()
1666 .map_or_else(|| String::from(""),
1667 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1668 let label = a.Label.first()
1669 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1670 info!(" subs {stype:>18} | {lang:>10} |{role}{label}");
1671}
1672
1673fn print_available_subtitles_adaptation(a: &AdaptationSet) {
1674 a.representations.iter()
1675 .for_each(|r| print_available_subtitles_representation(r, a));
1676}
1677
1678fn print_available_streams_representation(r: &Representation, a: &AdaptationSet, typ: &str) {
1680 let unspecified = "<unspecified>".to_string();
1682 let w = r.width.unwrap_or(a.width.unwrap_or(0));
1683 let h = r.height.unwrap_or(a.height.unwrap_or(0));
1684 let codec = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&unspecified));
1685 let bw = r.bandwidth.unwrap_or(a.maxBandwidth.unwrap_or(0));
1686 let fmt = if typ.eq("audio") {
1687 let unknown = String::from("?");
1688 format!("lang={}", r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unknown)))
1689 } else if w == 0 || h == 0 {
1690 String::from("")
1693 } else {
1694 format!("{w}x{h}")
1695 };
1696 let role = a.Role.first()
1697 .map_or_else(|| String::from(""),
1698 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1699 let label = a.Label.first()
1700 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1701 let maybe_id = if let Some(rid) = &r.id {
1702 format!(" (id={rid})")
1703 } else {
1704 String::from("")
1705 };
1706 info!(" {typ} {codec:17} | {:5} Kbps | {fmt:>9}{role}{label}{maybe_id}", bw / 1024);
1707}
1708
1709fn print_available_streams_adaptation(a: &AdaptationSet, typ: &str) {
1710 a.representations.iter()
1711 .for_each(|r| print_available_streams_representation(r, a, typ));
1712}
1713
1714fn print_available_streams_period(p: &Period) {
1715 p.adaptations.iter()
1716 .filter(is_audio_adaptation)
1717 .for_each(|a| print_available_streams_adaptation(a, "audio"));
1718 p.adaptations.iter()
1719 .filter(is_video_adaptation)
1720 .for_each(|a| print_available_streams_adaptation(a, "video"));
1721 p.adaptations.iter()
1722 .filter(is_subtitle_adaptation)
1723 .for_each(print_available_subtitles_adaptation);
1724}
1725
1726#[tracing::instrument(level="trace", skip_all)]
1727fn print_available_streams(mpd: &MPD) {
1728 use humantime::format_duration;
1729
1730 let mut counter = 0;
1731 for p in &mpd.periods {
1732 let mut period_duration_secs: f64 = -1.0;
1733 if let Some(d) = mpd.mediaPresentationDuration {
1734 period_duration_secs = d.as_secs_f64();
1735 }
1736 if let Some(d) = &p.duration {
1737 period_duration_secs = d.as_secs_f64();
1738 }
1739 counter += 1;
1740 let duration = if period_duration_secs > 0.0 {
1741 format_duration(Duration::from_secs_f64(period_duration_secs)).to_string()
1742 } else {
1743 String::from("unknown")
1744 };
1745 if let Some(id) = p.id.as_ref() {
1746 info!("Streams in period {id} (#{counter}), duration {duration}:");
1747 } else {
1748 info!("Streams in period #{counter}, duration {duration}:");
1749 }
1750 print_available_streams_period(p);
1751 }
1752}
1753
1754async fn extract_init_pssh(downloader: &DashDownloader, init_url: Url) -> Option<Vec<u8>> {
1755 use bstr::ByteSlice;
1756 use hex_literal::hex;
1757
1758 if let Some(client) = downloader.http_client.as_ref() {
1759 let mut req = client.get(init_url);
1760 if let Some(referer) = &downloader.referer {
1761 req = req.header("Referer", referer);
1762 }
1763 if let Some(username) = &downloader.auth_username {
1764 if let Some(password) = &downloader.auth_password {
1765 req = req.basic_auth(username, Some(password));
1766 }
1767 }
1768 if let Some(token) = &downloader.auth_bearer_token {
1769 req = req.bearer_auth(token);
1770 }
1771 if let Ok(mut resp) = req.send().await {
1772 let mut chunk_counter = 0;
1775 let mut segment_first_bytes = Vec::<u8>::new();
1776 while let Ok(Some(chunk)) = resp.chunk().await {
1777 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
1778 #[allow(clippy::redundant_pattern_matching)]
1779 if let Err(_) = throttle_download_rate(downloader, size).await {
1780 return None;
1781 }
1782 segment_first_bytes.append(&mut chunk.to_vec());
1783 chunk_counter += 1;
1784 if chunk_counter > 20 {
1785 break;
1786 }
1787 }
1788 let needle = b"pssh";
1789 for offset in segment_first_bytes.find_iter(needle) {
1790 #[allow(clippy::needless_range_loop)]
1791 for i in offset-4..offset+2 {
1792 if let Some(b) = segment_first_bytes.get(i) {
1793 if *b != 0 {
1794 continue;
1795 }
1796 }
1797 }
1798 #[allow(clippy::needless_range_loop)]
1799 for i in offset+4..offset+8 {
1800 if let Some(b) = segment_first_bytes.get(i) {
1801 if *b != 0 {
1802 continue;
1803 }
1804 }
1805 }
1806 if offset+24 > segment_first_bytes.len() {
1807 continue;
1808 }
1809 const WIDEVINE_SYSID: [u8; 16] = hex!("edef8ba979d64acea3c827dcd51d21ed");
1811 if let Some(sysid) = segment_first_bytes.get((offset+8)..(offset+24)) {
1812 if !sysid.eq(&WIDEVINE_SYSID) {
1813 continue;
1814 }
1815 }
1816 if let Some(length) = segment_first_bytes.get(offset-1) {
1817 let start = offset - 4;
1818 let end = start + *length as usize;
1819 if let Some(pssh) = &segment_first_bytes.get(start..end) {
1820 return Some(pssh.to_vec());
1821 }
1822 }
1823 }
1824 }
1825 None
1826 } else {
1827 None
1828 }
1829}
1830
1831
1832static URL_TEMPLATE_IDS: LazyLock<Vec<(&'static str, String, Regex)>> =
1841 LazyLock::new(|| vec!["RepresentationID", "Number", "Time", "Bandwidth"].into_iter()
1842 .map(|k| (k, format!("${k}$"), Regex::new(&format!("\\${k}%0([\\d])d\\$")).unwrap()))
1843 .collect());
1844
1845
1846fn resolve_url_template(template: &str, params: &HashMap<&str, String>) -> String {
1847 let mut result = template.to_string();
1848 for (k, ident, rx) in URL_TEMPLATE_IDS.iter() {
1849 if result.contains(ident) {
1851 if let Some(value) = params.get(k as &str) {
1852 result = result.replace(ident, value);
1853 }
1854 }
1855 if let Some(cap) = rx.captures(&result) {
1857 if let Some(value) = params.get(k as &str) {
1858 if let Ok(width) = cap[1].parse::<usize>() {
1859 if let Some(m) = rx.find(&result) {
1860 let count = format!("{value:0>width$}");
1861 result = result[..m.start()].to_owned() + &count + &result[m.end()..];
1862 }
1863 }
1864 }
1865 }
1866 }
1867 result
1868}
1869
1870
1871fn reqwest_error_transient_p(e: &reqwest::Error) -> bool {
1872 if e.is_timeout() {
1873 return true;
1874 }
1875 if let Some(s) = e.status() {
1876 if s == reqwest::StatusCode::REQUEST_TIMEOUT ||
1877 s == reqwest::StatusCode::TOO_MANY_REQUESTS ||
1878 s == reqwest::StatusCode::SERVICE_UNAVAILABLE ||
1879 s == reqwest::StatusCode::GATEWAY_TIMEOUT {
1880 return true;
1881 }
1882 }
1883 false
1884}
1885
1886fn notify_transient<E: std::fmt::Debug>(err: &E, dur: Duration) {
1887 warn!("Transient error after {dur:?}: {err:?}");
1888}
1889
1890fn network_error(why: &str, e: &reqwest::Error) -> DashMpdError {
1891 if e.is_timeout() {
1892 DashMpdError::NetworkTimeout(format!("{why}: {e:?}"))
1893 } else if e.is_connect() {
1894 DashMpdError::NetworkConnect(format!("{why}: {e:?}"))
1895 } else {
1896 DashMpdError::Network(format!("{why}: {e:?}"))
1897 }
1898}
1899
1900fn parse_error(why: &str, e: impl std::error::Error) -> DashMpdError {
1901 DashMpdError::Parsing(format!("{why}: {e:#?}"))
1902}
1903
1904
1905async fn reqwest_bytes_with_retries(
1909 client: &reqwest::Client,
1910 req: reqwest::Request,
1911 retry_count: u32) -> Result<Bytes, reqwest::Error>
1912{
1913 let mut last_error = None;
1914 for _ in 0..retry_count {
1915 if let Some(rqw) = req.try_clone() {
1916 match client.execute(rqw).await {
1917 Ok(response) => {
1918 match response.error_for_status() {
1919 Ok(resp) => {
1920 match resp.bytes().await {
1921 Ok(bytes) => return Ok(bytes),
1922 Err(e) => {
1923 info!("Retrying after HTTP error {e:?}");
1924 last_error = Some(e);
1925 },
1926 }
1927 },
1928 Err(e) => {
1929 info!("Retrying after HTTP error {e:?}");
1930 last_error = Some(e);
1931 },
1932 }
1933 },
1934 Err(e) => {
1935 info!("Retrying after HTTP error {e:?}");
1936 last_error = Some(e);
1937 },
1938 }
1939 }
1940 }
1941 Err(last_error.unwrap())
1942}
1943
1944#[allow(unused_variables)]
1957fn maybe_record_metainformation(path: &Path, downloader: &DashDownloader, mpd: &MPD) {
1958 #[cfg(target_family = "unix")]
1959 if downloader.record_metainformation && (downloader.fetch_audio || downloader.fetch_video) {
1960 if let Ok(origin_url) = Url::parse(&downloader.mpd_url) {
1961 #[allow(clippy::collapsible_if)]
1963 if origin_url.username().is_empty() && origin_url.password().is_none() {
1964 #[cfg(target_family = "unix")]
1965 if xattr::set(path, "user.xdg.origin.url", downloader.mpd_url.as_bytes()).is_err() {
1966 info!("Failed to set user.xdg.origin.url xattr on output file");
1967 }
1968 }
1969 for pi in &mpd.ProgramInformation {
1970 if let Some(t) = &pi.Title {
1971 if let Some(tc) = &t.content {
1972 if xattr::set(path, "user.dublincore.title", tc.as_bytes()).is_err() {
1973 info!("Failed to set user.dublincore.title xattr on output file");
1974 }
1975 }
1976 }
1977 if let Some(source) = &pi.Source {
1978 if let Some(sc) = &source.content {
1979 if xattr::set(path, "user.dublincore.source", sc.as_bytes()).is_err() {
1980 info!("Failed to set user.dublincore.source xattr on output file");
1981 }
1982 }
1983 }
1984 if let Some(copyright) = &pi.Copyright {
1985 if let Some(cc) = ©right.content {
1986 if xattr::set(path, "user.dublincore.rights", cc.as_bytes()).is_err() {
1987 info!("Failed to set user.dublincore.rights xattr on output file");
1988 }
1989 }
1990 }
1991 }
1992 }
1993 }
1994}
1995
1996fn fetchable_xlink_href(href: &str) -> bool {
2000 (!href.is_empty()) && href.ne("urn:mpeg:dash:resolve-to-zero:2013")
2001}
2002
2003fn element_resolves_to_zero(xot: &mut Xot, element: xot::Node) -> bool {
2004 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
2005 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
2006 if let Some(href) = xot.get_attribute(element, xlink_href_name.into()) {
2007 return href.eq("urn:mpeg:dash:resolve-to-zero:2013");
2008 }
2009 false
2010}
2011
2012fn skip_xml_preamble(input: &str) -> &str {
2013 if input.starts_with("<?xml") {
2014 if let Some(end_pos) = input.find("?>") {
2015 return &input[end_pos + 2..]; }
2018 }
2019 input
2021}
2022
2023async fn apply_xslt_stylesheets(
2024 downloader: &DashDownloader,
2025 xot: &mut Xot,
2026 doc: xot::Node) -> Result<String, DashMpdError> {
2027 #[cfg(feature = "xee-xslt")]
2028 return apply_xslt_stylesheets_xee(downloader, xot, doc).await;
2029 #[cfg(not(feature = "xee-xslt"))]
2030 return apply_xslt_stylesheets_xsltproc(downloader, xot, doc).await;
2031}
2032
2033#[allow(dead_code)]
2037async fn apply_xslt_stylesheets_xsltproc(
2038 downloader: &DashDownloader,
2039 xot: &mut Xot,
2040 doc: xot::Node) -> Result<String, DashMpdError> {
2041 let mut buf = Vec::new();
2042 xot.write(doc, &mut buf)
2043 .map_err(|e| parse_error("serializing rewritten manifest", e))?;
2044 for ss in &downloader.xslt_stylesheets {
2045 if downloader.verbosity > 0 {
2046 info!("Applying XSLT stylesheet {} with xsltproc", ss.display());
2047 }
2048 let tmpmpd = tmp_file_path("dashxslt", OsStr::new("xslt"))?;
2049 fs::write(&tmpmpd, &buf).await
2050 .map_err(|e| DashMpdError::Io(e, String::from("writing MPD")))?;
2051 let xsltproc = Command::new("xsltproc")
2052 .args([ss, &tmpmpd])
2053 .output()
2054 .map_err(|e| DashMpdError::Io(e, String::from("spawning xsltproc")))?;
2055 if !xsltproc.status.success() {
2056 let msg = format!("xsltproc returned {}", xsltproc.status);
2057 let out = partial_process_output(&xsltproc.stderr).clone();
2058 return Err(DashMpdError::Io(std::io::Error::other(msg), out));
2059 }
2060 if env::var("DASHMPD_PERSIST_FILES").is_err() {
2061 if let Err(e) = fs::remove_file(&tmpmpd).await {
2062 warn!("Error removing temporary MPD after XSLT processing: {e:?}");
2063 }
2064 }
2065 buf.clone_from(&xsltproc.stdout);
2066 if downloader.verbosity > 2 {
2067 println!("Rewritten XSLT: {}", String::from_utf8_lossy(&buf));
2068 }
2069 }
2070 String::from_utf8(buf)
2071 .map_err(|e| parse_error("parsing UTF-8", e))
2072}
2073
2074#[allow(dead_code)]
2078#[cfg(feature = "xee-xslt")]
2079async fn apply_xslt_stylesheets_xee(
2080 downloader: &DashDownloader,
2081 xot: &mut Xot,
2082 doc: xot::Node) -> Result<String, DashMpdError>
2083{
2084 use xee_xslt_compiler::evaluate;
2085 use std::fmt::Write;
2086
2087 let mut xml = xot.to_string(doc)
2088 .map_err(|e| parse_error("serializing rewritten manifest", e))?;
2089 for ss in &downloader.xslt_stylesheets {
2090 if downloader.verbosity > 0 {
2091 info!(" Applying XSLT stylesheet {} with xee", ss.display());
2092 }
2093 let xslt = fs::read_to_string(ss).await
2094 .map_err(|_| DashMpdError::Other(String::from("reading XSLT stylesheet")))?;
2095 let seq = evaluate(xot, &xml, &xslt)
2096 .map_err(|e| DashMpdError::Other(format!("applying XSLT: {e:?}")))?;
2097 let mut f = String::new();
2098 for item in seq.iter() {
2099 match item.to_node() {
2100 Ok(n) => f.write_str(&xot.to_string(n).expect("writing to string"))
2101 .expect("writing to string"),
2102 Err(e) => error!("xee non-node item {item:?}: {e:?}"),
2103 }
2104 }
2105 xml = f;
2106 }
2107 Ok(xml)
2108}
2109
2110async fn resolve_xlink_references(
2114 downloader: &DashDownloader,
2115 xot: &mut Xot,
2116 node: xot::Node) -> Result<(), DashMpdError>
2117{
2118 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
2119 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
2120 let xlinked = xot.descendants(node)
2121 .filter(|d| xot.get_attribute(*d, xlink_href_name.into()).is_some())
2122 .collect::<Vec<_>>();
2123 for xl in xlinked {
2124 if element_resolves_to_zero(xot, xl) {
2125 trace!("Removing node with resolve-to-zero xlink:href {xl:?}");
2126 if let Err(e) = xot.remove(xl) {
2127 return Err(parse_error("Failed to remove resolve-to-zero XML node", e));
2128 }
2129 } else if let Some(href) = xot.get_attribute(xl, xlink_href_name.into()) {
2130 if fetchable_xlink_href(href) {
2131 let xlink_url = if is_absolute_url(href) {
2132 Url::parse(href)
2133 .map_err(|e|
2134 if let Ok(ns) = xot.to_string(node) {
2135 parse_error(&format!("parsing XLink on {ns}"), e)
2136 } else {
2137 parse_error("parsing XLink", e)
2138 }
2139 )?
2140 } else {
2141 let mut merged = downloader.redirected_url.join(href)
2144 .map_err(|e|
2145 if let Ok(ns) = xot.to_string(node) {
2146 parse_error(&format!("parsing XLink on {ns}"), e)
2147 } else {
2148 parse_error("parsing XLink", e)
2149 }
2150 )?;
2151 merged.set_query(downloader.redirected_url.query());
2152 merged
2153 };
2154 let client = downloader.http_client.as_ref().unwrap();
2155 trace!("Fetching XLinked element {}", xlink_url.clone());
2156 let mut req = client.get(xlink_url.clone())
2157 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
2158 .header("Accept-Language", "en-US,en")
2159 .header("Sec-Fetch-Mode", "navigate");
2160 if let Some(referer) = &downloader.referer {
2161 req = req.header("Referer", referer);
2162 } else {
2163 req = req.header("Referer", downloader.redirected_url.to_string());
2164 }
2165 if let Some(username) = &downloader.auth_username {
2166 if let Some(password) = &downloader.auth_password {
2167 req = req.basic_auth(username, Some(password));
2168 }
2169 }
2170 if let Some(token) = &downloader.auth_bearer_token {
2171 req = req.bearer_auth(token);
2172 }
2173 let xml = req.send().await
2174 .map_err(|e|
2175 if let Ok(ns) = xot.to_string(node) {
2176 network_error(&format!("fetching XLink for {ns}"), &e)
2177 } else {
2178 network_error("fetching XLink", &e)
2179 }
2180 )?
2181 .error_for_status()
2182 .map_err(|e|
2183 if let Ok(ns) = xot.to_string(node) {
2184 network_error(&format!("fetching XLink for {ns}"), &e)
2185 } else {
2186 network_error("fetching XLink", &e)
2187 }
2188 )?
2189 .text().await
2190 .map_err(|e|
2191 if let Ok(ns) = xot.to_string(node) {
2192 network_error(&format!("resolving XLink for {ns}"), &e)
2193 } else {
2194 network_error("resolving XLink", &e)
2195 }
2196 )?;
2197 if downloader.verbosity > 2 {
2198 if let Ok(ns) = xot.to_string(node) {
2199 info!(" Resolved onLoad XLink {xlink_url} on {ns} -> {} octets", xml.len());
2200 } else {
2201 info!(" Resolved onLoad XLink {xlink_url} -> {} octets", xml.len());
2202 }
2203 }
2204 let wrapped_xml = r#"<?xml version="1.0" encoding="utf-8"?>"#.to_owned() +
2210 r#"<wrapper xmlns="urn:mpeg:dash:schema:mpd:2011" "# +
2211 r#"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" "# +
2212 r#"xmlns:cenc="urn:mpeg:cenc:2013" "# +
2213 r#"xmlns:mspr="urn:microsoft:playready" "# +
2214 r#"xmlns:xlink="http://www.w3.org/1999/xlink">"# +
2215 skip_xml_preamble(&xml) +
2216 r"</wrapper>";
2217 let wrapper_doc = xot.parse(&wrapped_xml)
2218 .map_err(|e| parse_error("parsing xlinked content", e))?;
2219 let wrapper_doc_el = xot.document_element(wrapper_doc)
2220 .map_err(|e| parse_error("extracting XML document element", e))?;
2221 for needs_insertion in xot.children(wrapper_doc_el).collect::<Vec<_>>() {
2222 xot.insert_after(xl, needs_insertion)
2224 .map_err(|e| parse_error("inserting XLinked content", e))?;
2225 }
2226 xot.remove(xl)
2227 .map_err(|e| parse_error("removing XLink node", e))?;
2228 }
2229 }
2230 }
2231 Ok(())
2232}
2233
2234#[tracing::instrument(level="trace", skip_all)]
2235pub async fn parse_resolving_xlinks(
2236 downloader: &DashDownloader,
2237 xml: &[u8]) -> Result<MPD, DashMpdError>
2238{
2239 use xot::xmlname::NameStrInfo;
2240
2241 let mut xot = Xot::new();
2242 let doc = xot.parse_bytes(xml)
2243 .map_err(|e| parse_error("XML parsing", e))?;
2244 let doc_el = xot.document_element(doc)
2245 .map_err(|e| parse_error("extracting XML document element", e))?;
2246 let doc_name = match xot.node_name(doc_el) {
2247 Some(n) => n,
2248 None => return Err(DashMpdError::Parsing(String::from("missing root node name"))),
2249 };
2250 let root_name = xot.name_ref(doc_name, doc_el)
2251 .map_err(|e| parse_error("extracting root node name", e))?;
2252 let root_local_name = root_name.local_name();
2253 if !root_local_name.eq("MPD") {
2254 return Err(DashMpdError::Parsing(format!("root element is {root_local_name}, expecting <MPD>")));
2255 }
2256 for _ in 1..5 {
2259 resolve_xlink_references(downloader, &mut xot, doc).await?;
2260 }
2261 let rewritten = apply_xslt_stylesheets(downloader, &mut xot, doc).await?;
2262 let mpd = parse(&rewritten)?;
2264 if downloader.conformity_checks {
2265 for emsg in check_conformity(&mpd) {
2266 warn!("DASH conformity error in manifest: {emsg}");
2267 }
2268 }
2269 Ok(mpd)
2270}
2271
2272async fn do_segmentbase_indexrange(
2273 downloader: &DashDownloader,
2274 period_counter: u8,
2275 base_url: Url,
2276 sb: &SegmentBase,
2277 dict: &HashMap<&str, String>
2278) -> Result<Vec<MediaFragment>, DashMpdError>
2279{
2280 let mut fragments = Vec::new();
2313 let mut start_byte: Option<u64> = None;
2314 let mut end_byte: Option<u64> = None;
2315 let mut indexable_segments = false;
2316 if downloader.use_index_range {
2317 if let Some(ir) = &sb.indexRange {
2318 let (s, e) = parse_range(ir)?;
2320 trace!("Fetching sidx for {}", base_url.clone());
2321 let mut req = downloader.http_client.as_ref()
2322 .unwrap()
2323 .get(base_url.clone())
2324 .header(RANGE, format!("bytes={s}-{e}"))
2325 .header("Referer", downloader.redirected_url.to_string())
2326 .header("Sec-Fetch-Mode", "navigate");
2327 if let Some(username) = &downloader.auth_username {
2328 if let Some(password) = &downloader.auth_password {
2329 req = req.basic_auth(username, Some(password));
2330 }
2331 }
2332 if let Some(token) = &downloader.auth_bearer_token {
2333 req = req.bearer_auth(token);
2334 }
2335 let mut resp = req.send().await
2336 .map_err(|e| network_error("fetching index data", &e))?
2337 .error_for_status()
2338 .map_err(|e| network_error("fetching index data", &e))?;
2339 let headers = std::mem::take(resp.headers_mut());
2340 if let Some(content_type) = headers.get(CONTENT_TYPE) {
2341 let idx = resp.bytes().await
2342 .map_err(|e| network_error("fetching index data", &e))?;
2343 if idx.len() as u64 != e - s + 1 {
2344 warn!(" HTTP server does not support Range requests; can't use indexRange addressing");
2345 } else {
2346 #[allow(clippy::collapsible_else_if)]
2347 if content_type.eq("video/mp4") ||
2348 content_type.eq("audio/mp4") {
2349 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2356 .with_range(Some(0), Some(e))
2357 .build();
2358 fragments.push(mf);
2359 let mut max_chunk_pos = 0;
2360 if let Ok(segment_chunks) = crate::sidx::from_isobmff_sidx(&idx, e+1) {
2361 trace!("Have {} segment chunks in sidx data", segment_chunks.len());
2362 for chunk in segment_chunks {
2363 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2364 .with_range(Some(chunk.start), Some(chunk.end))
2365 .build();
2366 fragments.push(mf);
2367 if chunk.end > max_chunk_pos {
2368 max_chunk_pos = chunk.end;
2369 }
2370 }
2371 indexable_segments = true;
2372 }
2373 }
2374 }
2381 }
2382 }
2383 }
2384 if indexable_segments {
2385 if let Some(init) = &sb.Initialization {
2386 if let Some(range) = &init.range {
2387 let (s, e) = parse_range(range)?;
2388 start_byte = Some(s);
2389 end_byte = Some(e);
2390 }
2391 if let Some(su) = &init.sourceURL {
2392 let path = resolve_url_template(su, dict);
2393 let u = merge_baseurls(&base_url, &path)?;
2394 let mf = MediaFragmentBuilder::new(period_counter, u)
2395 .with_range(start_byte, end_byte)
2396 .set_init()
2397 .build();
2398 fragments.push(mf);
2399 } else {
2400 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2402 .with_range(start_byte, end_byte)
2403 .set_init()
2404 .build();
2405 fragments.push(mf);
2406 }
2407 }
2408 } else {
2409 trace!("Falling back to retrieving full SegmentBase for {}", base_url.clone());
2414 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2415 .with_timeout(Duration::new(10_000, 0))
2416 .build();
2417 fragments.push(mf);
2418 }
2419 Ok(fragments)
2420}
2421
2422
2423#[tracing::instrument(level="trace", skip_all)]
2424async fn do_period_audio(
2425 downloader: &DashDownloader,
2426 mpd: &MPD,
2427 period: &Period,
2428 period_counter: u8,
2429 base_url: Url
2430) -> Result<PeriodOutputs, DashMpdError>
2431{
2432 let mut fragments = Vec::new();
2433 let mut diagnostics = Vec::new();
2434 let mut opt_init: Option<String> = None;
2435 let mut opt_media: Option<String> = None;
2436 let mut opt_duration: Option<f64> = None;
2437 let mut timescale = 1;
2438 let mut start_number = 1;
2439 let mut period_duration_secs: f64 = -1.0;
2442 if let Some(d) = mpd.mediaPresentationDuration {
2443 period_duration_secs = d.as_secs_f64();
2444 }
2445 if let Some(d) = period.duration {
2446 period_duration_secs = d.as_secs_f64();
2447 }
2448 if let Some(s) = downloader.force_duration {
2449 period_duration_secs = s;
2450 }
2451 if let Some(st) = &period.SegmentTemplate {
2455 if let Some(i) = &st.initialization {
2456 opt_init = Some(i.clone());
2457 }
2458 if let Some(m) = &st.media {
2459 opt_media = Some(m.clone());
2460 }
2461 if let Some(d) = st.duration {
2462 opt_duration = Some(d);
2463 }
2464 if let Some(ts) = st.timescale {
2465 timescale = ts;
2466 }
2467 if let Some(s) = st.startNumber {
2468 start_number = s;
2469 }
2470 }
2471 let mut selected_audio_language = "unk";
2472 let audio_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2475 .filter(is_audio_adaptation)
2476 .collect();
2477 let representations: Vec<&Representation> = select_preferred_adaptations(audio_adaptations, downloader)
2478 .iter()
2479 .flat_map(|a| a.representations.iter())
2480 .collect();
2481 if let Some(audio_repr) = select_preferred_representation(&representations, downloader) {
2482 let audio_adaptation = period.adaptations.iter()
2486 .find(|a| a.representations.iter().any(|r| r.eq(audio_repr)))
2487 .unwrap();
2488 if let Some(lang) = audio_repr.lang.as_ref().or(audio_adaptation.lang.as_ref()) {
2489 selected_audio_language = lang;
2490 }
2491 let mut base_url = base_url.clone();
2494 if let Some(bu) = &audio_adaptation.BaseURL.first() {
2495 base_url = merge_baseurls(&base_url, &bu.base)?;
2496 }
2497 if let Some(bu) = audio_repr.BaseURL.first() {
2498 base_url = merge_baseurls(&base_url, &bu.base)?;
2499 }
2500 if downloader.verbosity > 0 {
2501 let bw = if let Some(bw) = audio_repr.bandwidth {
2502 format!("bw={} Kbps ", bw / 1024)
2503 } else {
2504 String::from("")
2505 };
2506 let unknown = String::from("?");
2507 let lang = audio_repr.lang.as_ref()
2508 .unwrap_or(audio_adaptation.lang.as_ref()
2509 .unwrap_or(&unknown));
2510 let codec = audio_repr.codecs.as_ref()
2511 .unwrap_or(audio_adaptation.codecs.as_ref()
2512 .unwrap_or(&unknown));
2513 let maybe_id = if let Some(rid) = &audio_repr.id {
2514 format!(" (id={rid})")
2515 } else {
2516 String::from("")
2517 };
2518 diagnostics.push(format!(" Audio stream selected: {bw}lang={lang} codec={codec}{maybe_id}"));
2519 for cp in audio_repr.ContentProtection.iter()
2521 .chain(audio_adaptation.ContentProtection.iter())
2522 {
2523 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2524 if let Some(kid) = &cp.default_KID {
2525 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2526 }
2527 for pssh_element in &cp.cenc_pssh {
2528 if let Some(pssh_b64) = &pssh_element.content {
2529 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
2530 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
2531 diagnostics.push(format!(" {pssh}"));
2532 }
2533 }
2534 }
2535 }
2536 }
2537 if let Some(st) = &audio_adaptation.SegmentTemplate {
2542 if let Some(i) = &st.initialization {
2543 opt_init = Some(i.clone());
2544 }
2545 if let Some(m) = &st.media {
2546 opt_media = Some(m.clone());
2547 }
2548 if let Some(d) = st.duration {
2549 opt_duration = Some(d);
2550 }
2551 if let Some(ts) = st.timescale {
2552 timescale = ts;
2553 }
2554 if let Some(s) = st.startNumber {
2555 start_number = s;
2556 }
2557 }
2558 let mut dict = HashMap::new();
2559 if let Some(rid) = &audio_repr.id {
2560 dict.insert("RepresentationID", rid.clone());
2561 }
2562 if let Some(b) = &audio_repr.bandwidth {
2563 dict.insert("Bandwidth", b.to_string());
2564 }
2565 if let Some(sl) = &audio_adaptation.SegmentList {
2574 if downloader.verbosity > 1 {
2577 info!(" Using AdaptationSet>SegmentList addressing mode for audio representation");
2578 }
2579 let mut start_byte: Option<u64> = None;
2580 let mut end_byte: Option<u64> = None;
2581 if let Some(init) = &sl.Initialization {
2582 if let Some(range) = &init.range {
2583 let (s, e) = parse_range(range)?;
2584 start_byte = Some(s);
2585 end_byte = Some(e);
2586 }
2587 if let Some(su) = &init.sourceURL {
2588 let path = resolve_url_template(su, &dict);
2589 let init_url = merge_baseurls(&base_url, &path)?;
2590 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2591 .with_range(start_byte, end_byte)
2592 .set_init()
2593 .build();
2594 fragments.push(mf);
2595 } else {
2596 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2597 .with_range(start_byte, end_byte)
2598 .set_init()
2599 .build();
2600 fragments.push(mf);
2601 }
2602 }
2603 for su in &sl.segment_urls {
2604 start_byte = None;
2605 end_byte = None;
2606 if let Some(range) = &su.mediaRange {
2608 let (s, e) = parse_range(range)?;
2609 start_byte = Some(s);
2610 end_byte = Some(e);
2611 }
2612 if let Some(m) = &su.media {
2613 let u = merge_baseurls(&base_url, m)?;
2614 let mf = MediaFragmentBuilder::new(period_counter, u)
2615 .with_range(start_byte, end_byte)
2616 .build();
2617 fragments.push(mf);
2618 } else if let Some(bu) = audio_adaptation.BaseURL.first() {
2619 let u = merge_baseurls(&base_url, &bu.base)?;
2620 let mf = MediaFragmentBuilder::new(period_counter, u)
2621 .with_range(start_byte, end_byte)
2622 .build();
2623 fragments.push(mf);
2624 }
2625 }
2626 }
2627 if let Some(sl) = &audio_repr.SegmentList {
2628 if downloader.verbosity > 1 {
2630 info!(" Using Representation>SegmentList addressing mode for audio representation");
2631 }
2632 let mut start_byte: Option<u64> = None;
2633 let mut end_byte: Option<u64> = None;
2634 if let Some(init) = &sl.Initialization {
2635 if let Some(range) = &init.range {
2636 let (s, e) = parse_range(range)?;
2637 start_byte = Some(s);
2638 end_byte = Some(e);
2639 }
2640 if let Some(su) = &init.sourceURL {
2641 let path = resolve_url_template(su, &dict);
2642 let init_url = merge_baseurls(&base_url, &path)?;
2643 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2644 .with_range(start_byte, end_byte)
2645 .set_init()
2646 .build();
2647 fragments.push(mf);
2648 } else {
2649 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2650 .with_range(start_byte, end_byte)
2651 .set_init()
2652 .build();
2653 fragments.push(mf);
2654 }
2655 }
2656 for su in &sl.segment_urls {
2657 start_byte = None;
2658 end_byte = None;
2659 if let Some(range) = &su.mediaRange {
2661 let (s, e) = parse_range(range)?;
2662 start_byte = Some(s);
2663 end_byte = Some(e);
2664 }
2665 if let Some(m) = &su.media {
2666 let u = merge_baseurls(&base_url, m)?;
2667 let mf = MediaFragmentBuilder::new(period_counter, u)
2668 .with_range(start_byte, end_byte)
2669 .build();
2670 fragments.push(mf);
2671 } else if let Some(bu) = audio_repr.BaseURL.first() {
2672 let u = merge_baseurls(&base_url, &bu.base)?;
2673 let mf = MediaFragmentBuilder::new(period_counter, u)
2674 .with_range(start_byte, end_byte)
2675 .build();
2676 fragments.push(mf);
2677 }
2678 }
2679 } else if audio_repr.SegmentTemplate.is_some() ||
2680 audio_adaptation.SegmentTemplate.is_some()
2681 {
2682 let st;
2685 if let Some(it) = &audio_repr.SegmentTemplate {
2686 st = it;
2687 } else if let Some(it) = &audio_adaptation.SegmentTemplate {
2688 st = it;
2689 } else {
2690 panic!("unreachable");
2691 }
2692 if let Some(i) = &st.initialization {
2693 opt_init = Some(i.clone());
2694 }
2695 if let Some(m) = &st.media {
2696 opt_media = Some(m.clone());
2697 }
2698 if let Some(ts) = st.timescale {
2699 timescale = ts;
2700 }
2701 if let Some(sn) = st.startNumber {
2702 start_number = sn;
2703 }
2704 if let Some(stl) = &audio_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
2705 .or(audio_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
2706 {
2707 if downloader.verbosity > 1 {
2710 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for audio representation");
2711 }
2712 if let Some(init) = opt_init {
2713 let path = resolve_url_template(&init, &dict);
2714 let u = merge_baseurls(&base_url, &path)?;
2715 let mf = MediaFragmentBuilder::new(period_counter, u)
2716 .set_init()
2717 .build();
2718 fragments.push(mf);
2719 }
2720 let mut elapsed_seconds = 0.0;
2721 if let Some(media) = opt_media {
2722 let audio_path = resolve_url_template(&media, &dict);
2723 let mut segment_time = 0;
2724 let mut segment_duration;
2725 let mut number = start_number;
2726 let mut target_duration = period_duration_secs;
2727 if let Some(target) = downloader.force_duration {
2728 if target > period_duration_secs {
2729 warn!(" Requested forced duration exceeds available content");
2730 } else {
2731 target_duration = target;
2732 }
2733 }
2734 'segment_loop: for s in &stl.segments {
2735 if let Some(t) = s.t {
2736 segment_time = t;
2737 }
2738 segment_duration = s.d;
2739 let dict = HashMap::from([("Time", segment_time.to_string()),
2741 ("Number", number.to_string())]);
2742 let path = resolve_url_template(&audio_path, &dict);
2743 let u = merge_baseurls(&base_url, &path)?;
2744 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2745 number += 1;
2746 elapsed_seconds += segment_duration as f64 / timescale as f64;
2747 if downloader.force_duration.is_some() &&
2748 target_duration > 0.0 &&
2749 elapsed_seconds > target_duration {
2750 break 'segment_loop;
2751 }
2752 if let Some(r) = s.r {
2753 let mut count = 0i64;
2754 loop {
2755 count += 1;
2756 if r >= 0 && count > r {
2761 break;
2762 }
2763 if downloader.force_duration.is_some() &&
2764 target_duration > 0.0 &&
2765 elapsed_seconds > target_duration {
2766 break 'segment_loop;
2767 }
2768 if let Some(end_number) = st.endNumber {
2769 if count as u64 > end_number {
2770 break;
2771 }
2772 }
2773 segment_time += segment_duration;
2774 elapsed_seconds += segment_duration as f64 / timescale as f64;
2775 let dict = HashMap::from([("Time", segment_time.to_string()),
2776 ("Number", number.to_string())]);
2777 let path = resolve_url_template(&audio_path, &dict);
2778 let u = merge_baseurls(&base_url, &path)?;
2779 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2780 number += 1;
2781 }
2782 }
2783 segment_time += segment_duration;
2784 }
2785 } else {
2786 return Err(DashMpdError::UnhandledMediaStream(
2787 "SegmentTimeline without a media attribute".to_string()));
2788 }
2789 } else { if downloader.verbosity > 1 {
2794 info!(" Using SegmentTemplate addressing mode for audio representation");
2795 }
2796 let mut total_number = 0i64;
2797 if let Some(init) = opt_init {
2798 let path = resolve_url_template(&init, &dict);
2799 let u = merge_baseurls(&base_url, &path)?;
2800 let mf = MediaFragmentBuilder::new(period_counter, u)
2801 .set_init()
2802 .build();
2803 fragments.push(mf);
2804 }
2805 if let Some(media) = opt_media {
2806 let audio_path = resolve_url_template(&media, &dict);
2807 let timescale = st.timescale.unwrap_or(timescale);
2808 let mut segment_duration: f64 = -1.0;
2809 if let Some(d) = opt_duration {
2810 segment_duration = d;
2812 }
2813 if let Some(std) = st.duration {
2814 if timescale == 0 {
2815 return Err(DashMpdError::UnhandledMediaStream(
2816 "SegmentTemplate@duration attribute cannot be zero".to_string()));
2817 }
2818 segment_duration = std / timescale as f64;
2819 }
2820 if segment_duration < 0.0 {
2821 return Err(DashMpdError::UnhandledMediaStream(
2822 "Audio representation is missing SegmentTemplate@duration attribute".to_string()));
2823 }
2824 total_number += (period_duration_secs / segment_duration).round() as i64;
2825 let mut number = start_number;
2826 if mpd_is_dynamic(mpd) {
2829 if let Some(start_time) = mpd.availabilityStartTime {
2830 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
2831 number = (elapsed + number as f64 - 1f64).floor() as u64;
2832 } else {
2833 return Err(DashMpdError::UnhandledMediaStream(
2834 "dynamic manifest is missing @availabilityStartTime".to_string()));
2835 }
2836 }
2837 if let Some(end_number) = st.endNumber {
2838 total_number = end_number as i64;
2839 }
2840 for _ in 1..=total_number {
2841 let dict = HashMap::from([("Number", number.to_string())]);
2842 let path = resolve_url_template(&audio_path, &dict);
2843 let u = merge_baseurls(&base_url, &path)?;
2844 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2845 number += 1;
2846 }
2847 }
2848 }
2849 } else if let Some(sb) = &audio_repr.SegmentBase {
2850 if downloader.verbosity > 1 {
2852 info!(" Using SegmentBase@indexRange addressing mode for audio representation");
2853 }
2854 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
2855 fragments.extend(mf);
2856 } else if fragments.is_empty() {
2857 if let Some(bu) = audio_repr.BaseURL.first() {
2858 if downloader.verbosity > 1 {
2860 info!(" Using BaseURL addressing mode for audio representation");
2861 }
2862 let u = merge_baseurls(&base_url, &bu.base)?;
2863 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2864 }
2865 }
2866 if fragments.is_empty() {
2867 return Err(DashMpdError::UnhandledMediaStream(
2868 "no usable addressing mode identified for audio representation".to_string()));
2869 }
2870 }
2871 Ok(PeriodOutputs {
2872 fragments,
2873 diagnostics,
2874 subtitle_formats: Vec::new(),
2875 selected_audio_language: String::from(selected_audio_language),
2876 selected_subtitle_language: String::from("")
2877
2878 })
2879}
2880
2881
2882#[tracing::instrument(level="trace", skip_all)]
2883async fn do_period_video(
2884 downloader: &DashDownloader,
2885 mpd: &MPD,
2886 period: &Period,
2887 period_counter: u8,
2888 base_url: Url
2889 ) -> Result<PeriodOutputs, DashMpdError>
2890{
2891 let mut fragments = Vec::new();
2892 let mut diagnostics = Vec::new();
2893 let mut period_duration_secs: f64 = 0.0;
2894 let mut opt_init: Option<String> = None;
2895 let mut opt_media: Option<String> = None;
2896 let mut opt_duration: Option<f64> = None;
2897 let mut timescale = 1;
2898 let mut start_number = 1;
2899 if let Some(d) = mpd.mediaPresentationDuration {
2900 period_duration_secs = d.as_secs_f64();
2901 }
2902 if let Some(d) = period.duration {
2903 period_duration_secs = d.as_secs_f64();
2904 }
2905 if let Some(s) = downloader.force_duration {
2906 period_duration_secs = s;
2907 }
2908 if let Some(st) = &period.SegmentTemplate {
2912 if let Some(i) = &st.initialization {
2913 opt_init = Some(i.clone());
2914 }
2915 if let Some(m) = &st.media {
2916 opt_media = Some(m.clone());
2917 }
2918 if let Some(d) = st.duration {
2919 opt_duration = Some(d);
2920 }
2921 if let Some(ts) = st.timescale {
2922 timescale = ts;
2923 }
2924 if let Some(s) = st.startNumber {
2925 start_number = s;
2926 }
2927 }
2928 let video_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2945 .filter(is_video_adaptation)
2946 .collect();
2947 let representations: Vec<&Representation> = select_preferred_adaptations(video_adaptations, downloader)
2948 .iter()
2949 .flat_map(|a| a.representations.iter())
2950 .collect();
2951 trace!("Before filtering we have {} Representations", representations.len());
2952 let representations = representation_filter_video_id(representations, downloader);
2953 trace!("After video_id filter we have {} Representations", representations.len());
2954 let representations = representation_filter_video_width(representations, downloader);
2955 trace!("After width filter we have {} Representations", representations.len());
2956 let representations = representation_filter_video_height(representations, downloader);
2957 trace!("After height filter we have {} Representations", representations.len());
2958 let representations = representation_filter_video_codec(representations, downloader);
2959 trace!("After video codec filter we have {} Representations", representations.len());
2960 let representations = representation_filter_video_quality(representations, downloader);
2961 trace!("After quality filter we have {} Representations", representations.len());
2962 if let Some(video_repr) = representations.first() {
2963 let video_adaptation = period.adaptations.iter()
2967 .find(|a| a.representations.iter().any(|r| r.eq(video_repr)))
2968 .unwrap();
2969 let mut base_url = base_url.clone();
2972 if let Some(bu) = &video_adaptation.BaseURL.first() {
2973 base_url = merge_baseurls(&base_url, &bu.base)?;
2974 }
2975 if let Some(bu) = &video_repr.BaseURL.first() {
2976 base_url = merge_baseurls(&base_url, &bu.base)?;
2977 }
2978 if downloader.verbosity > 0 {
2979 let bw = if let Some(bw) = video_repr.bandwidth.or(video_adaptation.maxBandwidth) {
2980 format!("bw={} Kbps ", bw / 1024)
2981 } else {
2982 String::from("")
2983 };
2984 let unknown = String::from("?");
2985 let w = video_repr.width.unwrap_or(video_adaptation.width.unwrap_or(0));
2986 let h = video_repr.height.unwrap_or(video_adaptation.height.unwrap_or(0));
2987 let fmt = if w == 0 || h == 0 {
2988 String::from("")
2989 } else {
2990 format!("resolution={w}x{h} ")
2991 };
2992 let codec = video_repr.codecs.as_ref()
2993 .unwrap_or(video_adaptation.codecs.as_ref().unwrap_or(&unknown));
2994 let maybe_id = if let Some(rid) = &video_repr.id {
2995 format!(" (id={rid})")
2996 } else {
2997 String::from("")
2998 };
2999 diagnostics.push(format!(" Video stream selected: {bw}{fmt}codec={codec}{maybe_id}"));
3000 for cp in video_repr.ContentProtection.iter()
3002 .chain(video_adaptation.ContentProtection.iter())
3003 {
3004 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
3005 if let Some(kid) = &cp.default_KID {
3006 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
3007 }
3008 for pssh_element in &cp.cenc_pssh {
3009 if let Some(pssh_b64) = &pssh_element.content {
3010 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
3011 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
3012 diagnostics.push(format!(" {pssh}"));
3013 }
3014 }
3015 }
3016 }
3017 }
3018 let mut dict = HashMap::new();
3019 if let Some(rid) = &video_repr.id {
3020 dict.insert("RepresentationID", rid.clone());
3021 }
3022 if let Some(b) = &video_repr.bandwidth {
3023 dict.insert("Bandwidth", b.to_string());
3024 }
3025 if let Some(st) = &video_adaptation.SegmentTemplate {
3030 if let Some(i) = &st.initialization {
3031 opt_init = Some(i.clone());
3032 }
3033 if let Some(m) = &st.media {
3034 opt_media = Some(m.clone());
3035 }
3036 if let Some(d) = st.duration {
3037 opt_duration = Some(d);
3038 }
3039 if let Some(ts) = st.timescale {
3040 timescale = ts;
3041 }
3042 if let Some(s) = st.startNumber {
3043 start_number = s;
3044 }
3045 }
3046 if let Some(sl) = &video_adaptation.SegmentList {
3050 if downloader.verbosity > 1 {
3052 info!(" Using AdaptationSet>SegmentList addressing mode for video representation");
3053 }
3054 let mut start_byte: Option<u64> = None;
3055 let mut end_byte: Option<u64> = None;
3056 if let Some(init) = &sl.Initialization {
3057 if let Some(range) = &init.range {
3058 let (s, e) = parse_range(range)?;
3059 start_byte = Some(s);
3060 end_byte = Some(e);
3061 }
3062 if let Some(su) = &init.sourceURL {
3063 let path = resolve_url_template(su, &dict);
3064 let u = merge_baseurls(&base_url, &path)?;
3065 let mf = MediaFragmentBuilder::new(period_counter, u)
3066 .with_range(start_byte, end_byte)
3067 .set_init()
3068 .build();
3069 fragments.push(mf);
3070 }
3071 } else {
3072 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3073 .with_range(start_byte, end_byte)
3074 .set_init()
3075 .build();
3076 fragments.push(mf);
3077 }
3078 for su in &sl.segment_urls {
3079 start_byte = None;
3080 end_byte = None;
3081 if let Some(range) = &su.mediaRange {
3083 let (s, e) = parse_range(range)?;
3084 start_byte = Some(s);
3085 end_byte = Some(e);
3086 }
3087 if let Some(m) = &su.media {
3088 let u = merge_baseurls(&base_url, m)?;
3089 let mf = MediaFragmentBuilder::new(period_counter, u)
3090 .with_range(start_byte, end_byte)
3091 .build();
3092 fragments.push(mf);
3093 } else if let Some(bu) = video_adaptation.BaseURL.first() {
3094 let u = merge_baseurls(&base_url, &bu.base)?;
3095 let mf = MediaFragmentBuilder::new(period_counter, u)
3096 .with_range(start_byte, end_byte)
3097 .build();
3098 fragments.push(mf);
3099 }
3100 }
3101 }
3102 if let Some(sl) = &video_repr.SegmentList {
3103 if downloader.verbosity > 1 {
3105 info!(" Using Representation>SegmentList addressing mode for video representation");
3106 }
3107 let mut start_byte: Option<u64> = None;
3108 let mut end_byte: Option<u64> = None;
3109 if let Some(init) = &sl.Initialization {
3110 if let Some(range) = &init.range {
3111 let (s, e) = parse_range(range)?;
3112 start_byte = Some(s);
3113 end_byte = Some(e);
3114 }
3115 if let Some(su) = &init.sourceURL {
3116 let path = resolve_url_template(su, &dict);
3117 let u = merge_baseurls(&base_url, &path)?;
3118 let mf = MediaFragmentBuilder::new(period_counter, u)
3119 .with_range(start_byte, end_byte)
3120 .set_init()
3121 .build();
3122 fragments.push(mf);
3123 } else {
3124 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3125 .with_range(start_byte, end_byte)
3126 .set_init()
3127 .build();
3128 fragments.push(mf);
3129 }
3130 }
3131 for su in &sl.segment_urls {
3132 start_byte = None;
3133 end_byte = None;
3134 if let Some(range) = &su.mediaRange {
3136 let (s, e) = parse_range(range)?;
3137 start_byte = Some(s);
3138 end_byte = Some(e);
3139 }
3140 if let Some(m) = &su.media {
3141 let u = merge_baseurls(&base_url, m)?;
3142 let mf = MediaFragmentBuilder::new(period_counter, u)
3143 .with_range(start_byte, end_byte)
3144 .build();
3145 fragments.push(mf);
3146 } else if let Some(bu) = video_repr.BaseURL.first() {
3147 let u = merge_baseurls(&base_url, &bu.base)?;
3148 let mf = MediaFragmentBuilder::new(period_counter, u)
3149 .with_range(start_byte, end_byte)
3150 .build();
3151 fragments.push(mf);
3152 }
3153 }
3154 } else if video_repr.SegmentTemplate.is_some() ||
3155 video_adaptation.SegmentTemplate.is_some() {
3156 let st;
3159 if let Some(it) = &video_repr.SegmentTemplate {
3160 st = it;
3161 } else if let Some(it) = &video_adaptation.SegmentTemplate {
3162 st = it;
3163 } else {
3164 panic!("impossible");
3165 }
3166 if let Some(i) = &st.initialization {
3167 opt_init = Some(i.clone());
3168 }
3169 if let Some(m) = &st.media {
3170 opt_media = Some(m.clone());
3171 }
3172 if let Some(ts) = st.timescale {
3173 timescale = ts;
3174 }
3175 if let Some(sn) = st.startNumber {
3176 start_number = sn;
3177 }
3178 if let Some(stl) = &video_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
3179 .or(video_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
3180 {
3181 if downloader.verbosity > 1 {
3183 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for video representation");
3184 }
3185 if let Some(init) = opt_init {
3186 let path = resolve_url_template(&init, &dict);
3187 let u = merge_baseurls(&base_url, &path)?;
3188 let mf = MediaFragmentBuilder::new(period_counter, u)
3189 .set_init()
3190 .build();
3191 fragments.push(mf);
3192 }
3193 let mut elapsed_seconds = 0.0;
3194 if let Some(media) = opt_media {
3195 let video_path = resolve_url_template(&media, &dict);
3196 let mut segment_time = 0;
3197 let mut segment_duration;
3198 let mut number = start_number;
3199 let mut target_duration = period_duration_secs;
3200 if let Some(target) = downloader.force_duration {
3201 if target > period_duration_secs {
3202 warn!(" Requested forced duration exceeds available content");
3203 } else {
3204 target_duration = target;
3205 }
3206 }
3207 'segment_loop: for s in &stl.segments {
3208 if let Some(t) = s.t {
3209 segment_time = t;
3210 }
3211 segment_duration = s.d;
3212 let dict = HashMap::from([("Time", segment_time.to_string()),
3214 ("Number", number.to_string())]);
3215 let path = resolve_url_template(&video_path, &dict);
3216 let u = merge_baseurls(&base_url, &path)?;
3217 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3218 fragments.push(mf);
3219 number += 1;
3220 elapsed_seconds += segment_duration as f64 / timescale as f64;
3221 if downloader.force_duration.is_some() &&
3222 target_duration > 0.0 &&
3223 elapsed_seconds > target_duration
3224 {
3225 break 'segment_loop;
3226 }
3227 if let Some(r) = s.r {
3228 let mut count = 0i64;
3229 loop {
3230 count += 1;
3231 if r >= 0 && count > r {
3237 break;
3238 }
3239 if downloader.force_duration.is_some() &&
3240 target_duration > 0.0 &&
3241 elapsed_seconds > target_duration
3242 {
3243 break 'segment_loop;
3244 }
3245 if let Some(end_number) = st.endNumber {
3246 if count as u64 > end_number {
3247 break;
3248 }
3249 }
3250 segment_time += segment_duration;
3251 elapsed_seconds += segment_duration as f64 / timescale as f64;
3252 let dict = HashMap::from([("Time", segment_time.to_string()),
3253 ("Number", number.to_string())]);
3254 let path = resolve_url_template(&video_path, &dict);
3255 let u = merge_baseurls(&base_url, &path)?;
3256 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3257 fragments.push(mf);
3258 number += 1;
3259 }
3260 }
3261 segment_time += segment_duration;
3262 }
3263 } else {
3264 return Err(DashMpdError::UnhandledMediaStream(
3265 "SegmentTimeline without a media attribute".to_string()));
3266 }
3267 } else { if downloader.verbosity > 1 {
3270 info!(" Using SegmentTemplate addressing mode for video representation");
3271 }
3272 let mut total_number = 0i64;
3273 if let Some(init) = opt_init {
3274 let path = resolve_url_template(&init, &dict);
3275 let u = merge_baseurls(&base_url, &path)?;
3276 let mf = MediaFragmentBuilder::new(period_counter, u)
3277 .set_init()
3278 .build();
3279 fragments.push(mf);
3280 }
3281 if let Some(media) = opt_media {
3282 let video_path = resolve_url_template(&media, &dict);
3283 let timescale = st.timescale.unwrap_or(timescale);
3284 let mut segment_duration: f64 = -1.0;
3285 if let Some(d) = opt_duration {
3286 segment_duration = d;
3288 }
3289 if let Some(std) = st.duration {
3290 if timescale == 0 {
3291 return Err(DashMpdError::UnhandledMediaStream(
3292 "SegmentTemplate@duration attribute cannot be zero".to_string()));
3293 }
3294 segment_duration = std / timescale as f64;
3295 }
3296 if segment_duration < 0.0 {
3297 return Err(DashMpdError::UnhandledMediaStream(
3298 "Video representation is missing SegmentTemplate@duration attribute".to_string()));
3299 }
3300 total_number += (period_duration_secs / segment_duration).round() as i64;
3301 let mut number = start_number;
3302 if mpd_is_dynamic(mpd) {
3312 if let Some(start_time) = mpd.availabilityStartTime {
3313 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
3314 number = (elapsed + number as f64 - 1f64).floor() as u64;
3315 } else {
3316 return Err(DashMpdError::UnhandledMediaStream(
3317 "dynamic manifest is missing @availabilityStartTime".to_string()));
3318 }
3319 }
3320 if let Some(end_number) = st.endNumber {
3321 total_number = end_number as i64;
3322 }
3323 for _ in 1..=total_number {
3324 let dict = HashMap::from([("Number", number.to_string())]);
3325 let path = resolve_url_template(&video_path, &dict);
3326 let u = merge_baseurls(&base_url, &path)?;
3327 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3328 fragments.push(mf);
3329 number += 1;
3330 }
3331 }
3332 }
3333 } else if let Some(sb) = &video_repr.SegmentBase {
3334 if downloader.verbosity > 1 {
3336 info!(" Using SegmentBase@indexRange addressing mode for video representation");
3337 }
3338 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
3339 fragments.extend(mf);
3340 } else if fragments.is_empty() {
3341 if let Some(bu) = video_repr.BaseURL.first() {
3342 if downloader.verbosity > 1 {
3344 info!(" Using BaseURL addressing mode for video representation");
3345 }
3346 let u = merge_baseurls(&base_url, &bu.base)?;
3347 let mf = MediaFragmentBuilder::new(period_counter, u)
3348 .with_timeout(Duration::new(10000, 0))
3349 .build();
3350 fragments.push(mf);
3351 }
3352 }
3353 if fragments.is_empty() {
3354 return Err(DashMpdError::UnhandledMediaStream(
3355 "no usable addressing mode identified for video representation".to_string()));
3356 }
3357 }
3358 Ok(PeriodOutputs {
3361 fragments,
3362 diagnostics,
3363 subtitle_formats: Vec::new(),
3364 selected_audio_language: String::from("unk"),
3365 selected_subtitle_language: String::from(""),
3366 })
3367}
3368
3369#[tracing::instrument(level="trace", skip_all)]
3370async fn do_period_subtitles(
3371 downloader: &DashDownloader,
3372 mpd: &MPD,
3373 period: &Period,
3374 period_counter: u8,
3375 base_url: Url
3376 ) -> Result<PeriodOutputs, DashMpdError>
3377{
3378 let client = downloader.http_client.as_ref()
3379 .ok_or_else(|| DashMpdError::Other(String::from("no HTTP client specified")))?;
3380 let output_path = &downloader.output_path.as_ref()
3381 .ok_or_else(|| DashMpdError::Other(String::from("no output_path set")))?
3382 .clone();
3383 let period_output_path = output_path_for_period(output_path, period_counter);
3384 let mut fragments = Vec::new();
3385 let mut subtitle_formats = Vec::new();
3386 let mut period_duration_secs: f64 = 0.0;
3387 if let Some(d) = mpd.mediaPresentationDuration {
3388 period_duration_secs = d.as_secs_f64();
3389 }
3390 if let Some(d) = period.duration {
3391 period_duration_secs = d.as_secs_f64();
3392 }
3393 let maybe_subtitle_adaptation = if let Some(ref lang) = downloader.language_preference_subtitles {
3394 period.adaptations.iter().filter(is_subtitle_adaptation)
3395 .min_by_key(|a| adaptation_lang_distance(a, lang))
3396 } else {
3397 period.adaptations.iter().find(is_subtitle_adaptation)
3399 };
3400 let mut subtitle_lang: Option<String> = None;
3401 if downloader.fetch_subtitles {
3402 if let Some(subtitle_adaptation) = maybe_subtitle_adaptation {
3403 if let Some(lang) = subtitle_adaptation.lang.as_ref() {
3404 subtitle_lang = Some(lang.clone());
3405 }
3406 let subtitle_format = subtitle_type(&subtitle_adaptation);
3407 subtitle_formats.push(subtitle_format);
3408 if downloader.verbosity > 1 && downloader.fetch_subtitles {
3409 info!(" Retrieving subtitles in format {subtitle_format:?}");
3410 }
3411 let mut base_url = base_url.clone();
3414 if let Some(bu) = &subtitle_adaptation.BaseURL.first() {
3415 base_url = merge_baseurls(&base_url, &bu.base)?;
3416 }
3417 if let Some(rep) = subtitle_adaptation.representations.first() {
3420 if subtitle_lang.is_none() {
3421 if let Some(lang) = rep.lang.as_ref() {
3422 subtitle_lang = Some(lang.clone());
3423 }
3424 }
3425 if !rep.BaseURL.is_empty() {
3426 for st_bu in &rep.BaseURL {
3427 let st_url = merge_baseurls(&base_url, &st_bu.base)?;
3428 let mut req = client.get(st_url.clone());
3429 if let Some(referer) = &downloader.referer {
3430 req = req.header("Referer", referer);
3431 } else {
3432 req = req.header("Referer", base_url.to_string());
3433 }
3434 let rqw = req.build()
3435 .map_err(|e| network_error("building request", &e))?;
3436 let subs = reqwest_bytes_with_retries(client, rqw, 5).await
3437 .map_err(|e| network_error("fetching subtitles", &e))?;
3438 let mut subs_path = period_output_path.clone();
3439 let subtitle_format = subtitle_type(&subtitle_adaptation);
3440 match subtitle_format {
3441 SubtitleType::Vtt => subs_path.set_extension("vtt"),
3442 SubtitleType::Srt => subs_path.set_extension("srt"),
3443 SubtitleType::Sami => subs_path.set_extension("sami"),
3444 SubtitleType::Wvtt => subs_path.set_extension("wvtt"),
3445 SubtitleType::Ttml | SubtitleType::Stpp => subs_path.set_extension("ttml"),
3446 _ => subs_path.set_extension("sub"),
3447 };
3448 subtitle_formats.push(subtitle_format);
3449 let mut subs_file = File::create(&subs_path).await
3450 .map_err(|e| DashMpdError::Io(e, String::from("creating subtitle file")))?;
3451 if downloader.verbosity > 2 {
3452 info!(" Subtitle {st_url} -> {} octets", subs.len());
3453 }
3454 match subs_file.write_all(&subs).await {
3455 Ok(()) => {
3456 if downloader.verbosity > 0 {
3457 info!(" Downloaded subtitles ({subtitle_format:?}) to {}",
3458 subs_path.display());
3459 }
3460 },
3461 Err(e) => {
3462 error!("Unable to write subtitle file: {e:?}");
3463 return Err(DashMpdError::Io(e, String::from("writing subtitle data")));
3464 },
3465 }
3466 if subtitle_formats.contains(&SubtitleType::Ttml) {
3467 if let Err(e) = convert_ttml_srt(downloader, &subs_path).await {
3468 warn!("Error while converting TTML subtitles to SubRip: {e}");
3469 }
3470 }
3471 if subtitle_formats.contains(&SubtitleType::Vtt) {
3472 if let Err(e) = convert_vtt_srt(downloader, &subs_path).await {
3473 warn!("Error while converting VTT subtitles to SubRip: {e}");
3474 }
3475 }
3476 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
3477 subtitle_formats.contains(&SubtitleType::Ttxt)
3478 {
3479 if let Err(e) = wvtt_extract(downloader, &subs_path).await {
3480 warn!("Error while extracting wvtt subtitles: {e}");
3481 }
3482 }
3483 }
3484 } else if rep.SegmentTemplate.is_some() || subtitle_adaptation.SegmentTemplate.is_some() {
3485 let mut opt_init: Option<String> = None;
3486 let mut opt_media: Option<String> = None;
3487 let mut opt_duration: Option<f64> = None;
3488 let mut timescale = 1;
3489 let mut start_number = 1;
3490 if let Some(st) = &rep.SegmentTemplate {
3495 if let Some(i) = &st.initialization {
3496 opt_init = Some(i.clone());
3497 }
3498 if let Some(m) = &st.media {
3499 opt_media = Some(m.clone());
3500 }
3501 if let Some(d) = st.duration {
3502 opt_duration = Some(d);
3503 }
3504 if let Some(ts) = st.timescale {
3505 timescale = ts;
3506 }
3507 if let Some(s) = st.startNumber {
3508 start_number = s;
3509 }
3510 }
3511 let rid = match &rep.id {
3512 Some(id) => id,
3513 None => return Err(
3514 DashMpdError::UnhandledMediaStream(
3515 "Missing @id on Representation node".to_string())),
3516 };
3517 let mut dict = HashMap::from([("RepresentationID", rid.clone())]);
3518 if let Some(b) = &rep.bandwidth {
3519 dict.insert("Bandwidth", b.to_string());
3520 }
3521 if let Some(sl) = &rep.SegmentList {
3525 if downloader.verbosity > 1 {
3528 info!(" Using AdaptationSet>SegmentList addressing mode for subtitle representation");
3529 }
3530 let mut start_byte: Option<u64> = None;
3531 let mut end_byte: Option<u64> = None;
3532 if let Some(init) = &sl.Initialization {
3533 if let Some(range) = &init.range {
3534 let (s, e) = parse_range(range)?;
3535 start_byte = Some(s);
3536 end_byte = Some(e);
3537 }
3538 if let Some(su) = &init.sourceURL {
3539 let path = resolve_url_template(su, &dict);
3540 let u = merge_baseurls(&base_url, &path)?;
3541 let mf = MediaFragmentBuilder::new(period_counter, u)
3542 .with_range(start_byte, end_byte)
3543 .set_init()
3544 .build();
3545 fragments.push(mf);
3546 } else {
3547 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3548 .with_range(start_byte, end_byte)
3549 .set_init()
3550 .build();
3551 fragments.push(mf);
3552 }
3553 }
3554 for su in &sl.segment_urls {
3555 start_byte = None;
3556 end_byte = None;
3557 if let Some(range) = &su.mediaRange {
3559 let (s, e) = parse_range(range)?;
3560 start_byte = Some(s);
3561 end_byte = Some(e);
3562 }
3563 if let Some(m) = &su.media {
3564 let u = merge_baseurls(&base_url, m)?;
3565 let mf = MediaFragmentBuilder::new(period_counter, u)
3566 .with_range(start_byte, end_byte)
3567 .build();
3568 fragments.push(mf);
3569 } else if let Some(bu) = subtitle_adaptation.BaseURL.first() {
3570 let u = merge_baseurls(&base_url, &bu.base)?;
3571 let mf = MediaFragmentBuilder::new(period_counter, u)
3572 .with_range(start_byte, end_byte)
3573 .build();
3574 fragments.push(mf);
3575 }
3576 }
3577 }
3578 if let Some(sl) = &rep.SegmentList {
3579 if downloader.verbosity > 1 {
3581 info!(" Using Representation>SegmentList addressing mode for subtitle representation");
3582 }
3583 let mut start_byte: Option<u64> = None;
3584 let mut end_byte: Option<u64> = None;
3585 if let Some(init) = &sl.Initialization {
3586 if let Some(range) = &init.range {
3587 let (s, e) = parse_range(range)?;
3588 start_byte = Some(s);
3589 end_byte = Some(e);
3590 }
3591 if let Some(su) = &init.sourceURL {
3592 let path = resolve_url_template(su, &dict);
3593 let u = merge_baseurls(&base_url, &path)?;
3594 let mf = MediaFragmentBuilder::new(period_counter, u)
3595 .with_range(start_byte, end_byte)
3596 .set_init()
3597 .build();
3598 fragments.push(mf);
3599 } else {
3600 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3601 .with_range(start_byte, end_byte)
3602 .set_init()
3603 .build();
3604 fragments.push(mf);
3605 }
3606 }
3607 for su in &sl.segment_urls {
3608 start_byte = None;
3609 end_byte = None;
3610 if let Some(range) = &su.mediaRange {
3612 let (s, e) = parse_range(range)?;
3613 start_byte = Some(s);
3614 end_byte = Some(e);
3615 }
3616 if let Some(m) = &su.media {
3617 let u = merge_baseurls(&base_url, m)?;
3618 let mf = MediaFragmentBuilder::new(period_counter, u)
3619 .with_range(start_byte, end_byte)
3620 .build();
3621 fragments.push(mf);
3622 } else if let Some(bu) = &rep.BaseURL.first() {
3623 let u = merge_baseurls(&base_url, &bu.base)?;
3624 let mf = MediaFragmentBuilder::new(period_counter, u)
3625 .with_range(start_byte, end_byte)
3626 .build();
3627 fragments.push(mf);
3628 }
3629 }
3630 } else if rep.SegmentTemplate.is_some() ||
3631 subtitle_adaptation.SegmentTemplate.is_some()
3632 {
3633 let st;
3636 if let Some(it) = &rep.SegmentTemplate {
3637 st = it;
3638 } else if let Some(it) = &subtitle_adaptation.SegmentTemplate {
3639 st = it;
3640 } else {
3641 panic!("unreachable");
3642 }
3643 if let Some(i) = &st.initialization {
3644 opt_init = Some(i.clone());
3645 }
3646 if let Some(m) = &st.media {
3647 opt_media = Some(m.clone());
3648 }
3649 if let Some(ts) = st.timescale {
3650 timescale = ts;
3651 }
3652 if let Some(sn) = st.startNumber {
3653 start_number = sn;
3654 }
3655 if let Some(stl) = &rep.SegmentTemplate.as_ref()
3656 .and_then(|st| st.SegmentTimeline.clone())
3657 .or(subtitle_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
3658 {
3659 if downloader.verbosity > 1 {
3662 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for subtitle representation");
3663 }
3664 if let Some(init) = opt_init {
3665 let path = resolve_url_template(&init, &dict);
3666 let u = merge_baseurls(&base_url, &path)?;
3667 let mf = MediaFragmentBuilder::new(period_counter, u)
3668 .set_init()
3669 .build();
3670 fragments.push(mf);
3671 }
3672 if let Some(media) = opt_media {
3673 let sub_path = resolve_url_template(&media, &dict);
3674 let mut segment_time = 0;
3675 let mut segment_duration;
3676 let mut number = start_number;
3677 for s in &stl.segments {
3678 if let Some(t) = s.t {
3679 segment_time = t;
3680 }
3681 segment_duration = s.d;
3682 let dict = HashMap::from([("Time", segment_time.to_string()),
3684 ("Number", number.to_string())]);
3685 let path = resolve_url_template(&sub_path, &dict);
3686 let u = merge_baseurls(&base_url, &path)?;
3687 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3688 fragments.push(mf);
3689 number += 1;
3690 if let Some(r) = s.r {
3691 let mut count = 0i64;
3692 let end_time = period_duration_secs * timescale as f64;
3694 loop {
3695 count += 1;
3696 if r >= 0 {
3702 if count > r {
3703 break;
3704 }
3705 if downloader.force_duration.is_some() &&
3706 segment_time as f64 > end_time
3707 {
3708 break;
3709 }
3710 } else if segment_time as f64 > end_time {
3711 break;
3712 }
3713 if let Some(end_number) = st.endNumber {
3714 if count as u64 > end_number {
3715 break;
3716 }
3717 }
3718 segment_time += segment_duration;
3719 let dict = HashMap::from([("Time", segment_time.to_string()),
3720 ("Number", number.to_string())]);
3721 let path = resolve_url_template(&sub_path, &dict);
3722 let u = merge_baseurls(&base_url, &path)?;
3723 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3724 fragments.push(mf);
3725 number += 1;
3726 }
3727 }
3728 segment_time += segment_duration;
3729 }
3730 } else {
3731 return Err(DashMpdError::UnhandledMediaStream(
3732 "SegmentTimeline without a media attribute".to_string()));
3733 }
3734 } else { if downloader.verbosity > 0 {
3739 info!(" Using SegmentTemplate addressing mode for stpp subtitles");
3740 }
3741 if let Some(i) = &st.initialization {
3742 opt_init = Some(i.clone());
3743 }
3744 if let Some(m) = &st.media {
3745 opt_media = Some(m.clone());
3746 }
3747 if let Some(d) = st.duration {
3748 opt_duration = Some(d);
3749 }
3750 if let Some(ts) = st.timescale {
3751 timescale = ts;
3752 }
3753 if let Some(s) = st.startNumber {
3754 start_number = s;
3755 }
3756 let rid = match &rep.id {
3757 Some(id) => id,
3758 None => return Err(
3759 DashMpdError::UnhandledMediaStream(
3760 "Missing @id on Representation node".to_string())),
3761 };
3762 let mut dict = HashMap::from([("RepresentationID", rid.clone())]);
3763 if let Some(b) = &rep.bandwidth {
3764 dict.insert("Bandwidth", b.to_string());
3765 }
3766 let mut total_number = 0i64;
3767 if let Some(init) = opt_init {
3768 let path = resolve_url_template(&init, &dict);
3769 let u = merge_baseurls(&base_url, &path)?;
3770 let mf = MediaFragmentBuilder::new(period_counter, u)
3771 .set_init()
3772 .build();
3773 fragments.push(mf);
3774 }
3775 if let Some(media) = opt_media {
3776 let sub_path = resolve_url_template(&media, &dict);
3777 let mut segment_duration: f64 = -1.0;
3778 if let Some(d) = opt_duration {
3779 segment_duration = d;
3781 }
3782 if let Some(std) = st.duration {
3783 if timescale == 0 {
3784 return Err(DashMpdError::UnhandledMediaStream(
3785 "SegmentTemplate@duration attribute cannot be zero".to_string()));
3786 }
3787 segment_duration = std / timescale as f64;
3788 }
3789 if segment_duration < 0.0 {
3790 return Err(DashMpdError::UnhandledMediaStream(
3791 "Subtitle representation is missing SegmentTemplate@duration".to_string()));
3792 }
3793 total_number += (period_duration_secs / segment_duration).ceil() as i64;
3794 if let Some(end_number) = st.endNumber {
3795 total_number = end_number as i64;
3796 }
3797 let mut number = start_number;
3798 #[allow(clippy::explicit_counter_loop)]
3799 for _ in 1..=total_number {
3800 let dict = HashMap::from([("Number", number.to_string())]);
3801 let path = resolve_url_template(&sub_path, &dict);
3802 let u = merge_baseurls(&base_url, &path)?;
3803 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3804 fragments.push(mf);
3805 number += 1;
3806 }
3807 }
3808 }
3809 } else if let Some(sb) = &rep.SegmentBase {
3810 info!(" Using SegmentBase@indexRange for subs");
3812 if downloader.verbosity > 1 {
3813 info!(" Using SegmentBase@indexRange addressing mode for subtitle representation");
3814 }
3815 let mut start_byte: Option<u64> = None;
3816 let mut end_byte: Option<u64> = None;
3817 if let Some(init) = &sb.Initialization {
3818 if let Some(range) = &init.range {
3819 let (s, e) = parse_range(range)?;
3820 start_byte = Some(s);
3821 end_byte = Some(e);
3822 }
3823 if let Some(su) = &init.sourceURL {
3824 let path = resolve_url_template(su, &dict);
3825 let u = merge_baseurls(&base_url, &path)?;
3826 let mf = MediaFragmentBuilder::new(period_counter, u)
3827 .with_range(start_byte, end_byte)
3828 .set_init()
3829 .build();
3830 fragments.push(mf);
3831 }
3832 }
3833 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3834 .set_init()
3835 .build();
3836 fragments.push(mf);
3837 }
3840 }
3841 }
3842 }
3843 }
3844 Ok(PeriodOutputs {
3845 fragments,
3846 diagnostics: Vec::new(),
3847 subtitle_formats,
3848 selected_audio_language: String::from("unk"),
3849 selected_subtitle_language: subtitle_lang.unwrap_or_else(|| String::from("unk")),
3850 })
3851}
3852
3853
3854struct DownloadState {
3857 period_counter: u8,
3858 segment_count: usize,
3859 segment_counter: usize,
3860 download_errors: u32
3861}
3862
3863#[tracing::instrument(level="trace", skip_all)]
3870async fn fetch_fragment(
3871 downloader: &mut DashDownloader,
3872 frag: &MediaFragment,
3873 fragment_type: &str,
3874 progress_percent: u32) -> Result<File, DashMpdError>
3875{
3876 let send_request = || async {
3877 trace!("send_request {}", frag.url.clone());
3878 let mut req = downloader.http_client.as_ref().unwrap()
3881 .get(frag.url.clone())
3882 .header("Accept", format!("{fragment_type}/*;q=0.9,*/*;q=0.5"))
3883 .header("Sec-Fetch-Mode", "navigate");
3884 if let Some(sb) = &frag.start_byte {
3885 if let Some(eb) = &frag.end_byte {
3886 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
3887 }
3888 }
3889 if let Some(ts) = &frag.timeout {
3890 req = req.timeout(*ts);
3891 }
3892 if let Some(referer) = &downloader.referer {
3893 req = req.header("Referer", referer);
3894 } else {
3895 req = req.header("Referer", downloader.redirected_url.to_string());
3896 }
3897 if let Some(username) = &downloader.auth_username {
3898 if let Some(password) = &downloader.auth_password {
3899 req = req.basic_auth(username, Some(password));
3900 }
3901 }
3902 if let Some(token) = &downloader.auth_bearer_token {
3903 req = req.bearer_auth(token);
3904 }
3905 req.send().await?
3906 .error_for_status()
3907 };
3908 match send_request
3909 .retry(ExponentialBuilder::default())
3910 .when(reqwest_error_transient_p)
3911 .notify(notify_transient)
3912 .await
3913 {
3914 Ok(response) => {
3915 match response.error_for_status() {
3916 Ok(mut resp) => {
3917 let tmp_out_std = tempfile::tempfile()
3918 .map_err(|e| DashMpdError::Io(e, String::from("creating tmpfile for fragment")))?;
3919 let mut tmp_out = tokio::fs::File::from_std(tmp_out_std);
3920 let content_type_checker = if fragment_type.eq("audio") {
3921 content_type_audio_p
3922 } else if fragment_type.eq("video") {
3923 content_type_video_p
3924 } else {
3925 panic!("fragment_type not audio or video");
3926 };
3927 if !downloader.content_type_checks || content_type_checker(&resp) {
3928 let mut fragment_out: Option<File> = None;
3929 if let Some(ref fragment_path) = downloader.fragment_path {
3930 if let Some(path) = frag.url.path_segments()
3931 .unwrap_or_else(|| "".split(' '))
3932 .next_back()
3933 {
3934 let vf_file = fragment_path.clone().join(fragment_type).join(path);
3935 if let Ok(f) = File::create(vf_file).await {
3936 fragment_out = Some(f);
3937 }
3938 }
3939 }
3940 let mut segment_size = 0;
3941 while let Some(chunk) = resp.chunk().await
3947 .map_err(|e| network_error(&format!("fetching DASH {fragment_type} segment"), &e))?
3948 {
3949 segment_size += chunk.len();
3950 downloader.bw_estimator_bytes += chunk.len();
3951 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
3952 throttle_download_rate(downloader, size).await?;
3953 if let Err(e) = tmp_out.write_all(&chunk).await {
3954 return Err(DashMpdError::Io(e, format!("writing DASH {fragment_type} data")));
3955 }
3956 if let Some(ref mut fout) = fragment_out {
3957 fout.write_all(&chunk)
3958 .map_err(|e| DashMpdError::Io(e, format!("writing {fragment_type} fragment")))
3959 .await?;
3960 }
3961 let elapsed = downloader.bw_estimator_started.elapsed().as_secs_f64();
3962 if (elapsed > 0.5) || (downloader.bw_estimator_bytes > 50_000) {
3963 let bw = downloader.bw_estimator_bytes as f64 / elapsed;
3964 for observer in &downloader.progress_observers {
3965 observer.update(progress_percent, bw as u64, &format!("Fetching {fragment_type} segments"));
3966 }
3967 downloader.bw_estimator_started = Instant::now();
3968 downloader.bw_estimator_bytes = 0;
3969 }
3970 }
3971 if downloader.verbosity > 2 {
3972 if let Some(sb) = &frag.start_byte {
3973 if let Some(eb) = &frag.end_byte {
3974 info!(" {fragment_type} segment {} range {sb}-{eb} -> {} octets",
3975 frag.url, segment_size);
3976 }
3977 } else {
3978 info!(" {fragment_type} segment {} -> {segment_size} octets", &frag.url);
3979 }
3980 }
3981 } else {
3982 warn!("Ignoring segment {} with non-{fragment_type} content-type", frag.url);
3983 }
3984 tmp_out.sync_all().await
3985 .map_err(|e| DashMpdError::Io(e, format!("syncing {fragment_type} fragment")))?;
3986 Ok(tmp_out)
3987 },
3988 Err(e) => Err(network_error("HTTP error", &e)),
3989 }
3990 },
3991 Err(e) => Err(network_error(&format!("{e:?}"), &e)),
3992 }
3993}
3994
3995
3996#[tracing::instrument(level="trace", skip_all)]
3998async fn fetch_period_audio(
3999 downloader: &mut DashDownloader,
4000 tmppath: &Path,
4001 audio_fragments: &[MediaFragment],
4002 ds: &mut DownloadState) -> Result<bool, DashMpdError>
4003{
4004 let start_download = Instant::now();
4005 let mut have_audio = false;
4006 {
4007 let tmpfile_audio = File::create(tmppath).await
4011 .map_err(|e| DashMpdError::Io(e, String::from("creating audio tmpfile")))?;
4012 ensure_permissions_readable(tmppath).await?;
4013 let mut tmpfile_audio = BufWriter::new(tmpfile_audio);
4014 if let Some(ref fragment_path) = downloader.fragment_path {
4016 let audio_fragment_dir = fragment_path.join("audio");
4017 if !audio_fragment_dir.exists() {
4018 fs::create_dir_all(audio_fragment_dir).await
4019 .map_err(|e| DashMpdError::Io(e, String::from("creating audio fragment dir")))?;
4020 }
4021 }
4022 for frag in audio_fragments.iter().filter(|f| f.period == ds.period_counter) {
4026 ds.segment_counter += 1;
4027 let progress_percent = min(98, (100.0 * ds.segment_counter as f32 / (2.0 + ds.segment_count as f32)).ceil() as u32);
4030 let url = &frag.url;
4031 if url.scheme() == "data" {
4035 let us = &url.to_string();
4036 let du = DataUrl::process(us)
4037 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
4038 if du.mime_type().type_ != "audio" {
4039 return Err(DashMpdError::UnhandledMediaStream(
4040 String::from("expecting audio content in data URL")));
4041 }
4042 let (body, _fragment) = du.decode_to_vec()
4043 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
4044 if downloader.verbosity > 2 {
4045 info!(" Audio segment data URL -> {} octets", body.len());
4046 }
4047 tmpfile_audio.write_all(&body)
4048 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH audio data")))
4049 .await?;
4050 have_audio = true;
4051 } else {
4052 'done: for _ in 0..downloader.fragment_retry_count {
4054 match fetch_fragment(downloader, frag, "audio", progress_percent).await {
4055 Ok(mut frag_file) => {
4056 frag_file.rewind().await
4057 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
4058 let mut buf = Vec::new();
4059 frag_file.read_to_end(&mut buf).await
4060 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
4061 tmpfile_audio.write_all(&buf)
4062 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH audio data")))
4063 .await?;
4064 have_audio = true;
4065 break 'done;
4066 },
4067 Err(e) => {
4068 if downloader.verbosity > 0 {
4069 error!("Error fetching audio segment {url}: {e:?}");
4070 }
4071 ds.download_errors += 1;
4072 if ds.download_errors > downloader.max_error_count {
4073 error!("max_error_count network errors encountered");
4074 return Err(DashMpdError::Network(
4075 String::from("more than max_error_count network errors")));
4076 }
4077 },
4078 }
4079 info!(" Retrying audio segment {url}");
4080 if downloader.sleep_between_requests > 0 {
4081 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
4082 }
4083 }
4084 }
4085 }
4086 tmpfile_audio.flush().map_err(|e| {
4087 error!("Couldn't flush DASH audio file: {e}");
4088 DashMpdError::Io(e, String::from("flushing DASH audio file"))
4089 }).await?;
4090 } if !downloader.decryption_keys.is_empty() {
4092 if downloader.verbosity > 0 {
4093 let metadata = fs::metadata(tmppath).await
4094 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted audio metadata")))?;
4095 info!(" Attempting to decrypt audio stream ({} kB) with {}",
4096 metadata.len() / 1024,
4097 downloader.decryptor_preference);
4098 }
4099 let out_ext = downloader.output_path.as_ref()
4100 .ok_or_else(|| DashMpdError::Other(String::from("no output_path set")))?
4101 .extension()
4102 .unwrap_or(OsStr::new("mp4"));
4103 let decrypted = tmp_file_path("dashmpd-decrypted-audio", out_ext)?;
4104 if downloader.decryptor_preference.eq("mp4decrypt") {
4105 decrypt_mp4decrypt(downloader, tmppath, &decrypted, "audio").await?;
4106 } else if downloader.decryptor_preference.eq("shaka") {
4107 decrypt_shaka(downloader, tmppath, &decrypted, "audio").await?;
4108 } else if downloader.decryptor_preference.eq("shaka-container") {
4109 decrypt_shaka_container(downloader, tmppath, &decrypted, "audio").await?;
4110 } else if downloader.decryptor_preference.eq("mp4box") {
4111 decrypt_mp4box(downloader, tmppath, &decrypted, "audio").await?;
4112 } else if downloader.decryptor_preference.eq("mp4box-container") {
4113 decrypt_mp4box_container(downloader, tmppath, &decrypted, "audio").await?;
4114 } else {
4115 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
4116 }
4117 if let Err(e) = fs::metadata(&decrypted).await {
4118 return Err(DashMpdError::Decrypting(format!("missing decrypted audio file: {e:?}")));
4119 }
4120 fs::remove_file(&tmppath).await
4121 .map_err(|e| DashMpdError::Io(e, String::from("deleting encrypted audio tmpfile")))?;
4122 fs::rename(&decrypted, &tmppath).await
4123 .map_err(|e| {
4124 let dbg = Command::new("bash")
4125 .args(["-c", &format!("id;ls -l {}", decrypted.display())])
4126 .output()
4127 .unwrap();
4128 warn!("debugging ls: {}", String::from_utf8_lossy(&dbg.stdout));
4129 DashMpdError::Io(e, format!("renaming decrypted audio {}->{}", decrypted.display(), tmppath.display()))
4130 })?;
4131 }
4132 if let Ok(metadata) = fs::metadata(&tmppath).await {
4133 if downloader.verbosity > 1 {
4134 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4135 let elapsed = start_download.elapsed();
4136 info!(" Wrote {mbytes:.1}MB to DASH audio file ({:.1} MB/s)",
4137 mbytes / elapsed.as_secs_f64());
4138 }
4139 }
4140 Ok(have_audio)
4141}
4142
4143
4144#[tracing::instrument(level="trace", skip_all)]
4146async fn fetch_period_video(
4147 downloader: &mut DashDownloader,
4148 tmppath: &Path,
4149 video_fragments: &[MediaFragment],
4150 ds: &mut DownloadState) -> Result<bool, DashMpdError>
4151{
4152 let start_download = Instant::now();
4153 let mut have_video = false;
4154 {
4155 let tmpfile_video = File::create(tmppath).await
4159 .map_err(|e| DashMpdError::Io(e, String::from("creating video tmpfile")))?;
4160 ensure_permissions_readable(tmppath).await?;
4161 let mut tmpfile_video = BufWriter::new(tmpfile_video);
4162 if let Some(ref fragment_path) = downloader.fragment_path {
4164 let video_fragment_dir = fragment_path.join("video");
4165 if !video_fragment_dir.exists() {
4166 fs::create_dir_all(video_fragment_dir).await
4167 .map_err(|e| DashMpdError::Io(e, String::from("creating video fragment dir")))?;
4168 }
4169 }
4170 for frag in video_fragments.iter().filter(|f| f.period == ds.period_counter) {
4171 ds.segment_counter += 1;
4172 let progress_percent = min(98, (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32);
4175 if frag.url.scheme() == "data" {
4176 let us = &frag.url.to_string();
4177 let du = DataUrl::process(us)
4178 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
4179 if du.mime_type().type_ != "video" {
4180 return Err(DashMpdError::UnhandledMediaStream(
4181 String::from("expecting video content in data URL")));
4182 }
4183 let (body, _fragment) = du.decode_to_vec()
4184 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
4185 if downloader.verbosity > 2 {
4186 info!(" Video segment data URL -> {} octets", body.len());
4187 }
4188 tmpfile_video.write_all(&body)
4189 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH video data")))
4190 .await?;
4191 have_video = true;
4192 } else {
4193 'done: for _ in 0..downloader.fragment_retry_count {
4194 match fetch_fragment(downloader, frag, "video", progress_percent).await {
4195 Ok(mut frag_file) => {
4196 frag_file.rewind().await
4197 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
4198 let mut buf = Vec::new();
4199 frag_file.read_to_end(&mut buf).await
4200 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
4201 tmpfile_video.write_all(&buf)
4202 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH video data")))
4203 .await?;
4204 have_video = true;
4205 break 'done;
4206 },
4207 Err(e) => {
4208 if downloader.verbosity > 0 {
4209 error!(" Error fetching video segment {}: {e:?}", frag.url);
4210 }
4211 ds.download_errors += 1;
4212 if ds.download_errors > downloader.max_error_count {
4213 return Err(DashMpdError::Network(
4214 String::from("more than max_error_count network errors")));
4215 }
4216 },
4217 }
4218 info!(" Retrying video segment {}", frag.url);
4219 if downloader.sleep_between_requests > 0 {
4220 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
4221 }
4222 }
4223 }
4224 }
4225 tmpfile_video.flush().map_err(|e| {
4226 error!(" Couldn't flush video file: {e}");
4227 DashMpdError::Io(e, String::from("flushing video file"))
4228 }).await?;
4229 } if !downloader.decryption_keys.is_empty() {
4231 if downloader.verbosity > 0 {
4232 let metadata = fs::metadata(tmppath).await
4233 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted video metadata")))?;
4234 info!(" Attempting to decrypt video stream ({} kB) with {}",
4235 metadata.len() / 1024,
4236 downloader.decryptor_preference);
4237 }
4238 let out_ext = downloader.output_path.as_ref()
4239 .ok_or_else(|| DashMpdError::Other(String::from("no output_path set")))?
4240 .extension()
4241 .unwrap_or(OsStr::new("mp4"));
4242 let decrypted = tmp_file_path("dashmpd-decrypted-video", out_ext)?;
4243 if downloader.decryptor_preference.eq("mp4decrypt") {
4244 decrypt_mp4decrypt(downloader, tmppath, &decrypted, "video").await?;
4245 } else if downloader.decryptor_preference.eq("shaka") {
4246 decrypt_shaka(downloader, tmppath, &decrypted, "video").await?;
4247 } else if downloader.decryptor_preference.eq("shaka-container") {
4248 decrypt_shaka_container(downloader, tmppath, &decrypted, "video").await?;
4249 } else if downloader.decryptor_preference.eq("mp4box") {
4250 decrypt_mp4box(downloader, tmppath, &decrypted, "video").await?;
4251 } else if downloader.decryptor_preference.eq("mp4box-container") {
4252 decrypt_mp4box_container(downloader, tmppath, &decrypted, "video").await?;
4253 } else {
4254 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
4255 }
4256 if let Err(e) = fs::metadata(&decrypted).await {
4257 return Err(DashMpdError::Decrypting(format!("missing decrypted video file: {e:?}")));
4258 }
4259 fs::remove_file(&tmppath).await
4260 .map_err(|e| DashMpdError::Io(e, String::from("deleting encrypted video tmpfile")))?;
4261 fs::rename(&decrypted, &tmppath).await
4262 .map_err(|e| DashMpdError::Io(e, String::from("renaming decrypted video")))?;
4263 }
4264 if let Ok(metadata) = fs::metadata(&tmppath).await {
4265 if downloader.verbosity > 1 {
4266 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4267 let elapsed = start_download.elapsed();
4268 info!(" Wrote {mbytes:.1}MB to DASH video file ({:.1} MB/s)",
4269 mbytes / elapsed.as_secs_f64());
4270 }
4271 }
4272 Ok(have_video)
4273}
4274
4275
4276#[tracing::instrument(level="trace", skip_all)]
4278async fn fetch_period_subtitles(
4279 downloader: &DashDownloader,
4280 tmppath: &Path,
4281 subtitle_fragments: &[MediaFragment],
4282 subtitle_formats: &[SubtitleType],
4283 ds: &mut DownloadState) -> Result<bool, DashMpdError>
4284{
4285 use crate::stpp::StppDocument;
4286 use crate::vtt::VttDocument;
4287
4288 let client = downloader.http_client.clone().unwrap();
4289 let start_download = Instant::now();
4290 let mut have_subtitles = false;
4291 {
4292 let tmpfile_subs = File::create(tmppath).await
4293 .map_err(|e| DashMpdError::Io(e, String::from("creating subs tmpfile")))?;
4294 let mut stpp_document = StppDocument::new();
4296 let mut vtt_document = VttDocument::new();
4298 ensure_permissions_readable(tmppath).await?;
4299 let mut tmpfile_subs = BufWriter::new(tmpfile_subs);
4300 for frag in subtitle_fragments {
4301 ds.segment_counter += 1;
4303 let progress_percent = min(98, (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32);
4304 for observer in &downloader.progress_observers {
4305 observer.update(progress_percent, 1, "Fetching subtitle segments");
4306 }
4307 if frag.url.scheme() == "data" {
4308 let us = &frag.url.to_string();
4309 let du = DataUrl::process(us)
4310 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
4311 if du.mime_type().type_ != "video" {
4312 return Err(DashMpdError::UnhandledMediaStream(
4313 String::from("expecting video content in data URL")));
4314 }
4315 let (body, _fragment) = du.decode_to_vec()
4316 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
4317 if downloader.verbosity > 2 {
4318 info!(" Subtitle segment data URL -> {} octets", body.len());
4319 }
4320 tmpfile_subs.write_all(&body)
4321 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH subtitle data")))
4322 .await?;
4323 have_subtitles = true;
4324 } else {
4325 let fetch = || async {
4326 let mut req = client.get(frag.url.clone())
4327 .header("Sec-Fetch-Mode", "navigate");
4328 if let Some(sb) = &frag.start_byte {
4329 if let Some(eb) = &frag.end_byte {
4330 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
4331 }
4332 }
4333 if let Some(referer) = &downloader.referer {
4334 req = req.header("Referer", referer);
4335 } else {
4336 req = req.header("Referer", downloader.redirected_url.to_string());
4337 }
4338 if let Some(username) = &downloader.auth_username {
4339 if let Some(password) = &downloader.auth_password {
4340 req = req.basic_auth(username, Some(password));
4341 }
4342 }
4343 if let Some(token) = &downloader.auth_bearer_token {
4344 req = req.bearer_auth(token);
4345 }
4346 req.send().await?
4347 .error_for_status()
4348 };
4349 let mut failure = None;
4350 match fetch
4351 .retry(ExponentialBuilder::default())
4352 .when(reqwest_error_transient_p)
4353 .notify(notify_transient)
4354 .await
4355 {
4356 Ok(response) => {
4357 if response.status().is_success() {
4358 let content_bytes = response.bytes().await
4359 .map_err(|e| network_error("fetching DASH subtitle segment", &e))?;
4360 if downloader.verbosity > 2 {
4361 if let Some(sb) = &frag.start_byte {
4362 if let Some(eb) = &frag.end_byte {
4363 info!(" Subtitle segment {} range {sb}-{eb} -> {} octets",
4364 &frag.url, content_bytes.len());
4365 }
4366 } else {
4367 info!(" Subtitle segment {} -> {} octets", &frag.url, content_bytes.len());
4368 }
4369 }
4370 let size = min((content_bytes.len()/1024 + 1) as u32, u32::MAX);
4371 throttle_download_rate(downloader, size).await?;
4372 if subtitle_formats.contains(&SubtitleType::Stpp) {
4373 stpp_document.add_from_mp4(&content_bytes)?;
4374 } else if subtitle_formats.contains(&SubtitleType::Vtt) {
4378 vtt_document.add_bytes(&content_bytes)?;
4379 } else {
4380 tmpfile_subs.write_all(&content_bytes)
4381 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH subtitle data")))
4382 .await?;
4383 }
4384 have_subtitles = true;
4385 } else {
4386 failure = Some(format!("HTTP error {}", response.status().as_str()));
4387 }
4388 },
4389 Err(e) => failure = Some(format!("{e}")),
4390 }
4391 if let Some(f) = failure {
4392 if downloader.verbosity > 0 {
4393 error!("{f} fetching subtitle segment {}", &frag.url);
4394 }
4395 ds.download_errors += 1;
4396 if ds.download_errors > downloader.max_error_count {
4397 return Err(DashMpdError::Network(
4398 String::from("more than max_error_count network errors")));
4399 }
4400 }
4401 }
4402 if downloader.sleep_between_requests > 0 {
4403 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
4404 }
4405 }
4406 if subtitle_formats.contains(&SubtitleType::Stpp) {
4407 if downloader.verbosity > 1 {
4408 info!(" Writing TTML subtitles to {tmppath:?}");
4409 }
4410 tmpfile_subs.write_all(stpp_document.to_string().as_bytes())
4411 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH TTML subtitle data")))
4412 .await?;
4413 tmpfile_subs.flush().map_err(|e| {
4414 error!("Couldn't flush subs file: {e}");
4415 DashMpdError::Io(e, String::from("flushing subtitle file"))
4416 }).await?;
4417 }
4418 if subtitle_formats.contains(&SubtitleType::Vtt) {
4419 if downloader.verbosity > 1 {
4420 info!(" Writing VTT subtitles to {tmppath:?}");
4421 }
4422 tmpfile_subs.write_all(vtt_document.to_string().as_bytes())
4423 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH VTT subtitle data")))
4424 .await?;
4425 tmpfile_subs.flush().map_err(|e| {
4426 error!("Couldn't flush subs file: {e}");
4427 DashMpdError::Io(e, String::from("flushing subtitle file"))
4428 }).await?;
4429 }
4430 } if have_subtitles {
4432 if let Ok(metadata) = fs::metadata(tmppath).await {
4433 if downloader.verbosity > 1 {
4434 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4435 let elapsed = start_download.elapsed();
4436 info!(" Wrote {mbytes:.1}MB to DASH subtitle file ({:.1} MB/s)",
4437 mbytes / elapsed.as_secs_f64());
4438 }
4439 }
4440 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
4443 subtitle_formats.contains(&SubtitleType::Ttxt)
4444 {
4445 if let Err(e) = wvtt_extract(downloader, tmppath).await {
4446 warn!("Error while extracting wvtt subtitles: {e}");
4447 }
4448 }
4449 if subtitle_formats.contains(&SubtitleType::Stpp) {
4450 let tmpfile_in = File::open(tmppath).await
4454 .map_err(|e| DashMpdError::Io(
4455 e, String::from("opening tmp subtitle output")))?;
4456 let ttml_path = downloader.output_path.as_ref()
4457 .ok_or_else(|| DashMpdError::Other(String::from("no output_path set")))?
4458 .with_extension("ttml");
4459 let ttml_file = File::create(&ttml_path).await
4460 .map_err(|e| DashMpdError::Io(
4461 e, String::from("opening TTML output file")))?;
4462 io::copy(&mut BufReader::new(tmpfile_in), &mut BufWriter::new(ttml_file)).await
4463 .map_err(|e| DashMpdError::Io(
4464 e, String::from("copying TTML subtitles")))?;
4465 info!(" Copied TTML subtitles to {}", ttml_path.display());
4466 if let Err(e) = convert_ttml_srt(downloader, &ttml_path).await {
4467 warn!("Error while converting TTML subtitles to SubRip: {e}");
4468 }
4469 }
4470 if subtitle_formats.contains(&SubtitleType::Vtt) {
4471 let tmpfile_in = File::open(tmppath).await
4475 .map_err(|e| DashMpdError::Io(
4476 e, String::from("opening tmp subtitle output")))?;
4477 let vtt_path = downloader.output_path.as_ref()
4478 .ok_or_else(|| DashMpdError::Other(String::from("no output_path set")))?
4479 .with_extension("vtt");
4480 let vtt_file = File::create(&vtt_path).await
4481 .map_err(|e| DashMpdError::Io(
4482 e, String::from("opening VTT output file")))?;
4483 io::copy(&mut BufReader::new(tmpfile_in), &mut BufWriter::new(vtt_file)).await
4484 .map_err(|e| DashMpdError::Io(
4485 e, String::from("copying VTT subtitles")))?;
4486 info!(" Copied WebVTT subtitles to {}", vtt_path.display());
4487 if let Err(e) = convert_vtt_srt(downloader, &vtt_path).await {
4488 warn!("Error while converting WebVTT subtitles to SubRip: {e}");
4489 }
4490 }
4491 }
4492 Ok(have_subtitles)
4493}
4494
4495
4496async fn fetch_mpd_http(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4498 let client = &downloader.http_client.clone().unwrap();
4499 let send_request = || async {
4500 let mut req = client.get(&downloader.mpd_url)
4501 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4502 .header("Accept-Language", "en-US,en")
4503 .header("Upgrade-Insecure-Requests", "1")
4504 .header("Sec-Fetch-Mode", "navigate");
4505 if let Some(referer) = &downloader.referer {
4506 req = req.header("Referer", referer);
4507 }
4508 if let Some(username) = &downloader.auth_username {
4509 if let Some(password) = &downloader.auth_password {
4510 req = req.basic_auth(username, Some(password));
4511 }
4512 }
4513 if let Some(token) = &downloader.auth_bearer_token {
4514 req = req.bearer_auth(token);
4515 }
4516 req.send().await?
4517 .error_for_status()
4518 };
4519 for observer in &downloader.progress_observers {
4520 observer.update(1, 1, "Fetching DASH manifest");
4521 }
4522 if downloader.verbosity > 0 {
4523 if !downloader.fetch_audio && !downloader.fetch_video && !downloader.fetch_subtitles {
4524 info!("Only simulating media downloads");
4525 }
4526 info!("Fetching the DASH manifest");
4527 }
4528 let response = send_request
4529 .retry(ExponentialBuilder::default())
4530 .when(reqwest_error_transient_p)
4531 .notify(notify_transient)
4532 .await
4533 .map_err(|e| network_error("requesting DASH manifest", &e))?;
4534 if !response.status().is_success() {
4535 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4536 return Err(DashMpdError::Network(msg));
4537 }
4538 downloader.redirected_url = response.url().clone();
4539 response.bytes().await
4540 .map_err(|e| network_error("fetching DASH manifest", &e))
4541}
4542
4543async fn fetch_mpd_file_url(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4546 if ! &downloader.mpd_url.starts_with("file://") {
4547 return Err(DashMpdError::Other(String::from("expecting file:// URL scheme")));
4548 }
4549 let url = Url::parse(&downloader.mpd_url)
4550 .map_err(|_| DashMpdError::Other(String::from("parsing MPD URL")))?;
4551 let path = url.to_file_path()
4552 .map_err(|_| DashMpdError::Other(String::from("extracting path from file:// URL")))?;
4553 let octets = fs::read(path).await
4554 .map_err(|_| DashMpdError::Other(String::from("reading from file:// URL")))?;
4555 Ok(Bytes::from(octets))
4556}
4557
4558
4559async fn fetch_mpd_data_url(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4561 if !&downloader.mpd_url.starts_with("data:") {
4562 return Err(DashMpdError::Other(String::from("expecting data: URL scheme")));
4563 }
4564 let du = DataUrl::process(&downloader.mpd_url)
4565 .map_err(|_| DashMpdError::Parsing(String::from("parsing mpd data URL")))?;
4566 let (body, _fragment) = du.decode_to_vec()
4567 .map_err(|_| DashMpdError::Parsing(String::from("decoding mpd data URL")))?;
4568 Ok(Bytes::from(body))
4569}
4570
4571
4572#[tracing::instrument(level="trace", skip_all)]
4573async fn fetch_mpd(downloader: &mut DashDownloader) -> Result<PathBuf, DashMpdError> {
4574 #[cfg(all(feature = "sandbox", target_os = "linux"))]
4575 if downloader.sandbox {
4576 if let Err(e) = restrict_thread(downloader) {
4577 warn!("Sandboxing failed: {e:?}");
4578 }
4579 }
4580 let xml = if downloader.mpd_url.starts_with("file://") {
4581 fetch_mpd_file_url(downloader).await?
4582 } else if downloader.mpd_url.starts_with("data:") {
4583 fetch_mpd_data_url(downloader).await?
4584 } else {
4585 fetch_mpd_http(downloader).await?
4586 };
4587 let mut mpd: MPD = parse_resolving_xlinks(downloader, &xml).await
4588 .map_err(|e| parse_error("parsing DASH XML", e))?;
4589 let client = &downloader.http_client.clone().unwrap();
4592 if let Some(new_location) = &mpd.locations.first() {
4593 let new_url = &new_location.url;
4594 if downloader.verbosity > 0 {
4595 info!("Redirecting to new manifest <Location> {new_url}");
4596 }
4597 let send_request = || async {
4598 let mut req = client.get(new_url)
4599 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4600 .header("Accept-Language", "en-US,en")
4601 .header("Sec-Fetch-Mode", "navigate");
4602 if let Some(referer) = &downloader.referer {
4603 req = req.header("Referer", referer);
4604 } else {
4605 req = req.header("Referer", downloader.redirected_url.to_string());
4606 }
4607 if let Some(username) = &downloader.auth_username {
4608 if let Some(password) = &downloader.auth_password {
4609 req = req.basic_auth(username, Some(password));
4610 }
4611 }
4612 if let Some(token) = &downloader.auth_bearer_token {
4613 req = req.bearer_auth(token);
4614 }
4615 req.send().await?
4616 .error_for_status()
4617 };
4618 let response = send_request
4619 .retry(ExponentialBuilder::default())
4620 .when(reqwest_error_transient_p)
4621 .notify(notify_transient)
4622 .await
4623 .map_err(|e| network_error("requesting relocated DASH manifest", &e))?;
4624 if !response.status().is_success() {
4625 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4626 return Err(DashMpdError::Network(msg));
4627 }
4628 downloader.redirected_url = response.url().clone();
4629 let xml = response.bytes().await
4630 .map_err(|e| network_error("fetching relocated DASH manifest", &e))?;
4631 mpd = parse_resolving_xlinks(downloader, &xml).await
4632 .map_err(|e| parse_error("parsing relocated DASH XML", e))?;
4633 }
4634 if mpd_is_dynamic(&mpd) {
4635 if downloader.allow_live_streams {
4638 if downloader.verbosity > 0 {
4639 warn!("Attempting to download from live stream (this may not work).");
4640 }
4641 } else {
4642 return Err(DashMpdError::UnhandledMediaStream("Don't know how to download dynamic MPD".to_string()));
4643 }
4644 }
4645 let mut toplevel_base_url = downloader.redirected_url.clone();
4646 if let Some(bu) = &mpd.base_url.first() {
4648 toplevel_base_url = merge_baseurls(&downloader.redirected_url, &bu.base)?;
4649 }
4650 if let Some(base) = &downloader.base_url {
4653 toplevel_base_url = merge_baseurls(&downloader.redirected_url, base)?;
4654 }
4655 if downloader.verbosity > 0 {
4656 let pcount = mpd.periods.len();
4657 info!("DASH manifest has {pcount} period{}", if pcount > 1 { "s" } else { "" });
4658 print_available_streams(&mpd);
4659 }
4660 let mut pds: Vec<PeriodDownloads> = Vec::new();
4668 let mut period_counter = 0;
4669 for mpd_period in &mpd.periods {
4670 let period = mpd_period.clone();
4671 period_counter += 1;
4672 if let Some(min) = downloader.minimum_period_duration {
4673 if let Some(duration) = period.duration {
4674 if duration < min {
4675 if let Some(id) = period.id.as_ref() {
4676 info!("Skipping period {id} (#{period_counter}): duration is less than requested minimum");
4677 } else {
4678 info!("Skipping period #{period_counter}: duration is less than requested minimum");
4679 }
4680 continue;
4681 }
4682 }
4683 }
4684 let mut pd = PeriodDownloads { period_counter, ..Default::default() };
4685 if let Some(id) = period.id.as_ref() {
4686 pd.id = Some(id.clone());
4687 }
4688 if downloader.verbosity > 0 && !downloader.fetch_audio && !downloader.fetch_video && !downloader.fetch_subtitles {
4689 if let Some(id) = period.id.as_ref() {
4690 info!("Preparing download for period {id} (#{period_counter})");
4691 } else {
4692 info!("Preparing download for period #{period_counter}");
4693 }
4694 }
4695 let mut base_url = toplevel_base_url.clone();
4696 if let Some(bu) = period.BaseURL.first() {
4698 base_url = merge_baseurls(&base_url, &bu.base)?;
4699 }
4700 let mut audio_outputs = PeriodOutputs::default();
4701 if downloader.fetch_audio {
4702 audio_outputs = do_period_audio(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4703 for f in audio_outputs.fragments {
4704 pd.audio_fragments.push(f);
4705 }
4706 pd.selected_audio_language = audio_outputs.selected_audio_language;
4707 }
4708 let mut video_outputs = PeriodOutputs::default();
4709 if downloader.fetch_video {
4710 video_outputs = do_period_video(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4711 for f in video_outputs.fragments {
4712 pd.video_fragments.push(f);
4713 }
4714 }
4715 match do_period_subtitles(downloader, &mpd, &period, period_counter, base_url.clone()).await {
4716 Ok(subtitle_outputs) => {
4717 for f in subtitle_outputs.fragments {
4718 pd.subtitle_fragments.push(f);
4719 }
4720 for f in subtitle_outputs.subtitle_formats {
4721 pd.subtitle_formats.push(f);
4722 }
4723 pd.selected_subtitle_language = subtitle_outputs.selected_subtitle_language;
4724 },
4725 Err(e) => warn!(" Ignoring error triggered while processing subtitles: {e}"),
4726 }
4727 if downloader.verbosity > 0 {
4729 use base64::prelude::{Engine as _, BASE64_STANDARD};
4730
4731 audio_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4732 for f in pd.audio_fragments.iter().filter(|f| f.is_init) {
4733 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4734 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4735 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4736 info!(" {}", pssh.to_string());
4737 }
4738 }
4739 }
4740 video_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4741 for f in pd.video_fragments.iter().filter(|f| f.is_init) {
4742 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4743 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4744 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4745 info!(" {}", pssh.to_string());
4746 }
4747 }
4748 }
4749 }
4750 pds.push(pd);
4751 } let output_path = &downloader.output_path.as_ref()
4756 .ok_or_else(|| DashMpdError::Other(String::from("no output_path set")))?
4757 .clone();
4758 let mut period_output_pathbufs: Vec<PathBuf> = Vec::new();
4759 let mut ds = DownloadState {
4760 period_counter: 0,
4761 segment_count: pds.iter().map(period_fragment_count).sum(),
4763 segment_counter: 0,
4764 download_errors: 0
4765 };
4766 for pd in pds {
4767 let mut have_audio = false;
4768 let mut have_video = false;
4769 let mut have_subtitles = false;
4770 ds.period_counter = pd.period_counter;
4771 let period_output_path = output_path_for_period(output_path, pd.period_counter);
4772 #[allow(clippy::collapsible_if)]
4773 if downloader.verbosity > 0 {
4774 if downloader.fetch_audio || downloader.fetch_video || downloader.fetch_subtitles {
4775 let idnum = if let Some(id) = pd.id {
4776 format!("id={} (#{})", id, pd.period_counter)
4777 } else {
4778 format!("#{}", pd.period_counter)
4779 };
4780 info!("Period {idnum}: fetching {} audio, {} video and {} subtitle segments",
4781 pd.audio_fragments.len(),
4782 pd.video_fragments.len(),
4783 pd.subtitle_fragments.len());
4784 }
4785 }
4786 let output_ext = downloader.output_path.as_ref()
4787 .ok_or_else(|| DashMpdError::Other(String::from("no output_path set")))?
4788 .extension()
4789 .unwrap_or(OsStr::new("mp4"));
4790 let tmppath_audio = if let Some(ref path) = downloader.keep_audio {
4791 path.clone()
4792 } else {
4793 tmp_file_path("dashmpd-audio", output_ext)?
4794 };
4795 let tmppath_video = if let Some(ref path) = downloader.keep_video {
4796 path.clone()
4797 } else {
4798 tmp_file_path("dashmpd-video", output_ext)?
4799 };
4800 let tmppath_subs = tmp_file_path("dashmpd-subs", OsStr::new("sub"))?;
4801 if downloader.fetch_audio && !pd.audio_fragments.is_empty() {
4802 have_audio = fetch_period_audio(downloader,
4806 &tmppath_audio, &pd.audio_fragments,
4807 &mut ds).await?;
4808 }
4809 if downloader.fetch_video && !pd.video_fragments.is_empty() {
4810 have_video = fetch_period_video(downloader,
4811 &tmppath_video, &pd.video_fragments,
4812 &mut ds).await?;
4813 }
4814 if downloader.fetch_subtitles && !pd.subtitle_fragments.is_empty() {
4818 have_subtitles = fetch_period_subtitles(downloader,
4819 &tmppath_subs,
4820 &pd.subtitle_fragments,
4821 &pd.subtitle_formats,
4822 &mut ds).await?;
4823 }
4824
4825 if have_audio && have_video {
4828 for observer in &downloader.progress_observers {
4829 observer.update(99, 1, "Muxing audio and video");
4830 }
4831 if downloader.verbosity > 1 {
4832 info!(" Muxing audio and video streams");
4833 }
4834 let audio_tracks = vec![
4835 AudioTrack {
4836 language: pd.selected_audio_language,
4837 path: tmppath_audio.clone()
4838 }];
4839 mux_audio_video(downloader, &period_output_path, &audio_tracks, &tmppath_video).await?;
4840 if pd.subtitle_formats.contains(&SubtitleType::Stpp) {
4841 let container = match &period_output_path.extension() {
4842 Some(ext) => ext.to_str().unwrap_or("mp4"),
4843 None => "mp4",
4844 };
4845 if container.eq("mp4") {
4846 if downloader.verbosity > 1 {
4847 if let Some(fmt) = &pd.subtitle_formats.first() {
4848 info!(" Downloaded media contains subtitles in {fmt:?} format");
4849 }
4850 info!(" Running MP4Box to merge subtitles with output MP4 container");
4851 }
4852 let tmp_str = tmppath_subs.to_string_lossy();
4855 let period_output_str = period_output_path.to_string_lossy();
4856 let subtitle_lang = format!("3={}", pd.selected_subtitle_language);
4857 let verbosity = match downloader.verbosity {
4858 0 => "all@error",
4859 1 => "all@warning",
4860 2 => "all@info",
4861 _ => "all@debug",
4862 };
4863 let args = vec![
4864 "-logs", verbosity,
4865 "-noprog",
4866 "-lang", &subtitle_lang,
4867 "-add", &tmp_str,
4868 &period_output_str];
4869 if downloader.verbosity > 0 {
4870 info!(" Running MP4Box {}", args.join(" "));
4871 }
4872 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4873 .args(args)
4874 .output()
4875 {
4876 let msg = partial_process_output(&mp4box.stdout);
4877 if !msg.is_empty() {
4878 info!(" MP4Box stdout: {msg}");
4879 }
4880 let msg = partial_process_output(&mp4box.stderr);
4881 if !msg.is_empty() {
4882 info!(" MP4Box stderr: {msg}");
4883 }
4884 if mp4box.status.success() {
4885 info!(" Merged subtitles with MP4 container");
4886 } else {
4887 warn!(" Error running MP4Box to merge subtitles");
4888 }
4889 } else {
4890 warn!(" Failed to spawn MP4Box to merge subtitles");
4891 }
4892 } else if container.eq("mkv") || container.eq("webm") {
4893 let srt = period_output_path.with_extension("srt");
4905 if srt.exists() {
4906 if downloader.verbosity > 0 {
4907 info!(" Running mkvmerge to merge subtitles with output Matroska container");
4908 }
4909 let tmppath = temporary_outpath(".mkv")?;
4910 let pop_arg = &period_output_path.to_string_lossy();
4911 let srt_arg = &srt.to_string_lossy();
4912 let mkvmerge_args = vec!["-o", &tmppath, pop_arg, srt_arg];
4913 if downloader.verbosity > 0 {
4914 info!(" Running mkvmerge {}", mkvmerge_args.join(" "));
4915 }
4916 if let Ok(mkvmerge) = Command::new(downloader.mkvmerge_location.clone())
4917 .args(mkvmerge_args)
4918 .output()
4919 {
4920 let msg = partial_process_output(&mkvmerge.stdout);
4921 if !msg.is_empty() {
4922 info!(" mkvmerge stdout: {msg}");
4923 }
4924 let msg = partial_process_output(&mkvmerge.stderr);
4925 if !msg.is_empty() {
4926 info!(" mkvmerge stderr: {msg}");
4927 }
4928 if mkvmerge.status.success() {
4929 info!(" Merged subtitles with Matroska container");
4930 {
4933 let tmpfile = File::open(tmppath.clone()).await
4934 .map_err(|e| DashMpdError::Io(
4935 e, String::from("opening mkvmerge output")))?;
4936 let mut merged = BufReader::new(tmpfile);
4937 let outfile = File::create(period_output_path.clone()).await
4939 .map_err(|e| DashMpdError::Io(
4940 e, String::from("creating output file")))?;
4941 let mut sink = BufWriter::new(outfile);
4942 io::copy(&mut merged, &mut sink).await
4943 .map_err(|e| DashMpdError::Io(
4944 e, String::from("copying mkvmerge output to output file")))?;
4945 }
4946 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4947 if let Err(e) = fs::remove_file(tmppath).await {
4948 warn!(" Error deleting temporary mkvmerge output: {e}");
4949 }
4950 }
4951 } else {
4952 warn!(" Error running mkvmerge to merge subtitles");
4953 }
4954 }
4955 }
4956 }
4957 }
4958 } else if have_audio {
4959 copy_audio_to_container(downloader, &period_output_path, &tmppath_audio).await?;
4960 } else if have_video {
4961 copy_video_to_container(downloader, &period_output_path, &tmppath_video).await?;
4962 } else if downloader.fetch_video && downloader.fetch_audio {
4963 return Err(DashMpdError::UnhandledMediaStream("no audio or video streams found".to_string()));
4964 } else if downloader.fetch_video {
4965 return Err(DashMpdError::UnhandledMediaStream("no video streams found".to_string()));
4966 } else if downloader.fetch_audio {
4967 return Err(DashMpdError::UnhandledMediaStream("no audio streams found".to_string()));
4968 }
4969 #[allow(clippy::collapsible_if)]
4970 if downloader.keep_audio.is_none() && downloader.fetch_audio {
4971 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4972 if tmppath_audio.exists() && fs::remove_file(tmppath_audio).await.is_err() {
4973 info!(" Failed to delete temporary file for audio stream");
4974 }
4975 }
4976 }
4977 #[allow(clippy::collapsible_if)]
4978 if downloader.keep_video.is_none() && downloader.fetch_video {
4979 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4980 if tmppath_video.exists() && fs::remove_file(tmppath_video).await.is_err() {
4981 info!(" Failed to delete temporary file for video stream");
4982 }
4983 }
4984 }
4985 #[allow(clippy::collapsible_if)]
4986 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4987 if downloader.fetch_subtitles && tmppath_subs.exists() &&
4988 fs::remove_file(tmppath_subs).await.is_err() {
4989 info!(" Failed to delete temporary file for subtitles");
4990 }
4991 }
4992 if downloader.verbosity > 1 && (downloader.fetch_audio || downloader.fetch_video || have_subtitles) {
4993 if let Ok(metadata) = fs::metadata(&period_output_path).await {
4994 info!(" Wrote {:.1}MB to media file", metadata.len() as f64 / (1024.0 * 1024.0));
4995 }
4996 }
4997 if have_audio || have_video {
4998 period_output_pathbufs.push(period_output_path);
4999 }
5000 } let period_output_paths: Vec<&Path> = period_output_pathbufs
5002 .iter()
5003 .map(PathBuf::as_path)
5004 .collect();
5005 #[allow(clippy::comparison_chain)]
5006 if period_output_paths.len() == 1 {
5007 maybe_record_metainformation(output_path, downloader, &mpd);
5009 } else if period_output_paths.len() > 1 {
5010 #[allow(unused_mut)]
5015 let mut concatenated = false;
5016 #[cfg(not(feature = "libav"))]
5017 if downloader.concatenate_periods && video_containers_concatable(downloader, &period_output_paths) {
5019 info!("Preparing to concatenate multiple Periods into one output file");
5020 concat_output_files(downloader, &period_output_paths).await?;
5021 for p in &period_output_paths[1..] {
5022 if fs::remove_file(p).await.is_err() {
5023 warn!(" Failed to delete temporary file {}", p.display());
5024 }
5025 }
5026 concatenated = true;
5027 if let Some(pop) = period_output_paths.first() {
5028 maybe_record_metainformation(pop, downloader, &mpd);
5029 }
5030 }
5031 if !concatenated {
5032 info!("Media content has been saved in a separate file for each period:");
5033 period_counter = 0;
5035 for p in period_output_paths {
5036 period_counter += 1;
5037 info!(" Period #{period_counter}: {}", p.display());
5038 maybe_record_metainformation(p, downloader, &mpd);
5039 }
5040 }
5041 }
5042 let have_content_protection = mpd.periods.iter().any(
5043 |p| p.adaptations.iter().any(
5044 |a| (!a.ContentProtection.is_empty()) ||
5045 a.representations.iter().any(
5046 |r| !r.ContentProtection.is_empty())));
5047 if have_content_protection && downloader.decryption_keys.is_empty() {
5048 warn!("Manifest seems to use ContentProtection (DRM), but you didn't provide decryption keys.");
5049 }
5050 for observer in &downloader.progress_observers {
5051 observer.update(100, 1, "Done");
5052 }
5053 Ok(PathBuf::from(output_path))
5054}
5055
5056
5057#[cfg(test)]
5058mod tests {
5059 #[test]
5060 fn test_resolve_url_template() {
5061 use std::collections::HashMap;
5062 use super::resolve_url_template;
5063
5064 assert_eq!(resolve_url_template("AA$Time$BB", &HashMap::from([("Time", "ZZZ".to_string())])),
5065 "AAZZZBB");
5066 assert_eq!(resolve_url_template("AA$Number%06d$BB", &HashMap::from([("Number", "42".to_string())])),
5067 "AA000042BB");
5068 let dict = HashMap::from([("RepresentationID", "640x480".to_string()),
5069 ("Number", "42".to_string()),
5070 ("Time", "ZZZ".to_string())]);
5071 assert_eq!(resolve_url_template("AA/$RepresentationID$/segment-$Number%05d$.mp4", &dict),
5072 "AA/640x480/segment-00042.mp4");
5073 }
5074}