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 std::cmp::max;
12use tokio::time::Instant;
13use chrono::Utc;
14use std::sync::Arc;
15use std::borrow::Cow;
16use std::collections::HashMap;
17use std::cmp::min;
18use std::ffi::OsStr;
19use std::num::NonZeroU32;
20use futures_util::TryFutureExt;
21use tracing::{trace, info, warn, error};
22use regex::Regex;
23use url::Url;
24use bytes::Bytes;
25use data_url::DataUrl;
26use reqwest::header::{RANGE, CONTENT_TYPE};
27use backon::{ExponentialBuilder, Retryable};
28use governor::{Quota, RateLimiter};
29use lazy_static::lazy_static;
30use xot::{xmlname, Xot};
31use edit_distance::edit_distance;
32use crate::{MPD, Period, Representation, AdaptationSet, SegmentBase, DashMpdError};
33use crate::{parse, mux_audio_video, copy_video_to_container, copy_audio_to_container};
34use crate::{is_audio_adaptation, is_video_adaptation, is_subtitle_adaptation};
35use crate::{subtitle_type, content_protection_type, SubtitleType};
36use crate::check_conformity;
37#[cfg(not(feature = "libav"))]
38use crate::ffmpeg::concat_output_files;
39use crate::media::{temporary_outpath, AudioTrack};
40use crate::decryption::{
41 decrypt_mp4decrypt,
42 decrypt_shaka,
43 decrypt_shaka_container,
44 decrypt_mp4box,
45 decrypt_mp4box_container
46};
47#[allow(unused_imports)]
48use crate::media::video_containers_concatable;
49
50#[cfg(all(feature = "sandbox", target_os = "linux"))]
51use crate::sandbox::{restrict_thread};
52
53
54pub type HttpClient = reqwest::Client;
56type DirectRateLimiter = RateLimiter<governor::state::direct::NotKeyed,
57 governor::state::InMemoryState,
58 governor::clock::DefaultClock,
59 governor::middleware::NoOpMiddleware>;
60
61
62pub 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 fetch_video: bool,
171 fetch_audio: bool,
172 fetch_subtitles: bool,
173 keep_video: Option<PathBuf>,
174 keep_audio: Option<PathBuf>,
176 concatenate_periods: bool,
177 fragment_path: Option<PathBuf>,
178 pub decryption_keys: HashMap<String, String>,
179 xslt_stylesheets: Vec<PathBuf>,
180 minimum_period_duration: Option<Duration>,
181 content_type_checks: bool,
182 conformity_checks: bool,
183 use_index_range: bool,
184 fragment_retry_count: u32,
185 max_error_count: u32,
186 progress_observers: Vec<Arc<dyn ProgressObserver>>,
187 sleep_between_requests: u8,
188 allow_live_streams: bool,
189 force_duration: Option<f64>,
190 rate_limit: u64,
191 bw_limiter: Option<DirectRateLimiter>,
192 bw_estimator_started: Instant,
193 bw_estimator_bytes: usize,
194 pub sandbox: bool,
195 pub verbosity: u8,
196 record_metainformation: bool,
197 pub muxer_preference: HashMap<String, String>,
198 pub concat_preference: HashMap<String, String>,
199 pub decryptor_preference: String,
200 pub ffmpeg_location: String,
201 pub vlc_location: String,
202 pub mkvmerge_location: String,
203 pub mp4box_location: String,
204 pub mp4decrypt_location: String,
205 pub shaka_packager_location: String,
206}
207
208
209#[cfg(not(doctest))]
212impl DashDownloader {
231 pub fn new(mpd_url: &str) -> DashDownloader {
237 DashDownloader {
238 mpd_url: String::from(mpd_url),
239 redirected_url: Url::parse(mpd_url).unwrap(),
240 base_url: None,
241 referer: None,
242 auth_username: None,
243 auth_password: None,
244 auth_bearer_token: None,
245 output_path: None,
246 http_client: None,
247 quality_preference: QualityPreference::Lowest,
248 language_preference_audio: None,
249 language_preference_subtitles: None,
250 role_preference: vec!["main".to_string(), "alternate".to_string()],
251 video_width_preference: None,
252 video_height_preference: None,
253 fetch_video: true,
254 fetch_audio: true,
255 fetch_subtitles: false,
256 keep_video: None,
257 keep_audio: None,
258 concatenate_periods: true,
259 fragment_path: None,
260 decryption_keys: HashMap::new(),
261 xslt_stylesheets: Vec::new(),
262 minimum_period_duration: None,
263 content_type_checks: true,
264 conformity_checks: true,
265 use_index_range: true,
266 fragment_retry_count: 10,
267 max_error_count: 30,
268 progress_observers: Vec::new(),
269 sleep_between_requests: 0,
270 allow_live_streams: false,
271 force_duration: None,
272 rate_limit: 0,
273 bw_limiter: None,
274 bw_estimator_started: Instant::now(),
275 bw_estimator_bytes: 0,
276 sandbox: false,
277 verbosity: 0,
278 record_metainformation: true,
279 muxer_preference: HashMap::new(),
280 concat_preference: HashMap::new(),
281 decryptor_preference: String::from("mp4decrypt"),
282 ffmpeg_location: String::from("ffmpeg"),
283 vlc_location: if cfg!(target_os = "windows") {
284 String::from("c:/Program Files/VideoLAN/VLC/vlc.exe")
287 } else {
288 String::from("vlc")
289 },
290 mkvmerge_location: String::from("mkvmerge"),
291 mp4box_location: if cfg!(target_os = "windows") {
292 String::from("MP4Box.exe")
293 } else if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
294 String::from("MP4Box")
295 } else {
296 String::from("mp4box")
297 },
298 mp4decrypt_location: String::from("mp4decrypt"),
299 shaka_packager_location: String::from("shaka-packager"),
300 }
301 }
302
303 #[must_use]
306 pub fn with_base_url(mut self, base_url: String) -> DashDownloader {
307 self.base_url = Some(base_url);
308 self
309 }
310
311
312 #[must_use]
334 pub fn with_http_client(mut self, client: HttpClient) -> DashDownloader {
335 self.http_client = Some(client);
336 self
337 }
338
339 #[must_use]
343 pub fn with_referer(mut self, referer: String) -> DashDownloader {
344 self.referer = Some(referer);
345 self
346 }
347
348 #[must_use]
351 pub fn with_authentication(mut self, username: &str, password: &str) -> DashDownloader {
352 self.auth_username = Some(username.to_string());
353 self.auth_password = Some(password.to_string());
354 self
355 }
356
357 #[must_use]
360 pub fn with_auth_bearer(mut self, token: &str) -> DashDownloader {
361 self.auth_bearer_token = Some(token.to_string());
362 self
363 }
364
365 #[must_use]
368 pub fn add_progress_observer(mut self, observer: Arc<dyn ProgressObserver>) -> DashDownloader {
369 self.progress_observers.push(observer);
370 self
371 }
372
373 #[must_use]
376 pub fn best_quality(mut self) -> DashDownloader {
377 self.quality_preference = QualityPreference::Highest;
378 self
379 }
380
381 #[must_use]
384 pub fn intermediate_quality(mut self) -> DashDownloader {
385 self.quality_preference = QualityPreference::Intermediate;
386 self
387 }
388
389 #[must_use]
392 pub fn worst_quality(mut self) -> DashDownloader {
393 self.quality_preference = QualityPreference::Lowest;
394 self
395 }
396
397 #[must_use]
404 pub fn prefer_language(mut self, lang: String) -> DashDownloader {
405 self.language_preference_audio = Some(lang.clone());
406 self.language_preference_subtitles = Some(lang);
407 self
408 }
409
410 #[must_use]
415 pub fn prefer_audio_language(mut self, lang: String) -> DashDownloader {
416 self.language_preference_audio = Some(lang);
417 self
418 }
419
420 #[must_use]
425 pub fn prefer_subtitle_language(mut self, lang: String) -> DashDownloader {
426 self.language_preference_subtitles = Some(lang);
427 self
428 }
429
430
431 #[must_use]
441 pub fn prefer_roles(mut self, role_preference: Vec<String>) -> DashDownloader {
442 if role_preference.len() < u8::MAX as usize {
443 self.role_preference = role_preference;
444 } else {
445 warn!("Ignoring role_preference ordering due to excessive length");
446 }
447 self
448 }
449
450 #[must_use]
453 pub fn prefer_video_width(mut self, width: u64) -> DashDownloader {
454 self.video_width_preference = Some(width);
455 self
456 }
457
458 #[must_use]
461 pub fn prefer_video_height(mut self, height: u64) -> DashDownloader {
462 self.video_height_preference = Some(height);
463 self
464 }
465
466 #[must_use]
468 pub fn video_only(mut self) -> DashDownloader {
469 self.fetch_audio = false;
470 self.fetch_video = true;
471 self
472 }
473
474 #[must_use]
476 pub fn audio_only(mut self) -> DashDownloader {
477 self.fetch_audio = true;
478 self.fetch_video = false;
479 self
480 }
481
482 #[must_use]
485 pub fn keep_video_as<P: Into<PathBuf>>(mut self, video_path: P) -> DashDownloader {
486 self.keep_video = Some(video_path.into());
487 self
488 }
489
490 #[must_use]
493 pub fn keep_audio_as<P: Into<PathBuf>>(mut self, audio_path: P) -> DashDownloader {
494 self.keep_audio = Some(audio_path.into());
495 self
496 }
497
498 #[must_use]
501 pub fn save_fragments_to<P: Into<PathBuf>>(mut self, fragment_path: P) -> DashDownloader {
502 self.fragment_path = Some(fragment_path.into());
503 self
504 }
505
506 #[must_use]
518 pub fn add_decryption_key(mut self, id: String, key: String) -> DashDownloader {
519 self.decryption_keys.insert(id, key);
520 self
521 }
522
523 #[must_use]
535 pub fn with_xslt_stylesheet<P: Into<PathBuf>>(mut self, stylesheet: P) -> DashDownloader {
536 self.xslt_stylesheets.push(stylesheet.into());
537 self
538 }
539
540 #[must_use]
543 pub fn minimum_period_duration(mut self, value: Duration) -> DashDownloader {
544 self.minimum_period_duration = Some(value);
545 self
546 }
547
548 #[must_use]
552 pub fn fetch_audio(mut self, value: bool) -> DashDownloader {
553 self.fetch_audio = value;
554 self
555 }
556
557 #[must_use]
561 pub fn fetch_video(mut self, value: bool) -> DashDownloader {
562 self.fetch_video = value;
563 self
564 }
565
566 #[must_use]
574 pub fn fetch_subtitles(mut self, value: bool) -> DashDownloader {
575 self.fetch_subtitles = value;
576 self
577 }
578
579 #[must_use]
583 pub fn concatenate_periods(mut self, value: bool) -> DashDownloader {
584 self.concatenate_periods = value;
585 self
586 }
587
588 #[must_use]
591 pub fn without_content_type_checks(mut self) -> DashDownloader {
592 self.content_type_checks = false;
593 self
594 }
595
596 #[must_use]
599 pub fn content_type_checks(mut self, value: bool) -> DashDownloader {
600 self.content_type_checks = value;
601 self
602 }
603
604 #[must_use]
607 pub fn conformity_checks(mut self, value: bool) -> DashDownloader {
608 self.conformity_checks = value;
609 self
610 }
611
612 #[must_use]
627 pub fn use_index_range(mut self, value: bool) -> DashDownloader {
628 self.use_index_range = value;
629 self
630 }
631
632 #[must_use]
636 pub fn fragment_retry_count(mut self, count: u32) -> DashDownloader {
637 self.fragment_retry_count = count;
638 self
639 }
640
641 #[must_use]
648 pub fn max_error_count(mut self, count: u32) -> DashDownloader {
649 self.max_error_count = count;
650 self
651 }
652
653 #[must_use]
655 pub fn sleep_between_requests(mut self, seconds: u8) -> DashDownloader {
656 self.sleep_between_requests = seconds;
657 self
658 }
659
660 #[must_use]
672 pub fn allow_live_streams(mut self, value: bool) -> DashDownloader {
673 self.allow_live_streams = value;
674 self
675 }
676
677 #[must_use]
683 pub fn force_duration(mut self, seconds: f64) -> DashDownloader {
684 if seconds < 0.0 {
685 warn!("Ignoring negative value for force_duration()");
686 } else {
687 self.force_duration = Some(seconds);
688 if self.verbosity > 1 {
689 info!("Setting forced duration to {seconds:.1} seconds");
690 }
691 }
692 self
693 }
694
695 #[must_use]
701 pub fn with_rate_limit(mut self, bps: u64) -> DashDownloader {
702 if bps < 10 * 1024 {
703 warn!("Limiting bandwidth below 10kB/s is unlikely to be stable");
704 }
705 if self.verbosity > 1 {
706 info!("Limiting bandwidth to {} kB/s", bps/1024);
707 }
708 self.rate_limit = bps;
709 let mut kps = 1 + bps / 1024;
715 if kps > u64::from(u32::MAX) {
716 warn!("Throttling bandwidth limit");
717 kps = u32::MAX.into();
718 }
719 if let Some(bw_limit) = NonZeroU32::new(kps as u32) {
720 if let Some(burst) = NonZeroU32::new(10 * 1024) {
721 let bw_quota = Quota::per_second(bw_limit)
722 .allow_burst(burst);
723 self.bw_limiter = Some(RateLimiter::direct(bw_quota));
724 }
725 }
726 self
727 }
728
729 #[must_use]
739 pub fn verbosity(mut self, level: u8) -> DashDownloader {
740 self.verbosity = level;
741 self
742 }
743
744 #[must_use]
754 pub fn sandbox(mut self, enable: bool) -> DashDownloader {
755 #[cfg(not(all(feature = "sandbox", target_os = "linux")))]
756 if enable {
757 warn!("Sandboxing only available on Linux with crate feature sandbox enabled");
758 }
759 if self.verbosity > 1 && enable {
760 info!("Enabling sandboxing support");
761 }
762 self.sandbox = enable;
763 self
764 }
765
766 #[must_use]
770 pub fn record_metainformation(mut self, record: bool) -> DashDownloader {
771 self.record_metainformation = record;
772 self
773 }
774
775 #[must_use]
797 pub fn with_muxer_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
798 self.muxer_preference.insert(container.to_string(), ordering.to_string());
799 self
800 }
801
802 #[must_use]
825 pub fn with_concat_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
826 self.concat_preference.insert(container.to_string(), ordering.to_string());
827 self
828 }
829
830 #[must_use]
839 pub fn with_decryptor_preference(mut self, decryption_tool: &str) -> DashDownloader {
840 self.decryptor_preference = decryption_tool.to_string();
841 self
842 }
843
844 #[must_use]
859 pub fn with_ffmpeg(mut self, ffmpeg_path: &str) -> DashDownloader {
860 self.ffmpeg_location = ffmpeg_path.to_string();
861 self
862 }
863
864 #[must_use]
879 pub fn with_vlc(mut self, vlc_path: &str) -> DashDownloader {
880 self.vlc_location = vlc_path.to_string();
881 self
882 }
883
884 #[must_use]
892 pub fn with_mkvmerge(mut self, path: &str) -> DashDownloader {
893 self.mkvmerge_location = path.to_string();
894 self
895 }
896
897 #[must_use]
905 pub fn with_mp4box(mut self, path: &str) -> DashDownloader {
906 self.mp4box_location = path.to_string();
907 self
908 }
909
910 #[must_use]
918 pub fn with_mp4decrypt(mut self, path: &str) -> DashDownloader {
919 self.mp4decrypt_location = path.to_string();
920 self
921 }
922
923 #[must_use]
931 pub fn with_shaka_packager(mut self, path: &str) -> DashDownloader {
932 self.shaka_packager_location = path.to_string();
933 self
934 }
935
936 pub async fn download_to<P: Into<PathBuf>>(mut self, out: P) -> Result<PathBuf, DashMpdError> {
946 self.output_path = Some(out.into());
947 if self.http_client.is_none() {
948 let client = reqwest::Client::builder()
949 .timeout(Duration::new(30, 0))
950 .cookie_store(true)
951 .build()
952 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
953 self.http_client = Some(client);
954 }
955 fetch_mpd(&mut self).await
956 }
957
958 pub async fn download(mut self) -> Result<PathBuf, DashMpdError> {
965 let cwd = env::current_dir()
966 .map_err(|e| DashMpdError::Io(e, String::from("obtaining current directory")))?;
967 let filename = generate_filename_from_url(&self.mpd_url);
968 let outpath = cwd.join(filename);
969 self.output_path = Some(outpath);
970 if self.http_client.is_none() {
971 let client = reqwest::Client::builder()
972 .timeout(Duration::new(30, 0))
973 .cookie_store(true)
974 .build()
975 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
976 self.http_client = Some(client);
977 }
978 fetch_mpd(&mut self).await
979 }
980}
981
982
983fn mpd_is_dynamic(mpd: &MPD) -> bool {
984 if let Some(mpdtype) = mpd.mpdtype.as_ref() {
985 return mpdtype.eq("dynamic");
986 }
987 false
988}
989
990fn parse_range(range: &str) -> Result<(u64, u64), DashMpdError> {
993 let v: Vec<&str> = range.split_terminator('-').collect();
994 if v.len() != 2 {
995 return Err(DashMpdError::Parsing(format!("invalid range specifier: {range}")));
996 }
997 #[allow(clippy::indexing_slicing)]
998 let start: u64 = v[0].parse()
999 .map_err(|_| DashMpdError::Parsing(String::from("invalid start for range specifier")))?;
1000 #[allow(clippy::indexing_slicing)]
1001 let end: u64 = v[1].parse()
1002 .map_err(|_| DashMpdError::Parsing(String::from("invalid end for range specifier")))?;
1003 Ok((start, end))
1004}
1005
1006#[derive(Debug)]
1007struct MediaFragment {
1008 period: u8,
1009 url: Url,
1010 start_byte: Option<u64>,
1011 end_byte: Option<u64>,
1012 is_init: bool,
1013 timeout: Option<Duration>,
1014}
1015
1016#[derive(Debug)]
1017struct MediaFragmentBuilder {
1018 period: u8,
1019 url: Url,
1020 start_byte: Option<u64>,
1021 end_byte: Option<u64>,
1022 is_init: bool,
1023 timeout: Option<Duration>,
1024}
1025
1026impl MediaFragmentBuilder {
1027 pub fn new(period: u8, url: Url) -> MediaFragmentBuilder {
1028 MediaFragmentBuilder {
1029 period, url, start_byte: None, end_byte: None, is_init: false, timeout: None
1030 }
1031 }
1032
1033 pub fn with_range(mut self, start_byte: Option<u64>, end_byte: Option<u64>) -> MediaFragmentBuilder {
1034 self.start_byte = start_byte;
1035 self.end_byte = end_byte;
1036 self
1037 }
1038
1039 pub fn with_timeout(mut self, timeout: Duration) -> MediaFragmentBuilder {
1040 self.timeout = Some(timeout);
1041 self
1042 }
1043
1044 pub fn set_init(mut self) -> MediaFragmentBuilder {
1045 self.is_init = true;
1046 self
1047 }
1048
1049 pub fn build(self) -> MediaFragment {
1050 MediaFragment {
1051 period: self.period,
1052 url: self.url,
1053 start_byte: self.start_byte,
1054 end_byte: self.end_byte,
1055 is_init: self.is_init,
1056 timeout: self.timeout
1057 }
1058 }
1059}
1060
1061#[derive(Debug, Default)]
1065struct PeriodOutputs {
1066 fragments: Vec<MediaFragment>,
1067 diagnostics: Vec<String>,
1068 subtitle_formats: Vec<SubtitleType>,
1069 selected_audio_language: String,
1070}
1071
1072#[derive(Debug, Default)]
1073struct PeriodDownloads {
1074 audio_fragments: Vec<MediaFragment>,
1075 video_fragments: Vec<MediaFragment>,
1076 subtitle_fragments: Vec<MediaFragment>,
1077 subtitle_formats: Vec<SubtitleType>,
1078 period_counter: u8,
1079 id: Option<String>,
1080 selected_audio_language: String,
1081}
1082
1083fn period_fragment_count(pd: &PeriodDownloads) -> usize {
1084 pd.audio_fragments.len() +
1085 pd.video_fragments.len() +
1086 pd.subtitle_fragments.len()
1087}
1088
1089
1090
1091async fn throttle_download_rate(downloader: &DashDownloader, size: u32) -> Result<(), DashMpdError> {
1092 if downloader.rate_limit > 0 {
1093 if let Some(cells) = NonZeroU32::new(size) {
1094 if let Some(limiter) = downloader.bw_limiter.as_ref() {
1095 #[allow(clippy::redundant_pattern_matching)]
1096 if let Err(_) = limiter.until_n_ready(cells).await {
1097 return Err(DashMpdError::Other(
1098 "Bandwidth limit is too low".to_string()));
1099 }
1100 }
1101 }
1102 }
1103 Ok(())
1104}
1105
1106
1107fn generate_filename_from_url(url: &str) -> PathBuf {
1108 use sanitise_file_name::{sanitise_with_options, Options};
1109
1110 let mut path = url;
1111 if let Some(p) = path.strip_prefix("http://") {
1112 path = p;
1113 } else if let Some(p) = path.strip_prefix("https://") {
1114 path = p;
1115 } else if let Some(p) = path.strip_prefix("file://") {
1116 path = p;
1117 }
1118 if let Some(p) = path.strip_prefix("www.") {
1119 path = p;
1120 }
1121 if let Some(p) = path.strip_prefix("ftp.") {
1122 path = p;
1123 }
1124 if let Some(p) = path.strip_suffix(".mpd") {
1125 path = p;
1126 }
1127 let mut sanitize_opts = Options::DEFAULT;
1128 sanitize_opts.length_limit = 150;
1129 PathBuf::from(sanitise_with_options(path, &sanitize_opts) + ".mp4")
1134}
1135
1136fn output_path_for_period(base: &Path, period: u8) -> PathBuf {
1153 assert!(period > 0);
1154 if period == 1 {
1155 base.to_path_buf()
1156 } else {
1157 if let Some(stem) = base.file_stem() {
1158 if let Some(ext) = base.extension() {
1159 let fname = format!("{}-p{period}.{}", stem.to_string_lossy(), ext.to_string_lossy());
1160 return base.with_file_name(fname);
1161 }
1162 }
1163 let p = format!("dashmpd-p{period}");
1164 tmp_file_path(&p, base.extension().unwrap_or(OsStr::new("mp4")))
1165 .unwrap_or_else(|_| p.into())
1166 }
1167}
1168
1169fn is_absolute_url(s: &str) -> bool {
1170 s.starts_with("http://") ||
1171 s.starts_with("https://") ||
1172 s.starts_with("file://") ||
1173 s.starts_with("ftp://")
1174}
1175
1176fn merge_baseurls(current: &Url, new: &str) -> Result<Url, DashMpdError> {
1177 if is_absolute_url(new) {
1178 Url::parse(new)
1179 .map_err(|e| parse_error("parsing BaseURL", e))
1180 } else {
1181 let mut merged = current.join(new)
1194 .map_err(|e| parse_error("joining base with BaseURL", e))?;
1195 if merged.query().is_none() {
1196 merged.set_query(current.query());
1197 }
1198 Ok(merged)
1199 }
1200}
1201
1202fn content_type_audio_p(response: &reqwest::Response) -> bool {
1207 match response.headers().get("content-type") {
1208 Some(ct) => {
1209 let ctb = ct.as_bytes();
1210 ctb.starts_with(b"audio/") ||
1211 ctb.starts_with(b"video/") ||
1212 ctb.starts_with(b"application/octet-stream")
1213 },
1214 None => false,
1215 }
1216}
1217
1218fn content_type_video_p(response: &reqwest::Response) -> bool {
1220 match response.headers().get("content-type") {
1221 Some(ct) => {
1222 let ctb = ct.as_bytes();
1223 ctb.starts_with(b"video/") ||
1224 ctb.starts_with(b"application/octet-stream")
1225 },
1226 None => false,
1227 }
1228}
1229
1230
1231fn adaptation_lang_distance(a: &AdaptationSet, language_preference: &str) -> u8 {
1235 if let Some(lang) = &a.lang {
1236 if lang.eq(language_preference) {
1237 return 0;
1238 }
1239 edit_distance(lang, language_preference)
1241 .try_into()
1242 .unwrap_or(u8::MAX)
1243 } else {
1244 100
1245 }
1246}
1247
1248fn adaptation_roles(a: &AdaptationSet) -> Vec<String> {
1251 let mut roles = Vec::new();
1252 for r in &a.Role {
1253 if let Some(rv) = &r.value {
1254 roles.push(String::from(rv));
1255 }
1256 }
1257 for cc in &a.ContentComponent {
1258 for r in &cc.Role {
1259 if let Some(rv) = &r.value {
1260 roles.push(String::from(rv));
1261 }
1262 }
1263 }
1264 roles
1265}
1266
1267fn adaptation_role_distance(a: &AdaptationSet, role_preference: &[String]) -> u8 {
1269 adaptation_roles(a).iter()
1270 .map(|r| role_preference.binary_search(r).unwrap_or(u8::MAX.into()))
1271 .map(|u| u8::try_from(u).unwrap_or(u8::MAX))
1272 .min()
1273 .unwrap_or(u8::MAX)
1274}
1275
1276
1277fn select_preferred_adaptations<'a>(
1285 adaptations: Vec<&'a AdaptationSet>,
1286 downloader: &DashDownloader) -> Vec<&'a AdaptationSet>
1287{
1288 let mut preferred: Vec<&'a AdaptationSet>;
1289 if let Some(ref lang) = downloader.language_preference_audio {
1291 preferred = Vec::new();
1292 let distance: Vec<u8> = adaptations.iter()
1293 .map(|a| adaptation_lang_distance(a, lang))
1294 .collect();
1295 let min_distance = distance.iter().min().unwrap_or(&0);
1296 for (i, a) in adaptations.iter().enumerate() {
1297 if let Some(di) = distance.get(i) {
1298 if di == min_distance {
1299 preferred.push(a);
1300 }
1301 }
1302 }
1303 } else {
1304 preferred = adaptations;
1305 }
1306 let role_distance: Vec<u8> = preferred.iter()
1312 .map(|a| adaptation_role_distance(a, &downloader.role_preference))
1313 .collect();
1314 let role_distance_min = role_distance.iter().min().unwrap_or(&0);
1315 let mut best = Vec::new();
1316 for (i, a) in preferred.into_iter().enumerate() {
1317 if let Some(rdi) = role_distance.get(i) {
1318 if rdi == role_distance_min {
1319 best.push(a);
1320 }
1321 }
1322 }
1323 best
1324}
1325
1326
1327fn select_preferred_representation<'a>(
1333 representations: &[&'a Representation],
1334 downloader: &DashDownloader) -> Option<&'a Representation>
1335{
1336 if representations.iter().all(|x| x.qualityRanking.is_some()) {
1337 match downloader.quality_preference {
1340 QualityPreference::Lowest =>
1341 representations.iter()
1342 .max_by_key(|r| r.qualityRanking.unwrap_or(u8::MAX))
1343 .copied(),
1344 QualityPreference::Highest =>
1345 representations.iter().min_by_key(|r| r.qualityRanking.unwrap_or(0))
1346 .copied(),
1347 QualityPreference::Intermediate => {
1348 let count = representations.len();
1349 match count {
1350 0 => None,
1351 1 => Some(representations[0]),
1352 _ => {
1353 let mut ranking: Vec<u8> = representations.iter()
1354 .map(|r| r.qualityRanking.unwrap_or(u8::MAX))
1355 .collect();
1356 ranking.sort_unstable();
1357 if let Some(want_ranking) = ranking.get(count / 2) {
1358 representations.iter()
1359 .find(|r| r.qualityRanking.unwrap_or(u8::MAX) == *want_ranking)
1360 .copied()
1361 } else {
1362 representations.first().copied()
1363 }
1364 },
1365 }
1366 },
1367 }
1368 } else {
1369 match downloader.quality_preference {
1371 QualityPreference::Lowest => representations.iter()
1372 .min_by_key(|r| r.bandwidth.unwrap_or(1_000_000_000))
1373 .copied(),
1374 QualityPreference::Highest => representations.iter()
1375 .max_by_key(|r| r.bandwidth.unwrap_or(0))
1376 .copied(),
1377 QualityPreference::Intermediate => {
1378 let count = representations.len();
1379 match count {
1380 0 => None,
1381 1 => Some(representations[0]),
1382 _ => {
1383 let mut ranking: Vec<u64> = representations.iter()
1384 .map(|r| r.bandwidth.unwrap_or(100_000_000))
1385 .collect();
1386 ranking.sort_unstable();
1387 if let Some(want_ranking) = ranking.get(count / 2) {
1388 representations.iter()
1389 .find(|r| r.bandwidth.unwrap_or(100_000_000) == *want_ranking)
1390 .copied()
1391 } else {
1392 representations.first().copied()
1393 }
1394 },
1395 }
1396 },
1397 }
1398 }
1399}
1400
1401
1402fn print_available_subtitles_representation(r: &Representation, a: &AdaptationSet) {
1404 let unspecified = "<unspecified>".to_string();
1405 let empty = "".to_string();
1406 let lang = r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unspecified));
1407 let codecs = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&empty));
1408 let typ = subtitle_type(&a);
1409 let stype = if !codecs.is_empty() {
1410 format!("{typ:?}/{codecs}")
1411 } else {
1412 format!("{typ:?}")
1413 };
1414 let role = a.Role.first()
1415 .map_or_else(|| String::from(""),
1416 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1417 let label = a.Label.first()
1418 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1419 info!(" subs {stype:>18} | {lang:>10} |{role}{label}");
1420}
1421
1422fn print_available_subtitles_adaptation(a: &AdaptationSet) {
1423 a.representations.iter()
1424 .for_each(|r| print_available_subtitles_representation(r, a));
1425}
1426
1427fn print_available_streams_representation(r: &Representation, a: &AdaptationSet, typ: &str) {
1429 let unspecified = "<unspecified>".to_string();
1431 let w = r.width.unwrap_or(a.width.unwrap_or(0));
1432 let h = r.height.unwrap_or(a.height.unwrap_or(0));
1433 let codec = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&unspecified));
1434 let bw = r.bandwidth.unwrap_or(a.maxBandwidth.unwrap_or(0));
1435 let fmt = if typ.eq("audio") {
1436 let unknown = String::from("?");
1437 format!("lang={}", r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unknown)))
1438 } else if w == 0 || h == 0 {
1439 String::from("")
1442 } else {
1443 format!("{w}x{h}")
1444 };
1445 let role = a.Role.first()
1446 .map_or_else(|| String::from(""),
1447 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1448 let label = a.Label.first()
1449 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1450 info!(" {typ} {codec:17} | {:5} Kbps | {fmt:>9}{role}{label}", bw / 1024);
1451}
1452
1453fn print_available_streams_adaptation(a: &AdaptationSet, typ: &str) {
1454 a.representations.iter()
1455 .for_each(|r| print_available_streams_representation(r, a, typ));
1456}
1457
1458fn print_available_streams_period(p: &Period) {
1459 p.adaptations.iter()
1460 .filter(is_audio_adaptation)
1461 .for_each(|a| print_available_streams_adaptation(a, "audio"));
1462 p.adaptations.iter()
1463 .filter(is_video_adaptation)
1464 .for_each(|a| print_available_streams_adaptation(a, "video"));
1465 p.adaptations.iter()
1466 .filter(is_subtitle_adaptation)
1467 .for_each(print_available_subtitles_adaptation);
1468}
1469
1470#[tracing::instrument(level="trace", skip_all)]
1471fn print_available_streams(mpd: &MPD) {
1472 use humantime::format_duration;
1473
1474 let mut counter = 0;
1475 for p in &mpd.periods {
1476 let mut period_duration_secs: f64 = -1.0;
1477 if let Some(d) = mpd.mediaPresentationDuration {
1478 period_duration_secs = d.as_secs_f64();
1479 }
1480 if let Some(d) = &p.duration {
1481 period_duration_secs = d.as_secs_f64();
1482 }
1483 counter += 1;
1484 let duration = if period_duration_secs > 0.0 {
1485 format_duration(Duration::from_secs_f64(period_duration_secs)).to_string()
1486 } else {
1487 String::from("unknown")
1488 };
1489 if let Some(id) = p.id.as_ref() {
1490 info!("Streams in period {id} (#{counter}), duration {duration}:");
1491 } else {
1492 info!("Streams in period #{counter}, duration {duration}:");
1493 }
1494 print_available_streams_period(p);
1495 }
1496}
1497
1498async fn extract_init_pssh(downloader: &DashDownloader, init_url: Url) -> Option<Vec<u8>> {
1499 use bstr::ByteSlice;
1500 use hex_literal::hex;
1501
1502 if let Some(client) = downloader.http_client.as_ref() {
1503 let mut req = client.get(init_url);
1504 if let Some(referer) = &downloader.referer {
1505 req = req.header("Referer", referer);
1506 }
1507 if let Some(username) = &downloader.auth_username {
1508 if let Some(password) = &downloader.auth_password {
1509 req = req.basic_auth(username, Some(password));
1510 }
1511 }
1512 if let Some(token) = &downloader.auth_bearer_token {
1513 req = req.bearer_auth(token);
1514 }
1515 if let Ok(mut resp) = req.send().await {
1516 let mut chunk_counter = 0;
1519 let mut segment_first_bytes = Vec::<u8>::new();
1520 while let Ok(Some(chunk)) = resp.chunk().await {
1521 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
1522 #[allow(clippy::redundant_pattern_matching)]
1523 if let Err(_) = throttle_download_rate(downloader, size).await {
1524 return None;
1525 }
1526 segment_first_bytes.append(&mut chunk.to_vec());
1527 chunk_counter += 1;
1528 if chunk_counter > 20 {
1529 break;
1530 }
1531 }
1532 let needle = b"pssh";
1533 for offset in segment_first_bytes.find_iter(needle) {
1534 #[allow(clippy::needless_range_loop)]
1535 for i in offset-4..offset+2 {
1536 if let Some(b) = segment_first_bytes.get(i) {
1537 if *b != 0 {
1538 continue;
1539 }
1540 }
1541 }
1542 #[allow(clippy::needless_range_loop)]
1543 for i in offset+4..offset+8 {
1544 if let Some(b) = segment_first_bytes.get(i) {
1545 if *b != 0 {
1546 continue;
1547 }
1548 }
1549 }
1550 if offset+24 > segment_first_bytes.len() {
1551 continue;
1552 }
1553 const WIDEVINE_SYSID: [u8; 16] = hex!("edef8ba979d64acea3c827dcd51d21ed");
1555 if let Some(sysid) = segment_first_bytes.get((offset+8)..(offset+24)) {
1556 if !sysid.eq(&WIDEVINE_SYSID) {
1557 continue;
1558 }
1559 }
1560 if let Some(length) = segment_first_bytes.get(offset-1) {
1561 let start = offset - 4;
1562 let end = start + *length as usize;
1563 if let Some(pssh) = &segment_first_bytes.get(start..end) {
1564 return Some(pssh.to_vec());
1565 }
1566 }
1567 }
1568 }
1569 None
1570 } else {
1571 None
1572 }
1573}
1574
1575
1576lazy_static! {
1585 static ref URL_TEMPLATE_IDS: Vec<(&'static str, String, Regex)> = {
1586 vec!["RepresentationID", "Number", "Time", "Bandwidth"].into_iter()
1587 .map(|k| (k, format!("${k}$"), Regex::new(&format!("\\${k}%0([\\d])d\\$")).unwrap()))
1588 .collect()
1589 };
1590}
1591
1592fn resolve_url_template(template: &str, params: &HashMap<&str, String>) -> String {
1593 let mut result = template.to_string();
1594 for (k, ident, rx) in URL_TEMPLATE_IDS.iter() {
1595 if result.contains(ident) {
1597 if let Some(value) = params.get(k as &str) {
1598 result = result.replace(ident, value);
1599 }
1600 }
1601 if let Some(cap) = rx.captures(&result) {
1603 if let Some(value) = params.get(k as &str) {
1604 if let Ok(width) = cap[1].parse::<usize>() {
1605 if let Some(m) = rx.find(&result) {
1606 let count = format!("{value:0>width$}");
1607 result = result[..m.start()].to_owned() + &count + &result[m.end()..];
1608 }
1609 }
1610 }
1611 }
1612 }
1613 result
1614}
1615
1616
1617fn reqwest_error_transient_p(e: &reqwest::Error) -> bool {
1618 if e.is_timeout() {
1619 return true;
1620 }
1621 if let Some(s) = e.status() {
1622 if s == reqwest::StatusCode::REQUEST_TIMEOUT ||
1623 s == reqwest::StatusCode::TOO_MANY_REQUESTS ||
1624 s == reqwest::StatusCode::SERVICE_UNAVAILABLE ||
1625 s == reqwest::StatusCode::GATEWAY_TIMEOUT {
1626 return true;
1627 }
1628 }
1629 false
1630}
1631
1632fn notify_transient<E: std::fmt::Debug>(err: &E, dur: Duration) {
1633 warn!("Transient error after {dur:?}: {err:?}");
1634}
1635
1636fn network_error(why: &str, e: &reqwest::Error) -> DashMpdError {
1637 if e.is_timeout() {
1638 DashMpdError::NetworkTimeout(format!("{why}: {e:?}"))
1639 } else if e.is_connect() {
1640 DashMpdError::NetworkConnect(format!("{why}: {e:?}"))
1641 } else {
1642 DashMpdError::Network(format!("{why}: {e:?}"))
1643 }
1644}
1645
1646fn parse_error(why: &str, e: impl std::error::Error) -> DashMpdError {
1647 DashMpdError::Parsing(format!("{why}: {e:#?}"))
1648}
1649
1650
1651async fn reqwest_bytes_with_retries(
1655 client: &reqwest::Client,
1656 req: reqwest::Request,
1657 retry_count: u32) -> Result<Bytes, reqwest::Error>
1658{
1659 let mut last_error = None;
1660 for _ in 0..retry_count {
1661 if let Some(rqw) = req.try_clone() {
1662 match client.execute(rqw).await {
1663 Ok(response) => {
1664 match response.error_for_status() {
1665 Ok(resp) => {
1666 match resp.bytes().await {
1667 Ok(bytes) => return Ok(bytes),
1668 Err(e) => {
1669 info!("Retrying after HTTP error {e:?}");
1670 last_error = Some(e);
1671 },
1672 }
1673 },
1674 Err(e) => {
1675 info!("Retrying after HTTP error {e:?}");
1676 last_error = Some(e);
1677 },
1678 }
1679 },
1680 Err(e) => {
1681 info!("Retrying after HTTP error {e:?}");
1682 last_error = Some(e);
1683 },
1684 }
1685 }
1686 }
1687 Err(last_error.unwrap())
1688}
1689
1690#[allow(unused_variables)]
1703fn maybe_record_metainformation(path: &Path, downloader: &DashDownloader, mpd: &MPD) {
1704 #[cfg(target_family = "unix")]
1705 if downloader.record_metainformation && (downloader.fetch_audio || downloader.fetch_video) {
1706 if let Ok(origin_url) = Url::parse(&downloader.mpd_url) {
1707 #[allow(clippy::collapsible_if)]
1709 if origin_url.username().is_empty() && origin_url.password().is_none() {
1710 #[cfg(target_family = "unix")]
1711 if xattr::set(path, "user.xdg.origin.url", downloader.mpd_url.as_bytes()).is_err() {
1712 info!("Failed to set user.xdg.origin.url xattr on output file");
1713 }
1714 }
1715 for pi in &mpd.ProgramInformation {
1716 if let Some(t) = &pi.Title {
1717 if let Some(tc) = &t.content {
1718 if xattr::set(path, "user.dublincore.title", tc.as_bytes()).is_err() {
1719 info!("Failed to set user.dublincore.title xattr on output file");
1720 }
1721 }
1722 }
1723 if let Some(source) = &pi.Source {
1724 if let Some(sc) = &source.content {
1725 if xattr::set(path, "user.dublincore.source", sc.as_bytes()).is_err() {
1726 info!("Failed to set user.dublincore.source xattr on output file");
1727 }
1728 }
1729 }
1730 if let Some(copyright) = &pi.Copyright {
1731 if let Some(cc) = ©right.content {
1732 if xattr::set(path, "user.dublincore.rights", cc.as_bytes()).is_err() {
1733 info!("Failed to set user.dublincore.rights xattr on output file");
1734 }
1735 }
1736 }
1737 }
1738 }
1739 }
1740}
1741
1742fn fetchable_xlink_href(href: &str) -> bool {
1746 (!href.is_empty()) && href.ne("urn:mpeg:dash:resolve-to-zero:2013")
1747}
1748
1749fn element_resolves_to_zero(xot: &mut Xot, element: xot::Node) -> bool {
1750 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
1751 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
1752 if let Some(href) = xot.get_attribute(element, xlink_href_name.into()) {
1753 return href.eq("urn:mpeg:dash:resolve-to-zero:2013");
1754 }
1755 false
1756}
1757
1758fn skip_xml_preamble(input: &str) -> &str {
1759 if input.starts_with("<?xml") {
1760 if let Some(end_pos) = input.find("?>") {
1761 return &input[end_pos + 2..]; }
1764 }
1765 input
1767}
1768
1769async fn apply_xslt_stylesheets_xsltproc(
1773 downloader: &DashDownloader,
1774 xot: &mut Xot,
1775 doc: xot::Node) -> Result<String, DashMpdError> {
1776 let mut buf = Vec::new();
1777 xot.write(doc, &mut buf)
1778 .map_err(|e| parse_error("serializing rewritten manifest", e))?;
1779 for ss in &downloader.xslt_stylesheets {
1780 if downloader.verbosity > 0 {
1781 info!("Applying XSLT stylesheet {} with xsltproc", ss.display());
1782 }
1783 let tmpmpd = tmp_file_path("dashxslt", OsStr::new("xslt"))?;
1784 fs::write(&tmpmpd, &buf).await
1785 .map_err(|e| DashMpdError::Io(e, String::from("writing MPD")))?;
1786 let xsltproc = Command::new("xsltproc")
1787 .args([ss, &tmpmpd])
1788 .output()
1789 .map_err(|e| DashMpdError::Io(e, String::from("spawning xsltproc")))?;
1790 if !xsltproc.status.success() {
1791 let msg = format!("xsltproc returned {}", xsltproc.status);
1792 let out = partial_process_output(&xsltproc.stderr).to_string();
1793 return Err(DashMpdError::Io(std::io::Error::other(msg), out));
1794 }
1795 if env::var("DASHMPD_PERSIST_FILES").is_err() {
1796 if let Err(e) = fs::remove_file(&tmpmpd).await {
1797 warn!("Error removing temporary MPD after XSLT processing: {e:?}");
1798 }
1799 }
1800 buf.clone_from(&xsltproc.stdout);
1801 if downloader.verbosity > 2 {
1802 println!("Rewritten XSLT: {}", String::from_utf8_lossy(&buf));
1803 }
1804 }
1805 String::from_utf8(buf)
1806 .map_err(|e| parse_error("parsing UTF-8", e))
1807}
1808
1809async fn resolve_xlink_references(
1844 downloader: &DashDownloader,
1845 xot: &mut Xot,
1846 node: xot::Node) -> Result<(), DashMpdError>
1847{
1848 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
1849 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
1850 let xlinked = xot.descendants(node)
1851 .filter(|d| xot.get_attribute(*d, xlink_href_name.into()).is_some())
1852 .collect::<Vec<_>>();
1853 for xl in xlinked {
1854 if element_resolves_to_zero(xot, xl) {
1855 trace!("Removing node with resolve-to-zero xlink:href {xl:?}");
1856 if let Err(e) = xot.remove(xl) {
1857 return Err(parse_error("Failed to remove resolve-to-zero XML node", e));
1858 }
1859 } else if let Some(href) = xot.get_attribute(xl, xlink_href_name.into()) {
1860 if fetchable_xlink_href(href) {
1861 let xlink_url = if is_absolute_url(href) {
1862 Url::parse(href)
1863 .map_err(|e|
1864 if let Ok(ns) = xot.to_string(node) {
1865 parse_error(&format!("parsing XLink on {ns}"), e)
1866 } else {
1867 parse_error("parsing XLink", e)
1868 }
1869 )?
1870 } else {
1871 let mut merged = downloader.redirected_url.join(href)
1874 .map_err(|e|
1875 if let Ok(ns) = xot.to_string(node) {
1876 parse_error(&format!("parsing XLink on {ns}"), e)
1877 } else {
1878 parse_error("parsing XLink", e)
1879 }
1880 )?;
1881 merged.set_query(downloader.redirected_url.query());
1882 merged
1883 };
1884 let client = downloader.http_client.as_ref().unwrap();
1885 trace!("Fetching XLinked element {}", xlink_url.clone());
1886 let mut req = client.get(xlink_url.clone())
1887 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
1888 .header("Accept-Language", "en-US,en")
1889 .header("Sec-Fetch-Mode", "navigate");
1890 if let Some(referer) = &downloader.referer {
1891 req = req.header("Referer", referer);
1892 } else {
1893 req = req.header("Referer", downloader.redirected_url.to_string());
1894 }
1895 if let Some(username) = &downloader.auth_username {
1896 if let Some(password) = &downloader.auth_password {
1897 req = req.basic_auth(username, Some(password));
1898 }
1899 }
1900 if let Some(token) = &downloader.auth_bearer_token {
1901 req = req.bearer_auth(token);
1902 }
1903 let xml = req.send().await
1904 .map_err(|e|
1905 if let Ok(ns) = xot.to_string(node) {
1906 network_error(&format!("fetching XLink for {ns}"), &e)
1907 } else {
1908 network_error("fetching XLink", &e)
1909 }
1910 )?
1911 .error_for_status()
1912 .map_err(|e|
1913 if let Ok(ns) = xot.to_string(node) {
1914 network_error(&format!("fetching XLink for {ns}"), &e)
1915 } else {
1916 network_error("fetching XLink", &e)
1917 }
1918 )?
1919 .text().await
1920 .map_err(|e|
1921 if let Ok(ns) = xot.to_string(node) {
1922 network_error(&format!("resolving XLink for {ns}"), &e)
1923 } else {
1924 network_error("resolving XLink", &e)
1925 }
1926 )?;
1927 if downloader.verbosity > 2 {
1928 if let Ok(ns) = xot.to_string(node) {
1929 info!(" Resolved onLoad XLink {xlink_url} on {ns} -> {} octets", xml.len());
1930 } else {
1931 info!(" Resolved onLoad XLink {xlink_url} -> {} octets", xml.len());
1932 }
1933 }
1934 let wrapped_xml = r#"<?xml version="1.0" encoding="utf-8"?>"#.to_owned() +
1940 r#"<wrapper xmlns="urn:mpeg:dash:schema:mpd:2011" "# +
1941 r#"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" "# +
1942 r#"xmlns:cenc="urn:mpeg:cenc:2013" "# +
1943 r#"xmlns:mspr="urn:microsoft:playready" "# +
1944 r#"xmlns:xlink="http://www.w3.org/1999/xlink">"# +
1945 skip_xml_preamble(&xml) +
1946 r"</wrapper>";
1947 let wrapper_doc = xot.parse(&wrapped_xml)
1948 .map_err(|e| parse_error("parsing xlinked content", e))?;
1949 let wrapper_doc_el = xot.document_element(wrapper_doc)
1950 .map_err(|e| parse_error("extracting XML document element", e))?;
1951 for needs_insertion in xot.children(wrapper_doc_el).collect::<Vec<_>>() {
1952 xot.insert_after(xl, needs_insertion)
1954 .map_err(|e| parse_error("inserting XLinked content", e))?;
1955 }
1956 xot.remove(xl)
1957 .map_err(|e| parse_error("removing XLink node", e))?;
1958 }
1959 }
1960 }
1961 Ok(())
1962}
1963
1964#[tracing::instrument(level="trace", skip_all)]
1965pub async fn parse_resolving_xlinks(
1966 downloader: &DashDownloader,
1967 xml: &[u8]) -> Result<MPD, DashMpdError>
1968{
1969 use xot::xmlname::NameStrInfo;
1970
1971 let mut xot = Xot::new();
1972 let doc = xot.parse_bytes(xml)
1973 .map_err(|e| parse_error("XML parsing", e))?;
1974 let doc_el = xot.document_element(doc)
1975 .map_err(|e| parse_error("extracting XML document element", e))?;
1976 let doc_name = match xot.node_name(doc_el) {
1977 Some(n) => n,
1978 None => return Err(DashMpdError::Parsing(String::from("missing root node name"))),
1979 };
1980 let root_name = xot.name_ref(doc_name, doc_el)
1981 .map_err(|e| parse_error("extracting root node name", e))?;
1982 let root_local_name = root_name.local_name();
1983 if !root_local_name.eq("MPD") {
1984 return Err(DashMpdError::Parsing(format!("root element is {root_local_name}, expecting <MPD>")));
1985 }
1986 for _ in 1..5 {
1989 resolve_xlink_references(downloader, &mut xot, doc).await?;
1990 }
1991 let rewritten = apply_xslt_stylesheets_xsltproc(downloader, &mut xot, doc).await?;
1992 let mpd = parse(&rewritten)?;
1994 if downloader.conformity_checks {
1995 for emsg in check_conformity(&mpd) {
1996 warn!("DASH conformity error in manifest: {emsg}");
1997 }
1998 }
1999 Ok(mpd)
2000}
2001
2002async fn do_segmentbase_indexrange(
2003 downloader: &DashDownloader,
2004 period_counter: u8,
2005 base_url: Url,
2006 sb: &SegmentBase,
2007 dict: &HashMap<&str, String>
2008) -> Result<Vec<MediaFragment>, DashMpdError>
2009{
2010 let mut fragments = Vec::new();
2043 let mut start_byte: Option<u64> = None;
2044 let mut end_byte: Option<u64> = None;
2045 let mut indexable_segments = false;
2046 if downloader.use_index_range {
2047 if let Some(ir) = &sb.indexRange {
2048 let (s, e) = parse_range(ir)?;
2050 trace!("Fetching sidx for {}", base_url.clone());
2051 let mut req = downloader.http_client.as_ref()
2052 .unwrap()
2053 .get(base_url.clone())
2054 .header(RANGE, format!("bytes={s}-{e}"))
2055 .header("Referer", downloader.redirected_url.to_string())
2056 .header("Sec-Fetch-Mode", "navigate");
2057 if let Some(username) = &downloader.auth_username {
2058 if let Some(password) = &downloader.auth_password {
2059 req = req.basic_auth(username, Some(password));
2060 }
2061 }
2062 if let Some(token) = &downloader.auth_bearer_token {
2063 req = req.bearer_auth(token);
2064 }
2065 let mut resp = req.send().await
2066 .map_err(|e| network_error("fetching index data", &e))?
2067 .error_for_status()
2068 .map_err(|e| network_error("fetching index data", &e))?;
2069 let headers = std::mem::take(resp.headers_mut());
2070 if let Some(content_type) = headers.get(CONTENT_TYPE) {
2071 let idx = resp.bytes().await
2072 .map_err(|e| network_error("fetching index data", &e))?;
2073 if idx.len() as u64 != e - s + 1 {
2074 warn!(" HTTP server does not support Range requests; can't use indexRange addressing");
2075 } else {
2076 #[allow(clippy::collapsible_else_if)]
2077 if content_type.eq("video/mp4") ||
2078 content_type.eq("audio/mp4") {
2079 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2086 .with_range(Some(0), Some(e))
2087 .build();
2088 fragments.push(mf);
2089 let mut max_chunk_pos = 0;
2090 if let Ok(segment_chunks) = crate::sidx::from_isobmff_sidx(&idx, e+1) {
2091 trace!("Have {} segment chunks in sidx data", segment_chunks.len());
2092 for chunk in segment_chunks {
2093 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2094 .with_range(Some(chunk.start), Some(chunk.end))
2095 .build();
2096 fragments.push(mf);
2097 if chunk.end > max_chunk_pos {
2098 max_chunk_pos = chunk.end;
2099 }
2100 }
2101 indexable_segments = true;
2102 }
2103 }
2104 }
2111 }
2112 }
2113 }
2114 if indexable_segments {
2115 if let Some(init) = &sb.Initialization {
2116 if let Some(range) = &init.range {
2117 let (s, e) = parse_range(range)?;
2118 start_byte = Some(s);
2119 end_byte = Some(e);
2120 }
2121 if let Some(su) = &init.sourceURL {
2122 let path = resolve_url_template(su, dict);
2123 let u = merge_baseurls(&base_url, &path)?;
2124 let mf = MediaFragmentBuilder::new(period_counter, u)
2125 .with_range(start_byte, end_byte)
2126 .set_init()
2127 .build();
2128 fragments.push(mf);
2129 } else {
2130 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2132 .with_range(start_byte, end_byte)
2133 .set_init()
2134 .build();
2135 fragments.push(mf);
2136 }
2137 }
2138 } else {
2139 trace!("Falling back to retrieving full SegmentBase for {}", base_url.clone());
2144 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2145 .with_timeout(Duration::new(10_000, 0))
2146 .build();
2147 fragments.push(mf);
2148 }
2149 Ok(fragments)
2150}
2151
2152
2153#[tracing::instrument(level="trace", skip_all)]
2154async fn do_period_audio(
2155 downloader: &DashDownloader,
2156 mpd: &MPD,
2157 period: &Period,
2158 period_counter: u8,
2159 base_url: Url
2160) -> Result<PeriodOutputs, DashMpdError>
2161{
2162 let mut fragments = Vec::new();
2163 let mut diagnostics = Vec::new();
2164 let mut opt_init: Option<String> = None;
2165 let mut opt_media: Option<String> = None;
2166 let mut opt_duration: Option<f64> = None;
2167 let mut timescale = 1;
2168 let mut start_number = 1;
2169 let mut period_duration_secs: f64 = -1.0;
2172 if let Some(d) = mpd.mediaPresentationDuration {
2173 period_duration_secs = d.as_secs_f64();
2174 }
2175 if let Some(d) = period.duration {
2176 period_duration_secs = d.as_secs_f64();
2177 }
2178 if let Some(s) = downloader.force_duration {
2179 period_duration_secs = s;
2180 }
2181 if let Some(st) = &period.SegmentTemplate {
2185 if let Some(i) = &st.initialization {
2186 opt_init = Some(i.clone());
2187 }
2188 if let Some(m) = &st.media {
2189 opt_media = Some(m.clone());
2190 }
2191 if let Some(d) = st.duration {
2192 opt_duration = Some(d);
2193 }
2194 if let Some(ts) = st.timescale {
2195 timescale = ts;
2196 }
2197 if let Some(s) = st.startNumber {
2198 start_number = s;
2199 }
2200 }
2201 let mut selected_audio_language = "unk";
2202 let audio_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2205 .filter(is_audio_adaptation)
2206 .collect();
2207 let representations: Vec<&Representation> = select_preferred_adaptations(audio_adaptations, downloader)
2208 .iter()
2209 .flat_map(|a| a.representations.iter())
2210 .collect();
2211 if let Some(audio_repr) = select_preferred_representation(&representations, downloader) {
2212 let audio_adaptation = period.adaptations.iter()
2216 .find(|a| a.representations.iter().any(|r| r.eq(audio_repr)))
2217 .unwrap();
2218 if let Some(lang) = audio_repr.lang.as_ref().or(audio_adaptation.lang.as_ref()) {
2219 selected_audio_language = lang;
2220 }
2221 let mut base_url = base_url.clone();
2224 if let Some(bu) = &audio_adaptation.BaseURL.first() {
2225 base_url = merge_baseurls(&base_url, &bu.base)?;
2226 }
2227 if let Some(bu) = audio_repr.BaseURL.first() {
2228 base_url = merge_baseurls(&base_url, &bu.base)?;
2229 }
2230 if downloader.verbosity > 0 {
2231 let bw = if let Some(bw) = audio_repr.bandwidth {
2232 format!("bw={} Kbps ", bw / 1024)
2233 } else {
2234 String::from("")
2235 };
2236 let unknown = String::from("?");
2237 let lang = audio_repr.lang.as_ref()
2238 .unwrap_or(audio_adaptation.lang.as_ref()
2239 .unwrap_or(&unknown));
2240 let codec = audio_repr.codecs.as_ref()
2241 .unwrap_or(audio_adaptation.codecs.as_ref()
2242 .unwrap_or(&unknown));
2243 diagnostics.push(format!(" Audio stream selected: {bw}lang={lang} codec={codec}"));
2244 for cp in audio_repr.ContentProtection.iter()
2246 .chain(audio_adaptation.ContentProtection.iter())
2247 {
2248 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2249 if let Some(kid) = &cp.default_KID {
2250 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2251 }
2252 for pssh_element in &cp.cenc_pssh {
2253 if let Some(pssh_b64) = &pssh_element.content {
2254 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
2255 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
2256 diagnostics.push(format!(" {pssh}"));
2257 }
2258 }
2259 }
2260 }
2261 }
2262 if let Some(st) = &audio_adaptation.SegmentTemplate {
2267 if let Some(i) = &st.initialization {
2268 opt_init = Some(i.clone());
2269 }
2270 if let Some(m) = &st.media {
2271 opt_media = Some(m.clone());
2272 }
2273 if let Some(d) = st.duration {
2274 opt_duration = Some(d);
2275 }
2276 if let Some(ts) = st.timescale {
2277 timescale = ts;
2278 }
2279 if let Some(s) = st.startNumber {
2280 start_number = s;
2281 }
2282 }
2283 let mut dict = HashMap::new();
2284 if let Some(rid) = &audio_repr.id {
2285 dict.insert("RepresentationID", rid.clone());
2286 }
2287 if let Some(b) = &audio_repr.bandwidth {
2288 dict.insert("Bandwidth", b.to_string());
2289 }
2290 if let Some(sl) = &audio_adaptation.SegmentList {
2299 if downloader.verbosity > 1 {
2302 info!(" Using AdaptationSet>SegmentList addressing mode for audio representation");
2303 }
2304 let mut start_byte: Option<u64> = None;
2305 let mut end_byte: Option<u64> = None;
2306 if let Some(init) = &sl.Initialization {
2307 if let Some(range) = &init.range {
2308 let (s, e) = parse_range(range)?;
2309 start_byte = Some(s);
2310 end_byte = Some(e);
2311 }
2312 if let Some(su) = &init.sourceURL {
2313 let path = resolve_url_template(su, &dict);
2314 let init_url = merge_baseurls(&base_url, &path)?;
2315 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2316 .with_range(start_byte, end_byte)
2317 .set_init()
2318 .build();
2319 fragments.push(mf);
2320 } else {
2321 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2322 .with_range(start_byte, end_byte)
2323 .set_init()
2324 .build();
2325 fragments.push(mf);
2326 }
2327 }
2328 for su in &sl.segment_urls {
2329 start_byte = None;
2330 end_byte = None;
2331 if let Some(range) = &su.mediaRange {
2333 let (s, e) = parse_range(range)?;
2334 start_byte = Some(s);
2335 end_byte = Some(e);
2336 }
2337 if let Some(m) = &su.media {
2338 let u = merge_baseurls(&base_url, m)?;
2339 let mf = MediaFragmentBuilder::new(period_counter, u)
2340 .with_range(start_byte, end_byte)
2341 .build();
2342 fragments.push(mf);
2343 } else if let Some(bu) = audio_adaptation.BaseURL.first() {
2344 let u = merge_baseurls(&base_url, &bu.base)?;
2345 let mf = MediaFragmentBuilder::new(period_counter, u)
2346 .with_range(start_byte, end_byte)
2347 .build();
2348 fragments.push(mf);
2349 }
2350 }
2351 }
2352 if let Some(sl) = &audio_repr.SegmentList {
2353 if downloader.verbosity > 1 {
2355 info!(" Using Representation>SegmentList addressing mode for audio representation");
2356 }
2357 let mut start_byte: Option<u64> = None;
2358 let mut end_byte: Option<u64> = None;
2359 if let Some(init) = &sl.Initialization {
2360 if let Some(range) = &init.range {
2361 let (s, e) = parse_range(range)?;
2362 start_byte = Some(s);
2363 end_byte = Some(e);
2364 }
2365 if let Some(su) = &init.sourceURL {
2366 let path = resolve_url_template(su, &dict);
2367 let init_url = merge_baseurls(&base_url, &path)?;
2368 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2369 .with_range(start_byte, end_byte)
2370 .set_init()
2371 .build();
2372 fragments.push(mf);
2373 } else {
2374 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2375 .with_range(start_byte, end_byte)
2376 .set_init()
2377 .build();
2378 fragments.push(mf);
2379 }
2380 }
2381 for su in &sl.segment_urls {
2382 start_byte = None;
2383 end_byte = None;
2384 if let Some(range) = &su.mediaRange {
2386 let (s, e) = parse_range(range)?;
2387 start_byte = Some(s);
2388 end_byte = Some(e);
2389 }
2390 if let Some(m) = &su.media {
2391 let u = merge_baseurls(&base_url, m)?;
2392 let mf = MediaFragmentBuilder::new(period_counter, u)
2393 .with_range(start_byte, end_byte)
2394 .build();
2395 fragments.push(mf);
2396 } else if let Some(bu) = audio_repr.BaseURL.first() {
2397 let u = merge_baseurls(&base_url, &bu.base)?;
2398 let mf = MediaFragmentBuilder::new(period_counter, u)
2399 .with_range(start_byte, end_byte)
2400 .build();
2401 fragments.push(mf);
2402 }
2403 }
2404 } else if audio_repr.SegmentTemplate.is_some() ||
2405 audio_adaptation.SegmentTemplate.is_some()
2406 {
2407 let st;
2410 if let Some(it) = &audio_repr.SegmentTemplate {
2411 st = it;
2412 } else if let Some(it) = &audio_adaptation.SegmentTemplate {
2413 st = it;
2414 } else {
2415 panic!("unreachable");
2416 }
2417 if let Some(i) = &st.initialization {
2418 opt_init = Some(i.clone());
2419 }
2420 if let Some(m) = &st.media {
2421 opt_media = Some(m.clone());
2422 }
2423 if let Some(ts) = st.timescale {
2424 timescale = ts;
2425 }
2426 if let Some(sn) = st.startNumber {
2427 start_number = sn;
2428 }
2429 if let Some(stl) = &audio_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
2430 .or(audio_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
2431 {
2432 if downloader.verbosity > 1 {
2435 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for audio representation");
2436 }
2437 if let Some(init) = opt_init {
2438 let path = resolve_url_template(&init, &dict);
2439 let u = merge_baseurls(&base_url, &path)?;
2440 let mf = MediaFragmentBuilder::new(period_counter, u)
2441 .set_init()
2442 .build();
2443 fragments.push(mf);
2444 }
2445 let mut elapsed_seconds = 0.0;
2446 if let Some(media) = opt_media {
2447 let audio_path = resolve_url_template(&media, &dict);
2448 let mut segment_time = 0;
2449 let mut segment_duration;
2450 let mut number = start_number;
2451 let mut target_duration = period_duration_secs;
2452 if let Some(target) = downloader.force_duration {
2453 if target > period_duration_secs {
2454 warn!(" Requested forced duration exceeds available content");
2455 } else {
2456 target_duration = target;
2457 }
2458 }
2459 'segment_loop: for s in &stl.segments {
2460 if let Some(t) = s.t {
2461 segment_time = t;
2462 }
2463 segment_duration = s.d;
2464 let dict = HashMap::from([("Time", segment_time.to_string()),
2466 ("Number", number.to_string())]);
2467 let path = resolve_url_template(&audio_path, &dict);
2468 let u = merge_baseurls(&base_url, &path)?;
2469 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2470 number += 1;
2471 elapsed_seconds += segment_duration as f64 / timescale as f64;
2472 if downloader.force_duration.is_some() &&
2473 target_duration > 0.0 &&
2474 elapsed_seconds > target_duration {
2475 break 'segment_loop;
2476 }
2477 if let Some(r) = s.r {
2478 let mut count = 0i64;
2479 loop {
2480 count += 1;
2481 if r >= 0 && count > r {
2486 break;
2487 }
2488 if downloader.force_duration.is_some() &&
2489 target_duration > 0.0 &&
2490 elapsed_seconds > target_duration {
2491 break 'segment_loop;
2492 }
2493 segment_time += segment_duration;
2494 elapsed_seconds += segment_duration as f64 / timescale as f64;
2495 let dict = HashMap::from([("Time", segment_time.to_string()),
2496 ("Number", number.to_string())]);
2497 let path = resolve_url_template(&audio_path, &dict);
2498 let u = merge_baseurls(&base_url, &path)?;
2499 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2500 number += 1;
2501 }
2502 }
2503 segment_time += segment_duration;
2504 }
2505 } else {
2506 return Err(DashMpdError::UnhandledMediaStream(
2507 "SegmentTimeline without a media attribute".to_string()));
2508 }
2509 } else { if downloader.verbosity > 1 {
2514 info!(" Using SegmentTemplate addressing mode for audio representation");
2515 }
2516 let mut total_number = 0i64;
2517 if let Some(init) = opt_init {
2518 let path = resolve_url_template(&init, &dict);
2519 let u = merge_baseurls(&base_url, &path)?;
2520 let mf = MediaFragmentBuilder::new(period_counter, u)
2521 .set_init()
2522 .build();
2523 fragments.push(mf);
2524 }
2525 if let Some(media) = opt_media {
2526 let audio_path = resolve_url_template(&media, &dict);
2527 let timescale = st.timescale.unwrap_or(timescale);
2528 let mut segment_duration: f64 = -1.0;
2529 if let Some(d) = opt_duration {
2530 segment_duration = d;
2532 }
2533 if let Some(std) = st.duration {
2534 if timescale == 0 {
2535 return Err(DashMpdError::UnhandledMediaStream(
2536 "SegmentTemplate@duration attribute cannot be zero".to_string()));
2537 }
2538 segment_duration = std / timescale as f64;
2539 }
2540 if segment_duration < 0.0 {
2541 return Err(DashMpdError::UnhandledMediaStream(
2542 "Audio representation is missing SegmentTemplate@duration attribute".to_string()));
2543 }
2544 total_number += (period_duration_secs / segment_duration).round() as i64;
2545 let mut number = start_number;
2546 if mpd_is_dynamic(mpd) {
2549 if let Some(start_time) = mpd.availabilityStartTime {
2550 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
2551 number = (elapsed + number as f64 - 1f64).floor() as u64;
2552 } else {
2553 return Err(DashMpdError::UnhandledMediaStream(
2554 "dynamic manifest is missing @availabilityStartTime".to_string()));
2555 }
2556 }
2557 for _ in 1..=total_number {
2558 let dict = HashMap::from([("Number", number.to_string())]);
2559 let path = resolve_url_template(&audio_path, &dict);
2560 let u = merge_baseurls(&base_url, &path)?;
2561 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2562 number += 1;
2563 }
2564 }
2565 }
2566 } else if let Some(sb) = &audio_repr.SegmentBase {
2567 if downloader.verbosity > 1 {
2569 info!(" Using SegmentBase@indexRange addressing mode for audio representation");
2570 }
2571 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
2572 fragments.extend(mf);
2573 } else if fragments.is_empty() {
2574 if let Some(bu) = audio_repr.BaseURL.first() {
2575 if downloader.verbosity > 1 {
2577 info!(" Using BaseURL addressing mode for audio representation");
2578 }
2579 let u = merge_baseurls(&base_url, &bu.base)?;
2580 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2581 }
2582 }
2583 if fragments.is_empty() {
2584 return Err(DashMpdError::UnhandledMediaStream(
2585 "no usable addressing mode identified for audio representation".to_string()));
2586 }
2587 }
2588 Ok(PeriodOutputs {
2589 fragments, diagnostics, subtitle_formats: Vec::new(),
2590 selected_audio_language: String::from(selected_audio_language)
2591 })
2592}
2593
2594
2595#[tracing::instrument(level="trace", skip_all)]
2596async fn do_period_video(
2597 downloader: &DashDownloader,
2598 mpd: &MPD,
2599 period: &Period,
2600 period_counter: u8,
2601 base_url: Url
2602 ) -> Result<PeriodOutputs, DashMpdError>
2603{
2604 let mut fragments = Vec::new();
2605 let mut diagnostics = Vec::new();
2606 let mut period_duration_secs: f64 = 0.0;
2607 let mut opt_init: Option<String> = None;
2608 let mut opt_media: Option<String> = None;
2609 let mut opt_duration: Option<f64> = None;
2610 let mut timescale = 1;
2611 let mut start_number = 1;
2612 if let Some(d) = mpd.mediaPresentationDuration {
2613 period_duration_secs = d.as_secs_f64();
2614 }
2615 if let Some(d) = period.duration {
2616 period_duration_secs = d.as_secs_f64();
2617 }
2618 if let Some(s) = downloader.force_duration {
2619 period_duration_secs = s;
2620 }
2621 if let Some(st) = &period.SegmentTemplate {
2625 if let Some(i) = &st.initialization {
2626 opt_init = Some(i.clone());
2627 }
2628 if let Some(m) = &st.media {
2629 opt_media = Some(m.clone());
2630 }
2631 if let Some(d) = st.duration {
2632 opt_duration = Some(d);
2633 }
2634 if let Some(ts) = st.timescale {
2635 timescale = ts;
2636 }
2637 if let Some(s) = st.startNumber {
2638 start_number = s;
2639 }
2640 }
2641 let video_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2648 .filter(is_video_adaptation)
2649 .collect();
2650 let representations: Vec<&Representation> = select_preferred_adaptations(video_adaptations, downloader)
2651 .iter()
2652 .flat_map(|a| a.representations.iter())
2653 .collect();
2654 let maybe_video_repr = if let Some(want) = downloader.video_width_preference {
2655 representations.iter()
2656 .min_by_key(|x| if let Some(w) = x.width { want.abs_diff(w) } else { u64::MAX })
2657 .copied()
2658 } else if let Some(want) = downloader.video_height_preference {
2659 representations.iter()
2660 .min_by_key(|x| if let Some(h) = x.height { want.abs_diff(h) } else { u64::MAX })
2661 .copied()
2662 } else {
2663 select_preferred_representation(&representations, downloader)
2664 };
2665 if let Some(video_repr) = maybe_video_repr {
2666 let video_adaptation = period.adaptations.iter()
2670 .find(|a| a.representations.iter().any(|r| r.eq(video_repr)))
2671 .unwrap();
2672 let mut base_url = base_url.clone();
2675 if let Some(bu) = &video_adaptation.BaseURL.first() {
2676 base_url = merge_baseurls(&base_url, &bu.base)?;
2677 }
2678 if let Some(bu) = &video_repr.BaseURL.first() {
2679 base_url = merge_baseurls(&base_url, &bu.base)?;
2680 }
2681 if downloader.verbosity > 0 {
2682 let bw = if let Some(bw) = video_repr.bandwidth.or(video_adaptation.maxBandwidth) {
2683 format!("bw={} Kbps ", bw / 1024)
2684 } else {
2685 String::from("")
2686 };
2687 let unknown = String::from("?");
2688 let w = video_repr.width.unwrap_or(video_adaptation.width.unwrap_or(0));
2689 let h = video_repr.height.unwrap_or(video_adaptation.height.unwrap_or(0));
2690 let fmt = if w == 0 || h == 0 {
2691 String::from("")
2692 } else {
2693 format!("resolution={w}x{h} ")
2694 };
2695 let codec = video_repr.codecs.as_ref()
2696 .unwrap_or(video_adaptation.codecs.as_ref().unwrap_or(&unknown));
2697 diagnostics.push(format!(" Video stream selected: {bw}{fmt}codec={codec}"));
2698 for cp in video_repr.ContentProtection.iter()
2700 .chain(video_adaptation.ContentProtection.iter())
2701 {
2702 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2703 if let Some(kid) = &cp.default_KID {
2704 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2705 }
2706 for pssh_element in &cp.cenc_pssh {
2707 if let Some(pssh_b64) = &pssh_element.content {
2708 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
2709 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
2710 diagnostics.push(format!(" {pssh}"));
2711 }
2712 }
2713 }
2714 }
2715 }
2716 let mut dict = HashMap::new();
2717 if let Some(rid) = &video_repr.id {
2718 dict.insert("RepresentationID", rid.clone());
2719 }
2720 if let Some(b) = &video_repr.bandwidth {
2721 dict.insert("Bandwidth", b.to_string());
2722 }
2723 if let Some(st) = &video_adaptation.SegmentTemplate {
2728 if let Some(i) = &st.initialization {
2729 opt_init = Some(i.clone());
2730 }
2731 if let Some(m) = &st.media {
2732 opt_media = Some(m.clone());
2733 }
2734 if let Some(d) = st.duration {
2735 opt_duration = Some(d);
2736 }
2737 if let Some(ts) = st.timescale {
2738 timescale = ts;
2739 }
2740 if let Some(s) = st.startNumber {
2741 start_number = s;
2742 }
2743 }
2744 if let Some(sl) = &video_adaptation.SegmentList {
2748 if downloader.verbosity > 1 {
2750 info!(" Using AdaptationSet>SegmentList addressing mode for video representation");
2751 }
2752 let mut start_byte: Option<u64> = None;
2753 let mut end_byte: Option<u64> = None;
2754 if let Some(init) = &sl.Initialization {
2755 if let Some(range) = &init.range {
2756 let (s, e) = parse_range(range)?;
2757 start_byte = Some(s);
2758 end_byte = Some(e);
2759 }
2760 if let Some(su) = &init.sourceURL {
2761 let path = resolve_url_template(su, &dict);
2762 let u = merge_baseurls(&base_url, &path)?;
2763 let mf = MediaFragmentBuilder::new(period_counter, u)
2764 .with_range(start_byte, end_byte)
2765 .set_init()
2766 .build();
2767 fragments.push(mf);
2768 }
2769 } else {
2770 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2771 .with_range(start_byte, end_byte)
2772 .set_init()
2773 .build();
2774 fragments.push(mf);
2775 }
2776 for su in &sl.segment_urls {
2777 start_byte = None;
2778 end_byte = None;
2779 if let Some(range) = &su.mediaRange {
2781 let (s, e) = parse_range(range)?;
2782 start_byte = Some(s);
2783 end_byte = Some(e);
2784 }
2785 if let Some(m) = &su.media {
2786 let u = merge_baseurls(&base_url, m)?;
2787 let mf = MediaFragmentBuilder::new(period_counter, u)
2788 .with_range(start_byte, end_byte)
2789 .build();
2790 fragments.push(mf);
2791 } else if let Some(bu) = video_adaptation.BaseURL.first() {
2792 let u = merge_baseurls(&base_url, &bu.base)?;
2793 let mf = MediaFragmentBuilder::new(period_counter, u)
2794 .with_range(start_byte, end_byte)
2795 .build();
2796 fragments.push(mf);
2797 }
2798 }
2799 }
2800 if let Some(sl) = &video_repr.SegmentList {
2801 if downloader.verbosity > 1 {
2803 info!(" Using Representation>SegmentList addressing mode for video representation");
2804 }
2805 let mut start_byte: Option<u64> = None;
2806 let mut end_byte: Option<u64> = None;
2807 if let Some(init) = &sl.Initialization {
2808 if let Some(range) = &init.range {
2809 let (s, e) = parse_range(range)?;
2810 start_byte = Some(s);
2811 end_byte = Some(e);
2812 }
2813 if let Some(su) = &init.sourceURL {
2814 let path = resolve_url_template(su, &dict);
2815 let u = merge_baseurls(&base_url, &path)?;
2816 let mf = MediaFragmentBuilder::new(period_counter, u)
2817 .with_range(start_byte, end_byte)
2818 .set_init()
2819 .build();
2820 fragments.push(mf);
2821 } else {
2822 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2823 .with_range(start_byte, end_byte)
2824 .set_init()
2825 .build();
2826 fragments.push(mf);
2827 }
2828 }
2829 for su in sl.segment_urls.iter() {
2830 start_byte = None;
2831 end_byte = None;
2832 if let Some(range) = &su.mediaRange {
2834 let (s, e) = parse_range(range)?;
2835 start_byte = Some(s);
2836 end_byte = Some(e);
2837 }
2838 if let Some(m) = &su.media {
2839 let u = merge_baseurls(&base_url, m)?;
2840 let mf = MediaFragmentBuilder::new(period_counter, u)
2841 .with_range(start_byte, end_byte)
2842 .build();
2843 fragments.push(mf);
2844 } else if let Some(bu) = video_repr.BaseURL.first() {
2845 let u = merge_baseurls(&base_url, &bu.base)?;
2846 let mf = MediaFragmentBuilder::new(period_counter, u)
2847 .with_range(start_byte, end_byte)
2848 .build();
2849 fragments.push(mf);
2850 }
2851 }
2852 } else if video_repr.SegmentTemplate.is_some() ||
2853 video_adaptation.SegmentTemplate.is_some() {
2854 let st;
2857 if let Some(it) = &video_repr.SegmentTemplate {
2858 st = it;
2859 } else if let Some(it) = &video_adaptation.SegmentTemplate {
2860 st = it;
2861 } else {
2862 panic!("impossible");
2863 }
2864 if let Some(i) = &st.initialization {
2865 opt_init = Some(i.clone());
2866 }
2867 if let Some(m) = &st.media {
2868 opt_media = Some(m.clone());
2869 }
2870 if let Some(ts) = st.timescale {
2871 timescale = ts;
2872 }
2873 if let Some(sn) = st.startNumber {
2874 start_number = sn;
2875 }
2876 if let Some(stl) = &video_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
2877 .or(video_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
2878 {
2879 if downloader.verbosity > 1 {
2881 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for video representation");
2882 }
2883 if let Some(init) = opt_init {
2884 let path = resolve_url_template(&init, &dict);
2885 let u = merge_baseurls(&base_url, &path)?;
2886 let mf = MediaFragmentBuilder::new(period_counter, u)
2887 .set_init()
2888 .build();
2889 fragments.push(mf);
2890 }
2891 let mut elapsed_seconds = 0.0;
2892 if let Some(media) = opt_media {
2893 let video_path = resolve_url_template(&media, &dict);
2894 let mut segment_time = 0;
2895 let mut segment_duration;
2896 let mut number = start_number;
2897 let mut target_duration = period_duration_secs;
2898 if let Some(target) = downloader.force_duration {
2899 if target > period_duration_secs {
2900 warn!(" Requested forced duration exceeds available content");
2901 } else {
2902 target_duration = target;
2903 }
2904 }
2905 'segment_loop: for s in &stl.segments {
2906 if let Some(t) = s.t {
2907 segment_time = t;
2908 }
2909 segment_duration = s.d;
2910 let dict = HashMap::from([("Time", segment_time.to_string()),
2912 ("Number", number.to_string())]);
2913 let path = resolve_url_template(&video_path, &dict);
2914 let u = merge_baseurls(&base_url, &path)?;
2915 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2916 fragments.push(mf);
2917 number += 1;
2918 elapsed_seconds += segment_duration as f64 / timescale as f64;
2919 if downloader.force_duration.is_some() &&
2920 target_duration > 0.0 &&
2921 elapsed_seconds > target_duration
2922 {
2923 break 'segment_loop;
2924 }
2925 if let Some(r) = s.r {
2926 let mut count = 0i64;
2927 loop {
2928 count += 1;
2929 if r >= 0 && count > r {
2935 break;
2936 }
2937 if downloader.force_duration.is_some() &&
2938 target_duration > 0.0 &&
2939 elapsed_seconds > target_duration
2940 {
2941 break 'segment_loop;
2942 }
2943 segment_time += segment_duration;
2944 elapsed_seconds += segment_duration as f64 / timescale as f64;
2945 let dict = HashMap::from([("Time", segment_time.to_string()),
2946 ("Number", number.to_string())]);
2947 let path = resolve_url_template(&video_path, &dict);
2948 let u = merge_baseurls(&base_url, &path)?;
2949 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2950 fragments.push(mf);
2951 number += 1;
2952 }
2953 }
2954 segment_time += segment_duration;
2955 }
2956 } else {
2957 return Err(DashMpdError::UnhandledMediaStream(
2958 "SegmentTimeline without a media attribute".to_string()));
2959 }
2960 } else { if downloader.verbosity > 1 {
2963 info!(" Using SegmentTemplate addressing mode for video representation");
2964 }
2965 let mut total_number = 0i64;
2966 if let Some(init) = opt_init {
2967 let path = resolve_url_template(&init, &dict);
2968 let u = merge_baseurls(&base_url, &path)?;
2969 let mf = MediaFragmentBuilder::new(period_counter, u)
2970 .set_init()
2971 .build();
2972 fragments.push(mf);
2973 }
2974 if let Some(media) = opt_media {
2975 let video_path = resolve_url_template(&media, &dict);
2976 let timescale = st.timescale.unwrap_or(timescale);
2977 let mut segment_duration: f64 = -1.0;
2978 if let Some(d) = opt_duration {
2979 segment_duration = d;
2981 }
2982 if let Some(std) = st.duration {
2983 if timescale == 0 {
2984 return Err(DashMpdError::UnhandledMediaStream(
2985 "SegmentTemplate@duration attribute cannot be zero".to_string()));
2986 }
2987 segment_duration = std / timescale as f64;
2988 }
2989 if segment_duration < 0.0 {
2990 return Err(DashMpdError::UnhandledMediaStream(
2991 "Video representation is missing SegmentTemplate@duration attribute".to_string()));
2992 }
2993 total_number += (period_duration_secs / segment_duration).round() as i64;
2994 let mut number = start_number;
2995 if mpd_is_dynamic(mpd) {
3005 if let Some(start_time) = mpd.availabilityStartTime {
3006 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
3007 number = (elapsed + number as f64 - 1f64).floor() as u64;
3008 } else {
3009 return Err(DashMpdError::UnhandledMediaStream(
3010 "dynamic manifest is missing @availabilityStartTime".to_string()));
3011 }
3012 }
3013 for _ in 1..=total_number {
3014 let dict = HashMap::from([("Number", number.to_string())]);
3015 let path = resolve_url_template(&video_path, &dict);
3016 let u = merge_baseurls(&base_url, &path)?;
3017 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3018 fragments.push(mf);
3019 number += 1;
3020 }
3021 }
3022 }
3023 } else if let Some(sb) = &video_repr.SegmentBase {
3024 if downloader.verbosity > 1 {
3026 info!(" Using SegmentBase@indexRange addressing mode for video representation");
3027 }
3028 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
3029 fragments.extend(mf);
3030 } else if fragments.is_empty() {
3031 if let Some(bu) = video_repr.BaseURL.first() {
3032 if downloader.verbosity > 1 {
3034 info!(" Using BaseURL addressing mode for video representation");
3035 }
3036 let u = merge_baseurls(&base_url, &bu.base)?;
3037 let mf = MediaFragmentBuilder::new(period_counter, u)
3038 .with_timeout(Duration::new(10000, 0))
3039 .build();
3040 fragments.push(mf);
3041 }
3042 }
3043 if fragments.is_empty() {
3044 return Err(DashMpdError::UnhandledMediaStream(
3045 "no usable addressing mode identified for video representation".to_string()));
3046 }
3047 }
3048 Ok(PeriodOutputs {
3051 fragments,
3052 diagnostics,
3053 subtitle_formats: Vec::new(),
3054 selected_audio_language: String::from("unk")
3055 })
3056}
3057
3058#[tracing::instrument(level="trace", skip_all)]
3059async fn do_period_subtitles(
3060 downloader: &DashDownloader,
3061 mpd: &MPD,
3062 period: &Period,
3063 period_counter: u8,
3064 base_url: Url
3065 ) -> Result<PeriodOutputs, DashMpdError>
3066{
3067 let client = downloader.http_client.as_ref().unwrap();
3068 let output_path = &downloader.output_path.as_ref().unwrap().clone();
3069 let period_output_path = output_path_for_period(output_path, period_counter);
3070 let mut fragments = Vec::new();
3071 let mut subtitle_formats = Vec::new();
3072 let mut period_duration_secs: f64 = 0.0;
3073 if let Some(d) = mpd.mediaPresentationDuration {
3074 period_duration_secs = d.as_secs_f64();
3075 }
3076 if let Some(d) = period.duration {
3077 period_duration_secs = d.as_secs_f64();
3078 }
3079 let maybe_subtitle_adaptation = if let Some(ref lang) = downloader.language_preference_subtitles {
3080 period.adaptations.iter().filter(is_subtitle_adaptation)
3081 .min_by_key(|a| adaptation_lang_distance(a, lang))
3082 } else {
3083 period.adaptations.iter().find(is_subtitle_adaptation)
3085 };
3086 if downloader.fetch_subtitles {
3087 if let Some(subtitle_adaptation) = maybe_subtitle_adaptation {
3088 let subtitle_format = subtitle_type(&subtitle_adaptation);
3089 subtitle_formats.push(subtitle_format);
3090 if downloader.verbosity > 1 && downloader.fetch_subtitles {
3091 info!(" Retrieving subtitles in format {subtitle_format:?}");
3092 }
3093 let mut base_url = base_url.clone();
3096 if let Some(bu) = &subtitle_adaptation.BaseURL.first() {
3097 base_url = merge_baseurls(&base_url, &bu.base)?;
3098 }
3099 if let Some(rep) = subtitle_adaptation.representations.first() {
3102 if !rep.BaseURL.is_empty() {
3103 for st_bu in &rep.BaseURL {
3104 let st_url = merge_baseurls(&base_url, &st_bu.base)?;
3105 let mut req = client.get(st_url.clone());
3106 if let Some(referer) = &downloader.referer {
3107 req = req.header("Referer", referer);
3108 } else {
3109 req = req.header("Referer", base_url.to_string());
3110 }
3111 let rqw = req.build()
3112 .map_err(|e| network_error("building request", &e))?;
3113 let subs = reqwest_bytes_with_retries(client, rqw, 5).await
3114 .map_err(|e| network_error("fetching subtitles", &e))?;
3115 let mut subs_path = period_output_path.clone();
3116 let subtitle_format = subtitle_type(&subtitle_adaptation);
3117 match subtitle_format {
3118 SubtitleType::Vtt => subs_path.set_extension("vtt"),
3119 SubtitleType::Srt => subs_path.set_extension("srt"),
3120 SubtitleType::Ttml => subs_path.set_extension("ttml"),
3121 SubtitleType::Sami => subs_path.set_extension("sami"),
3122 SubtitleType::Wvtt => subs_path.set_extension("wvtt"),
3123 SubtitleType::Stpp => subs_path.set_extension("stpp"),
3124 _ => subs_path.set_extension("sub"),
3125 };
3126 subtitle_formats.push(subtitle_format);
3127 let mut subs_file = File::create(subs_path.clone()).await
3128 .map_err(|e| DashMpdError::Io(e, String::from("creating subtitle file")))?;
3129 if downloader.verbosity > 2 {
3130 info!(" Subtitle {st_url} -> {} octets", subs.len());
3131 }
3132 match subs_file.write_all(&subs).await {
3133 Ok(()) => {
3134 if downloader.verbosity > 0 {
3135 info!(" Downloaded subtitles ({subtitle_format:?}) to {}",
3136 subs_path.display());
3137 }
3138 },
3139 Err(e) => {
3140 error!("Unable to write subtitle file: {e:?}");
3141 return Err(DashMpdError::Io(e, String::from("writing subtitle data")));
3142 },
3143 }
3144 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
3145 subtitle_formats.contains(&SubtitleType::Ttxt)
3146 {
3147 if downloader.verbosity > 0 {
3148 info!(" Converting subtitles to SRT format with MP4Box ");
3149 }
3150 let out = subs_path.with_extension("srt");
3151 let out_str = out.to_string_lossy();
3158 let subs_str = subs_path.to_string_lossy();
3159 let args = vec![
3160 "-srt", "1",
3161 "-out", &out_str,
3162 &subs_str];
3163 if downloader.verbosity > 0 {
3164 info!(" Running MPBox {}", args.join(" "));
3165 }
3166 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
3167 .args(args)
3168 .output()
3169 {
3170 let msg = partial_process_output(&mp4box.stdout);
3171 if !msg.is_empty() {
3172 info!("MP4Box stdout: {msg}");
3173 }
3174 let msg = partial_process_output(&mp4box.stderr);
3175 if !msg.is_empty() {
3176 info!("MP4Box stderr: {msg}");
3177 }
3178 if mp4box.status.success() {
3179 info!(" Converted subtitles to SRT");
3180 } else {
3181 warn!("Error running MP4Box to convert subtitles");
3182 }
3183 }
3184 }
3185 }
3186 } else if rep.SegmentTemplate.is_some() || subtitle_adaptation.SegmentTemplate.is_some() {
3187 let mut opt_init: Option<String> = None;
3188 let mut opt_media: Option<String> = None;
3189 let mut opt_duration: Option<f64> = None;
3190 let mut timescale = 1;
3191 let mut start_number = 1;
3192 if let Some(st) = &rep.SegmentTemplate {
3197 if let Some(i) = &st.initialization {
3198 opt_init = Some(i.clone());
3199 }
3200 if let Some(m) = &st.media {
3201 opt_media = Some(m.clone());
3202 }
3203 if let Some(d) = st.duration {
3204 opt_duration = Some(d);
3205 }
3206 if let Some(ts) = st.timescale {
3207 timescale = ts;
3208 }
3209 if let Some(s) = st.startNumber {
3210 start_number = s;
3211 }
3212 }
3213 let rid = match &rep.id {
3214 Some(id) => id,
3215 None => return Err(
3216 DashMpdError::UnhandledMediaStream(
3217 "Missing @id on Representation node".to_string())),
3218 };
3219 let mut dict = HashMap::from([("RepresentationID", rid.clone())]);
3220 if let Some(b) = &rep.bandwidth {
3221 dict.insert("Bandwidth", b.to_string());
3222 }
3223 if let Some(sl) = &rep.SegmentList {
3227 if downloader.verbosity > 1 {
3230 info!(" Using AdaptationSet>SegmentList addressing mode for subtitle representation");
3231 }
3232 let mut start_byte: Option<u64> = None;
3233 let mut end_byte: Option<u64> = None;
3234 if let Some(init) = &sl.Initialization {
3235 if let Some(range) = &init.range {
3236 let (s, e) = parse_range(range)?;
3237 start_byte = Some(s);
3238 end_byte = Some(e);
3239 }
3240 if let Some(su) = &init.sourceURL {
3241 let path = resolve_url_template(su, &dict);
3242 let u = merge_baseurls(&base_url, &path)?;
3243 let mf = MediaFragmentBuilder::new(period_counter, u)
3244 .with_range(start_byte, end_byte)
3245 .set_init()
3246 .build();
3247 fragments.push(mf);
3248 } else {
3249 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3250 .with_range(start_byte, end_byte)
3251 .set_init()
3252 .build();
3253 fragments.push(mf);
3254 }
3255 }
3256 for su in &sl.segment_urls {
3257 start_byte = None;
3258 end_byte = None;
3259 if let Some(range) = &su.mediaRange {
3261 let (s, e) = parse_range(range)?;
3262 start_byte = Some(s);
3263 end_byte = Some(e);
3264 }
3265 if let Some(m) = &su.media {
3266 let u = merge_baseurls(&base_url, m)?;
3267 let mf = MediaFragmentBuilder::new(period_counter, u)
3268 .with_range(start_byte, end_byte)
3269 .build();
3270 fragments.push(mf);
3271 } else if let Some(bu) = subtitle_adaptation.BaseURL.first() {
3272 let u = merge_baseurls(&base_url, &bu.base)?;
3273 let mf = MediaFragmentBuilder::new(period_counter, u)
3274 .with_range(start_byte, end_byte)
3275 .build();
3276 fragments.push(mf);
3277 }
3278 }
3279 }
3280 if let Some(sl) = &rep.SegmentList {
3281 if downloader.verbosity > 1 {
3283 info!(" Using Representation>SegmentList addressing mode for subtitle representation");
3284 }
3285 let mut start_byte: Option<u64> = None;
3286 let mut end_byte: Option<u64> = None;
3287 if let Some(init) = &sl.Initialization {
3288 if let Some(range) = &init.range {
3289 let (s, e) = parse_range(range)?;
3290 start_byte = Some(s);
3291 end_byte = Some(e);
3292 }
3293 if let Some(su) = &init.sourceURL {
3294 let path = resolve_url_template(su, &dict);
3295 let u = merge_baseurls(&base_url, &path)?;
3296 let mf = MediaFragmentBuilder::new(period_counter, u)
3297 .with_range(start_byte, end_byte)
3298 .set_init()
3299 .build();
3300 fragments.push(mf);
3301 } else {
3302 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3303 .with_range(start_byte, end_byte)
3304 .set_init()
3305 .build();
3306 fragments.push(mf);
3307 }
3308 }
3309 for su in sl.segment_urls.iter() {
3310 start_byte = None;
3311 end_byte = None;
3312 if let Some(range) = &su.mediaRange {
3314 let (s, e) = parse_range(range)?;
3315 start_byte = Some(s);
3316 end_byte = Some(e);
3317 }
3318 if let Some(m) = &su.media {
3319 let u = merge_baseurls(&base_url, m)?;
3320 let mf = MediaFragmentBuilder::new(period_counter, u)
3321 .with_range(start_byte, end_byte)
3322 .build();
3323 fragments.push(mf);
3324 } else if let Some(bu) = &rep.BaseURL.first() {
3325 let u = merge_baseurls(&base_url, &bu.base)?;
3326 let mf = MediaFragmentBuilder::new(period_counter, u)
3327 .with_range(start_byte, end_byte)
3328 .build();
3329 fragments.push(mf);
3330 };
3331 }
3332 } else if rep.SegmentTemplate.is_some() ||
3333 subtitle_adaptation.SegmentTemplate.is_some()
3334 {
3335 let st;
3338 if let Some(it) = &rep.SegmentTemplate {
3339 st = it;
3340 } else if let Some(it) = &subtitle_adaptation.SegmentTemplate {
3341 st = it;
3342 } else {
3343 panic!("unreachable");
3344 }
3345 if let Some(i) = &st.initialization {
3346 opt_init = Some(i.clone());
3347 }
3348 if let Some(m) = &st.media {
3349 opt_media = Some(m.clone());
3350 }
3351 if let Some(ts) = st.timescale {
3352 timescale = ts;
3353 }
3354 if let Some(sn) = st.startNumber {
3355 start_number = sn;
3356 }
3357 if let Some(stl) = &rep.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
3358 .or(subtitle_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
3359 {
3360 if downloader.verbosity > 1 {
3363 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for subtitle representation");
3364 }
3365 if let Some(init) = opt_init {
3366 let path = resolve_url_template(&init, &dict);
3367 let u = merge_baseurls(&base_url, &path)?;
3368 let mf = MediaFragmentBuilder::new(period_counter, u)
3369 .set_init()
3370 .build();
3371 fragments.push(mf);
3372 }
3373 if let Some(media) = opt_media {
3374 let sub_path = resolve_url_template(&media, &dict);
3375 let mut segment_time = 0;
3376 let mut segment_duration;
3377 let mut number = start_number;
3378 for s in &stl.segments {
3379 if let Some(t) = s.t {
3380 segment_time = t;
3381 }
3382 segment_duration = s.d;
3383 let dict = HashMap::from([("Time", segment_time.to_string()),
3385 ("Number", number.to_string())]);
3386 let path = resolve_url_template(&sub_path, &dict);
3387 let u = merge_baseurls(&base_url, &path)?;
3388 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3389 fragments.push(mf);
3390 number += 1;
3391 if let Some(r) = s.r {
3392 let mut count = 0i64;
3393 let end_time = period_duration_secs * timescale as f64;
3395 loop {
3396 count += 1;
3397 if r >= 0 {
3403 if count > r {
3404 break;
3405 }
3406 if downloader.force_duration.is_some() &&
3407 segment_time as f64 > end_time
3408 {
3409 break;
3410 }
3411 } else if segment_time as f64 > end_time {
3412 break;
3413 }
3414 segment_time += segment_duration;
3415 let dict = HashMap::from([("Time", segment_time.to_string()),
3416 ("Number", number.to_string())]);
3417 let path = resolve_url_template(&sub_path, &dict);
3418 let u = merge_baseurls(&base_url, &path)?;
3419 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3420 fragments.push(mf);
3421 number += 1;
3422 }
3423 }
3424 segment_time += segment_duration;
3425 }
3426 } else {
3427 return Err(DashMpdError::UnhandledMediaStream(
3428 "SegmentTimeline without a media attribute".to_string()));
3429 }
3430 } else { if downloader.verbosity > 0 {
3435 info!(" Using SegmentTemplate addressing mode for stpp subtitles");
3436 }
3437 if let Some(i) = &st.initialization {
3438 opt_init = Some(i.to_string());
3439 }
3440 if let Some(m) = &st.media {
3441 opt_media = Some(m.to_string());
3442 }
3443 if let Some(d) = st.duration {
3444 opt_duration = Some(d);
3445 }
3446 if let Some(ts) = st.timescale {
3447 timescale = ts;
3448 }
3449 if let Some(s) = st.startNumber {
3450 start_number = s;
3451 }
3452 let rid = match &rep.id {
3453 Some(id) => id,
3454 None => return Err(
3455 DashMpdError::UnhandledMediaStream(
3456 "Missing @id on Representation node".to_string())),
3457 };
3458 let mut dict = HashMap::from([("RepresentationID", rid.clone())]);
3459 if let Some(b) = &rep.bandwidth {
3460 dict.insert("Bandwidth", b.to_string());
3461 }
3462 let mut total_number = 0i64;
3463 if let Some(init) = opt_init {
3464 let path = resolve_url_template(&init, &dict);
3465 let u = merge_baseurls(&base_url, &path)?;
3466 let mf = MediaFragmentBuilder::new(period_counter, u)
3467 .set_init()
3468 .build();
3469 fragments.push(mf);
3470 }
3471 if let Some(media) = opt_media {
3472 let sub_path = resolve_url_template(&media, &dict);
3473 let mut segment_duration: f64 = -1.0;
3474 if let Some(d) = opt_duration {
3475 segment_duration = d;
3477 }
3478 if let Some(std) = st.duration {
3479 if timescale == 0 {
3480 return Err(DashMpdError::UnhandledMediaStream(
3481 "SegmentTemplate@duration attribute cannot be zero".to_string()));
3482 }
3483 segment_duration = std / timescale as f64;
3484 }
3485 if segment_duration < 0.0 {
3486 return Err(DashMpdError::UnhandledMediaStream(
3487 "Subtitle representation is missing SegmentTemplate@duration".to_string()));
3488 }
3489 total_number += (period_duration_secs / segment_duration).ceil() as i64;
3490 let mut number = start_number;
3491 for _ in 1..=total_number {
3492 let dict = HashMap::from([("Number", number.to_string())]);
3493 let path = resolve_url_template(&sub_path, &dict);
3494 let u = merge_baseurls(&base_url, &path)?;
3495 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3496 fragments.push(mf);
3497 number += 1;
3498 }
3499 }
3500 }
3501 } else if let Some(sb) = &rep.SegmentBase {
3502 info!(" Using SegmentBase@indexRange for subs");
3504 if downloader.verbosity > 1 {
3505 info!(" Using SegmentBase@indexRange addressing mode for subtitle representation");
3506 }
3507 let mut start_byte: Option<u64> = None;
3508 let mut end_byte: Option<u64> = None;
3509 if let Some(init) = &sb.Initialization {
3510 if let Some(range) = &init.range {
3511 let (s, e) = parse_range(range)?;
3512 start_byte = Some(s);
3513 end_byte = Some(e);
3514 }
3515 if let Some(su) = &init.sourceURL {
3516 let path = resolve_url_template(su, &dict);
3517 let u = merge_baseurls(&base_url, &path)?;
3518 let mf = MediaFragmentBuilder::new(period_counter, u)
3519 .with_range(start_byte, end_byte)
3520 .set_init()
3521 .build();
3522 fragments.push(mf);
3523 }
3524 }
3525 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3526 .set_init()
3527 .build();
3528 fragments.push(mf);
3529 }
3532 }
3533 }
3534 }
3535 }
3536 Ok(PeriodOutputs {
3537 fragments,
3538 diagnostics: Vec::new(),
3539 subtitle_formats,
3540 selected_audio_language: String::from("unk")
3541 })
3542}
3543
3544
3545struct DownloadState {
3548 period_counter: u8,
3549 segment_count: usize,
3550 segment_counter: usize,
3551 download_errors: u32
3552}
3553
3554#[tracing::instrument(level="trace", skip_all)]
3561async fn fetch_fragment(
3562 downloader: &mut DashDownloader,
3563 frag: &MediaFragment,
3564 fragment_type: &str,
3565 progress_percent: u32) -> Result<File, DashMpdError>
3566{
3567 let send_request = || async {
3568 trace!("send_request {}", frag.url.clone());
3569 let mut req = downloader.http_client.as_ref().unwrap()
3572 .get(frag.url.clone())
3573 .header("Accept", format!("{fragment_type}/*;q=0.9,*/*;q=0.5"))
3574 .header("Sec-Fetch-Mode", "navigate");
3575 if let Some(sb) = &frag.start_byte {
3576 if let Some(eb) = &frag.end_byte {
3577 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
3578 }
3579 }
3580 if let Some(ts) = &frag.timeout {
3581 req = req.timeout(*ts);
3582 }
3583 if let Some(referer) = &downloader.referer {
3584 req = req.header("Referer", referer);
3585 } else {
3586 req = req.header("Referer", downloader.redirected_url.to_string());
3587 }
3588 if let Some(username) = &downloader.auth_username {
3589 if let Some(password) = &downloader.auth_password {
3590 req = req.basic_auth(username, Some(password));
3591 }
3592 }
3593 if let Some(token) = &downloader.auth_bearer_token {
3594 req = req.bearer_auth(token);
3595 }
3596 req.send().await?
3597 .error_for_status()
3598 };
3599 match send_request
3600 .retry(ExponentialBuilder::default())
3601 .when(reqwest_error_transient_p)
3602 .notify(notify_transient)
3603 .await
3604 {
3605 Ok(response) => {
3606 match response.error_for_status() {
3607 Ok(mut resp) => {
3608 let tmp_out_std = tempfile::tempfile()
3609 .map_err(|e| DashMpdError::Io(e, String::from("creating tmpfile for fragment")))?;
3610 let mut tmp_out = tokio::fs::File::from_std(tmp_out_std);
3611 let content_type_checker = if fragment_type.eq("audio") {
3612 content_type_audio_p
3613 } else if fragment_type.eq("video") {
3614 content_type_video_p
3615 } else {
3616 panic!("fragment_type not audio or video");
3617 };
3618 if !downloader.content_type_checks || content_type_checker(&resp) {
3619 let mut fragment_out: Option<File> = None;
3620 if let Some(ref fragment_path) = downloader.fragment_path {
3621 if let Some(path) = frag.url.path_segments()
3622 .unwrap_or_else(|| "".split(' '))
3623 .next_back()
3624 {
3625 let vf_file = fragment_path.clone().join(fragment_type).join(path);
3626 if let Ok(f) = File::create(vf_file).await {
3627 fragment_out = Some(f)
3628 }
3629 }
3630 }
3631 let mut segment_size = 0;
3632 while let Some(chunk) = resp.chunk().await
3638 .map_err(|e| network_error(&format!("fetching DASH {fragment_type} segment"), &e))?
3639 {
3640 segment_size += chunk.len();
3641 downloader.bw_estimator_bytes += chunk.len();
3642 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
3643 throttle_download_rate(downloader, size).await?;
3644 if let Err(e) = tmp_out.write_all(&chunk).await {
3645 return Err(DashMpdError::Io(e, format!("writing DASH {fragment_type} data")));
3646 }
3647 if let Some(ref mut fout) = fragment_out {
3648 fout.write_all(&chunk)
3649 .map_err(|e| DashMpdError::Io(e, format!("writing {fragment_type} fragment")))
3650 .await?;
3651 }
3652 let elapsed = downloader.bw_estimator_started.elapsed().as_secs_f64();
3653 if (elapsed > 0.5) || (downloader.bw_estimator_bytes > 50_000) {
3654 let bw = downloader.bw_estimator_bytes as f64 / elapsed;
3655 for observer in &downloader.progress_observers {
3656 observer.update(progress_percent, bw as u64, &format!("Fetching {fragment_type} segments"));
3657 }
3658 downloader.bw_estimator_started = Instant::now();
3659 downloader.bw_estimator_bytes = 0;
3660 }
3661 }
3662 if downloader.verbosity > 2 {
3663 if let Some(sb) = &frag.start_byte {
3664 if let Some(eb) = &frag.end_byte {
3665 info!(" {fragment_type} segment {} range {sb}-{eb} -> {} octets",
3666 frag.url, segment_size);
3667 }
3668 } else {
3669 info!(" {fragment_type} segment {} -> {segment_size} octets", &frag.url);
3670 }
3671 }
3672 } else {
3673 warn!("Ignoring segment {} with non-{fragment_type} content-type", frag.url);
3674 };
3675 tmp_out.sync_all().await
3676 .map_err(|e| DashMpdError::Io(e, format!("syncing {fragment_type} fragment")))?;
3677 Ok(tmp_out)
3678 },
3679 Err(e) => Err(network_error("HTTP error", &e)),
3680 }
3681 },
3682 Err(e) => Err(network_error(&format!("{e:?}"), &e)),
3683 }
3684}
3685
3686
3687#[tracing::instrument(level="trace", skip_all)]
3689async fn fetch_period_audio(
3690 downloader: &mut DashDownloader,
3691 tmppath: &Path,
3692 audio_fragments: &[MediaFragment],
3693 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3694{
3695 let start_download = Instant::now();
3696 let mut have_audio = false;
3697 {
3698 let tmpfile_audio = File::create(tmppath).await
3702 .map_err(|e| DashMpdError::Io(e, String::from("creating audio tmpfile")))?;
3703 ensure_permissions_readable(tmppath).await?;
3704 let mut tmpfile_audio = BufWriter::new(tmpfile_audio);
3705 if let Some(ref fragment_path) = downloader.fragment_path {
3707 let audio_fragment_dir = fragment_path.join("audio");
3708 if !audio_fragment_dir.exists() {
3709 fs::create_dir_all(audio_fragment_dir).await
3710 .map_err(|e| DashMpdError::Io(e, String::from("creating audio fragment dir")))?;
3711 }
3712 }
3713 for frag in audio_fragments.iter().filter(|f| f.period == ds.period_counter) {
3717 ds.segment_counter += 1;
3718 let progress_percent = max(98, (100.0 * ds.segment_counter as f32 / (2.0 + ds.segment_count as f32)).ceil() as u32);
3721 let url = &frag.url;
3722 if url.scheme() == "data" {
3726 let us = &url.to_string();
3727 let du = DataUrl::process(us)
3728 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
3729 if du.mime_type().type_ != "audio" {
3730 return Err(DashMpdError::UnhandledMediaStream(
3731 String::from("expecting audio content in data URL")));
3732 }
3733 let (body, _fragment) = du.decode_to_vec()
3734 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
3735 if downloader.verbosity > 2 {
3736 info!(" Audio segment data URL -> {} octets", body.len());
3737 }
3738 tmpfile_audio.write_all(&body)
3739 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH audio data")))
3740 .await?;
3741 have_audio = true;
3742 } else {
3743 'done: for _ in 0..downloader.fragment_retry_count {
3745 match fetch_fragment(downloader, frag, "audio", progress_percent).await {
3746 Ok(mut frag_file) => {
3747 frag_file.rewind().await
3748 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
3749 let mut buf = Vec::new();
3750 frag_file.read_to_end(&mut buf).await
3751 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
3752 tmpfile_audio.write_all(&buf)
3753 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH audio data")))
3754 .await?;
3755 have_audio = true;
3756 break 'done;
3757 },
3758 Err(e) => {
3759 if downloader.verbosity > 0 {
3760 error!("Error fetching audio segment {url}: {e:?}");
3761 }
3762 ds.download_errors += 1;
3763 if ds.download_errors > downloader.max_error_count {
3764 error!("max_error_count network errors encountered");
3765 return Err(DashMpdError::Network(
3766 String::from("more than max_error_count network errors")));
3767 }
3768 },
3769 }
3770 info!(" Retrying audio segment {url}");
3771 if downloader.sleep_between_requests > 0 {
3772 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
3773 }
3774 }
3775 }
3776 }
3777 tmpfile_audio.flush().map_err(|e| {
3778 error!("Couldn't flush DASH audio file: {e}");
3779 DashMpdError::Io(e, String::from("flushing DASH audio file"))
3780 }).await?;
3781 } if !downloader.decryption_keys.is_empty() {
3783 if downloader.verbosity > 0 {
3784 let metadata = fs::metadata(tmppath).await
3785 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted audio metadata")))?;
3786 info!(" Attempting to decrypt audio stream ({} kB) with {}",
3787 metadata.len() / 1024,
3788 downloader.decryptor_preference);
3789 }
3790 let out_ext = downloader.output_path.as_ref().unwrap()
3791 .extension()
3792 .unwrap_or(OsStr::new("mp4"));
3793 let decrypted = tmp_file_path("dashmpd-decrypted-audio", out_ext)?;
3794 if downloader.decryptor_preference.eq("mp4decrypt") {
3795 decrypt_mp4decrypt(downloader, tmppath, &decrypted, "audio").await?;
3796 } else if downloader.decryptor_preference.eq("shaka") {
3797 decrypt_shaka(downloader, tmppath, &decrypted, "audio").await?;
3798 } else if downloader.decryptor_preference.eq("shaka-container") {
3799 decrypt_shaka_container(downloader, tmppath, &decrypted, "audio").await?;
3800 } else if downloader.decryptor_preference.eq("mp4box") {
3801 decrypt_mp4box(downloader, tmppath, &decrypted, "audio").await?;
3802 } else if downloader.decryptor_preference.eq("mp4box-container") {
3803 decrypt_mp4box_container(downloader, tmppath, &decrypted, "audio").await?;
3804 } else {
3805 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
3806 }
3807 if let Err(e) = fs::metadata(&decrypted).await {
3808 return Err(DashMpdError::Decrypting(format!("missing decrypted audio file: {e:?}")));
3809 }
3810 fs::remove_file(&tmppath).await
3811 .map_err(|e| DashMpdError::Io(e, String::from("deleting encrypted audio tmpfile")))?;
3812 fs::rename(&decrypted, &tmppath).await
3813 .map_err(|e| {
3814 let dbg = Command::new("bash")
3815 .args(["-c", &format!("id;ls -l {}", decrypted.display())])
3816 .output()
3817 .unwrap();
3818 warn!("debugging ls: {}", String::from_utf8_lossy(&dbg.stdout));
3819 DashMpdError::Io(e, format!("renaming decrypted audio {}->{}", decrypted.display(), tmppath.display()))
3820 })?;
3821 }
3822 if let Ok(metadata) = fs::metadata(&tmppath).await {
3823 if downloader.verbosity > 1 {
3824 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
3825 let elapsed = start_download.elapsed();
3826 info!(" Wrote {mbytes:.1}MB to DASH audio file ({:.1} MB/s)",
3827 mbytes / elapsed.as_secs_f64());
3828 }
3829 }
3830 Ok(have_audio)
3831}
3832
3833
3834#[tracing::instrument(level="trace", skip_all)]
3836async fn fetch_period_video(
3837 downloader: &mut DashDownloader,
3838 tmppath: &Path,
3839 video_fragments: &[MediaFragment],
3840 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3841{
3842 let start_download = Instant::now();
3843 let mut have_video = false;
3844 {
3845 let tmpfile_video = File::create(tmppath).await
3849 .map_err(|e| DashMpdError::Io(e, String::from("creating video tmpfile")))?;
3850 ensure_permissions_readable(tmppath).await?;
3851 let mut tmpfile_video = BufWriter::new(tmpfile_video);
3852 if let Some(ref fragment_path) = downloader.fragment_path {
3854 let video_fragment_dir = fragment_path.join("video");
3855 if !video_fragment_dir.exists() {
3856 fs::create_dir_all(video_fragment_dir).await
3857 .map_err(|e| DashMpdError::Io(e, String::from("creating video fragment dir")))?;
3858 }
3859 }
3860 for frag in video_fragments.iter().filter(|f| f.period == ds.period_counter) {
3861 ds.segment_counter += 1;
3862 let progress_percent = max(98, (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32);
3865 if frag.url.scheme() == "data" {
3866 let us = &frag.url.to_string();
3867 let du = DataUrl::process(us)
3868 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
3869 if du.mime_type().type_ != "video" {
3870 return Err(DashMpdError::UnhandledMediaStream(
3871 String::from("expecting video content in data URL")));
3872 }
3873 let (body, _fragment) = du.decode_to_vec()
3874 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
3875 if downloader.verbosity > 2 {
3876 info!(" Video segment data URL -> {} octets", body.len());
3877 }
3878 tmpfile_video.write_all(&body)
3879 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH video data")))
3880 .await?;
3881 have_video = true;
3882 } else {
3883 'done: for _ in 0..downloader.fragment_retry_count {
3884 match fetch_fragment(downloader, frag, "video", progress_percent).await {
3885 Ok(mut frag_file) => {
3886 frag_file.rewind().await
3887 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
3888 let mut buf = Vec::new();
3889 frag_file.read_to_end(&mut buf).await
3890 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
3891 tmpfile_video.write_all(&buf)
3892 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH video data")))
3893 .await?;
3894 have_video = true;
3895 break 'done;
3896 },
3897 Err(e) => {
3898 if downloader.verbosity > 0 {
3899 error!(" Error fetching video segment {}: {e:?}", frag.url);
3900 }
3901 ds.download_errors += 1;
3902 if ds.download_errors > downloader.max_error_count {
3903 return Err(DashMpdError::Network(
3904 String::from("more than max_error_count network errors")));
3905 }
3906 },
3907 }
3908 info!(" Retrying video segment {}", frag.url);
3909 if downloader.sleep_between_requests > 0 {
3910 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
3911 }
3912 }
3913 }
3914 }
3915 tmpfile_video.flush().map_err(|e| {
3916 error!(" Couldn't flush video file: {e}");
3917 DashMpdError::Io(e, String::from("flushing video file"))
3918 }).await?;
3919 } if !downloader.decryption_keys.is_empty() {
3921 if downloader.verbosity > 0 {
3922 let metadata = fs::metadata(tmppath).await
3923 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted video metadata")))?;
3924 info!(" Attempting to decrypt video stream ({} kB) with {}",
3925 metadata.len() / 1024,
3926 downloader.decryptor_preference);
3927 }
3928 let out_ext = downloader.output_path.as_ref().unwrap()
3929 .extension()
3930 .unwrap_or(OsStr::new("mp4"));
3931 let decrypted = tmp_file_path("dashmpd-decrypted-video", out_ext)?;
3932 if downloader.decryptor_preference.eq("mp4decrypt") {
3933 decrypt_mp4decrypt(downloader, tmppath, &decrypted, "video").await?;
3934 } else if downloader.decryptor_preference.eq("shaka") {
3935 decrypt_shaka(downloader, tmppath, &decrypted, "video").await?;
3936 } else if downloader.decryptor_preference.eq("shaka-container") {
3937 decrypt_shaka_container(downloader, tmppath, &decrypted, "video").await?;
3938 } else if downloader.decryptor_preference.eq("mp4box") {
3939 decrypt_mp4box(downloader, tmppath, &decrypted, "video").await?;
3940 } else if downloader.decryptor_preference.eq("mp4box-container") {
3941 decrypt_mp4box_container(downloader, tmppath, &decrypted, "video").await?;
3942 } else {
3943 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
3944 }
3945 if let Err(e) = fs::metadata(&decrypted).await {
3946 return Err(DashMpdError::Decrypting(format!("missing decrypted video file: {e:?}")));
3947 }
3948 fs::remove_file(&tmppath).await
3949 .map_err(|e| DashMpdError::Io(e, String::from("deleting encrypted video tmpfile")))?;
3950 fs::rename(&decrypted, &tmppath).await
3951 .map_err(|e| DashMpdError::Io(e, String::from("renaming decrypted video")))?;
3952 }
3953 if let Ok(metadata) = fs::metadata(&tmppath).await {
3954 if downloader.verbosity > 1 {
3955 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
3956 let elapsed = start_download.elapsed();
3957 info!(" Wrote {mbytes:.1}MB to DASH video file ({:.1} MB/s)",
3958 mbytes / elapsed.as_secs_f64());
3959 }
3960 }
3961 Ok(have_video)
3962}
3963
3964
3965#[tracing::instrument(level="trace", skip_all)]
3967async fn fetch_period_subtitles(
3968 downloader: &DashDownloader,
3969 tmppath: &Path,
3970 subtitle_fragments: &[MediaFragment],
3971 subtitle_formats: &[SubtitleType],
3972 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3973{
3974 let client = downloader.http_client.clone().unwrap();
3975 let start_download = Instant::now();
3976 let mut have_subtitles = false;
3977 {
3978 let tmpfile_subs = File::create(tmppath).await
3979 .map_err(|e| DashMpdError::Io(e, String::from("creating subs tmpfile")))?;
3980 ensure_permissions_readable(tmppath).await?;
3981 let mut tmpfile_subs = BufWriter::new(tmpfile_subs);
3982 for frag in subtitle_fragments {
3983 ds.segment_counter += 1;
3985 let progress_percent = max(98, (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32);
3986 for observer in &downloader.progress_observers {
3987 observer.update(progress_percent, 1, "Fetching subtitle segments");
3988 }
3989 if frag.url.scheme() == "data" {
3990 let us = &frag.url.to_string();
3991 let du = DataUrl::process(us)
3992 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
3993 if du.mime_type().type_ != "video" {
3994 return Err(DashMpdError::UnhandledMediaStream(
3995 String::from("expecting video content in data URL")));
3996 }
3997 let (body, _fragment) = du.decode_to_vec()
3998 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
3999 if downloader.verbosity > 2 {
4000 info!(" Subtitle segment data URL -> {} octets", body.len());
4001 }
4002 tmpfile_subs.write_all(&body)
4003 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH subtitle data")))
4004 .await?;
4005 have_subtitles = true;
4006 } else {
4007 let fetch = || async {
4008 let mut req = client.get(frag.url.clone())
4009 .header("Sec-Fetch-Mode", "navigate");
4010 if let Some(sb) = &frag.start_byte {
4011 if let Some(eb) = &frag.end_byte {
4012 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
4013 }
4014 }
4015 if let Some(referer) = &downloader.referer {
4016 req = req.header("Referer", referer);
4017 } else {
4018 req = req.header("Referer", downloader.redirected_url.to_string());
4019 }
4020 if let Some(username) = &downloader.auth_username {
4021 if let Some(password) = &downloader.auth_password {
4022 req = req.basic_auth(username, Some(password));
4023 }
4024 }
4025 if let Some(token) = &downloader.auth_bearer_token {
4026 req = req.bearer_auth(token);
4027 }
4028 req.send().await?
4029 .error_for_status()
4030 };
4031 let mut failure = None;
4032 match fetch
4033 .retry(ExponentialBuilder::default())
4034 .when(reqwest_error_transient_p)
4035 .notify(notify_transient)
4036 .await
4037 {
4038 Ok(response) => {
4039 if response.status().is_success() {
4040 let dash_bytes = response.bytes().await
4041 .map_err(|e| network_error("fetching DASH subtitle segment", &e))?;
4042 if downloader.verbosity > 2 {
4043 if let Some(sb) = &frag.start_byte {
4044 if let Some(eb) = &frag.end_byte {
4045 info!(" Subtitle segment {} range {sb}-{eb} -> {} octets",
4046 &frag.url, dash_bytes.len());
4047 }
4048 } else {
4049 info!(" Subtitle segment {} -> {} octets", &frag.url, dash_bytes.len());
4050 }
4051 }
4052 let size = min((dash_bytes.len()/1024 + 1) as u32, u32::MAX);
4053 throttle_download_rate(downloader, size).await?;
4054 tmpfile_subs.write_all(&dash_bytes)
4055 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH subtitle data")))
4056 .await?;
4057 have_subtitles = true;
4058 } else {
4059 failure = Some(format!("HTTP error {}", response.status().as_str()));
4060 }
4061 },
4062 Err(e) => failure = Some(format!("{e}")),
4063 }
4064 if let Some(f) = failure {
4065 if downloader.verbosity > 0 {
4066 error!("{f} fetching subtitle segment {}", &frag.url);
4067 }
4068 ds.download_errors += 1;
4069 if ds.download_errors > downloader.max_error_count {
4070 return Err(DashMpdError::Network(
4071 String::from("more than max_error_count network errors")));
4072 }
4073 }
4074 }
4075 if downloader.sleep_between_requests > 0 {
4076 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
4077 }
4078 }
4079 tmpfile_subs.flush().map_err(|e| {
4080 error!("Couldn't flush subs file: {e}");
4081 DashMpdError::Io(e, String::from("flushing subtitle file"))
4082 }).await?;
4083 } if have_subtitles {
4085 if let Ok(metadata) = fs::metadata(tmppath).await {
4086 if downloader.verbosity > 1 {
4087 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4088 let elapsed = start_download.elapsed();
4089 info!(" Wrote {mbytes:.1}MB to DASH subtitle file ({:.1} MB/s)",
4090 mbytes / elapsed.as_secs_f64());
4091 }
4092 }
4093 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
4096 subtitle_formats.contains(&SubtitleType::Ttxt)
4097 {
4098 if downloader.verbosity > 0 {
4100 if let Some(fmt) = subtitle_formats.first() {
4101 info!(" Downloaded media contains subtitles in {fmt:?} format");
4102 }
4103 info!(" Running MP4Box to extract subtitles");
4104 }
4105 let out = downloader.output_path.as_ref().unwrap()
4106 .with_extension("srt");
4107 let out_str = out.to_string_lossy();
4108 let tmp_str = tmppath.to_string_lossy();
4109 let args = vec![
4110 "-srt", "1",
4111 "-out", &out_str,
4112 &tmp_str];
4113 if downloader.verbosity > 0 {
4114 info!(" Running MP4Box {}", args.join(" "));
4115 }
4116 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4117 .args(args)
4118 .output()
4119 {
4120 let msg = partial_process_output(&mp4box.stdout);
4121 if !msg.is_empty() {
4122 info!(" MP4Box stdout: {msg}");
4123 }
4124 let msg = partial_process_output(&mp4box.stderr);
4125 if !msg.is_empty() {
4126 info!(" MP4Box stderr: {msg}");
4127 }
4128 if mp4box.status.success() {
4129 info!(" Extracted subtitles as SRT");
4130 } else {
4131 warn!(" Error running MP4Box to extract subtitles");
4132 }
4133 } else {
4134 warn!(" Failed to spawn MP4Box to extract subtitles");
4135 }
4136 }
4137 if subtitle_formats.contains(&SubtitleType::Stpp) {
4138 if downloader.verbosity > 0 {
4139 info!(" Converting STPP subtitles to TTML format with ffmpeg");
4140 }
4141 let out = downloader.output_path.as_ref().unwrap()
4142 .with_extension("ttml");
4143 let tmppath_arg = tmppath.to_string_lossy();
4144 let out_arg = &out.to_string_lossy();
4145 let ffmpeg_args = vec![
4146 "-hide_banner",
4147 "-nostats",
4148 "-loglevel", "error",
4149 "-y", "-nostdin",
4151 "-i", &tmppath_arg,
4152 "-f", "data",
4153 "-map", "0",
4154 "-c", "copy",
4155 out_arg];
4156 if downloader.verbosity > 0 {
4157 info!(" Running ffmpeg {}", ffmpeg_args.join(" "));
4158 }
4159 if let Ok(ffmpeg) = Command::new(downloader.ffmpeg_location.clone())
4160 .args(ffmpeg_args)
4161 .output()
4162 {
4163 let msg = partial_process_output(&ffmpeg.stdout);
4164 if !msg.is_empty() {
4165 info!(" ffmpeg stdout: {msg}");
4166 }
4167 let msg = partial_process_output(&ffmpeg.stderr);
4168 if !msg.is_empty() {
4169 info!(" ffmpeg stderr: {msg}");
4170 }
4171 if ffmpeg.status.success() {
4172 info!(" Converted STPP subtitles to TTML format");
4173 } else {
4174 warn!(" Error running ffmpeg to convert subtitles");
4175 }
4176 }
4177 }
4181
4182 }
4183 Ok(have_subtitles)
4184}
4185
4186
4187async fn fetch_mpd_http(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4189 let client = &downloader.http_client.clone().unwrap();
4190 let send_request = || async {
4191 let mut req = client.get(&downloader.mpd_url)
4192 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4193 .header("Accept-Language", "en-US,en")
4194 .header("Upgrade-Insecure-Requests", "1")
4195 .header("Sec-Fetch-Mode", "navigate");
4196 if let Some(referer) = &downloader.referer {
4197 req = req.header("Referer", referer);
4198 }
4199 if let Some(username) = &downloader.auth_username {
4200 if let Some(password) = &downloader.auth_password {
4201 req = req.basic_auth(username, Some(password));
4202 }
4203 }
4204 if let Some(token) = &downloader.auth_bearer_token {
4205 req = req.bearer_auth(token);
4206 }
4207 req.send().await?
4208 .error_for_status()
4209 };
4210 for observer in &downloader.progress_observers {
4211 observer.update(1, 1, "Fetching DASH manifest");
4212 }
4213 if downloader.verbosity > 0 {
4214 if !downloader.fetch_audio && !downloader.fetch_video && !downloader.fetch_subtitles {
4215 info!("Only simulating media downloads");
4216 }
4217 info!("Fetching the DASH manifest");
4218 }
4219 let response = send_request
4220 .retry(ExponentialBuilder::default())
4221 .when(reqwest_error_transient_p)
4222 .notify(notify_transient)
4223 .await
4224 .map_err(|e| network_error("requesting DASH manifest", &e))?;
4225 if !response.status().is_success() {
4226 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4227 return Err(DashMpdError::Network(msg));
4228 }
4229 downloader.redirected_url = response.url().clone();
4230 response.bytes().await
4231 .map_err(|e| network_error("fetching DASH manifest", &e))
4232}
4233
4234async fn fetch_mpd_file(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4237 if ! &downloader.mpd_url.starts_with("file://") {
4238 return Err(DashMpdError::Other(String::from("expecting file:// URL scheme")));
4239 }
4240 let url = Url::parse(&downloader.mpd_url)
4241 .map_err(|_| DashMpdError::Other(String::from("parsing MPD URL")))?;
4242 let path = url.to_file_path()
4243 .map_err(|_| DashMpdError::Other(String::from("extracting path from file:// URL")))?;
4244 let octets = fs::read(path).await
4245 .map_err(|_| DashMpdError::Other(String::from("reading from file:// URL")))?;
4246 Ok(Bytes::from(octets))
4247}
4248
4249
4250#[tracing::instrument(level="trace", skip_all)]
4251async fn fetch_mpd(downloader: &mut DashDownloader) -> Result<PathBuf, DashMpdError> {
4252 #[cfg(all(feature = "sandbox", target_os = "linux"))]
4253 if downloader.sandbox {
4254 if let Err(e) = restrict_thread(downloader) {
4255 warn!("Sandboxing failed: {e:?}");
4256 }
4257 }
4258 let xml = if downloader.mpd_url.starts_with("file://") {
4259 fetch_mpd_file(downloader).await?
4260 } else {
4261 fetch_mpd_http(downloader).await?
4262 };
4263 let mut mpd: MPD = parse_resolving_xlinks(downloader, &xml).await
4264 .map_err(|e| parse_error("parsing DASH XML", e))?;
4265 let client = &downloader.http_client.clone().unwrap();
4268 if let Some(new_location) = &mpd.locations.first() {
4269 let new_url = &new_location.url;
4270 if downloader.verbosity > 0 {
4271 info!("Redirecting to new manifest <Location> {new_url}");
4272 }
4273 let send_request = || async {
4274 let mut req = client.get(new_url)
4275 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4276 .header("Accept-Language", "en-US,en")
4277 .header("Sec-Fetch-Mode", "navigate");
4278 if let Some(referer) = &downloader.referer {
4279 req = req.header("Referer", referer);
4280 } else {
4281 req = req.header("Referer", downloader.redirected_url.to_string());
4282 }
4283 if let Some(username) = &downloader.auth_username {
4284 if let Some(password) = &downloader.auth_password {
4285 req = req.basic_auth(username, Some(password));
4286 }
4287 }
4288 if let Some(token) = &downloader.auth_bearer_token {
4289 req = req.bearer_auth(token);
4290 }
4291 req.send().await?
4292 .error_for_status()
4293 };
4294 let response = send_request
4295 .retry(ExponentialBuilder::default())
4296 .when(reqwest_error_transient_p)
4297 .notify(notify_transient)
4298 .await
4299 .map_err(|e| network_error("requesting relocated DASH manifest", &e))?;
4300 if !response.status().is_success() {
4301 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4302 return Err(DashMpdError::Network(msg));
4303 }
4304 downloader.redirected_url = response.url().clone();
4305 let xml = response.bytes().await
4306 .map_err(|e| network_error("fetching relocated DASH manifest", &e))?;
4307 mpd = parse_resolving_xlinks(downloader, &xml).await
4308 .map_err(|e| parse_error("parsing relocated DASH XML", e))?;
4309 }
4310 if mpd_is_dynamic(&mpd) {
4311 if downloader.allow_live_streams {
4314 if downloader.verbosity > 0 {
4315 warn!("Attempting to download from live stream (this may not work).");
4316 }
4317 } else {
4318 return Err(DashMpdError::UnhandledMediaStream("Don't know how to download dynamic MPD".to_string()));
4319 }
4320 }
4321 let mut toplevel_base_url = downloader.redirected_url.clone();
4322 if let Some(bu) = &mpd.base_url.first() {
4324 toplevel_base_url = merge_baseurls(&downloader.redirected_url, &bu.base)?;
4325 }
4326 if let Some(base) = &downloader.base_url {
4329 toplevel_base_url = merge_baseurls(&downloader.redirected_url, base)?;
4330 }
4331 if downloader.verbosity > 0 {
4332 let pcount = mpd.periods.len();
4333 info!("DASH manifest has {pcount} period{}", if pcount > 1 { "s" } else { "" });
4334 print_available_streams(&mpd);
4335 }
4336 let mut pds: Vec<PeriodDownloads> = Vec::new();
4344 let mut period_counter = 0;
4345 for mpd_period in &mpd.periods {
4346 let period = mpd_period.clone();
4347 period_counter += 1;
4348 if let Some(min) = downloader.minimum_period_duration {
4349 if let Some(duration) = period.duration {
4350 if duration < min {
4351 if let Some(id) = period.id.as_ref() {
4352 info!("Skipping period {id} (#{period_counter}): duration is less than requested minimum");
4353 } else {
4354 info!("Skipping period #{period_counter}: duration is less than requested minimum");
4355 }
4356 continue;
4357 }
4358 }
4359 }
4360 let mut pd = PeriodDownloads { period_counter, ..Default::default() };
4361 if let Some(id) = period.id.as_ref() {
4362 pd.id = Some(id.clone());
4363 }
4364 if downloader.verbosity > 0 {
4365 if let Some(id) = period.id.as_ref() {
4366 info!("Preparing download for period {id} (#{period_counter})");
4367 } else {
4368 info!("Preparing download for period #{period_counter}");
4369 }
4370 }
4371 let mut base_url = toplevel_base_url.clone();
4372 if let Some(bu) = period.BaseURL.first() {
4374 base_url = merge_baseurls(&base_url, &bu.base)?;
4375 }
4376 let mut audio_outputs = PeriodOutputs::default();
4377 if downloader.fetch_audio {
4378 audio_outputs = do_period_audio(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4379 for f in audio_outputs.fragments {
4380 pd.audio_fragments.push(f);
4381 }
4382 pd.selected_audio_language = audio_outputs.selected_audio_language;
4383 }
4384 let mut video_outputs = PeriodOutputs::default();
4385 if downloader.fetch_video {
4386 video_outputs = do_period_video(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4387 for f in video_outputs.fragments {
4388 pd.video_fragments.push(f);
4389 }
4390 }
4391 match do_period_subtitles(downloader, &mpd, &period, period_counter, base_url.clone()).await {
4392 Ok(subtitle_outputs) => {
4393 for f in subtitle_outputs.fragments {
4394 pd.subtitle_fragments.push(f);
4395 }
4396 for f in subtitle_outputs.subtitle_formats {
4397 pd.subtitle_formats.push(f);
4398 }
4399 },
4400 Err(e) => warn!(" Ignoring error triggered while processing subtitles: {e}"),
4401 }
4402 if downloader.verbosity > 0 {
4404 use base64::prelude::{Engine as _, BASE64_STANDARD};
4405
4406 audio_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4407 for f in pd.audio_fragments.iter().filter(|f| f.is_init) {
4408 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4409 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4410 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4411 info!(" {}", pssh.to_string());
4412 }
4413 }
4414 }
4415 video_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4416 for f in pd.video_fragments.iter().filter(|f| f.is_init) {
4417 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4418 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4419 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4420 info!(" {}", pssh.to_string());
4421 }
4422 }
4423 }
4424 }
4425 pds.push(pd);
4426 } let output_path = &downloader.output_path.as_ref().unwrap().clone();
4431 let mut period_output_pathbufs: Vec<PathBuf> = Vec::new();
4432 let mut ds = DownloadState {
4433 period_counter: 0,
4434 segment_count: pds.iter().map(period_fragment_count).sum(),
4436 segment_counter: 0,
4437 download_errors: 0
4438 };
4439 for pd in pds {
4440 let mut have_audio = false;
4441 let mut have_video = false;
4442 let mut have_subtitles = false;
4443 ds.period_counter = pd.period_counter;
4444 let period_output_path = output_path_for_period(output_path, pd.period_counter);
4445 #[allow(clippy::collapsible_if)]
4446 if downloader.verbosity > 0 {
4447 if downloader.fetch_audio || downloader.fetch_video || downloader.fetch_subtitles {
4448 let idnum = if let Some(id) = pd.id {
4449 format!("id={} (#{})", id, pd.period_counter)
4450 } else {
4451 format!("#{}", pd.period_counter)
4452 };
4453 info!("Period {idnum}: fetching {} audio, {} video and {} subtitle segments",
4454 pd.audio_fragments.len(),
4455 pd.video_fragments.len(),
4456 pd.subtitle_fragments.len());
4457 }
4458 }
4459 let output_ext = downloader.output_path.as_ref().unwrap()
4460 .extension()
4461 .unwrap_or(OsStr::new("mp4"));
4462 let tmppath_audio = if let Some(ref path) = downloader.keep_audio {
4463 path.clone()
4464 } else {
4465 tmp_file_path("dashmpd-audio", output_ext)?
4466 };
4467 let tmppath_video = if let Some(ref path) = downloader.keep_video {
4468 path.clone()
4469 } else {
4470 tmp_file_path("dashmpd-video", output_ext)?
4471 };
4472 let tmppath_subs = tmp_file_path("dashmpd-subs", OsStr::new("sub"))?;
4473 if downloader.fetch_audio && !pd.audio_fragments.is_empty() {
4474 have_audio = fetch_period_audio(downloader,
4478 &tmppath_audio, &pd.audio_fragments,
4479 &mut ds).await?;
4480 }
4481 if downloader.fetch_video && !pd.video_fragments.is_empty() {
4482 have_video = fetch_period_video(downloader,
4483 &tmppath_video, &pd.video_fragments,
4484 &mut ds).await?;
4485 }
4486 if downloader.fetch_subtitles && !pd.subtitle_fragments.is_empty() {
4490 have_subtitles = fetch_period_subtitles(downloader,
4491 &tmppath_subs,
4492 &pd.subtitle_fragments,
4493 &pd.subtitle_formats,
4494 &mut ds).await?;
4495 }
4496
4497 if have_audio && have_video {
4500 for observer in &downloader.progress_observers {
4501 observer.update(99, 1, "Muxing audio and video");
4502 }
4503 if downloader.verbosity > 1 {
4504 info!(" Muxing audio and video streams");
4505 }
4506 let audio_tracks = vec![
4507 AudioTrack {
4508 language: pd.selected_audio_language,
4509 path: tmppath_audio.clone()
4510 }];
4511 mux_audio_video(downloader, &period_output_path, &audio_tracks, &tmppath_video).await?;
4512 if pd.subtitle_formats.contains(&SubtitleType::Stpp) {
4513 let container = match &period_output_path.extension() {
4514 Some(ext) => ext.to_str().unwrap_or("mp4"),
4515 None => "mp4",
4516 };
4517 if container.eq("mp4") {
4518 if downloader.verbosity > 1 {
4519 if let Some(fmt) = &pd.subtitle_formats.first() {
4520 info!(" Downloaded media contains subtitles in {fmt:?} format");
4521 }
4522 info!(" Running MP4Box to merge subtitles with output MP4 container");
4523 }
4524 let tmp_str = tmppath_subs.to_string_lossy();
4527 let period_output_str = period_output_path.to_string_lossy();
4528 let args = vec!["-add", &tmp_str, &period_output_str];
4529 if downloader.verbosity > 0 {
4530 info!(" Running MP4Box {}", args.join(" "));
4531 }
4532 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4533 .args(args)
4534 .output()
4535 {
4536 let msg = partial_process_output(&mp4box.stdout);
4537 if !msg.is_empty() {
4538 info!(" MP4Box stdout: {msg}");
4539 }
4540 let msg = partial_process_output(&mp4box.stderr);
4541 if !msg.is_empty() {
4542 info!(" MP4Box stderr: {msg}");
4543 }
4544 if mp4box.status.success() {
4545 info!(" Merged subtitles with MP4 container");
4546 } else {
4547 warn!(" Error running MP4Box to merge subtitles");
4548 }
4549 } else {
4550 warn!(" Failed to spawn MP4Box to merge subtitles");
4551 }
4552 } else if container.eq("mkv") || container.eq("webm") {
4553 let srt = period_output_path.with_extension("srt");
4565 if srt.exists() {
4566 if downloader.verbosity > 0 {
4567 info!(" Running mkvmerge to merge subtitles with output Matroska container");
4568 }
4569 let tmppath = temporary_outpath(".mkv")?;
4570 let pop_arg = &period_output_path.to_string_lossy();
4571 let srt_arg = &srt.to_string_lossy();
4572 let mkvmerge_args = vec!["-o", &tmppath, pop_arg, srt_arg];
4573 if downloader.verbosity > 0 {
4574 info!(" Running mkvmerge {}", mkvmerge_args.join(" "));
4575 }
4576 if let Ok(mkvmerge) = Command::new(downloader.mkvmerge_location.clone())
4577 .args(mkvmerge_args)
4578 .output()
4579 {
4580 let msg = partial_process_output(&mkvmerge.stdout);
4581 if !msg.is_empty() {
4582 info!(" mkvmerge stdout: {msg}");
4583 }
4584 let msg = partial_process_output(&mkvmerge.stderr);
4585 if !msg.is_empty() {
4586 info!(" mkvmerge stderr: {msg}");
4587 }
4588 if mkvmerge.status.success() {
4589 info!(" Merged subtitles with Matroska container");
4590 {
4593 let tmpfile = File::open(tmppath.clone()).await
4594 .map_err(|e| DashMpdError::Io(
4595 e, String::from("opening mkvmerge output")))?;
4596 let mut merged = BufReader::new(tmpfile);
4597 let outfile = File::create(period_output_path.clone()).await
4599 .map_err(|e| DashMpdError::Io(
4600 e, String::from("creating output file")))?;
4601 let mut sink = BufWriter::new(outfile);
4602 io::copy(&mut merged, &mut sink).await
4603 .map_err(|e| DashMpdError::Io(
4604 e, String::from("copying mkvmerge output to output file")))?;
4605 }
4606 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4607 if let Err(e) = fs::remove_file(tmppath).await {
4608 warn!(" Error deleting temporary mkvmerge output: {e}");
4609 }
4610 }
4611 } else {
4612 warn!(" Error running mkvmerge to merge subtitles");
4613 }
4614 }
4615 }
4616 }
4617 }
4618 } else if have_audio {
4619 copy_audio_to_container(downloader, &period_output_path, &tmppath_audio).await?;
4620 } else if have_video {
4621 copy_video_to_container(downloader, &period_output_path, &tmppath_video).await?;
4622 } else if downloader.fetch_video && downloader.fetch_audio {
4623 return Err(DashMpdError::UnhandledMediaStream("no audio or video streams found".to_string()));
4624 } else if downloader.fetch_video {
4625 return Err(DashMpdError::UnhandledMediaStream("no video streams found".to_string()));
4626 } else if downloader.fetch_audio {
4627 return Err(DashMpdError::UnhandledMediaStream("no audio streams found".to_string()));
4628 }
4629 #[allow(clippy::collapsible_if)]
4630 if downloader.keep_audio.is_none() && downloader.fetch_audio {
4631 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4632 if tmppath_audio.exists() && fs::remove_file(tmppath_audio).await.is_err() {
4633 info!(" Failed to delete temporary file for audio stream");
4634 }
4635 }
4636 }
4637 #[allow(clippy::collapsible_if)]
4638 if downloader.keep_video.is_none() && downloader.fetch_video {
4639 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4640 if tmppath_video.exists() && fs::remove_file(tmppath_video).await.is_err() {
4641 info!(" Failed to delete temporary file for video stream");
4642 }
4643 }
4644 }
4645 #[allow(clippy::collapsible_if)]
4646 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4647 if downloader.fetch_subtitles && tmppath_subs.exists() &&
4648 fs::remove_file(tmppath_subs).await.is_err() {
4649 info!(" Failed to delete temporary file for subtitles");
4650 }
4651 }
4652 if downloader.verbosity > 1 && (downloader.fetch_audio || downloader.fetch_video || have_subtitles) {
4653 if let Ok(metadata) = fs::metadata(&period_output_path).await {
4654 info!(" Wrote {:.1}MB to media file", metadata.len() as f64 / (1024.0 * 1024.0));
4655 }
4656 }
4657 if have_audio || have_video {
4658 period_output_pathbufs.push(period_output_path);
4659 }
4660 } let period_output_paths: Vec<&Path> = period_output_pathbufs
4662 .iter()
4663 .map(PathBuf::as_path)
4664 .collect();
4665 #[allow(clippy::comparison_chain)]
4666 if period_output_paths.len() == 1 {
4667 maybe_record_metainformation(output_path, downloader, &mpd);
4669 } else if period_output_paths.len() > 1 {
4670 #[allow(unused_mut)]
4675 let mut concatenated = false;
4676 #[cfg(not(feature = "libav"))]
4677 if downloader.concatenate_periods && video_containers_concatable(downloader, &period_output_paths) {
4679 info!("Preparing to concatenate multiple Periods into one output file");
4680 concat_output_files(downloader, &period_output_paths).await?;
4681 for p in &period_output_paths[1..] {
4682 if fs::remove_file(p).await.is_err() {
4683 warn!(" Failed to delete temporary file {}", p.display());
4684 }
4685 }
4686 concatenated = true;
4687 if let Some(pop) = period_output_paths.first() {
4688 maybe_record_metainformation(pop, downloader, &mpd);
4689 }
4690 }
4691 if !concatenated {
4692 info!("Media content has been saved in a separate file for each period:");
4693 period_counter = 0;
4695 for p in period_output_paths {
4696 period_counter += 1;
4697 info!(" Period #{period_counter}: {}", p.display());
4698 maybe_record_metainformation(p, downloader, &mpd);
4699 }
4700 }
4701 }
4702 let have_content_protection = mpd.periods.iter().any(
4703 |p| p.adaptations.iter().any(
4704 |a| (!a.ContentProtection.is_empty()) ||
4705 a.representations.iter().any(
4706 |r| !r.ContentProtection.is_empty())));
4707 if have_content_protection && downloader.decryption_keys.is_empty() {
4708 warn!("Manifest seems to use ContentProtection (DRM), but you didn't provide decryption keys.");
4709 }
4710 for observer in &downloader.progress_observers {
4711 observer.update(100, 1, "Done");
4712 }
4713 Ok(PathBuf::from(output_path))
4714}
4715
4716
4717#[cfg(test)]
4718mod tests {
4719 #[test]
4720 fn test_resolve_url_template() {
4721 use std::collections::HashMap;
4722 use super::resolve_url_template;
4723
4724 assert_eq!(resolve_url_template("AA$Time$BB", &HashMap::from([("Time", "ZZZ".to_string())])),
4725 "AAZZZBB");
4726 assert_eq!(resolve_url_template("AA$Number%06d$BB", &HashMap::from([("Number", "42".to_string())])),
4727 "AA000042BB");
4728 let dict = HashMap::from([("RepresentationID", "640x480".to_string()),
4729 ("Number", "42".to_string()),
4730 ("Time", "ZZZ".to_string())]);
4731 assert_eq!(resolve_url_template("AA/$RepresentationID$/segment-$Number%05d$.mp4", &dict),
4732 "AA/640x480/segment-00042.mp4");
4733 }
4734}