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::borrow::Cow;
15use std::collections::HashMap;
16use std::cmp::min;
17use std::ffi::OsStr;
18use std::num::NonZeroU32;
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 lazy_static::lazy_static;
29use xot::{xmlname, Xot};
30use edit_distance::edit_distance;
31use crate::{MPD, Period, Representation, AdaptationSet, SegmentBase, DashMpdError};
32use crate::{parse, mux_audio_video, copy_video_to_container, copy_audio_to_container};
33use crate::{is_audio_adaptation, is_video_adaptation, is_subtitle_adaptation};
34use crate::{subtitle_type, content_protection_type, SubtitleType};
35use crate::check_conformity;
36#[cfg(not(feature = "libav"))]
37use crate::ffmpeg::concat_output_files;
38use crate::media::{temporary_outpath, AudioTrack};
39use crate::decryption::{
40 decrypt_mp4decrypt,
41 decrypt_shaka,
42 decrypt_shaka_container,
43 decrypt_mp4box,
44 decrypt_mp4box_container
45};
46#[allow(unused_imports)]
47use crate::media::video_containers_concatable;
48
49#[cfg(all(feature = "sandbox", target_os = "linux"))]
50use crate::sandbox::{restrict_thread};
51
52
53pub type HttpClient = reqwest::Client;
55type DirectRateLimiter = RateLimiter<governor::state::direct::NotKeyed,
56 governor::state::InMemoryState,
57 governor::clock::DefaultClock,
58 governor::middleware::NoOpMiddleware>;
59
60
61#[must_use]
64pub fn partial_process_output(output: &[u8]) -> Cow<'_, str> {
65 let len = min(output.len(), 4096);
66 #[allow(clippy::indexing_slicing)]
67 String::from_utf8_lossy(&output[0..len])
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
1831lazy_static! {
1840 static ref URL_TEMPLATE_IDS: Vec<(&'static str, String, Regex)> = {
1841 vec!["RepresentationID", "Number", "Time", "Bandwidth"].into_iter()
1842 .map(|k| (k, format!("${k}$"), Regex::new(&format!("\\${k}%0([\\d])d\\$")).unwrap()))
1843 .collect()
1844 };
1845}
1846
1847fn resolve_url_template(template: &str, params: &HashMap<&str, String>) -> String {
1848 let mut result = template.to_string();
1849 for (k, ident, rx) in URL_TEMPLATE_IDS.iter() {
1850 if result.contains(ident) {
1852 if let Some(value) = params.get(k as &str) {
1853 result = result.replace(ident, value);
1854 }
1855 }
1856 if let Some(cap) = rx.captures(&result) {
1858 if let Some(value) = params.get(k as &str) {
1859 if let Ok(width) = cap[1].parse::<usize>() {
1860 if let Some(m) = rx.find(&result) {
1861 let count = format!("{value:0>width$}");
1862 result = result[..m.start()].to_owned() + &count + &result[m.end()..];
1863 }
1864 }
1865 }
1866 }
1867 }
1868 result
1869}
1870
1871
1872fn reqwest_error_transient_p(e: &reqwest::Error) -> bool {
1873 if e.is_timeout() {
1874 return true;
1875 }
1876 if let Some(s) = e.status() {
1877 if s == reqwest::StatusCode::REQUEST_TIMEOUT ||
1878 s == reqwest::StatusCode::TOO_MANY_REQUESTS ||
1879 s == reqwest::StatusCode::SERVICE_UNAVAILABLE ||
1880 s == reqwest::StatusCode::GATEWAY_TIMEOUT {
1881 return true;
1882 }
1883 }
1884 false
1885}
1886
1887fn notify_transient<E: std::fmt::Debug>(err: &E, dur: Duration) {
1888 warn!("Transient error after {dur:?}: {err:?}");
1889}
1890
1891fn network_error(why: &str, e: &reqwest::Error) -> DashMpdError {
1892 if e.is_timeout() {
1893 DashMpdError::NetworkTimeout(format!("{why}: {e:?}"))
1894 } else if e.is_connect() {
1895 DashMpdError::NetworkConnect(format!("{why}: {e:?}"))
1896 } else {
1897 DashMpdError::Network(format!("{why}: {e:?}"))
1898 }
1899}
1900
1901fn parse_error(why: &str, e: impl std::error::Error) -> DashMpdError {
1902 DashMpdError::Parsing(format!("{why}: {e:#?}"))
1903}
1904
1905
1906async fn reqwest_bytes_with_retries(
1910 client: &reqwest::Client,
1911 req: reqwest::Request,
1912 retry_count: u32) -> Result<Bytes, reqwest::Error>
1913{
1914 let mut last_error = None;
1915 for _ in 0..retry_count {
1916 if let Some(rqw) = req.try_clone() {
1917 match client.execute(rqw).await {
1918 Ok(response) => {
1919 match response.error_for_status() {
1920 Ok(resp) => {
1921 match resp.bytes().await {
1922 Ok(bytes) => return Ok(bytes),
1923 Err(e) => {
1924 info!("Retrying after HTTP error {e:?}");
1925 last_error = Some(e);
1926 },
1927 }
1928 },
1929 Err(e) => {
1930 info!("Retrying after HTTP error {e:?}");
1931 last_error = Some(e);
1932 },
1933 }
1934 },
1935 Err(e) => {
1936 info!("Retrying after HTTP error {e:?}");
1937 last_error = Some(e);
1938 },
1939 }
1940 }
1941 }
1942 Err(last_error.unwrap())
1943}
1944
1945#[allow(unused_variables)]
1958fn maybe_record_metainformation(path: &Path, downloader: &DashDownloader, mpd: &MPD) {
1959 #[cfg(target_family = "unix")]
1960 if downloader.record_metainformation && (downloader.fetch_audio || downloader.fetch_video) {
1961 if let Ok(origin_url) = Url::parse(&downloader.mpd_url) {
1962 #[allow(clippy::collapsible_if)]
1964 if origin_url.username().is_empty() && origin_url.password().is_none() {
1965 #[cfg(target_family = "unix")]
1966 if xattr::set(path, "user.xdg.origin.url", downloader.mpd_url.as_bytes()).is_err() {
1967 info!("Failed to set user.xdg.origin.url xattr on output file");
1968 }
1969 }
1970 for pi in &mpd.ProgramInformation {
1971 if let Some(t) = &pi.Title {
1972 if let Some(tc) = &t.content {
1973 if xattr::set(path, "user.dublincore.title", tc.as_bytes()).is_err() {
1974 info!("Failed to set user.dublincore.title xattr on output file");
1975 }
1976 }
1977 }
1978 if let Some(source) = &pi.Source {
1979 if let Some(sc) = &source.content {
1980 if xattr::set(path, "user.dublincore.source", sc.as_bytes()).is_err() {
1981 info!("Failed to set user.dublincore.source xattr on output file");
1982 }
1983 }
1984 }
1985 if let Some(copyright) = &pi.Copyright {
1986 if let Some(cc) = ©right.content {
1987 if xattr::set(path, "user.dublincore.rights", cc.as_bytes()).is_err() {
1988 info!("Failed to set user.dublincore.rights xattr on output file");
1989 }
1990 }
1991 }
1992 }
1993 }
1994 }
1995}
1996
1997fn fetchable_xlink_href(href: &str) -> bool {
2001 (!href.is_empty()) && href.ne("urn:mpeg:dash:resolve-to-zero:2013")
2002}
2003
2004fn element_resolves_to_zero(xot: &mut Xot, element: xot::Node) -> bool {
2005 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
2006 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
2007 if let Some(href) = xot.get_attribute(element, xlink_href_name.into()) {
2008 return href.eq("urn:mpeg:dash:resolve-to-zero:2013");
2009 }
2010 false
2011}
2012
2013fn skip_xml_preamble(input: &str) -> &str {
2014 if input.starts_with("<?xml") {
2015 if let Some(end_pos) = input.find("?>") {
2016 return &input[end_pos + 2..]; }
2019 }
2020 input
2022}
2023
2024async fn apply_xslt_stylesheets(
2025 downloader: &DashDownloader,
2026 xot: &mut Xot,
2027 doc: xot::Node) -> Result<String, DashMpdError> {
2028 #[cfg(feature = "xee-xslt")]
2029 return apply_xslt_stylesheets_xee(downloader, xot, doc).await;
2030 #[cfg(not(feature = "xee-xslt"))]
2031 return apply_xslt_stylesheets_xsltproc(downloader, xot, doc).await;
2032}
2033
2034#[allow(dead_code)]
2038async fn apply_xslt_stylesheets_xsltproc(
2039 downloader: &DashDownloader,
2040 xot: &mut Xot,
2041 doc: xot::Node) -> Result<String, DashMpdError> {
2042 let mut buf = Vec::new();
2043 xot.write(doc, &mut buf)
2044 .map_err(|e| parse_error("serializing rewritten manifest", e))?;
2045 for ss in &downloader.xslt_stylesheets {
2046 if downloader.verbosity > 0 {
2047 info!("Applying XSLT stylesheet {} with xsltproc", ss.display());
2048 }
2049 let tmpmpd = tmp_file_path("dashxslt", OsStr::new("xslt"))?;
2050 fs::write(&tmpmpd, &buf).await
2051 .map_err(|e| DashMpdError::Io(e, String::from("writing MPD")))?;
2052 let xsltproc = Command::new("xsltproc")
2053 .args([ss, &tmpmpd])
2054 .output()
2055 .map_err(|e| DashMpdError::Io(e, String::from("spawning xsltproc")))?;
2056 if !xsltproc.status.success() {
2057 let msg = format!("xsltproc returned {}", xsltproc.status);
2058 let out = partial_process_output(&xsltproc.stderr).to_string();
2059 return Err(DashMpdError::Io(std::io::Error::other(msg), out));
2060 }
2061 if env::var("DASHMPD_PERSIST_FILES").is_err() {
2062 if let Err(e) = fs::remove_file(&tmpmpd).await {
2063 warn!("Error removing temporary MPD after XSLT processing: {e:?}");
2064 }
2065 }
2066 buf.clone_from(&xsltproc.stdout);
2067 if downloader.verbosity > 2 {
2068 println!("Rewritten XSLT: {}", String::from_utf8_lossy(&buf));
2069 }
2070 }
2071 String::from_utf8(buf)
2072 .map_err(|e| parse_error("parsing UTF-8", e))
2073}
2074
2075#[allow(dead_code)]
2079#[cfg(feature = "xee-xslt")]
2080async fn apply_xslt_stylesheets_xee(
2081 downloader: &DashDownloader,
2082 xot: &mut Xot,
2083 doc: xot::Node) -> Result<String, DashMpdError>
2084{
2085 use xee_xslt_compiler::evaluate;
2086 use std::fmt::Write;
2087
2088 let mut xml = xot.to_string(doc)
2089 .map_err(|e| parse_error("serializing rewritten manifest", e))?;
2090 for ss in &downloader.xslt_stylesheets {
2091 if downloader.verbosity > 0 {
2092 info!(" Applying XSLT stylesheet {} with xee", ss.display());
2093 }
2094 let xslt = fs::read_to_string(ss).await
2095 .map_err(|_| DashMpdError::Other(String::from("reading XSLT stylesheet")))?;
2096 let seq = evaluate(xot, &xml, &xslt)
2097 .map_err(|e| DashMpdError::Other(format!("applying XSLT: {e:?}")))?;
2098 let mut f = String::new();
2099 for item in seq.iter() {
2100 match item.to_node() {
2101 Ok(n) => f.write_str(&xot.to_string(n).expect("writing to string"))
2102 .expect("writing to string"),
2103 Err(e) => error!("xee non-node item {item:?}: {e:?}"),
2104 }
2105 }
2106 xml = f;
2107 }
2108 Ok(xml)
2109}
2110
2111async fn resolve_xlink_references(
2115 downloader: &DashDownloader,
2116 xot: &mut Xot,
2117 node: xot::Node) -> Result<(), DashMpdError>
2118{
2119 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
2120 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
2121 let xlinked = xot.descendants(node)
2122 .filter(|d| xot.get_attribute(*d, xlink_href_name.into()).is_some())
2123 .collect::<Vec<_>>();
2124 for xl in xlinked {
2125 if element_resolves_to_zero(xot, xl) {
2126 trace!("Removing node with resolve-to-zero xlink:href {xl:?}");
2127 if let Err(e) = xot.remove(xl) {
2128 return Err(parse_error("Failed to remove resolve-to-zero XML node", e));
2129 }
2130 } else if let Some(href) = xot.get_attribute(xl, xlink_href_name.into()) {
2131 if fetchable_xlink_href(href) {
2132 let xlink_url = if is_absolute_url(href) {
2133 Url::parse(href)
2134 .map_err(|e|
2135 if let Ok(ns) = xot.to_string(node) {
2136 parse_error(&format!("parsing XLink on {ns}"), e)
2137 } else {
2138 parse_error("parsing XLink", e)
2139 }
2140 )?
2141 } else {
2142 let mut merged = downloader.redirected_url.join(href)
2145 .map_err(|e|
2146 if let Ok(ns) = xot.to_string(node) {
2147 parse_error(&format!("parsing XLink on {ns}"), e)
2148 } else {
2149 parse_error("parsing XLink", e)
2150 }
2151 )?;
2152 merged.set_query(downloader.redirected_url.query());
2153 merged
2154 };
2155 let client = downloader.http_client.as_ref().unwrap();
2156 trace!("Fetching XLinked element {}", xlink_url.clone());
2157 let mut req = client.get(xlink_url.clone())
2158 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
2159 .header("Accept-Language", "en-US,en")
2160 .header("Sec-Fetch-Mode", "navigate");
2161 if let Some(referer) = &downloader.referer {
2162 req = req.header("Referer", referer);
2163 } else {
2164 req = req.header("Referer", downloader.redirected_url.to_string());
2165 }
2166 if let Some(username) = &downloader.auth_username {
2167 if let Some(password) = &downloader.auth_password {
2168 req = req.basic_auth(username, Some(password));
2169 }
2170 }
2171 if let Some(token) = &downloader.auth_bearer_token {
2172 req = req.bearer_auth(token);
2173 }
2174 let xml = req.send().await
2175 .map_err(|e|
2176 if let Ok(ns) = xot.to_string(node) {
2177 network_error(&format!("fetching XLink for {ns}"), &e)
2178 } else {
2179 network_error("fetching XLink", &e)
2180 }
2181 )?
2182 .error_for_status()
2183 .map_err(|e|
2184 if let Ok(ns) = xot.to_string(node) {
2185 network_error(&format!("fetching XLink for {ns}"), &e)
2186 } else {
2187 network_error("fetching XLink", &e)
2188 }
2189 )?
2190 .text().await
2191 .map_err(|e|
2192 if let Ok(ns) = xot.to_string(node) {
2193 network_error(&format!("resolving XLink for {ns}"), &e)
2194 } else {
2195 network_error("resolving XLink", &e)
2196 }
2197 )?;
2198 if downloader.verbosity > 2 {
2199 if let Ok(ns) = xot.to_string(node) {
2200 info!(" Resolved onLoad XLink {xlink_url} on {ns} -> {} octets", xml.len());
2201 } else {
2202 info!(" Resolved onLoad XLink {xlink_url} -> {} octets", xml.len());
2203 }
2204 }
2205 let wrapped_xml = r#"<?xml version="1.0" encoding="utf-8"?>"#.to_owned() +
2211 r#"<wrapper xmlns="urn:mpeg:dash:schema:mpd:2011" "# +
2212 r#"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" "# +
2213 r#"xmlns:cenc="urn:mpeg:cenc:2013" "# +
2214 r#"xmlns:mspr="urn:microsoft:playready" "# +
2215 r#"xmlns:xlink="http://www.w3.org/1999/xlink">"# +
2216 skip_xml_preamble(&xml) +
2217 r"</wrapper>";
2218 let wrapper_doc = xot.parse(&wrapped_xml)
2219 .map_err(|e| parse_error("parsing xlinked content", e))?;
2220 let wrapper_doc_el = xot.document_element(wrapper_doc)
2221 .map_err(|e| parse_error("extracting XML document element", e))?;
2222 for needs_insertion in xot.children(wrapper_doc_el).collect::<Vec<_>>() {
2223 xot.insert_after(xl, needs_insertion)
2225 .map_err(|e| parse_error("inserting XLinked content", e))?;
2226 }
2227 xot.remove(xl)
2228 .map_err(|e| parse_error("removing XLink node", e))?;
2229 }
2230 }
2231 }
2232 Ok(())
2233}
2234
2235#[tracing::instrument(level="trace", skip_all)]
2236pub async fn parse_resolving_xlinks(
2237 downloader: &DashDownloader,
2238 xml: &[u8]) -> Result<MPD, DashMpdError>
2239{
2240 use xot::xmlname::NameStrInfo;
2241
2242 let mut xot = Xot::new();
2243 let doc = xot.parse_bytes(xml)
2244 .map_err(|e| parse_error("XML parsing", e))?;
2245 let doc_el = xot.document_element(doc)
2246 .map_err(|e| parse_error("extracting XML document element", e))?;
2247 let doc_name = match xot.node_name(doc_el) {
2248 Some(n) => n,
2249 None => return Err(DashMpdError::Parsing(String::from("missing root node name"))),
2250 };
2251 let root_name = xot.name_ref(doc_name, doc_el)
2252 .map_err(|e| parse_error("extracting root node name", e))?;
2253 let root_local_name = root_name.local_name();
2254 if !root_local_name.eq("MPD") {
2255 return Err(DashMpdError::Parsing(format!("root element is {root_local_name}, expecting <MPD>")));
2256 }
2257 for _ in 1..5 {
2260 resolve_xlink_references(downloader, &mut xot, doc).await?;
2261 }
2262 let rewritten = apply_xslt_stylesheets(downloader, &mut xot, doc).await?;
2263 let mpd = parse(&rewritten)?;
2265 if downloader.conformity_checks {
2266 for emsg in check_conformity(&mpd) {
2267 warn!("DASH conformity error in manifest: {emsg}");
2268 }
2269 }
2270 Ok(mpd)
2271}
2272
2273async fn do_segmentbase_indexrange(
2274 downloader: &DashDownloader,
2275 period_counter: u8,
2276 base_url: Url,
2277 sb: &SegmentBase,
2278 dict: &HashMap<&str, String>
2279) -> Result<Vec<MediaFragment>, DashMpdError>
2280{
2281 let mut fragments = Vec::new();
2314 let mut start_byte: Option<u64> = None;
2315 let mut end_byte: Option<u64> = None;
2316 let mut indexable_segments = false;
2317 if downloader.use_index_range {
2318 if let Some(ir) = &sb.indexRange {
2319 let (s, e) = parse_range(ir)?;
2321 trace!("Fetching sidx for {}", base_url.clone());
2322 let mut req = downloader.http_client.as_ref()
2323 .unwrap()
2324 .get(base_url.clone())
2325 .header(RANGE, format!("bytes={s}-{e}"))
2326 .header("Referer", downloader.redirected_url.to_string())
2327 .header("Sec-Fetch-Mode", "navigate");
2328 if let Some(username) = &downloader.auth_username {
2329 if let Some(password) = &downloader.auth_password {
2330 req = req.basic_auth(username, Some(password));
2331 }
2332 }
2333 if let Some(token) = &downloader.auth_bearer_token {
2334 req = req.bearer_auth(token);
2335 }
2336 let mut resp = req.send().await
2337 .map_err(|e| network_error("fetching index data", &e))?
2338 .error_for_status()
2339 .map_err(|e| network_error("fetching index data", &e))?;
2340 let headers = std::mem::take(resp.headers_mut());
2341 if let Some(content_type) = headers.get(CONTENT_TYPE) {
2342 let idx = resp.bytes().await
2343 .map_err(|e| network_error("fetching index data", &e))?;
2344 if idx.len() as u64 != e - s + 1 {
2345 warn!(" HTTP server does not support Range requests; can't use indexRange addressing");
2346 } else {
2347 #[allow(clippy::collapsible_else_if)]
2348 if content_type.eq("video/mp4") ||
2349 content_type.eq("audio/mp4") {
2350 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2357 .with_range(Some(0), Some(e))
2358 .build();
2359 fragments.push(mf);
2360 let mut max_chunk_pos = 0;
2361 if let Ok(segment_chunks) = crate::sidx::from_isobmff_sidx(&idx, e+1) {
2362 trace!("Have {} segment chunks in sidx data", segment_chunks.len());
2363 for chunk in segment_chunks {
2364 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2365 .with_range(Some(chunk.start), Some(chunk.end))
2366 .build();
2367 fragments.push(mf);
2368 if chunk.end > max_chunk_pos {
2369 max_chunk_pos = chunk.end;
2370 }
2371 }
2372 indexable_segments = true;
2373 }
2374 }
2375 }
2382 }
2383 }
2384 }
2385 if indexable_segments {
2386 if let Some(init) = &sb.Initialization {
2387 if let Some(range) = &init.range {
2388 let (s, e) = parse_range(range)?;
2389 start_byte = Some(s);
2390 end_byte = Some(e);
2391 }
2392 if let Some(su) = &init.sourceURL {
2393 let path = resolve_url_template(su, dict);
2394 let u = merge_baseurls(&base_url, &path)?;
2395 let mf = MediaFragmentBuilder::new(period_counter, u)
2396 .with_range(start_byte, end_byte)
2397 .set_init()
2398 .build();
2399 fragments.push(mf);
2400 } else {
2401 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2403 .with_range(start_byte, end_byte)
2404 .set_init()
2405 .build();
2406 fragments.push(mf);
2407 }
2408 }
2409 } else {
2410 trace!("Falling back to retrieving full SegmentBase for {}", base_url.clone());
2415 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2416 .with_timeout(Duration::new(10_000, 0))
2417 .build();
2418 fragments.push(mf);
2419 }
2420 Ok(fragments)
2421}
2422
2423
2424#[tracing::instrument(level="trace", skip_all)]
2425async fn do_period_audio(
2426 downloader: &DashDownloader,
2427 mpd: &MPD,
2428 period: &Period,
2429 period_counter: u8,
2430 base_url: Url
2431) -> Result<PeriodOutputs, DashMpdError>
2432{
2433 let mut fragments = Vec::new();
2434 let mut diagnostics = Vec::new();
2435 let mut opt_init: Option<String> = None;
2436 let mut opt_media: Option<String> = None;
2437 let mut opt_duration: Option<f64> = None;
2438 let mut timescale = 1;
2439 let mut start_number = 1;
2440 let mut period_duration_secs: f64 = -1.0;
2443 if let Some(d) = mpd.mediaPresentationDuration {
2444 period_duration_secs = d.as_secs_f64();
2445 }
2446 if let Some(d) = period.duration {
2447 period_duration_secs = d.as_secs_f64();
2448 }
2449 if let Some(s) = downloader.force_duration {
2450 period_duration_secs = s;
2451 }
2452 if let Some(st) = &period.SegmentTemplate {
2456 if let Some(i) = &st.initialization {
2457 opt_init = Some(i.clone());
2458 }
2459 if let Some(m) = &st.media {
2460 opt_media = Some(m.clone());
2461 }
2462 if let Some(d) = st.duration {
2463 opt_duration = Some(d);
2464 }
2465 if let Some(ts) = st.timescale {
2466 timescale = ts;
2467 }
2468 if let Some(s) = st.startNumber {
2469 start_number = s;
2470 }
2471 }
2472 let mut selected_audio_language = "unk";
2473 let audio_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2476 .filter(is_audio_adaptation)
2477 .collect();
2478 let representations: Vec<&Representation> = select_preferred_adaptations(audio_adaptations, downloader)
2479 .iter()
2480 .flat_map(|a| a.representations.iter())
2481 .collect();
2482 if let Some(audio_repr) = select_preferred_representation(&representations, downloader) {
2483 let audio_adaptation = period.adaptations.iter()
2487 .find(|a| a.representations.iter().any(|r| r.eq(audio_repr)))
2488 .unwrap();
2489 if let Some(lang) = audio_repr.lang.as_ref().or(audio_adaptation.lang.as_ref()) {
2490 selected_audio_language = lang;
2491 }
2492 let mut base_url = base_url.clone();
2495 if let Some(bu) = &audio_adaptation.BaseURL.first() {
2496 base_url = merge_baseurls(&base_url, &bu.base)?;
2497 }
2498 if let Some(bu) = audio_repr.BaseURL.first() {
2499 base_url = merge_baseurls(&base_url, &bu.base)?;
2500 }
2501 if downloader.verbosity > 0 {
2502 let bw = if let Some(bw) = audio_repr.bandwidth {
2503 format!("bw={} Kbps ", bw / 1024)
2504 } else {
2505 String::from("")
2506 };
2507 let unknown = String::from("?");
2508 let lang = audio_repr.lang.as_ref()
2509 .unwrap_or(audio_adaptation.lang.as_ref()
2510 .unwrap_or(&unknown));
2511 let codec = audio_repr.codecs.as_ref()
2512 .unwrap_or(audio_adaptation.codecs.as_ref()
2513 .unwrap_or(&unknown));
2514 let maybe_id = if let Some(rid) = &audio_repr.id {
2515 format!(" (id={rid})")
2516 } else {
2517 String::from("")
2518 };
2519 diagnostics.push(format!(" Audio stream selected: {bw}lang={lang} codec={codec}{maybe_id}"));
2520 for cp in audio_repr.ContentProtection.iter()
2522 .chain(audio_adaptation.ContentProtection.iter())
2523 {
2524 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2525 if let Some(kid) = &cp.default_KID {
2526 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2527 }
2528 for pssh_element in &cp.cenc_pssh {
2529 if let Some(pssh_b64) = &pssh_element.content {
2530 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
2531 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
2532 diagnostics.push(format!(" {pssh}"));
2533 }
2534 }
2535 }
2536 }
2537 }
2538 if let Some(st) = &audio_adaptation.SegmentTemplate {
2543 if let Some(i) = &st.initialization {
2544 opt_init = Some(i.clone());
2545 }
2546 if let Some(m) = &st.media {
2547 opt_media = Some(m.clone());
2548 }
2549 if let Some(d) = st.duration {
2550 opt_duration = Some(d);
2551 }
2552 if let Some(ts) = st.timescale {
2553 timescale = ts;
2554 }
2555 if let Some(s) = st.startNumber {
2556 start_number = s;
2557 }
2558 }
2559 let mut dict = HashMap::new();
2560 if let Some(rid) = &audio_repr.id {
2561 dict.insert("RepresentationID", rid.clone());
2562 }
2563 if let Some(b) = &audio_repr.bandwidth {
2564 dict.insert("Bandwidth", b.to_string());
2565 }
2566 if let Some(sl) = &audio_adaptation.SegmentList {
2575 if downloader.verbosity > 1 {
2578 info!(" Using AdaptationSet>SegmentList addressing mode for audio representation");
2579 }
2580 let mut start_byte: Option<u64> = None;
2581 let mut end_byte: Option<u64> = None;
2582 if let Some(init) = &sl.Initialization {
2583 if let Some(range) = &init.range {
2584 let (s, e) = parse_range(range)?;
2585 start_byte = Some(s);
2586 end_byte = Some(e);
2587 }
2588 if let Some(su) = &init.sourceURL {
2589 let path = resolve_url_template(su, &dict);
2590 let init_url = merge_baseurls(&base_url, &path)?;
2591 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2592 .with_range(start_byte, end_byte)
2593 .set_init()
2594 .build();
2595 fragments.push(mf);
2596 } else {
2597 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2598 .with_range(start_byte, end_byte)
2599 .set_init()
2600 .build();
2601 fragments.push(mf);
2602 }
2603 }
2604 for su in &sl.segment_urls {
2605 start_byte = None;
2606 end_byte = None;
2607 if let Some(range) = &su.mediaRange {
2609 let (s, e) = parse_range(range)?;
2610 start_byte = Some(s);
2611 end_byte = Some(e);
2612 }
2613 if let Some(m) = &su.media {
2614 let u = merge_baseurls(&base_url, m)?;
2615 let mf = MediaFragmentBuilder::new(period_counter, u)
2616 .with_range(start_byte, end_byte)
2617 .build();
2618 fragments.push(mf);
2619 } else if let Some(bu) = audio_adaptation.BaseURL.first() {
2620 let u = merge_baseurls(&base_url, &bu.base)?;
2621 let mf = MediaFragmentBuilder::new(period_counter, u)
2622 .with_range(start_byte, end_byte)
2623 .build();
2624 fragments.push(mf);
2625 }
2626 }
2627 }
2628 if let Some(sl) = &audio_repr.SegmentList {
2629 if downloader.verbosity > 1 {
2631 info!(" Using Representation>SegmentList addressing mode for audio representation");
2632 }
2633 let mut start_byte: Option<u64> = None;
2634 let mut end_byte: Option<u64> = None;
2635 if let Some(init) = &sl.Initialization {
2636 if let Some(range) = &init.range {
2637 let (s, e) = parse_range(range)?;
2638 start_byte = Some(s);
2639 end_byte = Some(e);
2640 }
2641 if let Some(su) = &init.sourceURL {
2642 let path = resolve_url_template(su, &dict);
2643 let init_url = merge_baseurls(&base_url, &path)?;
2644 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2645 .with_range(start_byte, end_byte)
2646 .set_init()
2647 .build();
2648 fragments.push(mf);
2649 } else {
2650 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2651 .with_range(start_byte, end_byte)
2652 .set_init()
2653 .build();
2654 fragments.push(mf);
2655 }
2656 }
2657 for su in &sl.segment_urls {
2658 start_byte = None;
2659 end_byte = None;
2660 if let Some(range) = &su.mediaRange {
2662 let (s, e) = parse_range(range)?;
2663 start_byte = Some(s);
2664 end_byte = Some(e);
2665 }
2666 if let Some(m) = &su.media {
2667 let u = merge_baseurls(&base_url, m)?;
2668 let mf = MediaFragmentBuilder::new(period_counter, u)
2669 .with_range(start_byte, end_byte)
2670 .build();
2671 fragments.push(mf);
2672 } else if let Some(bu) = audio_repr.BaseURL.first() {
2673 let u = merge_baseurls(&base_url, &bu.base)?;
2674 let mf = MediaFragmentBuilder::new(period_counter, u)
2675 .with_range(start_byte, end_byte)
2676 .build();
2677 fragments.push(mf);
2678 }
2679 }
2680 } else if audio_repr.SegmentTemplate.is_some() ||
2681 audio_adaptation.SegmentTemplate.is_some()
2682 {
2683 let st;
2686 if let Some(it) = &audio_repr.SegmentTemplate {
2687 st = it;
2688 } else if let Some(it) = &audio_adaptation.SegmentTemplate {
2689 st = it;
2690 } else {
2691 panic!("unreachable");
2692 }
2693 if let Some(i) = &st.initialization {
2694 opt_init = Some(i.clone());
2695 }
2696 if let Some(m) = &st.media {
2697 opt_media = Some(m.clone());
2698 }
2699 if let Some(ts) = st.timescale {
2700 timescale = ts;
2701 }
2702 if let Some(sn) = st.startNumber {
2703 start_number = sn;
2704 }
2705 if let Some(stl) = &audio_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
2706 .or(audio_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
2707 {
2708 if downloader.verbosity > 1 {
2711 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for audio representation");
2712 }
2713 if let Some(init) = opt_init {
2714 let path = resolve_url_template(&init, &dict);
2715 let u = merge_baseurls(&base_url, &path)?;
2716 let mf = MediaFragmentBuilder::new(period_counter, u)
2717 .set_init()
2718 .build();
2719 fragments.push(mf);
2720 }
2721 let mut elapsed_seconds = 0.0;
2722 if let Some(media) = opt_media {
2723 let audio_path = resolve_url_template(&media, &dict);
2724 let mut segment_time = 0;
2725 let mut segment_duration;
2726 let mut number = start_number;
2727 let mut target_duration = period_duration_secs;
2728 if let Some(target) = downloader.force_duration {
2729 if target > period_duration_secs {
2730 warn!(" Requested forced duration exceeds available content");
2731 } else {
2732 target_duration = target;
2733 }
2734 }
2735 'segment_loop: for s in &stl.segments {
2736 if let Some(t) = s.t {
2737 segment_time = t;
2738 }
2739 segment_duration = s.d;
2740 let dict = HashMap::from([("Time", segment_time.to_string()),
2742 ("Number", number.to_string())]);
2743 let path = resolve_url_template(&audio_path, &dict);
2744 let u = merge_baseurls(&base_url, &path)?;
2745 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2746 number += 1;
2747 elapsed_seconds += segment_duration as f64 / timescale as f64;
2748 if downloader.force_duration.is_some() &&
2749 target_duration > 0.0 &&
2750 elapsed_seconds > target_duration {
2751 break 'segment_loop;
2752 }
2753 if let Some(r) = s.r {
2754 let mut count = 0i64;
2755 loop {
2756 count += 1;
2757 if r >= 0 && count > r {
2762 break;
2763 }
2764 if downloader.force_duration.is_some() &&
2765 target_duration > 0.0 &&
2766 elapsed_seconds > target_duration {
2767 break 'segment_loop;
2768 }
2769 if let Some(end_number) = st.endNumber {
2770 if count as u64 > end_number {
2771 break;
2772 }
2773 }
2774 segment_time += segment_duration;
2775 elapsed_seconds += segment_duration as f64 / timescale as f64;
2776 let dict = HashMap::from([("Time", segment_time.to_string()),
2777 ("Number", number.to_string())]);
2778 let path = resolve_url_template(&audio_path, &dict);
2779 let u = merge_baseurls(&base_url, &path)?;
2780 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2781 number += 1;
2782 }
2783 }
2784 segment_time += segment_duration;
2785 }
2786 } else {
2787 return Err(DashMpdError::UnhandledMediaStream(
2788 "SegmentTimeline without a media attribute".to_string()));
2789 }
2790 } else { if downloader.verbosity > 1 {
2795 info!(" Using SegmentTemplate addressing mode for audio representation");
2796 }
2797 let mut total_number = 0i64;
2798 if let Some(init) = opt_init {
2799 let path = resolve_url_template(&init, &dict);
2800 let u = merge_baseurls(&base_url, &path)?;
2801 let mf = MediaFragmentBuilder::new(period_counter, u)
2802 .set_init()
2803 .build();
2804 fragments.push(mf);
2805 }
2806 if let Some(media) = opt_media {
2807 let audio_path = resolve_url_template(&media, &dict);
2808 let timescale = st.timescale.unwrap_or(timescale);
2809 let mut segment_duration: f64 = -1.0;
2810 if let Some(d) = opt_duration {
2811 segment_duration = d;
2813 }
2814 if let Some(std) = st.duration {
2815 if timescale == 0 {
2816 return Err(DashMpdError::UnhandledMediaStream(
2817 "SegmentTemplate@duration attribute cannot be zero".to_string()));
2818 }
2819 segment_duration = std / timescale as f64;
2820 }
2821 if segment_duration < 0.0 {
2822 return Err(DashMpdError::UnhandledMediaStream(
2823 "Audio representation is missing SegmentTemplate@duration attribute".to_string()));
2824 }
2825 total_number += (period_duration_secs / segment_duration).round() as i64;
2826 let mut number = start_number;
2827 if mpd_is_dynamic(mpd) {
2830 if let Some(start_time) = mpd.availabilityStartTime {
2831 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
2832 number = (elapsed + number as f64 - 1f64).floor() as u64;
2833 } else {
2834 return Err(DashMpdError::UnhandledMediaStream(
2835 "dynamic manifest is missing @availabilityStartTime".to_string()));
2836 }
2837 }
2838 if let Some(end_number) = st.endNumber {
2839 total_number = end_number as i64;
2840 }
2841 for _ in 1..=total_number {
2842 let dict = HashMap::from([("Number", number.to_string())]);
2843 let path = resolve_url_template(&audio_path, &dict);
2844 let u = merge_baseurls(&base_url, &path)?;
2845 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2846 number += 1;
2847 }
2848 }
2849 }
2850 } else if let Some(sb) = &audio_repr.SegmentBase {
2851 if downloader.verbosity > 1 {
2853 info!(" Using SegmentBase@indexRange addressing mode for audio representation");
2854 }
2855 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
2856 fragments.extend(mf);
2857 } else if fragments.is_empty() {
2858 if let Some(bu) = audio_repr.BaseURL.first() {
2859 if downloader.verbosity > 1 {
2861 info!(" Using BaseURL addressing mode for audio representation");
2862 }
2863 let u = merge_baseurls(&base_url, &bu.base)?;
2864 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2865 }
2866 }
2867 if fragments.is_empty() {
2868 return Err(DashMpdError::UnhandledMediaStream(
2869 "no usable addressing mode identified for audio representation".to_string()));
2870 }
2871 }
2872 Ok(PeriodOutputs {
2873 fragments,
2874 diagnostics,
2875 subtitle_formats: Vec::new(),
2876 selected_audio_language: String::from(selected_audio_language),
2877 selected_subtitle_language: String::from("")
2878
2879 })
2880}
2881
2882
2883#[tracing::instrument(level="trace", skip_all)]
2884async fn do_period_video(
2885 downloader: &DashDownloader,
2886 mpd: &MPD,
2887 period: &Period,
2888 period_counter: u8,
2889 base_url: Url
2890 ) -> Result<PeriodOutputs, DashMpdError>
2891{
2892 let mut fragments = Vec::new();
2893 let mut diagnostics = Vec::new();
2894 let mut period_duration_secs: f64 = 0.0;
2895 let mut opt_init: Option<String> = None;
2896 let mut opt_media: Option<String> = None;
2897 let mut opt_duration: Option<f64> = None;
2898 let mut timescale = 1;
2899 let mut start_number = 1;
2900 if let Some(d) = mpd.mediaPresentationDuration {
2901 period_duration_secs = d.as_secs_f64();
2902 }
2903 if let Some(d) = period.duration {
2904 period_duration_secs = d.as_secs_f64();
2905 }
2906 if let Some(s) = downloader.force_duration {
2907 period_duration_secs = s;
2908 }
2909 if let Some(st) = &period.SegmentTemplate {
2913 if let Some(i) = &st.initialization {
2914 opt_init = Some(i.clone());
2915 }
2916 if let Some(m) = &st.media {
2917 opt_media = Some(m.clone());
2918 }
2919 if let Some(d) = st.duration {
2920 opt_duration = Some(d);
2921 }
2922 if let Some(ts) = st.timescale {
2923 timescale = ts;
2924 }
2925 if let Some(s) = st.startNumber {
2926 start_number = s;
2927 }
2928 }
2929 let video_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2946 .filter(is_video_adaptation)
2947 .collect();
2948 let representations: Vec<&Representation> = select_preferred_adaptations(video_adaptations, downloader)
2949 .iter()
2950 .flat_map(|a| a.representations.iter())
2951 .collect();
2952 trace!("Before filtering we have {} Representations", representations.len());
2953 let representations = representation_filter_video_id(representations, downloader);
2954 trace!("After video_id filter we have {} Representations", representations.len());
2955 let representations = representation_filter_video_width(representations, downloader);
2956 trace!("After width filter we have {} Representations", representations.len());
2957 let representations = representation_filter_video_height(representations, downloader);
2958 trace!("After height filter we have {} Representations", representations.len());
2959 let representations = representation_filter_video_codec(representations, downloader);
2960 trace!("After video codec filter we have {} Representations", representations.len());
2961 let representations = representation_filter_video_quality(representations, downloader);
2962 trace!("After quality filter we have {} Representations", representations.len());
2963 if let Some(video_repr) = representations.first() {
2964 let video_adaptation = period.adaptations.iter()
2968 .find(|a| a.representations.iter().any(|r| r.eq(video_repr)))
2969 .unwrap();
2970 let mut base_url = base_url.clone();
2973 if let Some(bu) = &video_adaptation.BaseURL.first() {
2974 base_url = merge_baseurls(&base_url, &bu.base)?;
2975 }
2976 if let Some(bu) = &video_repr.BaseURL.first() {
2977 base_url = merge_baseurls(&base_url, &bu.base)?;
2978 }
2979 if downloader.verbosity > 0 {
2980 let bw = if let Some(bw) = video_repr.bandwidth.or(video_adaptation.maxBandwidth) {
2981 format!("bw={} Kbps ", bw / 1024)
2982 } else {
2983 String::from("")
2984 };
2985 let unknown = String::from("?");
2986 let w = video_repr.width.unwrap_or(video_adaptation.width.unwrap_or(0));
2987 let h = video_repr.height.unwrap_or(video_adaptation.height.unwrap_or(0));
2988 let fmt = if w == 0 || h == 0 {
2989 String::from("")
2990 } else {
2991 format!("resolution={w}x{h} ")
2992 };
2993 let codec = video_repr.codecs.as_ref()
2994 .unwrap_or(video_adaptation.codecs.as_ref().unwrap_or(&unknown));
2995 let maybe_id = if let Some(rid) = &video_repr.id {
2996 format!(" (id={rid})")
2997 } else {
2998 String::from("")
2999 };
3000 diagnostics.push(format!(" Video stream selected: {bw}{fmt}codec={codec}{maybe_id}"));
3001 for cp in video_repr.ContentProtection.iter()
3003 .chain(video_adaptation.ContentProtection.iter())
3004 {
3005 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
3006 if let Some(kid) = &cp.default_KID {
3007 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
3008 }
3009 for pssh_element in &cp.cenc_pssh {
3010 if let Some(pssh_b64) = &pssh_element.content {
3011 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
3012 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
3013 diagnostics.push(format!(" {pssh}"));
3014 }
3015 }
3016 }
3017 }
3018 }
3019 let mut dict = HashMap::new();
3020 if let Some(rid) = &video_repr.id {
3021 dict.insert("RepresentationID", rid.clone());
3022 }
3023 if let Some(b) = &video_repr.bandwidth {
3024 dict.insert("Bandwidth", b.to_string());
3025 }
3026 if let Some(st) = &video_adaptation.SegmentTemplate {
3031 if let Some(i) = &st.initialization {
3032 opt_init = Some(i.clone());
3033 }
3034 if let Some(m) = &st.media {
3035 opt_media = Some(m.clone());
3036 }
3037 if let Some(d) = st.duration {
3038 opt_duration = Some(d);
3039 }
3040 if let Some(ts) = st.timescale {
3041 timescale = ts;
3042 }
3043 if let Some(s) = st.startNumber {
3044 start_number = s;
3045 }
3046 }
3047 if let Some(sl) = &video_adaptation.SegmentList {
3051 if downloader.verbosity > 1 {
3053 info!(" Using AdaptationSet>SegmentList addressing mode for video representation");
3054 }
3055 let mut start_byte: Option<u64> = None;
3056 let mut end_byte: Option<u64> = None;
3057 if let Some(init) = &sl.Initialization {
3058 if let Some(range) = &init.range {
3059 let (s, e) = parse_range(range)?;
3060 start_byte = Some(s);
3061 end_byte = Some(e);
3062 }
3063 if let Some(su) = &init.sourceURL {
3064 let path = resolve_url_template(su, &dict);
3065 let u = merge_baseurls(&base_url, &path)?;
3066 let mf = MediaFragmentBuilder::new(period_counter, u)
3067 .with_range(start_byte, end_byte)
3068 .set_init()
3069 .build();
3070 fragments.push(mf);
3071 }
3072 } else {
3073 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3074 .with_range(start_byte, end_byte)
3075 .set_init()
3076 .build();
3077 fragments.push(mf);
3078 }
3079 for su in &sl.segment_urls {
3080 start_byte = None;
3081 end_byte = None;
3082 if let Some(range) = &su.mediaRange {
3084 let (s, e) = parse_range(range)?;
3085 start_byte = Some(s);
3086 end_byte = Some(e);
3087 }
3088 if let Some(m) = &su.media {
3089 let u = merge_baseurls(&base_url, m)?;
3090 let mf = MediaFragmentBuilder::new(period_counter, u)
3091 .with_range(start_byte, end_byte)
3092 .build();
3093 fragments.push(mf);
3094 } else if let Some(bu) = video_adaptation.BaseURL.first() {
3095 let u = merge_baseurls(&base_url, &bu.base)?;
3096 let mf = MediaFragmentBuilder::new(period_counter, u)
3097 .with_range(start_byte, end_byte)
3098 .build();
3099 fragments.push(mf);
3100 }
3101 }
3102 }
3103 if let Some(sl) = &video_repr.SegmentList {
3104 if downloader.verbosity > 1 {
3106 info!(" Using Representation>SegmentList addressing mode for video representation");
3107 }
3108 let mut start_byte: Option<u64> = None;
3109 let mut end_byte: Option<u64> = None;
3110 if let Some(init) = &sl.Initialization {
3111 if let Some(range) = &init.range {
3112 let (s, e) = parse_range(range)?;
3113 start_byte = Some(s);
3114 end_byte = Some(e);
3115 }
3116 if let Some(su) = &init.sourceURL {
3117 let path = resolve_url_template(su, &dict);
3118 let u = merge_baseurls(&base_url, &path)?;
3119 let mf = MediaFragmentBuilder::new(period_counter, u)
3120 .with_range(start_byte, end_byte)
3121 .set_init()
3122 .build();
3123 fragments.push(mf);
3124 } else {
3125 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3126 .with_range(start_byte, end_byte)
3127 .set_init()
3128 .build();
3129 fragments.push(mf);
3130 }
3131 }
3132 for su in &sl.segment_urls {
3133 start_byte = None;
3134 end_byte = None;
3135 if let Some(range) = &su.mediaRange {
3137 let (s, e) = parse_range(range)?;
3138 start_byte = Some(s);
3139 end_byte = Some(e);
3140 }
3141 if let Some(m) = &su.media {
3142 let u = merge_baseurls(&base_url, m)?;
3143 let mf = MediaFragmentBuilder::new(period_counter, u)
3144 .with_range(start_byte, end_byte)
3145 .build();
3146 fragments.push(mf);
3147 } else if let Some(bu) = video_repr.BaseURL.first() {
3148 let u = merge_baseurls(&base_url, &bu.base)?;
3149 let mf = MediaFragmentBuilder::new(period_counter, u)
3150 .with_range(start_byte, end_byte)
3151 .build();
3152 fragments.push(mf);
3153 }
3154 }
3155 } else if video_repr.SegmentTemplate.is_some() ||
3156 video_adaptation.SegmentTemplate.is_some() {
3157 let st;
3160 if let Some(it) = &video_repr.SegmentTemplate {
3161 st = it;
3162 } else if let Some(it) = &video_adaptation.SegmentTemplate {
3163 st = it;
3164 } else {
3165 panic!("impossible");
3166 }
3167 if let Some(i) = &st.initialization {
3168 opt_init = Some(i.clone());
3169 }
3170 if let Some(m) = &st.media {
3171 opt_media = Some(m.clone());
3172 }
3173 if let Some(ts) = st.timescale {
3174 timescale = ts;
3175 }
3176 if let Some(sn) = st.startNumber {
3177 start_number = sn;
3178 }
3179 if let Some(stl) = &video_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
3180 .or(video_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
3181 {
3182 if downloader.verbosity > 1 {
3184 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for video representation");
3185 }
3186 if let Some(init) = opt_init {
3187 let path = resolve_url_template(&init, &dict);
3188 let u = merge_baseurls(&base_url, &path)?;
3189 let mf = MediaFragmentBuilder::new(period_counter, u)
3190 .set_init()
3191 .build();
3192 fragments.push(mf);
3193 }
3194 let mut elapsed_seconds = 0.0;
3195 if let Some(media) = opt_media {
3196 let video_path = resolve_url_template(&media, &dict);
3197 let mut segment_time = 0;
3198 let mut segment_duration;
3199 let mut number = start_number;
3200 let mut target_duration = period_duration_secs;
3201 if let Some(target) = downloader.force_duration {
3202 if target > period_duration_secs {
3203 warn!(" Requested forced duration exceeds available content");
3204 } else {
3205 target_duration = target;
3206 }
3207 }
3208 'segment_loop: for s in &stl.segments {
3209 if let Some(t) = s.t {
3210 segment_time = t;
3211 }
3212 segment_duration = s.d;
3213 let dict = HashMap::from([("Time", segment_time.to_string()),
3215 ("Number", number.to_string())]);
3216 let path = resolve_url_template(&video_path, &dict);
3217 let u = merge_baseurls(&base_url, &path)?;
3218 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3219 fragments.push(mf);
3220 number += 1;
3221 elapsed_seconds += segment_duration as f64 / timescale as f64;
3222 if downloader.force_duration.is_some() &&
3223 target_duration > 0.0 &&
3224 elapsed_seconds > target_duration
3225 {
3226 break 'segment_loop;
3227 }
3228 if let Some(r) = s.r {
3229 let mut count = 0i64;
3230 loop {
3231 count += 1;
3232 if r >= 0 && count > r {
3238 break;
3239 }
3240 if downloader.force_duration.is_some() &&
3241 target_duration > 0.0 &&
3242 elapsed_seconds > target_duration
3243 {
3244 break 'segment_loop;
3245 }
3246 if let Some(end_number) = st.endNumber {
3247 if count as u64 > end_number {
3248 break;
3249 }
3250 }
3251 segment_time += segment_duration;
3252 elapsed_seconds += segment_duration as f64 / timescale as f64;
3253 let dict = HashMap::from([("Time", segment_time.to_string()),
3254 ("Number", number.to_string())]);
3255 let path = resolve_url_template(&video_path, &dict);
3256 let u = merge_baseurls(&base_url, &path)?;
3257 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3258 fragments.push(mf);
3259 number += 1;
3260 }
3261 }
3262 segment_time += segment_duration;
3263 }
3264 } else {
3265 return Err(DashMpdError::UnhandledMediaStream(
3266 "SegmentTimeline without a media attribute".to_string()));
3267 }
3268 } else { if downloader.verbosity > 1 {
3271 info!(" Using SegmentTemplate addressing mode for video representation");
3272 }
3273 let mut total_number = 0i64;
3274 if let Some(init) = opt_init {
3275 let path = resolve_url_template(&init, &dict);
3276 let u = merge_baseurls(&base_url, &path)?;
3277 let mf = MediaFragmentBuilder::new(period_counter, u)
3278 .set_init()
3279 .build();
3280 fragments.push(mf);
3281 }
3282 if let Some(media) = opt_media {
3283 let video_path = resolve_url_template(&media, &dict);
3284 let timescale = st.timescale.unwrap_or(timescale);
3285 let mut segment_duration: f64 = -1.0;
3286 if let Some(d) = opt_duration {
3287 segment_duration = d;
3289 }
3290 if let Some(std) = st.duration {
3291 if timescale == 0 {
3292 return Err(DashMpdError::UnhandledMediaStream(
3293 "SegmentTemplate@duration attribute cannot be zero".to_string()));
3294 }
3295 segment_duration = std / timescale as f64;
3296 }
3297 if segment_duration < 0.0 {
3298 return Err(DashMpdError::UnhandledMediaStream(
3299 "Video representation is missing SegmentTemplate@duration attribute".to_string()));
3300 }
3301 total_number += (period_duration_secs / segment_duration).round() as i64;
3302 let mut number = start_number;
3303 if mpd_is_dynamic(mpd) {
3313 if let Some(start_time) = mpd.availabilityStartTime {
3314 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
3315 number = (elapsed + number as f64 - 1f64).floor() as u64;
3316 } else {
3317 return Err(DashMpdError::UnhandledMediaStream(
3318 "dynamic manifest is missing @availabilityStartTime".to_string()));
3319 }
3320 }
3321 if let Some(end_number) = st.endNumber {
3322 total_number = end_number as i64;
3323 }
3324 for _ in 1..=total_number {
3325 let dict = HashMap::from([("Number", number.to_string())]);
3326 let path = resolve_url_template(&video_path, &dict);
3327 let u = merge_baseurls(&base_url, &path)?;
3328 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3329 fragments.push(mf);
3330 number += 1;
3331 }
3332 }
3333 }
3334 } else if let Some(sb) = &video_repr.SegmentBase {
3335 if downloader.verbosity > 1 {
3337 info!(" Using SegmentBase@indexRange addressing mode for video representation");
3338 }
3339 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
3340 fragments.extend(mf);
3341 } else if fragments.is_empty() {
3342 if let Some(bu) = video_repr.BaseURL.first() {
3343 if downloader.verbosity > 1 {
3345 info!(" Using BaseURL addressing mode for video representation");
3346 }
3347 let u = merge_baseurls(&base_url, &bu.base)?;
3348 let mf = MediaFragmentBuilder::new(period_counter, u)
3349 .with_timeout(Duration::new(10000, 0))
3350 .build();
3351 fragments.push(mf);
3352 }
3353 }
3354 if fragments.is_empty() {
3355 return Err(DashMpdError::UnhandledMediaStream(
3356 "no usable addressing mode identified for video representation".to_string()));
3357 }
3358 }
3359 Ok(PeriodOutputs {
3362 fragments,
3363 diagnostics,
3364 subtitle_formats: Vec::new(),
3365 selected_audio_language: String::from("unk"),
3366 selected_subtitle_language: String::from(""),
3367 })
3368}
3369
3370#[tracing::instrument(level="trace", skip_all)]
3371async fn do_period_subtitles(
3372 downloader: &DashDownloader,
3373 mpd: &MPD,
3374 period: &Period,
3375 period_counter: u8,
3376 base_url: Url
3377 ) -> Result<PeriodOutputs, DashMpdError>
3378{
3379 let client = downloader.http_client.as_ref().unwrap();
3380 let output_path = &downloader.output_path.as_ref().unwrap().clone();
3381 let period_output_path = output_path_for_period(output_path, period_counter);
3382 let mut fragments = Vec::new();
3383 let mut subtitle_formats = Vec::new();
3384 let mut period_duration_secs: f64 = 0.0;
3385 if let Some(d) = mpd.mediaPresentationDuration {
3386 period_duration_secs = d.as_secs_f64();
3387 }
3388 if let Some(d) = period.duration {
3389 period_duration_secs = d.as_secs_f64();
3390 }
3391 let maybe_subtitle_adaptation = if let Some(ref lang) = downloader.language_preference_subtitles {
3392 period.adaptations.iter().filter(is_subtitle_adaptation)
3393 .min_by_key(|a| adaptation_lang_distance(a, lang))
3394 } else {
3395 period.adaptations.iter().find(is_subtitle_adaptation)
3397 };
3398 let mut subtitle_lang: Option<String> = None;
3399 if downloader.fetch_subtitles {
3400 if let Some(subtitle_adaptation) = maybe_subtitle_adaptation {
3401 if let Some(lang) = subtitle_adaptation.lang.as_ref() {
3402 subtitle_lang = Some(lang.clone());
3403 }
3404 let subtitle_format = subtitle_type(&subtitle_adaptation);
3405 subtitle_formats.push(subtitle_format);
3406 if downloader.verbosity > 1 && downloader.fetch_subtitles {
3407 info!(" Retrieving subtitles in format {subtitle_format:?}");
3408 }
3409 let mut base_url = base_url.clone();
3412 if let Some(bu) = &subtitle_adaptation.BaseURL.first() {
3413 base_url = merge_baseurls(&base_url, &bu.base)?;
3414 }
3415 if let Some(rep) = subtitle_adaptation.representations.first() {
3418 if subtitle_lang.is_none() {
3419 if let Some(lang) = rep.lang.as_ref() {
3420 subtitle_lang = Some(lang.clone());
3421 }
3422 }
3423 if !rep.BaseURL.is_empty() {
3424 for st_bu in &rep.BaseURL {
3425 let st_url = merge_baseurls(&base_url, &st_bu.base)?;
3426 let mut req = client.get(st_url.clone());
3427 if let Some(referer) = &downloader.referer {
3428 req = req.header("Referer", referer);
3429 } else {
3430 req = req.header("Referer", base_url.to_string());
3431 }
3432 let rqw = req.build()
3433 .map_err(|e| network_error("building request", &e))?;
3434 let subs = reqwest_bytes_with_retries(client, rqw, 5).await
3435 .map_err(|e| network_error("fetching subtitles", &e))?;
3436 let mut subs_path = period_output_path.clone();
3437 let subtitle_format = subtitle_type(&subtitle_adaptation);
3438 match subtitle_format {
3439 SubtitleType::Vtt => subs_path.set_extension("vtt"),
3440 SubtitleType::Srt => subs_path.set_extension("srt"),
3441 SubtitleType::Sami => subs_path.set_extension("sami"),
3442 SubtitleType::Wvtt => subs_path.set_extension("wvtt"),
3443 SubtitleType::Ttml | SubtitleType::Stpp => subs_path.set_extension("ttml"),
3444 _ => subs_path.set_extension("sub"),
3445 };
3446 subtitle_formats.push(subtitle_format);
3447 let mut subs_file = File::create(subs_path.clone()).await
3448 .map_err(|e| DashMpdError::Io(e, String::from("creating subtitle file")))?;
3449 if downloader.verbosity > 2 {
3450 info!(" Subtitle {st_url} -> {} octets", subs.len());
3451 }
3452 match subs_file.write_all(&subs).await {
3453 Ok(()) => {
3454 if downloader.verbosity > 0 {
3455 info!(" Downloaded subtitles ({subtitle_format:?}) to {}",
3456 subs_path.display());
3457 }
3458 },
3459 Err(e) => {
3460 error!("Unable to write subtitle file: {e:?}");
3461 return Err(DashMpdError::Io(e, String::from("writing subtitle data")));
3462 },
3463 }
3464 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
3465 subtitle_formats.contains(&SubtitleType::Ttxt)
3466 {
3467 if downloader.verbosity > 0 {
3468 info!(" Converting subtitles to SRT format with MP4Box ");
3469 }
3470 let out = subs_path.with_extension("srt");
3471 let out_str = out.to_string_lossy();
3478 let subs_str = subs_path.to_string_lossy();
3479 let args = vec![
3480 "-srt", "1",
3481 "-out", &out_str,
3482 &subs_str];
3483 if downloader.verbosity > 0 {
3484 info!(" Running MPBox {}", args.join(" "));
3485 }
3486 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
3487 .args(args)
3488 .output()
3489 {
3490 let msg = partial_process_output(&mp4box.stdout);
3491 if !msg.is_empty() {
3492 info!("MP4Box stdout: {msg}");
3493 }
3494 let msg = partial_process_output(&mp4box.stderr);
3495 if !msg.is_empty() {
3496 info!("MP4Box stderr: {msg}");
3497 }
3498 if mp4box.status.success() {
3499 info!(" Converted subtitles to SRT");
3500 } else {
3501 warn!("Error running MP4Box to convert subtitles");
3502 }
3503 }
3504 }
3505 }
3506 } else if rep.SegmentTemplate.is_some() || subtitle_adaptation.SegmentTemplate.is_some() {
3507 let mut opt_init: Option<String> = None;
3508 let mut opt_media: Option<String> = None;
3509 let mut opt_duration: Option<f64> = None;
3510 let mut timescale = 1;
3511 let mut start_number = 1;
3512 if let Some(st) = &rep.SegmentTemplate {
3517 if let Some(i) = &st.initialization {
3518 opt_init = Some(i.clone());
3519 }
3520 if let Some(m) = &st.media {
3521 opt_media = Some(m.clone());
3522 }
3523 if let Some(d) = st.duration {
3524 opt_duration = Some(d);
3525 }
3526 if let Some(ts) = st.timescale {
3527 timescale = ts;
3528 }
3529 if let Some(s) = st.startNumber {
3530 start_number = s;
3531 }
3532 }
3533 let rid = match &rep.id {
3534 Some(id) => id,
3535 None => return Err(
3536 DashMpdError::UnhandledMediaStream(
3537 "Missing @id on Representation node".to_string())),
3538 };
3539 let mut dict = HashMap::from([("RepresentationID", rid.clone())]);
3540 if let Some(b) = &rep.bandwidth {
3541 dict.insert("Bandwidth", b.to_string());
3542 }
3543 if let Some(sl) = &rep.SegmentList {
3547 if downloader.verbosity > 1 {
3550 info!(" Using AdaptationSet>SegmentList addressing mode for subtitle representation");
3551 }
3552 let mut start_byte: Option<u64> = None;
3553 let mut end_byte: Option<u64> = None;
3554 if let Some(init) = &sl.Initialization {
3555 if let Some(range) = &init.range {
3556 let (s, e) = parse_range(range)?;
3557 start_byte = Some(s);
3558 end_byte = Some(e);
3559 }
3560 if let Some(su) = &init.sourceURL {
3561 let path = resolve_url_template(su, &dict);
3562 let u = merge_baseurls(&base_url, &path)?;
3563 let mf = MediaFragmentBuilder::new(period_counter, u)
3564 .with_range(start_byte, end_byte)
3565 .set_init()
3566 .build();
3567 fragments.push(mf);
3568 } else {
3569 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3570 .with_range(start_byte, end_byte)
3571 .set_init()
3572 .build();
3573 fragments.push(mf);
3574 }
3575 }
3576 for su in &sl.segment_urls {
3577 start_byte = None;
3578 end_byte = None;
3579 if let Some(range) = &su.mediaRange {
3581 let (s, e) = parse_range(range)?;
3582 start_byte = Some(s);
3583 end_byte = Some(e);
3584 }
3585 if let Some(m) = &su.media {
3586 let u = merge_baseurls(&base_url, m)?;
3587 let mf = MediaFragmentBuilder::new(period_counter, u)
3588 .with_range(start_byte, end_byte)
3589 .build();
3590 fragments.push(mf);
3591 } else if let Some(bu) = subtitle_adaptation.BaseURL.first() {
3592 let u = merge_baseurls(&base_url, &bu.base)?;
3593 let mf = MediaFragmentBuilder::new(period_counter, u)
3594 .with_range(start_byte, end_byte)
3595 .build();
3596 fragments.push(mf);
3597 }
3598 }
3599 }
3600 if let Some(sl) = &rep.SegmentList {
3601 if downloader.verbosity > 1 {
3603 info!(" Using Representation>SegmentList addressing mode for subtitle representation");
3604 }
3605 let mut start_byte: Option<u64> = None;
3606 let mut end_byte: Option<u64> = None;
3607 if let Some(init) = &sl.Initialization {
3608 if let Some(range) = &init.range {
3609 let (s, e) = parse_range(range)?;
3610 start_byte = Some(s);
3611 end_byte = Some(e);
3612 }
3613 if let Some(su) = &init.sourceURL {
3614 let path = resolve_url_template(su, &dict);
3615 let u = merge_baseurls(&base_url, &path)?;
3616 let mf = MediaFragmentBuilder::new(period_counter, u)
3617 .with_range(start_byte, end_byte)
3618 .set_init()
3619 .build();
3620 fragments.push(mf);
3621 } else {
3622 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3623 .with_range(start_byte, end_byte)
3624 .set_init()
3625 .build();
3626 fragments.push(mf);
3627 }
3628 }
3629 for su in &sl.segment_urls {
3630 start_byte = None;
3631 end_byte = None;
3632 if let Some(range) = &su.mediaRange {
3634 let (s, e) = parse_range(range)?;
3635 start_byte = Some(s);
3636 end_byte = Some(e);
3637 }
3638 if let Some(m) = &su.media {
3639 let u = merge_baseurls(&base_url, m)?;
3640 let mf = MediaFragmentBuilder::new(period_counter, u)
3641 .with_range(start_byte, end_byte)
3642 .build();
3643 fragments.push(mf);
3644 } else if let Some(bu) = &rep.BaseURL.first() {
3645 let u = merge_baseurls(&base_url, &bu.base)?;
3646 let mf = MediaFragmentBuilder::new(period_counter, u)
3647 .with_range(start_byte, end_byte)
3648 .build();
3649 fragments.push(mf);
3650 }
3651 }
3652 } else if rep.SegmentTemplate.is_some() ||
3653 subtitle_adaptation.SegmentTemplate.is_some()
3654 {
3655 let st;
3658 if let Some(it) = &rep.SegmentTemplate {
3659 st = it;
3660 } else if let Some(it) = &subtitle_adaptation.SegmentTemplate {
3661 st = it;
3662 } else {
3663 panic!("unreachable");
3664 }
3665 if let Some(i) = &st.initialization {
3666 opt_init = Some(i.clone());
3667 }
3668 if let Some(m) = &st.media {
3669 opt_media = Some(m.clone());
3670 }
3671 if let Some(ts) = st.timescale {
3672 timescale = ts;
3673 }
3674 if let Some(sn) = st.startNumber {
3675 start_number = sn;
3676 }
3677 if let Some(stl) = &rep.SegmentTemplate.as_ref()
3678 .and_then(|st| st.SegmentTimeline.clone())
3679 .or(subtitle_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
3680 {
3681 if downloader.verbosity > 1 {
3684 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for subtitle representation");
3685 }
3686 if let Some(init) = opt_init {
3687 let path = resolve_url_template(&init, &dict);
3688 let u = merge_baseurls(&base_url, &path)?;
3689 let mf = MediaFragmentBuilder::new(period_counter, u)
3690 .set_init()
3691 .build();
3692 fragments.push(mf);
3693 }
3694 if let Some(media) = opt_media {
3695 let sub_path = resolve_url_template(&media, &dict);
3696 let mut segment_time = 0;
3697 let mut segment_duration;
3698 let mut number = start_number;
3699 for s in &stl.segments {
3700 if let Some(t) = s.t {
3701 segment_time = t;
3702 }
3703 segment_duration = s.d;
3704 let dict = HashMap::from([("Time", segment_time.to_string()),
3706 ("Number", number.to_string())]);
3707 let path = resolve_url_template(&sub_path, &dict);
3708 let u = merge_baseurls(&base_url, &path)?;
3709 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3710 fragments.push(mf);
3711 number += 1;
3712 if let Some(r) = s.r {
3713 let mut count = 0i64;
3714 let end_time = period_duration_secs * timescale as f64;
3716 loop {
3717 count += 1;
3718 if r >= 0 {
3724 if count > r {
3725 break;
3726 }
3727 if downloader.force_duration.is_some() &&
3728 segment_time as f64 > end_time
3729 {
3730 break;
3731 }
3732 } else if segment_time as f64 > end_time {
3733 break;
3734 }
3735 if let Some(end_number) = st.endNumber {
3736 if count as u64 > end_number {
3737 break;
3738 }
3739 }
3740 segment_time += segment_duration;
3741 let dict = HashMap::from([("Time", segment_time.to_string()),
3742 ("Number", number.to_string())]);
3743 let path = resolve_url_template(&sub_path, &dict);
3744 let u = merge_baseurls(&base_url, &path)?;
3745 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3746 fragments.push(mf);
3747 number += 1;
3748 }
3749 }
3750 segment_time += segment_duration;
3751 }
3752 } else {
3753 return Err(DashMpdError::UnhandledMediaStream(
3754 "SegmentTimeline without a media attribute".to_string()));
3755 }
3756 } else { if downloader.verbosity > 0 {
3761 info!(" Using SegmentTemplate addressing mode for stpp subtitles");
3762 }
3763 if let Some(i) = &st.initialization {
3764 opt_init = Some(i.clone());
3765 }
3766 if let Some(m) = &st.media {
3767 opt_media = Some(m.clone());
3768 }
3769 if let Some(d) = st.duration {
3770 opt_duration = Some(d);
3771 }
3772 if let Some(ts) = st.timescale {
3773 timescale = ts;
3774 }
3775 if let Some(s) = st.startNumber {
3776 start_number = s;
3777 }
3778 let rid = match &rep.id {
3779 Some(id) => id,
3780 None => return Err(
3781 DashMpdError::UnhandledMediaStream(
3782 "Missing @id on Representation node".to_string())),
3783 };
3784 let mut dict = HashMap::from([("RepresentationID", rid.clone())]);
3785 if let Some(b) = &rep.bandwidth {
3786 dict.insert("Bandwidth", b.to_string());
3787 }
3788 let mut total_number = 0i64;
3789 if let Some(init) = opt_init {
3790 let path = resolve_url_template(&init, &dict);
3791 let u = merge_baseurls(&base_url, &path)?;
3792 let mf = MediaFragmentBuilder::new(period_counter, u)
3793 .set_init()
3794 .build();
3795 fragments.push(mf);
3796 }
3797 if let Some(media) = opt_media {
3798 let sub_path = resolve_url_template(&media, &dict);
3799 let mut segment_duration: f64 = -1.0;
3800 if let Some(d) = opt_duration {
3801 segment_duration = d;
3803 }
3804 if let Some(std) = st.duration {
3805 if timescale == 0 {
3806 return Err(DashMpdError::UnhandledMediaStream(
3807 "SegmentTemplate@duration attribute cannot be zero".to_string()));
3808 }
3809 segment_duration = std / timescale as f64;
3810 }
3811 if segment_duration < 0.0 {
3812 return Err(DashMpdError::UnhandledMediaStream(
3813 "Subtitle representation is missing SegmentTemplate@duration".to_string()));
3814 }
3815 total_number += (period_duration_secs / segment_duration).ceil() as i64;
3816 if let Some(end_number) = st.endNumber {
3817 total_number = end_number as i64;
3818 }
3819 let mut number = start_number;
3820 #[allow(clippy::explicit_counter_loop)]
3821 for _ in 1..=total_number {
3822 let dict = HashMap::from([("Number", number.to_string())]);
3823 let path = resolve_url_template(&sub_path, &dict);
3824 let u = merge_baseurls(&base_url, &path)?;
3825 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3826 fragments.push(mf);
3827 number += 1;
3828 }
3829 }
3830 }
3831 } else if let Some(sb) = &rep.SegmentBase {
3832 info!(" Using SegmentBase@indexRange for subs");
3834 if downloader.verbosity > 1 {
3835 info!(" Using SegmentBase@indexRange addressing mode for subtitle representation");
3836 }
3837 let mut start_byte: Option<u64> = None;
3838 let mut end_byte: Option<u64> = None;
3839 if let Some(init) = &sb.Initialization {
3840 if let Some(range) = &init.range {
3841 let (s, e) = parse_range(range)?;
3842 start_byte = Some(s);
3843 end_byte = Some(e);
3844 }
3845 if let Some(su) = &init.sourceURL {
3846 let path = resolve_url_template(su, &dict);
3847 let u = merge_baseurls(&base_url, &path)?;
3848 let mf = MediaFragmentBuilder::new(period_counter, u)
3849 .with_range(start_byte, end_byte)
3850 .set_init()
3851 .build();
3852 fragments.push(mf);
3853 }
3854 }
3855 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3856 .set_init()
3857 .build();
3858 fragments.push(mf);
3859 }
3862 }
3863 }
3864 }
3865 }
3866 Ok(PeriodOutputs {
3867 fragments,
3868 diagnostics: Vec::new(),
3869 subtitle_formats,
3870 selected_audio_language: String::from("unk"),
3871 selected_subtitle_language: subtitle_lang.unwrap_or_else(|| String::from("unk")),
3872 })
3873}
3874
3875
3876struct DownloadState {
3879 period_counter: u8,
3880 segment_count: usize,
3881 segment_counter: usize,
3882 download_errors: u32
3883}
3884
3885#[tracing::instrument(level="trace", skip_all)]
3892async fn fetch_fragment(
3893 downloader: &mut DashDownloader,
3894 frag: &MediaFragment,
3895 fragment_type: &str,
3896 progress_percent: u32) -> Result<File, DashMpdError>
3897{
3898 let send_request = || async {
3899 trace!("send_request {}", frag.url.clone());
3900 let mut req = downloader.http_client.as_ref().unwrap()
3903 .get(frag.url.clone())
3904 .header("Accept", format!("{fragment_type}/*;q=0.9,*/*;q=0.5"))
3905 .header("Sec-Fetch-Mode", "navigate");
3906 if let Some(sb) = &frag.start_byte {
3907 if let Some(eb) = &frag.end_byte {
3908 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
3909 }
3910 }
3911 if let Some(ts) = &frag.timeout {
3912 req = req.timeout(*ts);
3913 }
3914 if let Some(referer) = &downloader.referer {
3915 req = req.header("Referer", referer);
3916 } else {
3917 req = req.header("Referer", downloader.redirected_url.to_string());
3918 }
3919 if let Some(username) = &downloader.auth_username {
3920 if let Some(password) = &downloader.auth_password {
3921 req = req.basic_auth(username, Some(password));
3922 }
3923 }
3924 if let Some(token) = &downloader.auth_bearer_token {
3925 req = req.bearer_auth(token);
3926 }
3927 req.send().await?
3928 .error_for_status()
3929 };
3930 match send_request
3931 .retry(ExponentialBuilder::default())
3932 .when(reqwest_error_transient_p)
3933 .notify(notify_transient)
3934 .await
3935 {
3936 Ok(response) => {
3937 match response.error_for_status() {
3938 Ok(mut resp) => {
3939 let tmp_out_std = tempfile::tempfile()
3940 .map_err(|e| DashMpdError::Io(e, String::from("creating tmpfile for fragment")))?;
3941 let mut tmp_out = tokio::fs::File::from_std(tmp_out_std);
3942 let content_type_checker = if fragment_type.eq("audio") {
3943 content_type_audio_p
3944 } else if fragment_type.eq("video") {
3945 content_type_video_p
3946 } else {
3947 panic!("fragment_type not audio or video");
3948 };
3949 if !downloader.content_type_checks || content_type_checker(&resp) {
3950 let mut fragment_out: Option<File> = None;
3951 if let Some(ref fragment_path) = downloader.fragment_path {
3952 if let Some(path) = frag.url.path_segments()
3953 .unwrap_or_else(|| "".split(' '))
3954 .next_back()
3955 {
3956 let vf_file = fragment_path.clone().join(fragment_type).join(path);
3957 if let Ok(f) = File::create(vf_file).await {
3958 fragment_out = Some(f);
3959 }
3960 }
3961 }
3962 let mut segment_size = 0;
3963 while let Some(chunk) = resp.chunk().await
3969 .map_err(|e| network_error(&format!("fetching DASH {fragment_type} segment"), &e))?
3970 {
3971 segment_size += chunk.len();
3972 downloader.bw_estimator_bytes += chunk.len();
3973 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
3974 throttle_download_rate(downloader, size).await?;
3975 if let Err(e) = tmp_out.write_all(&chunk).await {
3976 return Err(DashMpdError::Io(e, format!("writing DASH {fragment_type} data")));
3977 }
3978 if let Some(ref mut fout) = fragment_out {
3979 fout.write_all(&chunk)
3980 .map_err(|e| DashMpdError::Io(e, format!("writing {fragment_type} fragment")))
3981 .await?;
3982 }
3983 let elapsed = downloader.bw_estimator_started.elapsed().as_secs_f64();
3984 if (elapsed > 0.5) || (downloader.bw_estimator_bytes > 50_000) {
3985 let bw = downloader.bw_estimator_bytes as f64 / elapsed;
3986 for observer in &downloader.progress_observers {
3987 observer.update(progress_percent, bw as u64, &format!("Fetching {fragment_type} segments"));
3988 }
3989 downloader.bw_estimator_started = Instant::now();
3990 downloader.bw_estimator_bytes = 0;
3991 }
3992 }
3993 if downloader.verbosity > 2 {
3994 if let Some(sb) = &frag.start_byte {
3995 if let Some(eb) = &frag.end_byte {
3996 info!(" {fragment_type} segment {} range {sb}-{eb} -> {} octets",
3997 frag.url, segment_size);
3998 }
3999 } else {
4000 info!(" {fragment_type} segment {} -> {segment_size} octets", &frag.url);
4001 }
4002 }
4003 } else {
4004 warn!("Ignoring segment {} with non-{fragment_type} content-type", frag.url);
4005 }
4006 tmp_out.sync_all().await
4007 .map_err(|e| DashMpdError::Io(e, format!("syncing {fragment_type} fragment")))?;
4008 Ok(tmp_out)
4009 },
4010 Err(e) => Err(network_error("HTTP error", &e)),
4011 }
4012 },
4013 Err(e) => Err(network_error(&format!("{e:?}"), &e)),
4014 }
4015}
4016
4017
4018#[tracing::instrument(level="trace", skip_all)]
4020async fn fetch_period_audio(
4021 downloader: &mut DashDownloader,
4022 tmppath: &Path,
4023 audio_fragments: &[MediaFragment],
4024 ds: &mut DownloadState) -> Result<bool, DashMpdError>
4025{
4026 let start_download = Instant::now();
4027 let mut have_audio = false;
4028 {
4029 let tmpfile_audio = File::create(tmppath).await
4033 .map_err(|e| DashMpdError::Io(e, String::from("creating audio tmpfile")))?;
4034 ensure_permissions_readable(tmppath).await?;
4035 let mut tmpfile_audio = BufWriter::new(tmpfile_audio);
4036 if let Some(ref fragment_path) = downloader.fragment_path {
4038 let audio_fragment_dir = fragment_path.join("audio");
4039 if !audio_fragment_dir.exists() {
4040 fs::create_dir_all(audio_fragment_dir).await
4041 .map_err(|e| DashMpdError::Io(e, String::from("creating audio fragment dir")))?;
4042 }
4043 }
4044 for frag in audio_fragments.iter().filter(|f| f.period == ds.period_counter) {
4048 ds.segment_counter += 1;
4049 let progress_percent = min(98, (100.0 * ds.segment_counter as f32 / (2.0 + ds.segment_count as f32)).ceil() as u32);
4052 let url = &frag.url;
4053 if url.scheme() == "data" {
4057 let us = &url.to_string();
4058 let du = DataUrl::process(us)
4059 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
4060 if du.mime_type().type_ != "audio" {
4061 return Err(DashMpdError::UnhandledMediaStream(
4062 String::from("expecting audio content in data URL")));
4063 }
4064 let (body, _fragment) = du.decode_to_vec()
4065 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
4066 if downloader.verbosity > 2 {
4067 info!(" Audio segment data URL -> {} octets", body.len());
4068 }
4069 tmpfile_audio.write_all(&body)
4070 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH audio data")))
4071 .await?;
4072 have_audio = true;
4073 } else {
4074 'done: for _ in 0..downloader.fragment_retry_count {
4076 match fetch_fragment(downloader, frag, "audio", progress_percent).await {
4077 Ok(mut frag_file) => {
4078 frag_file.rewind().await
4079 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
4080 let mut buf = Vec::new();
4081 frag_file.read_to_end(&mut buf).await
4082 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
4083 tmpfile_audio.write_all(&buf)
4084 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH audio data")))
4085 .await?;
4086 have_audio = true;
4087 break 'done;
4088 },
4089 Err(e) => {
4090 if downloader.verbosity > 0 {
4091 error!("Error fetching audio segment {url}: {e:?}");
4092 }
4093 ds.download_errors += 1;
4094 if ds.download_errors > downloader.max_error_count {
4095 error!("max_error_count network errors encountered");
4096 return Err(DashMpdError::Network(
4097 String::from("more than max_error_count network errors")));
4098 }
4099 },
4100 }
4101 info!(" Retrying audio segment {url}");
4102 if downloader.sleep_between_requests > 0 {
4103 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
4104 }
4105 }
4106 }
4107 }
4108 tmpfile_audio.flush().map_err(|e| {
4109 error!("Couldn't flush DASH audio file: {e}");
4110 DashMpdError::Io(e, String::from("flushing DASH audio file"))
4111 }).await?;
4112 } if !downloader.decryption_keys.is_empty() {
4114 if downloader.verbosity > 0 {
4115 let metadata = fs::metadata(tmppath).await
4116 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted audio metadata")))?;
4117 info!(" Attempting to decrypt audio stream ({} kB) with {}",
4118 metadata.len() / 1024,
4119 downloader.decryptor_preference);
4120 }
4121 let out_ext = downloader.output_path.as_ref().unwrap()
4122 .extension()
4123 .unwrap_or(OsStr::new("mp4"));
4124 let decrypted = tmp_file_path("dashmpd-decrypted-audio", out_ext)?;
4125 if downloader.decryptor_preference.eq("mp4decrypt") {
4126 decrypt_mp4decrypt(downloader, tmppath, &decrypted, "audio").await?;
4127 } else if downloader.decryptor_preference.eq("shaka") {
4128 decrypt_shaka(downloader, tmppath, &decrypted, "audio").await?;
4129 } else if downloader.decryptor_preference.eq("shaka-container") {
4130 decrypt_shaka_container(downloader, tmppath, &decrypted, "audio").await?;
4131 } else if downloader.decryptor_preference.eq("mp4box") {
4132 decrypt_mp4box(downloader, tmppath, &decrypted, "audio").await?;
4133 } else if downloader.decryptor_preference.eq("mp4box-container") {
4134 decrypt_mp4box_container(downloader, tmppath, &decrypted, "audio").await?;
4135 } else {
4136 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
4137 }
4138 if let Err(e) = fs::metadata(&decrypted).await {
4139 return Err(DashMpdError::Decrypting(format!("missing decrypted audio file: {e:?}")));
4140 }
4141 fs::remove_file(&tmppath).await
4142 .map_err(|e| DashMpdError::Io(e, String::from("deleting encrypted audio tmpfile")))?;
4143 fs::rename(&decrypted, &tmppath).await
4144 .map_err(|e| {
4145 let dbg = Command::new("bash")
4146 .args(["-c", &format!("id;ls -l {}", decrypted.display())])
4147 .output()
4148 .unwrap();
4149 warn!("debugging ls: {}", String::from_utf8_lossy(&dbg.stdout));
4150 DashMpdError::Io(e, format!("renaming decrypted audio {}->{}", decrypted.display(), tmppath.display()))
4151 })?;
4152 }
4153 if let Ok(metadata) = fs::metadata(&tmppath).await {
4154 if downloader.verbosity > 1 {
4155 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4156 let elapsed = start_download.elapsed();
4157 info!(" Wrote {mbytes:.1}MB to DASH audio file ({:.1} MB/s)",
4158 mbytes / elapsed.as_secs_f64());
4159 }
4160 }
4161 Ok(have_audio)
4162}
4163
4164
4165#[tracing::instrument(level="trace", skip_all)]
4167async fn fetch_period_video(
4168 downloader: &mut DashDownloader,
4169 tmppath: &Path,
4170 video_fragments: &[MediaFragment],
4171 ds: &mut DownloadState) -> Result<bool, DashMpdError>
4172{
4173 let start_download = Instant::now();
4174 let mut have_video = false;
4175 {
4176 let tmpfile_video = File::create(tmppath).await
4180 .map_err(|e| DashMpdError::Io(e, String::from("creating video tmpfile")))?;
4181 ensure_permissions_readable(tmppath).await?;
4182 let mut tmpfile_video = BufWriter::new(tmpfile_video);
4183 if let Some(ref fragment_path) = downloader.fragment_path {
4185 let video_fragment_dir = fragment_path.join("video");
4186 if !video_fragment_dir.exists() {
4187 fs::create_dir_all(video_fragment_dir).await
4188 .map_err(|e| DashMpdError::Io(e, String::from("creating video fragment dir")))?;
4189 }
4190 }
4191 for frag in video_fragments.iter().filter(|f| f.period == ds.period_counter) {
4192 ds.segment_counter += 1;
4193 let progress_percent = min(98, (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32);
4196 if frag.url.scheme() == "data" {
4197 let us = &frag.url.to_string();
4198 let du = DataUrl::process(us)
4199 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
4200 if du.mime_type().type_ != "video" {
4201 return Err(DashMpdError::UnhandledMediaStream(
4202 String::from("expecting video content in data URL")));
4203 }
4204 let (body, _fragment) = du.decode_to_vec()
4205 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
4206 if downloader.verbosity > 2 {
4207 info!(" Video segment data URL -> {} octets", body.len());
4208 }
4209 tmpfile_video.write_all(&body)
4210 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH video data")))
4211 .await?;
4212 have_video = true;
4213 } else {
4214 'done: for _ in 0..downloader.fragment_retry_count {
4215 match fetch_fragment(downloader, frag, "video", progress_percent).await {
4216 Ok(mut frag_file) => {
4217 frag_file.rewind().await
4218 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
4219 let mut buf = Vec::new();
4220 frag_file.read_to_end(&mut buf).await
4221 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
4222 tmpfile_video.write_all(&buf)
4223 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH video data")))
4224 .await?;
4225 have_video = true;
4226 break 'done;
4227 },
4228 Err(e) => {
4229 if downloader.verbosity > 0 {
4230 error!(" Error fetching video segment {}: {e:?}", frag.url);
4231 }
4232 ds.download_errors += 1;
4233 if ds.download_errors > downloader.max_error_count {
4234 return Err(DashMpdError::Network(
4235 String::from("more than max_error_count network errors")));
4236 }
4237 },
4238 }
4239 info!(" Retrying video segment {}", frag.url);
4240 if downloader.sleep_between_requests > 0 {
4241 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
4242 }
4243 }
4244 }
4245 }
4246 tmpfile_video.flush().map_err(|e| {
4247 error!(" Couldn't flush video file: {e}");
4248 DashMpdError::Io(e, String::from("flushing video file"))
4249 }).await?;
4250 } if !downloader.decryption_keys.is_empty() {
4252 if downloader.verbosity > 0 {
4253 let metadata = fs::metadata(tmppath).await
4254 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted video metadata")))?;
4255 info!(" Attempting to decrypt video stream ({} kB) with {}",
4256 metadata.len() / 1024,
4257 downloader.decryptor_preference);
4258 }
4259 let out_ext = downloader.output_path.as_ref().unwrap()
4260 .extension()
4261 .unwrap_or(OsStr::new("mp4"));
4262 let decrypted = tmp_file_path("dashmpd-decrypted-video", out_ext)?;
4263 if downloader.decryptor_preference.eq("mp4decrypt") {
4264 decrypt_mp4decrypt(downloader, tmppath, &decrypted, "video").await?;
4265 } else if downloader.decryptor_preference.eq("shaka") {
4266 decrypt_shaka(downloader, tmppath, &decrypted, "video").await?;
4267 } else if downloader.decryptor_preference.eq("shaka-container") {
4268 decrypt_shaka_container(downloader, tmppath, &decrypted, "video").await?;
4269 } else if downloader.decryptor_preference.eq("mp4box") {
4270 decrypt_mp4box(downloader, tmppath, &decrypted, "video").await?;
4271 } else if downloader.decryptor_preference.eq("mp4box-container") {
4272 decrypt_mp4box_container(downloader, tmppath, &decrypted, "video").await?;
4273 } else {
4274 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
4275 }
4276 if let Err(e) = fs::metadata(&decrypted).await {
4277 return Err(DashMpdError::Decrypting(format!("missing decrypted video file: {e:?}")));
4278 }
4279 fs::remove_file(&tmppath).await
4280 .map_err(|e| DashMpdError::Io(e, String::from("deleting encrypted video tmpfile")))?;
4281 fs::rename(&decrypted, &tmppath).await
4282 .map_err(|e| DashMpdError::Io(e, String::from("renaming decrypted video")))?;
4283 }
4284 if let Ok(metadata) = fs::metadata(&tmppath).await {
4285 if downloader.verbosity > 1 {
4286 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4287 let elapsed = start_download.elapsed();
4288 info!(" Wrote {mbytes:.1}MB to DASH video file ({:.1} MB/s)",
4289 mbytes / elapsed.as_secs_f64());
4290 }
4291 }
4292 Ok(have_video)
4293}
4294
4295
4296#[tracing::instrument(level="trace", skip_all)]
4298async fn fetch_period_subtitles(
4299 downloader: &DashDownloader,
4300 tmppath: &Path,
4301 subtitle_fragments: &[MediaFragment],
4302 subtitle_formats: &[SubtitleType],
4303 ds: &mut DownloadState) -> Result<bool, DashMpdError>
4304{
4305 use crate::stpp::StppDocument;
4306 use crate::vtt::VttDocument;
4307
4308 let client = downloader.http_client.clone().unwrap();
4309 let start_download = Instant::now();
4310 let mut have_subtitles = false;
4311 {
4312 let tmpfile_subs = File::create(tmppath).await
4313 .map_err(|e| DashMpdError::Io(e, String::from("creating subs tmpfile")))?;
4314 let mut stpp_document = StppDocument::new();
4316 let mut vtt_document = VttDocument::new();
4318 ensure_permissions_readable(tmppath).await?;
4319 let mut tmpfile_subs = BufWriter::new(tmpfile_subs);
4320 for frag in subtitle_fragments {
4321 ds.segment_counter += 1;
4323 let progress_percent = min(98, (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32);
4324 for observer in &downloader.progress_observers {
4325 observer.update(progress_percent, 1, "Fetching subtitle segments");
4326 }
4327 if frag.url.scheme() == "data" {
4328 let us = &frag.url.to_string();
4329 let du = DataUrl::process(us)
4330 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
4331 if du.mime_type().type_ != "video" {
4332 return Err(DashMpdError::UnhandledMediaStream(
4333 String::from("expecting video content in data URL")));
4334 }
4335 let (body, _fragment) = du.decode_to_vec()
4336 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
4337 if downloader.verbosity > 2 {
4338 info!(" Subtitle segment data URL -> {} octets", body.len());
4339 }
4340 tmpfile_subs.write_all(&body)
4341 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH subtitle data")))
4342 .await?;
4343 have_subtitles = true;
4344 } else {
4345 let fetch = || async {
4346 let mut req = client.get(frag.url.clone())
4347 .header("Sec-Fetch-Mode", "navigate");
4348 if let Some(sb) = &frag.start_byte {
4349 if let Some(eb) = &frag.end_byte {
4350 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
4351 }
4352 }
4353 if let Some(referer) = &downloader.referer {
4354 req = req.header("Referer", referer);
4355 } else {
4356 req = req.header("Referer", downloader.redirected_url.to_string());
4357 }
4358 if let Some(username) = &downloader.auth_username {
4359 if let Some(password) = &downloader.auth_password {
4360 req = req.basic_auth(username, Some(password));
4361 }
4362 }
4363 if let Some(token) = &downloader.auth_bearer_token {
4364 req = req.bearer_auth(token);
4365 }
4366 req.send().await?
4367 .error_for_status()
4368 };
4369 let mut failure = None;
4370 match fetch
4371 .retry(ExponentialBuilder::default())
4372 .when(reqwest_error_transient_p)
4373 .notify(notify_transient)
4374 .await
4375 {
4376 Ok(response) => {
4377 if response.status().is_success() {
4378 let content_bytes = response.bytes().await
4379 .map_err(|e| network_error("fetching DASH subtitle segment", &e))?;
4380 if downloader.verbosity > 2 {
4381 if let Some(sb) = &frag.start_byte {
4382 if let Some(eb) = &frag.end_byte {
4383 info!(" Subtitle segment {} range {sb}-{eb} -> {} octets",
4384 &frag.url, content_bytes.len());
4385 }
4386 } else {
4387 info!(" Subtitle segment {} -> {} octets", &frag.url, content_bytes.len());
4388 }
4389 }
4390 let size = min((content_bytes.len()/1024 + 1) as u32, u32::MAX);
4391 throttle_download_rate(downloader, size).await?;
4392 if subtitle_formats.contains(&SubtitleType::Stpp) {
4393 stpp_document.add_from_mp4(&content_bytes)?;
4394 } else if subtitle_formats.contains(&SubtitleType::Vtt) {
4398 vtt_document.add_bytes(&content_bytes)?;
4399 } else {
4400 tmpfile_subs.write_all(&content_bytes)
4401 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH subtitle data")))
4402 .await?;
4403 }
4404 have_subtitles = true;
4405 } else {
4406 failure = Some(format!("HTTP error {}", response.status().as_str()));
4407 }
4408 },
4409 Err(e) => failure = Some(format!("{e}")),
4410 }
4411 if let Some(f) = failure {
4412 if downloader.verbosity > 0 {
4413 error!("{f} fetching subtitle segment {}", &frag.url);
4414 }
4415 ds.download_errors += 1;
4416 if ds.download_errors > downloader.max_error_count {
4417 return Err(DashMpdError::Network(
4418 String::from("more than max_error_count network errors")));
4419 }
4420 }
4421 }
4422 if downloader.sleep_between_requests > 0 {
4423 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
4424 }
4425 }
4426 if subtitle_formats.contains(&SubtitleType::Stpp) {
4427 if downloader.verbosity > 1 {
4428 info!(" Writing TTML subtitles to {tmppath:?}");
4429 }
4430 tmpfile_subs.write_all(stpp_document.to_string().as_bytes())
4431 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH TTML subtitle data")))
4432 .await?;
4433 }
4434 if subtitle_formats.contains(&SubtitleType::Vtt) {
4435 if downloader.verbosity > 1 {
4436 info!(" Writing VTT subtitles to {tmppath:?}");
4437 }
4438 tmpfile_subs.write_all(vtt_document.to_string().as_bytes())
4439 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH VTT subtitle data")))
4440 .await?;
4441 }
4442 tmpfile_subs.flush().map_err(|e| {
4443 error!("Couldn't flush subs file: {e}");
4444 DashMpdError::Io(e, String::from("flushing subtitle file"))
4445 }).await?;
4446 } if have_subtitles {
4448 if let Ok(metadata) = fs::metadata(tmppath).await {
4449 if downloader.verbosity > 1 {
4450 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4451 let elapsed = start_download.elapsed();
4452 info!(" Wrote {mbytes:.1}MB to DASH subtitle file ({:.1} MB/s)",
4453 mbytes / elapsed.as_secs_f64());
4454 }
4455 }
4456 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
4459 subtitle_formats.contains(&SubtitleType::Ttxt)
4460 {
4461 if downloader.verbosity > 0 {
4463 if let Some(fmt) = subtitle_formats.first() {
4464 info!(" Downloaded media contains subtitles in {fmt:?} format");
4465 }
4466 info!(" Running MP4Box to extract subtitles");
4467 }
4468 let out = downloader.output_path.as_ref().unwrap()
4469 .with_extension("srt");
4470 let out_str = out.to_string_lossy();
4471 let tmp_str = tmppath.to_string_lossy();
4472 let args = vec![
4473 "-srt", "1",
4474 "-out", &out_str,
4475 &tmp_str];
4476 if downloader.verbosity > 0 {
4477 info!(" Running MP4Box {}", args.join(" "));
4478 }
4479 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4480 .args(args)
4481 .output()
4482 {
4483 let msg = partial_process_output(&mp4box.stdout);
4484 if !msg.is_empty() {
4485 info!(" MP4Box stdout: {msg}");
4486 }
4487 let msg = partial_process_output(&mp4box.stderr);
4488 if !msg.is_empty() {
4489 info!(" MP4Box stderr: {msg}");
4490 }
4491 if mp4box.status.success() {
4492 info!(" Extracted subtitles as SRT");
4493 } else {
4494 warn!(" Error running MP4Box to extract subtitles");
4495 }
4496 } else {
4497 warn!(" Failed to spawn MP4Box to extract subtitles");
4498 }
4499 }
4500 if subtitle_formats.contains(&SubtitleType::Stpp) {
4501 let tmpfile_in = File::open(tmppath).await
4505 .map_err(|e| DashMpdError::Io(
4506 e, String::from("opening tmp subtitle output")))?;
4507 let ttml_path = downloader.output_path.as_ref().unwrap()
4508 .with_extension("ttml");
4509 let ttml_file = File::create(ttml_path.clone()).await
4510 .map_err(|e| DashMpdError::Io(
4511 e, String::from("opening TTML output file")))?;
4512 io::copy(&mut BufReader::new(tmpfile_in), &mut BufWriter::new(ttml_file)).await
4513 .map_err(|e| DashMpdError::Io(
4514 e, String::from("copying TTML subtitles")))?;
4515 }
4516 if subtitle_formats.contains(&SubtitleType::Vtt) {
4517 let tmpfile_in = File::open(tmppath).await
4521 .map_err(|e| DashMpdError::Io(
4522 e, String::from("opening tmp subtitle output")))?;
4523 let vtt_path = downloader.output_path.as_ref().unwrap()
4524 .with_extension("vtt");
4525 let vtt_file = File::create(vtt_path.clone()).await
4526 .map_err(|e| DashMpdError::Io(
4527 e, String::from("opening VTT output file")))?;
4528 io::copy(&mut BufReader::new(tmpfile_in), &mut BufWriter::new(vtt_file)).await
4529 .map_err(|e| DashMpdError::Io(
4530 e, String::from("copying VTT subtitles")))?;
4531 }
4532 }
4537 Ok(have_subtitles)
4538}
4539
4540
4541async fn fetch_mpd_http(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4543 let client = &downloader.http_client.clone().unwrap();
4544 let send_request = || async {
4545 let mut req = client.get(&downloader.mpd_url)
4546 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4547 .header("Accept-Language", "en-US,en")
4548 .header("Upgrade-Insecure-Requests", "1")
4549 .header("Sec-Fetch-Mode", "navigate");
4550 if let Some(referer) = &downloader.referer {
4551 req = req.header("Referer", referer);
4552 }
4553 if let Some(username) = &downloader.auth_username {
4554 if let Some(password) = &downloader.auth_password {
4555 req = req.basic_auth(username, Some(password));
4556 }
4557 }
4558 if let Some(token) = &downloader.auth_bearer_token {
4559 req = req.bearer_auth(token);
4560 }
4561 req.send().await?
4562 .error_for_status()
4563 };
4564 for observer in &downloader.progress_observers {
4565 observer.update(1, 1, "Fetching DASH manifest");
4566 }
4567 if downloader.verbosity > 0 {
4568 if !downloader.fetch_audio && !downloader.fetch_video && !downloader.fetch_subtitles {
4569 info!("Only simulating media downloads");
4570 }
4571 info!("Fetching the DASH manifest");
4572 }
4573 let response = send_request
4574 .retry(ExponentialBuilder::default())
4575 .when(reqwest_error_transient_p)
4576 .notify(notify_transient)
4577 .await
4578 .map_err(|e| network_error("requesting DASH manifest", &e))?;
4579 if !response.status().is_success() {
4580 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4581 return Err(DashMpdError::Network(msg));
4582 }
4583 downloader.redirected_url = response.url().clone();
4584 response.bytes().await
4585 .map_err(|e| network_error("fetching DASH manifest", &e))
4586}
4587
4588async fn fetch_mpd_file(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4591 if ! &downloader.mpd_url.starts_with("file://") {
4592 return Err(DashMpdError::Other(String::from("expecting file:// URL scheme")));
4593 }
4594 let url = Url::parse(&downloader.mpd_url)
4595 .map_err(|_| DashMpdError::Other(String::from("parsing MPD URL")))?;
4596 let path = url.to_file_path()
4597 .map_err(|_| DashMpdError::Other(String::from("extracting path from file:// URL")))?;
4598 let octets = fs::read(path).await
4599 .map_err(|_| DashMpdError::Other(String::from("reading from file:// URL")))?;
4600 Ok(Bytes::from(octets))
4601}
4602
4603
4604#[tracing::instrument(level="trace", skip_all)]
4605async fn fetch_mpd(downloader: &mut DashDownloader) -> Result<PathBuf, DashMpdError> {
4606 #[cfg(all(feature = "sandbox", target_os = "linux"))]
4607 if downloader.sandbox {
4608 if let Err(e) = restrict_thread(downloader) {
4609 warn!("Sandboxing failed: {e:?}");
4610 }
4611 }
4612 let xml = if downloader.mpd_url.starts_with("file://") {
4613 fetch_mpd_file(downloader).await?
4614 } else {
4615 fetch_mpd_http(downloader).await?
4616 };
4617 let mut mpd: MPD = parse_resolving_xlinks(downloader, &xml).await
4618 .map_err(|e| parse_error("parsing DASH XML", e))?;
4619 let client = &downloader.http_client.clone().unwrap();
4622 if let Some(new_location) = &mpd.locations.first() {
4623 let new_url = &new_location.url;
4624 if downloader.verbosity > 0 {
4625 info!("Redirecting to new manifest <Location> {new_url}");
4626 }
4627 let send_request = || async {
4628 let mut req = client.get(new_url)
4629 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4630 .header("Accept-Language", "en-US,en")
4631 .header("Sec-Fetch-Mode", "navigate");
4632 if let Some(referer) = &downloader.referer {
4633 req = req.header("Referer", referer);
4634 } else {
4635 req = req.header("Referer", downloader.redirected_url.to_string());
4636 }
4637 if let Some(username) = &downloader.auth_username {
4638 if let Some(password) = &downloader.auth_password {
4639 req = req.basic_auth(username, Some(password));
4640 }
4641 }
4642 if let Some(token) = &downloader.auth_bearer_token {
4643 req = req.bearer_auth(token);
4644 }
4645 req.send().await?
4646 .error_for_status()
4647 };
4648 let response = send_request
4649 .retry(ExponentialBuilder::default())
4650 .when(reqwest_error_transient_p)
4651 .notify(notify_transient)
4652 .await
4653 .map_err(|e| network_error("requesting relocated DASH manifest", &e))?;
4654 if !response.status().is_success() {
4655 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4656 return Err(DashMpdError::Network(msg));
4657 }
4658 downloader.redirected_url = response.url().clone();
4659 let xml = response.bytes().await
4660 .map_err(|e| network_error("fetching relocated DASH manifest", &e))?;
4661 mpd = parse_resolving_xlinks(downloader, &xml).await
4662 .map_err(|e| parse_error("parsing relocated DASH XML", e))?;
4663 }
4664 if mpd_is_dynamic(&mpd) {
4665 if downloader.allow_live_streams {
4668 if downloader.verbosity > 0 {
4669 warn!("Attempting to download from live stream (this may not work).");
4670 }
4671 } else {
4672 return Err(DashMpdError::UnhandledMediaStream("Don't know how to download dynamic MPD".to_string()));
4673 }
4674 }
4675 let mut toplevel_base_url = downloader.redirected_url.clone();
4676 if let Some(bu) = &mpd.base_url.first() {
4678 toplevel_base_url = merge_baseurls(&downloader.redirected_url, &bu.base)?;
4679 }
4680 if let Some(base) = &downloader.base_url {
4683 toplevel_base_url = merge_baseurls(&downloader.redirected_url, base)?;
4684 }
4685 if downloader.verbosity > 0 {
4686 let pcount = mpd.periods.len();
4687 info!("DASH manifest has {pcount} period{}", if pcount > 1 { "s" } else { "" });
4688 print_available_streams(&mpd);
4689 }
4690 let mut pds: Vec<PeriodDownloads> = Vec::new();
4698 let mut period_counter = 0;
4699 for mpd_period in &mpd.periods {
4700 let period = mpd_period.clone();
4701 period_counter += 1;
4702 if let Some(min) = downloader.minimum_period_duration {
4703 if let Some(duration) = period.duration {
4704 if duration < min {
4705 if let Some(id) = period.id.as_ref() {
4706 info!("Skipping period {id} (#{period_counter}): duration is less than requested minimum");
4707 } else {
4708 info!("Skipping period #{period_counter}: duration is less than requested minimum");
4709 }
4710 continue;
4711 }
4712 }
4713 }
4714 let mut pd = PeriodDownloads { period_counter, ..Default::default() };
4715 if let Some(id) = period.id.as_ref() {
4716 pd.id = Some(id.clone());
4717 }
4718 if downloader.verbosity > 0 && !downloader.fetch_audio && !downloader.fetch_video && !downloader.fetch_subtitles {
4719 if let Some(id) = period.id.as_ref() {
4720 info!("Preparing download for period {id} (#{period_counter})");
4721 } else {
4722 info!("Preparing download for period #{period_counter}");
4723 }
4724 }
4725 let mut base_url = toplevel_base_url.clone();
4726 if let Some(bu) = period.BaseURL.first() {
4728 base_url = merge_baseurls(&base_url, &bu.base)?;
4729 }
4730 let mut audio_outputs = PeriodOutputs::default();
4731 if downloader.fetch_audio {
4732 audio_outputs = do_period_audio(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4733 for f in audio_outputs.fragments {
4734 pd.audio_fragments.push(f);
4735 }
4736 pd.selected_audio_language = audio_outputs.selected_audio_language;
4737 }
4738 let mut video_outputs = PeriodOutputs::default();
4739 if downloader.fetch_video {
4740 video_outputs = do_period_video(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4741 for f in video_outputs.fragments {
4742 pd.video_fragments.push(f);
4743 }
4744 }
4745 match do_period_subtitles(downloader, &mpd, &period, period_counter, base_url.clone()).await {
4746 Ok(subtitle_outputs) => {
4747 for f in subtitle_outputs.fragments {
4748 pd.subtitle_fragments.push(f);
4749 }
4750 for f in subtitle_outputs.subtitle_formats {
4751 pd.subtitle_formats.push(f);
4752 }
4753 pd.selected_subtitle_language = subtitle_outputs.selected_subtitle_language;
4754 },
4755 Err(e) => warn!(" Ignoring error triggered while processing subtitles: {e}"),
4756 }
4757 if downloader.verbosity > 0 {
4759 use base64::prelude::{Engine as _, BASE64_STANDARD};
4760
4761 audio_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4762 for f in pd.audio_fragments.iter().filter(|f| f.is_init) {
4763 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4764 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4765 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4766 info!(" {}", pssh.to_string());
4767 }
4768 }
4769 }
4770 video_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4771 for f in pd.video_fragments.iter().filter(|f| f.is_init) {
4772 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4773 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4774 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4775 info!(" {}", pssh.to_string());
4776 }
4777 }
4778 }
4779 }
4780 pds.push(pd);
4781 } let output_path = &downloader.output_path.as_ref().unwrap().clone();
4786 let mut period_output_pathbufs: Vec<PathBuf> = Vec::new();
4787 let mut ds = DownloadState {
4788 period_counter: 0,
4789 segment_count: pds.iter().map(period_fragment_count).sum(),
4791 segment_counter: 0,
4792 download_errors: 0
4793 };
4794 for pd in pds {
4795 let mut have_audio = false;
4796 let mut have_video = false;
4797 let mut have_subtitles = false;
4798 ds.period_counter = pd.period_counter;
4799 let period_output_path = output_path_for_period(output_path, pd.period_counter);
4800 #[allow(clippy::collapsible_if)]
4801 if downloader.verbosity > 0 {
4802 if downloader.fetch_audio || downloader.fetch_video || downloader.fetch_subtitles {
4803 let idnum = if let Some(id) = pd.id {
4804 format!("id={} (#{})", id, pd.period_counter)
4805 } else {
4806 format!("#{}", pd.period_counter)
4807 };
4808 info!("Period {idnum}: fetching {} audio, {} video and {} subtitle segments",
4809 pd.audio_fragments.len(),
4810 pd.video_fragments.len(),
4811 pd.subtitle_fragments.len());
4812 }
4813 }
4814 let output_ext = downloader.output_path.as_ref().unwrap()
4815 .extension()
4816 .unwrap_or(OsStr::new("mp4"));
4817 let tmppath_audio = if let Some(ref path) = downloader.keep_audio {
4818 path.clone()
4819 } else {
4820 tmp_file_path("dashmpd-audio", output_ext)?
4821 };
4822 let tmppath_video = if let Some(ref path) = downloader.keep_video {
4823 path.clone()
4824 } else {
4825 tmp_file_path("dashmpd-video", output_ext)?
4826 };
4827 let tmppath_subs = tmp_file_path("dashmpd-subs", OsStr::new("sub"))?;
4828 if downloader.fetch_audio && !pd.audio_fragments.is_empty() {
4829 have_audio = fetch_period_audio(downloader,
4833 &tmppath_audio, &pd.audio_fragments,
4834 &mut ds).await?;
4835 }
4836 if downloader.fetch_video && !pd.video_fragments.is_empty() {
4837 have_video = fetch_period_video(downloader,
4838 &tmppath_video, &pd.video_fragments,
4839 &mut ds).await?;
4840 }
4841 if downloader.fetch_subtitles && !pd.subtitle_fragments.is_empty() {
4845 have_subtitles = fetch_period_subtitles(downloader,
4846 &tmppath_subs,
4847 &pd.subtitle_fragments,
4848 &pd.subtitle_formats,
4849 &mut ds).await?;
4850 }
4851
4852 if have_audio && have_video {
4855 for observer in &downloader.progress_observers {
4856 observer.update(99, 1, "Muxing audio and video");
4857 }
4858 if downloader.verbosity > 1 {
4859 info!(" Muxing audio and video streams");
4860 }
4861 let audio_tracks = vec![
4862 AudioTrack {
4863 language: pd.selected_audio_language,
4864 path: tmppath_audio.clone()
4865 }];
4866 mux_audio_video(downloader, &period_output_path, &audio_tracks, &tmppath_video).await?;
4867 if pd.subtitle_formats.contains(&SubtitleType::Stpp) {
4868 let container = match &period_output_path.extension() {
4869 Some(ext) => ext.to_str().unwrap_or("mp4"),
4870 None => "mp4",
4871 };
4872 if container.eq("mp4") {
4873 if downloader.verbosity > 1 {
4874 if let Some(fmt) = &pd.subtitle_formats.first() {
4875 info!(" Downloaded media contains subtitles in {fmt:?} format");
4876 }
4877 info!(" Running MP4Box to merge subtitles with output MP4 container");
4878 }
4879 let tmp_str = tmppath_subs.to_string_lossy();
4882 let period_output_str = period_output_path.to_string_lossy();
4883 let subtitle_lang = format!("3={}", pd.selected_subtitle_language);
4884 let args = vec!["-lang", &subtitle_lang, "-add", &tmp_str, &period_output_str];
4885 if downloader.verbosity > 0 {
4886 info!(" Running MP4Box {}", args.join(" "));
4887 }
4888 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4889 .args(args)
4890 .output()
4891 {
4892 let msg = partial_process_output(&mp4box.stdout);
4893 if !msg.is_empty() {
4894 info!(" MP4Box stdout: {msg}");
4895 }
4896 let msg = partial_process_output(&mp4box.stderr);
4897 if !msg.is_empty() {
4898 info!(" MP4Box stderr: {msg}");
4899 }
4900 if mp4box.status.success() {
4901 info!(" Merged subtitles with MP4 container");
4902 } else {
4903 warn!(" Error running MP4Box to merge subtitles");
4904 }
4905 } else {
4906 warn!(" Failed to spawn MP4Box to merge subtitles");
4907 }
4908 } else if container.eq("mkv") || container.eq("webm") {
4909 let srt = period_output_path.with_extension("srt");
4921 if srt.exists() {
4922 if downloader.verbosity > 0 {
4923 info!(" Running mkvmerge to merge subtitles with output Matroska container");
4924 }
4925 let tmppath = temporary_outpath(".mkv")?;
4926 let pop_arg = &period_output_path.to_string_lossy();
4927 let srt_arg = &srt.to_string_lossy();
4928 let mkvmerge_args = vec!["-o", &tmppath, pop_arg, srt_arg];
4929 if downloader.verbosity > 0 {
4930 info!(" Running mkvmerge {}", mkvmerge_args.join(" "));
4931 }
4932 if let Ok(mkvmerge) = Command::new(downloader.mkvmerge_location.clone())
4933 .args(mkvmerge_args)
4934 .output()
4935 {
4936 let msg = partial_process_output(&mkvmerge.stdout);
4937 if !msg.is_empty() {
4938 info!(" mkvmerge stdout: {msg}");
4939 }
4940 let msg = partial_process_output(&mkvmerge.stderr);
4941 if !msg.is_empty() {
4942 info!(" mkvmerge stderr: {msg}");
4943 }
4944 if mkvmerge.status.success() {
4945 info!(" Merged subtitles with Matroska container");
4946 {
4949 let tmpfile = File::open(tmppath.clone()).await
4950 .map_err(|e| DashMpdError::Io(
4951 e, String::from("opening mkvmerge output")))?;
4952 let mut merged = BufReader::new(tmpfile);
4953 let outfile = File::create(period_output_path.clone()).await
4955 .map_err(|e| DashMpdError::Io(
4956 e, String::from("creating output file")))?;
4957 let mut sink = BufWriter::new(outfile);
4958 io::copy(&mut merged, &mut sink).await
4959 .map_err(|e| DashMpdError::Io(
4960 e, String::from("copying mkvmerge output to output file")))?;
4961 }
4962 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4963 if let Err(e) = fs::remove_file(tmppath).await {
4964 warn!(" Error deleting temporary mkvmerge output: {e}");
4965 }
4966 }
4967 } else {
4968 warn!(" Error running mkvmerge to merge subtitles");
4969 }
4970 }
4971 }
4972 }
4973 }
4974 } else if have_audio {
4975 copy_audio_to_container(downloader, &period_output_path, &tmppath_audio).await?;
4976 } else if have_video {
4977 copy_video_to_container(downloader, &period_output_path, &tmppath_video).await?;
4978 } else if downloader.fetch_video && downloader.fetch_audio {
4979 return Err(DashMpdError::UnhandledMediaStream("no audio or video streams found".to_string()));
4980 } else if downloader.fetch_video {
4981 return Err(DashMpdError::UnhandledMediaStream("no video streams found".to_string()));
4982 } else if downloader.fetch_audio {
4983 return Err(DashMpdError::UnhandledMediaStream("no audio streams found".to_string()));
4984 }
4985 #[allow(clippy::collapsible_if)]
4986 if downloader.keep_audio.is_none() && downloader.fetch_audio {
4987 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4988 if tmppath_audio.exists() && fs::remove_file(tmppath_audio).await.is_err() {
4989 info!(" Failed to delete temporary file for audio stream");
4990 }
4991 }
4992 }
4993 #[allow(clippy::collapsible_if)]
4994 if downloader.keep_video.is_none() && downloader.fetch_video {
4995 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4996 if tmppath_video.exists() && fs::remove_file(tmppath_video).await.is_err() {
4997 info!(" Failed to delete temporary file for video stream");
4998 }
4999 }
5000 }
5001 #[allow(clippy::collapsible_if)]
5002 if env::var("DASHMPD_PERSIST_FILES").is_err() {
5003 if downloader.fetch_subtitles && tmppath_subs.exists() &&
5004 fs::remove_file(tmppath_subs).await.is_err() {
5005 info!(" Failed to delete temporary file for subtitles");
5006 }
5007 }
5008 if downloader.verbosity > 1 && (downloader.fetch_audio || downloader.fetch_video || have_subtitles) {
5009 if let Ok(metadata) = fs::metadata(&period_output_path).await {
5010 info!(" Wrote {:.1}MB to media file", metadata.len() as f64 / (1024.0 * 1024.0));
5011 }
5012 }
5013 if have_audio || have_video {
5014 period_output_pathbufs.push(period_output_path);
5015 }
5016 } let period_output_paths: Vec<&Path> = period_output_pathbufs
5018 .iter()
5019 .map(PathBuf::as_path)
5020 .collect();
5021 #[allow(clippy::comparison_chain)]
5022 if period_output_paths.len() == 1 {
5023 maybe_record_metainformation(output_path, downloader, &mpd);
5025 } else if period_output_paths.len() > 1 {
5026 #[allow(unused_mut)]
5031 let mut concatenated = false;
5032 #[cfg(not(feature = "libav"))]
5033 if downloader.concatenate_periods && video_containers_concatable(downloader, &period_output_paths) {
5035 info!("Preparing to concatenate multiple Periods into one output file");
5036 concat_output_files(downloader, &period_output_paths).await?;
5037 for p in &period_output_paths[1..] {
5038 if fs::remove_file(p).await.is_err() {
5039 warn!(" Failed to delete temporary file {}", p.display());
5040 }
5041 }
5042 concatenated = true;
5043 if let Some(pop) = period_output_paths.first() {
5044 maybe_record_metainformation(pop, downloader, &mpd);
5045 }
5046 }
5047 if !concatenated {
5048 info!("Media content has been saved in a separate file for each period:");
5049 period_counter = 0;
5051 for p in period_output_paths {
5052 period_counter += 1;
5053 info!(" Period #{period_counter}: {}", p.display());
5054 maybe_record_metainformation(p, downloader, &mpd);
5055 }
5056 }
5057 }
5058 let have_content_protection = mpd.periods.iter().any(
5059 |p| p.adaptations.iter().any(
5060 |a| (!a.ContentProtection.is_empty()) ||
5061 a.representations.iter().any(
5062 |r| !r.ContentProtection.is_empty())));
5063 if have_content_protection && downloader.decryption_keys.is_empty() {
5064 warn!("Manifest seems to use ContentProtection (DRM), but you didn't provide decryption keys.");
5065 }
5066 for observer in &downloader.progress_observers {
5067 observer.update(100, 1, "Done");
5068 }
5069 Ok(PathBuf::from(output_path))
5070}
5071
5072
5073#[cfg(test)]
5074mod tests {
5075 #[test]
5076 fn test_resolve_url_template() {
5077 use std::collections::HashMap;
5078 use super::resolve_url_template;
5079
5080 assert_eq!(resolve_url_template("AA$Time$BB", &HashMap::from([("Time", "ZZZ".to_string())])),
5081 "AAZZZBB");
5082 assert_eq!(resolve_url_template("AA$Number%06d$BB", &HashMap::from([("Number", "42".to_string())])),
5083 "AA000042BB");
5084 let dict = HashMap::from([("RepresentationID", "640x480".to_string()),
5085 ("Number", "42".to_string()),
5086 ("Time", "ZZZ".to_string())]);
5087 assert_eq!(resolve_url_template("AA/$RepresentationID$/segment-$Number%05d$.mp4", &dict),
5088 "AA/640x480/segment-00042.mp4");
5089 }
5090}