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