1use std::env;
4use tokio::io;
5use tokio::fs;
6use tokio::fs::File;
7use tokio::io::{BufReader, BufWriter, AsyncWriteExt};
8use std::io::{Read, Write, Seek};
9use std::path::{Path, PathBuf};
10use std::process::Command;
11use std::time::Duration;
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 crate::{MPD, Period, Representation, AdaptationSet, SegmentBase, DashMpdError};
32use crate::{parse, mux_audio_video, copy_video_to_container, copy_audio_to_container};
33use crate::{is_audio_adaptation, is_video_adaptation, is_subtitle_adaptation};
34use crate::{subtitle_type, content_protection_type, SubtitleType};
35use crate::check_conformity;
36#[cfg(not(feature = "libav"))]
37use crate::ffmpeg::concat_output_files;
38use crate::media::{temporary_outpath, AudioTrack};
39use crate::decryption::{
40 decrypt_mp4decrypt,
41 decrypt_shaka,
42 decrypt_shaka_container,
43 decrypt_mp4box,
44 decrypt_mp4box_container
45};
46#[allow(unused_imports)]
47use crate::media::video_containers_concatable;
48
49#[cfg(all(feature = "sandbox", target_os = "linux"))]
50use crate::sandbox::{restrict_thread};
51
52
53pub type HttpClient = reqwest::Client;
55type DirectRateLimiter = RateLimiter<governor::state::direct::NotKeyed,
56 governor::state::InMemoryState,
57 governor::clock::DefaultClock,
58 governor::middleware::NoOpMiddleware>;
59
60
61pub fn partial_process_output(output: &[u8]) -> Cow<'_, str> {
64 let len = min(output.len(), 4096);
65 #[allow(clippy::indexing_slicing)]
66 String::from_utf8_lossy(&output[0..len])
67}
68
69
70pub fn tmp_file_path(prefix: &str, extension: &OsStr) -> Result<PathBuf, DashMpdError> {
73 if let Some(ext) = extension.to_str() {
74 let fmt = format!(".{}", extension.to_string_lossy());
76 let suffix = if ext.starts_with('.') {
77 extension
78 } else {
79 OsStr::new(&fmt)
80 };
81 let file = tempfile::Builder::new()
82 .prefix(prefix)
83 .suffix(suffix)
84 .rand_bytes(7)
85 .disable_cleanup(env::var("DASHMPD_PERSIST_FILES").is_ok())
86 .tempfile()
87 .map_err(|e| DashMpdError::Io(e, String::from("creating temporary file")))?;
88 Ok(file.path().to_path_buf())
89 } else {
90 Err(DashMpdError::Other(String::from("converting filename extension")))
91 }
92}
93
94
95#[cfg(unix)]
99async fn ensure_permissions_readable(path: &Path) -> Result<(), DashMpdError> {
100 use std::fs::Permissions;
101 use std::os::unix::fs::PermissionsExt;
102
103 let perms = Permissions::from_mode(0o644);
104 fs::set_permissions(path, perms)
105 .map_err(|e| DashMpdError::Io(e, String::from("setting file permissions"))).await?;
106 Ok(())
107}
108
109#[cfg(not(unix))]
110async fn ensure_permissions_readable(path: &Path) -> Result<(), DashMpdError> {
111 let mut perms = fs::metadata(path).await
112 .map_err(|e| DashMpdError::Io(e, String::from("reading file permissions")))?
113 .permissions();
114 perms.set_readonly(false);
115 fs::set_permissions(path, perms)
116 .map_err(|e| DashMpdError::Io(e, String::from("setting file permissions"))).await?;
117 Ok(())
118}
119
120
121pub trait ProgressObserver: Send + Sync {
124 fn update(&self, percent: u32, message: &str);
125}
126
127
128#[derive(PartialEq, Eq, Clone, Copy, Default)]
131pub enum QualityPreference { #[default] Lowest, Intermediate, Highest }
132
133
134pub struct DashDownloader {
154 pub mpd_url: String,
155 pub redirected_url: Url,
156 base_url: Option<String>,
157 referer: Option<String>,
158 auth_username: Option<String>,
159 auth_password: Option<String>,
160 auth_bearer_token: Option<String>,
161 pub output_path: Option<PathBuf>,
162 http_client: Option<HttpClient>,
163 quality_preference: QualityPreference,
164 language_preference: Option<String>,
165 role_preference: Vec<String>,
166 video_width_preference: Option<u64>,
167 video_height_preference: Option<u64>,
168 fetch_video: bool,
169 fetch_audio: bool,
170 fetch_subtitles: bool,
171 keep_video: Option<PathBuf>,
172 keep_audio: Option<PathBuf>,
173 concatenate_periods: bool,
174 fragment_path: Option<PathBuf>,
175 pub decryption_keys: HashMap<String, String>,
176 xslt_stylesheets: Vec<PathBuf>,
177 minimum_period_duration: Option<Duration>,
178 content_type_checks: bool,
179 conformity_checks: bool,
180 use_index_range: bool,
181 fragment_retry_count: u32,
182 max_error_count: u32,
183 progress_observers: Vec<Arc<dyn ProgressObserver>>,
184 sleep_between_requests: u8,
185 allow_live_streams: bool,
186 force_duration: Option<f64>,
187 rate_limit: u64,
188 bw_limiter: Option<DirectRateLimiter>,
189 bw_estimator_started: Instant,
190 bw_estimator_bytes: usize,
191 pub sandbox: bool,
192 pub verbosity: u8,
193 record_metainformation: bool,
194 pub muxer_preference: HashMap<String, String>,
195 pub concat_preference: HashMap<String, String>,
196 pub decryptor_preference: String,
197 pub ffmpeg_location: String,
198 pub vlc_location: String,
199 pub mkvmerge_location: String,
200 pub mp4box_location: String,
201 pub mp4decrypt_location: String,
202 pub shaka_packager_location: String,
203}
204
205
206#[cfg(not(doctest))]
209impl DashDownloader {
228 pub fn new(mpd_url: &str) -> DashDownloader {
234 DashDownloader {
235 mpd_url: String::from(mpd_url),
236 redirected_url: Url::parse(mpd_url).unwrap(),
237 base_url: None,
238 referer: None,
239 auth_username: None,
240 auth_password: None,
241 auth_bearer_token: None,
242 output_path: None,
243 http_client: None,
244 quality_preference: QualityPreference::Lowest,
245 language_preference: None,
246 role_preference: vec!["main".to_string(), "alternate".to_string()],
247 video_width_preference: None,
248 video_height_preference: None,
249 fetch_video: true,
250 fetch_audio: true,
251 fetch_subtitles: false,
252 keep_video: None,
253 keep_audio: None,
254 concatenate_periods: true,
255 fragment_path: None,
256 decryption_keys: HashMap::new(),
257 xslt_stylesheets: Vec::new(),
258 minimum_period_duration: None,
259 content_type_checks: true,
260 conformity_checks: true,
261 use_index_range: true,
262 fragment_retry_count: 10,
263 max_error_count: 30,
264 progress_observers: Vec::new(),
265 sleep_between_requests: 0,
266 allow_live_streams: false,
267 force_duration: None,
268 rate_limit: 0,
269 bw_limiter: None,
270 bw_estimator_started: Instant::now(),
271 bw_estimator_bytes: 0,
272 sandbox: false,
273 verbosity: 0,
274 record_metainformation: true,
275 muxer_preference: HashMap::new(),
276 concat_preference: HashMap::new(),
277 decryptor_preference: String::from("mp4decrypt"),
278 ffmpeg_location: String::from("ffmpeg"),
279 vlc_location: if cfg!(target_os = "windows") {
280 String::from("c:/Program Files/VideoLAN/VLC/vlc.exe")
283 } else {
284 String::from("vlc")
285 },
286 mkvmerge_location: String::from("mkvmerge"),
287 mp4box_location: if cfg!(target_os = "windows") {
288 String::from("MP4Box.exe")
289 } else if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
290 String::from("MP4Box")
291 } else {
292 String::from("mp4box")
293 },
294 mp4decrypt_location: String::from("mp4decrypt"),
295 shaka_packager_location: String::from("shaka-packager"),
296 }
297 }
298
299 #[must_use]
302 pub fn with_base_url(mut self, base_url: String) -> DashDownloader {
303 self.base_url = Some(base_url);
304 self
305 }
306
307
308 #[must_use]
330 pub fn with_http_client(mut self, client: HttpClient) -> DashDownloader {
331 self.http_client = Some(client);
332 self
333 }
334
335 #[must_use]
339 pub fn with_referer(mut self, referer: String) -> DashDownloader {
340 self.referer = Some(referer);
341 self
342 }
343
344 #[must_use]
347 pub fn with_authentication(mut self, username: &str, password: &str) -> DashDownloader {
348 self.auth_username = Some(username.to_string());
349 self.auth_password = Some(password.to_string());
350 self
351 }
352
353 #[must_use]
356 pub fn with_auth_bearer(mut self, token: &str) -> DashDownloader {
357 self.auth_bearer_token = Some(token.to_string());
358 self
359 }
360
361 #[must_use]
364 pub fn add_progress_observer(mut self, observer: Arc<dyn ProgressObserver>) -> DashDownloader {
365 self.progress_observers.push(observer);
366 self
367 }
368
369 #[must_use]
372 pub fn best_quality(mut self) -> DashDownloader {
373 self.quality_preference = QualityPreference::Highest;
374 self
375 }
376
377 #[must_use]
380 pub fn intermediate_quality(mut self) -> DashDownloader {
381 self.quality_preference = QualityPreference::Intermediate;
382 self
383 }
384
385 #[must_use]
388 pub fn worst_quality(mut self) -> DashDownloader {
389 self.quality_preference = QualityPreference::Lowest;
390 self
391 }
392
393 #[must_use]
400 pub fn prefer_language(mut self, lang: String) -> DashDownloader {
401 self.language_preference = Some(lang);
402 self
403 }
404
405 #[must_use]
415 pub fn prefer_roles(mut self, role_preference: Vec<String>) -> DashDownloader {
416 if role_preference.len() < u8::MAX as usize {
417 self.role_preference = role_preference;
418 } else {
419 warn!("Ignoring role_preference ordering due to excessive length");
420 }
421 self
422 }
423
424 #[must_use]
427 pub fn prefer_video_width(mut self, width: u64) -> DashDownloader {
428 self.video_width_preference = Some(width);
429 self
430 }
431
432 #[must_use]
435 pub fn prefer_video_height(mut self, height: u64) -> DashDownloader {
436 self.video_height_preference = Some(height);
437 self
438 }
439
440 #[must_use]
442 pub fn video_only(mut self) -> DashDownloader {
443 self.fetch_audio = false;
444 self.fetch_video = true;
445 self
446 }
447
448 #[must_use]
450 pub fn audio_only(mut self) -> DashDownloader {
451 self.fetch_audio = true;
452 self.fetch_video = false;
453 self
454 }
455
456 #[must_use]
459 pub fn keep_video_as<P: Into<PathBuf>>(mut self, video_path: P) -> DashDownloader {
460 self.keep_video = Some(video_path.into());
461 self
462 }
463
464 #[must_use]
467 pub fn keep_audio_as<P: Into<PathBuf>>(mut self, audio_path: P) -> DashDownloader {
468 self.keep_audio = Some(audio_path.into());
469 self
470 }
471
472 #[must_use]
475 pub fn save_fragments_to<P: Into<PathBuf>>(mut self, fragment_path: P) -> DashDownloader {
476 self.fragment_path = Some(fragment_path.into());
477 self
478 }
479
480 #[must_use]
492 pub fn add_decryption_key(mut self, id: String, key: String) -> DashDownloader {
493 self.decryption_keys.insert(id, key);
494 self
495 }
496
497 #[must_use]
509 pub fn with_xslt_stylesheet<P: Into<PathBuf>>(mut self, stylesheet: P) -> DashDownloader {
510 self.xslt_stylesheets.push(stylesheet.into());
511 self
512 }
513
514 #[must_use]
517 pub fn minimum_period_duration(mut self, value: Duration) -> DashDownloader {
518 self.minimum_period_duration = Some(value);
519 self
520 }
521
522 #[must_use]
526 pub fn fetch_audio(mut self, value: bool) -> DashDownloader {
527 self.fetch_audio = value;
528 self
529 }
530
531 #[must_use]
535 pub fn fetch_video(mut self, value: bool) -> DashDownloader {
536 self.fetch_video = value;
537 self
538 }
539
540 #[must_use]
548 pub fn fetch_subtitles(mut self, value: bool) -> DashDownloader {
549 self.fetch_subtitles = value;
550 self
551 }
552
553 #[must_use]
557 pub fn concatenate_periods(mut self, value: bool) -> DashDownloader {
558 self.concatenate_periods = value;
559 self
560 }
561
562 #[must_use]
565 pub fn without_content_type_checks(mut self) -> DashDownloader {
566 self.content_type_checks = false;
567 self
568 }
569
570 #[must_use]
573 pub fn content_type_checks(mut self, value: bool) -> DashDownloader {
574 self.content_type_checks = value;
575 self
576 }
577
578 #[must_use]
581 pub fn conformity_checks(mut self, value: bool) -> DashDownloader {
582 self.conformity_checks = value;
583 self
584 }
585
586 #[must_use]
601 pub fn use_index_range(mut self, value: bool) -> DashDownloader {
602 self.use_index_range = value;
603 self
604 }
605
606 #[must_use]
610 pub fn fragment_retry_count(mut self, count: u32) -> DashDownloader {
611 self.fragment_retry_count = count;
612 self
613 }
614
615 #[must_use]
622 pub fn max_error_count(mut self, count: u32) -> DashDownloader {
623 self.max_error_count = count;
624 self
625 }
626
627 #[must_use]
629 pub fn sleep_between_requests(mut self, seconds: u8) -> DashDownloader {
630 self.sleep_between_requests = seconds;
631 self
632 }
633
634 #[must_use]
646 pub fn allow_live_streams(mut self, value: bool) -> DashDownloader {
647 self.allow_live_streams = value;
648 self
649 }
650
651 #[must_use]
657 pub fn force_duration(mut self, seconds: f64) -> DashDownloader {
658 if seconds < 0.0 {
659 warn!("Ignoring negative value for force_duration()");
660 } else {
661 self.force_duration = Some(seconds);
662 if self.verbosity > 1 {
663 info!("Setting forced duration to {seconds:.1} seconds");
664 }
665 }
666 self
667 }
668
669 #[must_use]
675 pub fn with_rate_limit(mut self, bps: u64) -> DashDownloader {
676 if bps < 10 * 1024 {
677 warn!("Limiting bandwidth below 10kB/s is unlikely to be stable");
678 }
679 if self.verbosity > 1 {
680 info!("Limiting bandwidth to {} kB/s", bps/1024);
681 }
682 self.rate_limit = bps;
683 let mut kps = 1 + bps / 1024;
689 if kps > u64::from(u32::MAX) {
690 warn!("Throttling bandwidth limit");
691 kps = u32::MAX.into();
692 }
693 if let Some(bw_limit) = NonZeroU32::new(kps as u32) {
694 if let Some(burst) = NonZeroU32::new(10 * 1024) {
695 let bw_quota = Quota::per_second(bw_limit)
696 .allow_burst(burst);
697 self.bw_limiter = Some(RateLimiter::direct(bw_quota));
698 }
699 }
700 self
701 }
702
703 #[must_use]
713 pub fn verbosity(mut self, level: u8) -> DashDownloader {
714 self.verbosity = level;
715 self
716 }
717
718 #[must_use]
728 pub fn sandbox(mut self, enable: bool) -> DashDownloader {
729 #[cfg(not(all(feature = "sandbox", target_os = "linux")))]
730 if enable {
731 warn!("Sandboxing only available on Linux with crate feature sandbox enabled");
732 }
733 if self.verbosity > 1 && enable {
734 info!("Enabling sandboxing support");
735 }
736 self.sandbox = enable;
737 self
738 }
739
740 #[must_use]
744 pub fn record_metainformation(mut self, record: bool) -> DashDownloader {
745 self.record_metainformation = record;
746 self
747 }
748
749 #[must_use]
771 pub fn with_muxer_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
772 self.muxer_preference.insert(container.to_string(), ordering.to_string());
773 self
774 }
775
776 #[must_use]
799 pub fn with_concat_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
800 self.concat_preference.insert(container.to_string(), ordering.to_string());
801 self
802 }
803
804 #[must_use]
813 pub fn with_decryptor_preference(mut self, decryption_tool: &str) -> DashDownloader {
814 self.decryptor_preference = decryption_tool.to_string();
815 self
816 }
817
818 #[must_use]
833 pub fn with_ffmpeg(mut self, ffmpeg_path: &str) -> DashDownloader {
834 self.ffmpeg_location = ffmpeg_path.to_string();
835 self
836 }
837
838 #[must_use]
853 pub fn with_vlc(mut self, vlc_path: &str) -> DashDownloader {
854 self.vlc_location = vlc_path.to_string();
855 self
856 }
857
858 #[must_use]
866 pub fn with_mkvmerge(mut self, path: &str) -> DashDownloader {
867 self.mkvmerge_location = path.to_string();
868 self
869 }
870
871 #[must_use]
879 pub fn with_mp4box(mut self, path: &str) -> DashDownloader {
880 self.mp4box_location = path.to_string();
881 self
882 }
883
884 #[must_use]
892 pub fn with_mp4decrypt(mut self, path: &str) -> DashDownloader {
893 self.mp4decrypt_location = path.to_string();
894 self
895 }
896
897 #[must_use]
905 pub fn with_shaka_packager(mut self, path: &str) -> DashDownloader {
906 self.shaka_packager_location = path.to_string();
907 self
908 }
909
910 pub async fn download_to<P: Into<PathBuf>>(mut self, out: P) -> Result<PathBuf, DashMpdError> {
920 self.output_path = Some(out.into());
921 if self.http_client.is_none() {
922 let client = reqwest::Client::builder()
923 .timeout(Duration::new(30, 0))
924 .cookie_store(true)
925 .build()
926 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
927 self.http_client = Some(client);
928 }
929 fetch_mpd(&mut self).await
930 }
931
932 pub async fn download(mut self) -> Result<PathBuf, DashMpdError> {
939 let cwd = env::current_dir()
940 .map_err(|e| DashMpdError::Io(e, String::from("obtaining current directory")))?;
941 let filename = generate_filename_from_url(&self.mpd_url);
942 let outpath = cwd.join(filename);
943 self.output_path = Some(outpath);
944 if self.http_client.is_none() {
945 let client = reqwest::Client::builder()
946 .timeout(Duration::new(30, 0))
947 .cookie_store(true)
948 .build()
949 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
950 self.http_client = Some(client);
951 }
952 fetch_mpd(&mut self).await
953 }
954}
955
956
957fn mpd_is_dynamic(mpd: &MPD) -> bool {
958 if let Some(mpdtype) = mpd.mpdtype.as_ref() {
959 return mpdtype.eq("dynamic");
960 }
961 false
962}
963
964fn parse_range(range: &str) -> Result<(u64, u64), DashMpdError> {
967 let v: Vec<&str> = range.split_terminator('-').collect();
968 if v.len() != 2 {
969 return Err(DashMpdError::Parsing(format!("invalid range specifier: {range}")));
970 }
971 #[allow(clippy::indexing_slicing)]
972 let start: u64 = v[0].parse()
973 .map_err(|_| DashMpdError::Parsing(String::from("invalid start for range specifier")))?;
974 #[allow(clippy::indexing_slicing)]
975 let end: u64 = v[1].parse()
976 .map_err(|_| DashMpdError::Parsing(String::from("invalid end for range specifier")))?;
977 Ok((start, end))
978}
979
980#[derive(Debug)]
981struct MediaFragment {
982 period: u8,
983 url: Url,
984 start_byte: Option<u64>,
985 end_byte: Option<u64>,
986 is_init: bool,
987 timeout: Option<Duration>,
988}
989
990#[derive(Debug)]
991struct MediaFragmentBuilder {
992 period: u8,
993 url: Url,
994 start_byte: Option<u64>,
995 end_byte: Option<u64>,
996 is_init: bool,
997 timeout: Option<Duration>,
998}
999
1000impl MediaFragmentBuilder {
1001 pub fn new(period: u8, url: Url) -> MediaFragmentBuilder {
1002 MediaFragmentBuilder {
1003 period, url, start_byte: None, end_byte: None, is_init: false, timeout: None
1004 }
1005 }
1006
1007 pub fn with_range(mut self, start_byte: Option<u64>, end_byte: Option<u64>) -> MediaFragmentBuilder {
1008 self.start_byte = start_byte;
1009 self.end_byte = end_byte;
1010 self
1011 }
1012
1013 pub fn with_timeout(mut self, timeout: Duration) -> MediaFragmentBuilder {
1014 self.timeout = Some(timeout);
1015 self
1016 }
1017
1018 pub fn set_init(mut self) -> MediaFragmentBuilder {
1019 self.is_init = true;
1020 self
1021 }
1022
1023 pub fn build(self) -> MediaFragment {
1024 MediaFragment {
1025 period: self.period,
1026 url: self.url,
1027 start_byte: self.start_byte,
1028 end_byte: self.end_byte,
1029 is_init: self.is_init,
1030 timeout: self.timeout
1031 }
1032 }
1033}
1034
1035#[derive(Debug, Default)]
1039struct PeriodOutputs {
1040 fragments: Vec<MediaFragment>,
1041 diagnostics: Vec<String>,
1042 subtitle_formats: Vec<SubtitleType>,
1043 selected_audio_language: String,
1044}
1045
1046#[derive(Debug, Default)]
1047struct PeriodDownloads {
1048 audio_fragments: Vec<MediaFragment>,
1049 video_fragments: Vec<MediaFragment>,
1050 subtitle_fragments: Vec<MediaFragment>,
1051 subtitle_formats: Vec<SubtitleType>,
1052 period_counter: u8,
1053 id: Option<String>,
1054 selected_audio_language: String,
1055}
1056
1057fn period_fragment_count(pd: &PeriodDownloads) -> usize {
1058 pd.audio_fragments.len() +
1059 pd.video_fragments.len() +
1060 pd.subtitle_fragments.len()
1061}
1062
1063
1064
1065async fn throttle_download_rate(downloader: &DashDownloader, size: u32) -> Result<(), DashMpdError> {
1066 if downloader.rate_limit > 0 {
1067 if let Some(cells) = NonZeroU32::new(size) {
1068 if let Some(limiter) = downloader.bw_limiter.as_ref() {
1069 #[allow(clippy::redundant_pattern_matching)]
1070 if let Err(_) = limiter.until_n_ready(cells).await {
1071 return Err(DashMpdError::Other(
1072 "Bandwidth limit is too low".to_string()));
1073 }
1074 }
1075 }
1076 }
1077 Ok(())
1078}
1079
1080
1081fn generate_filename_from_url(url: &str) -> PathBuf {
1082 use sanitise_file_name::{sanitise_with_options, Options};
1083
1084 let mut path = url;
1085 if let Some(p) = path.strip_prefix("http://") {
1086 path = p;
1087 } else if let Some(p) = path.strip_prefix("https://") {
1088 path = p;
1089 } else if let Some(p) = path.strip_prefix("file://") {
1090 path = p;
1091 }
1092 if let Some(p) = path.strip_prefix("www.") {
1093 path = p;
1094 }
1095 if let Some(p) = path.strip_prefix("ftp.") {
1096 path = p;
1097 }
1098 if let Some(p) = path.strip_suffix(".mpd") {
1099 path = p;
1100 }
1101 let mut sanitize_opts = Options::DEFAULT;
1102 sanitize_opts.length_limit = 150;
1103 PathBuf::from(sanitise_with_options(path, &sanitize_opts) + ".mp4")
1108}
1109
1110fn output_path_for_period(base: &Path, period: u8) -> PathBuf {
1127 assert!(period > 0);
1128 if period == 1 {
1129 base.to_path_buf()
1130 } else {
1131 if let Some(stem) = base.file_stem() {
1132 if let Some(ext) = base.extension() {
1133 let fname = format!("{}-p{period}.{}", stem.to_string_lossy(), ext.to_string_lossy());
1134 return base.with_file_name(fname);
1135 }
1136 }
1137 let p = format!("dashmpd-p{period}");
1138 tmp_file_path(&p, base.extension().unwrap_or(OsStr::new("mp4")))
1139 .unwrap_or_else(|_| p.into())
1140 }
1141}
1142
1143fn is_absolute_url(s: &str) -> bool {
1144 s.starts_with("http://") ||
1145 s.starts_with("https://") ||
1146 s.starts_with("file://") ||
1147 s.starts_with("ftp://")
1148}
1149
1150fn merge_baseurls(current: &Url, new: &str) -> Result<Url, DashMpdError> {
1151 if is_absolute_url(new) {
1152 Url::parse(new)
1153 .map_err(|e| parse_error("parsing BaseURL", e))
1154 } else {
1155 let mut merged = current.join(new)
1168 .map_err(|e| parse_error("joining base with BaseURL", e))?;
1169 if merged.query().is_none() {
1170 merged.set_query(current.query());
1171 }
1172 Ok(merged)
1173 }
1174}
1175
1176fn content_type_audio_p(response: &reqwest::Response) -> bool {
1181 match response.headers().get("content-type") {
1182 Some(ct) => {
1183 let ctb = ct.as_bytes();
1184 ctb.starts_with(b"audio/") ||
1185 ctb.starts_with(b"video/") ||
1186 ctb.starts_with(b"application/octet-stream")
1187 },
1188 None => false,
1189 }
1190}
1191
1192fn content_type_video_p(response: &reqwest::Response) -> bool {
1194 match response.headers().get("content-type") {
1195 Some(ct) => {
1196 let ctb = ct.as_bytes();
1197 ctb.starts_with(b"video/") ||
1198 ctb.starts_with(b"application/octet-stream")
1199 },
1200 None => false,
1201 }
1202}
1203
1204
1205fn adaptation_lang_distance(a: &AdaptationSet, language_preference: &str) -> u8 {
1209 if let Some(lang) = &a.lang {
1210 if lang.eq(language_preference) {
1211 return 0;
1212 }
1213 if lang[0..2].eq(&language_preference[0..2]) {
1214 return 5;
1215 }
1216 100
1217 } else {
1218 100
1219 }
1220}
1221
1222fn adaptation_roles(a: &AdaptationSet) -> Vec<String> {
1225 let mut roles = Vec::new();
1226 for r in &a.Role {
1227 if let Some(rv) = &r.value {
1228 roles.push(String::from(rv));
1229 }
1230 }
1231 for cc in &a.ContentComponent {
1232 for r in &cc.Role {
1233 if let Some(rv) = &r.value {
1234 roles.push(String::from(rv));
1235 }
1236 }
1237 }
1238 roles
1239}
1240
1241fn adaptation_role_distance(a: &AdaptationSet, role_preference: &[String]) -> u8 {
1243 adaptation_roles(a).iter()
1244 .map(|r| role_preference.binary_search(r).unwrap_or(u8::MAX.into()))
1245 .map(|u| u8::try_from(u).unwrap_or(u8::MAX))
1246 .min()
1247 .unwrap_or(u8::MAX)
1248}
1249
1250
1251fn select_preferred_adaptations<'a>(
1259 adaptations: Vec<&'a AdaptationSet>,
1260 downloader: &DashDownloader) -> Vec<&'a AdaptationSet>
1261{
1262 let mut preferred: Vec<&'a AdaptationSet>;
1263 if let Some(ref lang) = downloader.language_preference {
1265 preferred = Vec::new();
1266 let distance: Vec<u8> = adaptations.iter()
1267 .map(|a| adaptation_lang_distance(a, lang))
1268 .collect();
1269 let min_distance = distance.iter().min().unwrap_or(&0);
1270 for (i, a) in adaptations.iter().enumerate() {
1271 if let Some(di) = distance.get(i) {
1272 if di == min_distance {
1273 preferred.push(a);
1274 }
1275 }
1276 }
1277 } else {
1278 preferred = adaptations;
1279 }
1280 let role_distance: Vec<u8> = preferred.iter()
1286 .map(|a| adaptation_role_distance(a, &downloader.role_preference))
1287 .collect();
1288 let role_distance_min = role_distance.iter().min().unwrap_or(&0);
1289 let mut best = Vec::new();
1290 for (i, a) in preferred.into_iter().enumerate() {
1291 if let Some(rdi) = role_distance.get(i) {
1292 if rdi == role_distance_min {
1293 best.push(a);
1294 }
1295 }
1296 }
1297 best
1298}
1299
1300
1301fn select_preferred_representation<'a>(
1307 representations: &[&'a Representation],
1308 downloader: &DashDownloader) -> Option<&'a Representation>
1309{
1310 if representations.iter().all(|x| x.qualityRanking.is_some()) {
1311 match downloader.quality_preference {
1314 QualityPreference::Lowest =>
1315 representations.iter()
1316 .max_by_key(|r| r.qualityRanking.unwrap_or(u8::MAX))
1317 .copied(),
1318 QualityPreference::Highest =>
1319 representations.iter().min_by_key(|r| r.qualityRanking.unwrap_or(0))
1320 .copied(),
1321 QualityPreference::Intermediate => {
1322 let count = representations.len();
1323 match count {
1324 0 => None,
1325 1 => Some(representations[0]),
1326 _ => {
1327 let mut ranking: Vec<u8> = representations.iter()
1328 .map(|r| r.qualityRanking.unwrap_or(u8::MAX))
1329 .collect();
1330 ranking.sort_unstable();
1331 if let Some(want_ranking) = ranking.get(count / 2) {
1332 representations.iter()
1333 .find(|r| r.qualityRanking.unwrap_or(u8::MAX) == *want_ranking)
1334 .copied()
1335 } else {
1336 representations.first().copied()
1337 }
1338 },
1339 }
1340 },
1341 }
1342 } else {
1343 match downloader.quality_preference {
1345 QualityPreference::Lowest => representations.iter()
1346 .min_by_key(|r| r.bandwidth.unwrap_or(1_000_000_000))
1347 .copied(),
1348 QualityPreference::Highest => representations.iter()
1349 .max_by_key(|r| r.bandwidth.unwrap_or(0))
1350 .copied(),
1351 QualityPreference::Intermediate => {
1352 let count = representations.len();
1353 match count {
1354 0 => None,
1355 1 => Some(representations[0]),
1356 _ => {
1357 let mut ranking: Vec<u64> = representations.iter()
1358 .map(|r| r.bandwidth.unwrap_or(100_000_000))
1359 .collect();
1360 ranking.sort_unstable();
1361 if let Some(want_ranking) = ranking.get(count / 2) {
1362 representations.iter()
1363 .find(|r| r.bandwidth.unwrap_or(100_000_000) == *want_ranking)
1364 .copied()
1365 } else {
1366 representations.first().copied()
1367 }
1368 },
1369 }
1370 },
1371 }
1372 }
1373}
1374
1375
1376fn print_available_subtitles_representation(r: &Representation, a: &AdaptationSet) {
1378 let unspecified = "<unspecified>".to_string();
1379 let empty = "".to_string();
1380 let lang = r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unspecified));
1381 let codecs = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&empty));
1382 let typ = subtitle_type(&a);
1383 let stype = if !codecs.is_empty() {
1384 format!("{typ:?}/{codecs}")
1385 } else {
1386 format!("{typ:?}")
1387 };
1388 let role = a.Role.first()
1389 .map_or_else(|| String::from(""),
1390 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1391 let label = a.Label.first()
1392 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1393 info!(" subs {stype:>18} | {lang:>10} |{role}{label}");
1394}
1395
1396fn print_available_subtitles_adaptation(a: &AdaptationSet) {
1397 a.representations.iter()
1398 .for_each(|r| print_available_subtitles_representation(r, a));
1399}
1400
1401fn print_available_streams_representation(r: &Representation, a: &AdaptationSet, typ: &str) {
1403 let unspecified = "<unspecified>".to_string();
1405 let w = r.width.unwrap_or(a.width.unwrap_or(0));
1406 let h = r.height.unwrap_or(a.height.unwrap_or(0));
1407 let codec = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&unspecified));
1408 let bw = r.bandwidth.unwrap_or(a.maxBandwidth.unwrap_or(0));
1409 let fmt = if typ.eq("audio") {
1410 let unknown = String::from("?");
1411 format!("lang={}", r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unknown)))
1412 } else if w == 0 || h == 0 {
1413 String::from("")
1416 } else {
1417 format!("{w}x{h}")
1418 };
1419 let role = a.Role.first()
1420 .map_or_else(|| String::from(""),
1421 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1422 let label = a.Label.first()
1423 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1424 info!(" {typ} {codec:17} | {:5} Kbps | {fmt:>9}{role}{label}", bw / 1024);
1425}
1426
1427fn print_available_streams_adaptation(a: &AdaptationSet, typ: &str) {
1428 a.representations.iter()
1429 .for_each(|r| print_available_streams_representation(r, a, typ));
1430}
1431
1432fn print_available_streams_period(p: &Period) {
1433 p.adaptations.iter()
1434 .filter(is_audio_adaptation)
1435 .for_each(|a| print_available_streams_adaptation(a, "audio"));
1436 p.adaptations.iter()
1437 .filter(is_video_adaptation)
1438 .for_each(|a| print_available_streams_adaptation(a, "video"));
1439 p.adaptations.iter()
1440 .filter(is_subtitle_adaptation)
1441 .for_each(print_available_subtitles_adaptation);
1442}
1443
1444#[tracing::instrument(level="trace", skip_all)]
1445fn print_available_streams(mpd: &MPD) {
1446 use humantime::format_duration;
1447
1448 let mut counter = 0;
1449 for p in &mpd.periods {
1450 let mut period_duration_secs: f64 = -1.0;
1451 if let Some(d) = mpd.mediaPresentationDuration {
1452 period_duration_secs = d.as_secs_f64();
1453 }
1454 if let Some(d) = &p.duration {
1455 period_duration_secs = d.as_secs_f64();
1456 }
1457 counter += 1;
1458 let duration = if period_duration_secs > 0.0 {
1459 format_duration(Duration::from_secs_f64(period_duration_secs)).to_string()
1460 } else {
1461 String::from("unknown")
1462 };
1463 if let Some(id) = p.id.as_ref() {
1464 info!("Streams in period {id} (#{counter}), duration {duration}:");
1465 } else {
1466 info!("Streams in period #{counter}, duration {duration}:");
1467 }
1468 print_available_streams_period(p);
1469 }
1470}
1471
1472async fn extract_init_pssh(downloader: &DashDownloader, init_url: Url) -> Option<Vec<u8>> {
1473 use bstr::ByteSlice;
1474 use hex_literal::hex;
1475
1476 if let Some(client) = downloader.http_client.as_ref() {
1477 let mut req = client.get(init_url);
1478 if let Some(referer) = &downloader.referer {
1479 req = req.header("Referer", referer);
1480 }
1481 if let Some(username) = &downloader.auth_username {
1482 if let Some(password) = &downloader.auth_password {
1483 req = req.basic_auth(username, Some(password));
1484 }
1485 }
1486 if let Some(token) = &downloader.auth_bearer_token {
1487 req = req.bearer_auth(token);
1488 }
1489 if let Ok(mut resp) = req.send().await {
1490 let mut chunk_counter = 0;
1493 let mut segment_first_bytes = Vec::<u8>::new();
1494 while let Ok(Some(chunk)) = resp.chunk().await {
1495 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
1496 #[allow(clippy::redundant_pattern_matching)]
1497 if let Err(_) = throttle_download_rate(downloader, size).await {
1498 return None;
1499 }
1500 segment_first_bytes.append(&mut chunk.to_vec());
1501 chunk_counter += 1;
1502 if chunk_counter > 20 {
1503 break;
1504 }
1505 }
1506 let needle = b"pssh";
1507 for offset in segment_first_bytes.find_iter(needle) {
1508 #[allow(clippy::needless_range_loop)]
1509 for i in offset-4..offset+2 {
1510 if let Some(b) = segment_first_bytes.get(i) {
1511 if *b != 0 {
1512 continue;
1513 }
1514 }
1515 }
1516 #[allow(clippy::needless_range_loop)]
1517 for i in offset+4..offset+8 {
1518 if let Some(b) = segment_first_bytes.get(i) {
1519 if *b != 0 {
1520 continue;
1521 }
1522 }
1523 }
1524 if offset+24 > segment_first_bytes.len() {
1525 continue;
1526 }
1527 const WIDEVINE_SYSID: [u8; 16] = hex!("edef8ba979d64acea3c827dcd51d21ed");
1529 if let Some(sysid) = segment_first_bytes.get((offset+8)..(offset+24)) {
1530 if !sysid.eq(&WIDEVINE_SYSID) {
1531 continue;
1532 }
1533 }
1534 if let Some(length) = segment_first_bytes.get(offset-1) {
1535 let start = offset - 4;
1536 let end = start + *length as usize;
1537 if let Some(pssh) = &segment_first_bytes.get(start..end) {
1538 return Some(pssh.to_vec());
1539 }
1540 }
1541 }
1542 }
1543 None
1544 } else {
1545 None
1546 }
1547}
1548
1549
1550lazy_static! {
1559 static ref URL_TEMPLATE_IDS: Vec<(&'static str, String, Regex)> = {
1560 vec!["RepresentationID", "Number", "Time", "Bandwidth"].into_iter()
1561 .map(|k| (k, format!("${k}$"), Regex::new(&format!("\\${k}%0([\\d])d\\$")).unwrap()))
1562 .collect()
1563 };
1564}
1565
1566fn resolve_url_template(template: &str, params: &HashMap<&str, String>) -> String {
1567 let mut result = template.to_string();
1568 for (k, ident, rx) in URL_TEMPLATE_IDS.iter() {
1569 if result.contains(ident) {
1571 if let Some(value) = params.get(k as &str) {
1572 result = result.replace(ident, value);
1573 }
1574 }
1575 if let Some(cap) = rx.captures(&result) {
1577 if let Some(value) = params.get(k as &str) {
1578 if let Ok(width) = cap[1].parse::<usize>() {
1579 if let Some(m) = rx.find(&result) {
1580 let count = format!("{value:0>width$}");
1581 result = result[..m.start()].to_owned() + &count + &result[m.end()..];
1582 }
1583 }
1584 }
1585 }
1586 }
1587 result
1588}
1589
1590
1591fn reqwest_error_transient_p(e: &reqwest::Error) -> bool {
1592 if e.is_timeout() {
1593 return true;
1594 }
1595 if let Some(s) = e.status() {
1596 if s == reqwest::StatusCode::REQUEST_TIMEOUT ||
1597 s == reqwest::StatusCode::TOO_MANY_REQUESTS ||
1598 s == reqwest::StatusCode::SERVICE_UNAVAILABLE ||
1599 s == reqwest::StatusCode::GATEWAY_TIMEOUT {
1600 return true;
1601 }
1602 }
1603 false
1604}
1605
1606fn notify_transient<E: std::fmt::Debug>(err: &E, dur: Duration) {
1607 warn!("Transient error after {dur:?}: {err:?}");
1608}
1609
1610fn network_error(why: &str, e: &reqwest::Error) -> DashMpdError {
1611 if e.is_timeout() {
1612 DashMpdError::NetworkTimeout(format!("{why}: {e:?}"))
1613 } else if e.is_connect() {
1614 DashMpdError::NetworkConnect(format!("{why}: {e:?}"))
1615 } else {
1616 DashMpdError::Network(format!("{why}: {e:?}"))
1617 }
1618}
1619
1620fn parse_error(why: &str, e: impl std::error::Error) -> DashMpdError {
1621 DashMpdError::Parsing(format!("{why}: {e:#?}"))
1622}
1623
1624
1625async fn reqwest_bytes_with_retries(
1629 client: &reqwest::Client,
1630 req: reqwest::Request,
1631 retry_count: u32) -> Result<Bytes, reqwest::Error>
1632{
1633 let mut last_error = None;
1634 for _ in 0..retry_count {
1635 if let Some(rqw) = req.try_clone() {
1636 match client.execute(rqw).await {
1637 Ok(response) => {
1638 match response.error_for_status() {
1639 Ok(resp) => {
1640 match resp.bytes().await {
1641 Ok(bytes) => return Ok(bytes),
1642 Err(e) => {
1643 info!("Retrying after HTTP error {e:?}");
1644 last_error = Some(e);
1645 },
1646 }
1647 },
1648 Err(e) => {
1649 info!("Retrying after HTTP error {e:?}");
1650 last_error = Some(e);
1651 },
1652 }
1653 },
1654 Err(e) => {
1655 info!("Retrying after HTTP error {e:?}");
1656 last_error = Some(e);
1657 },
1658 }
1659 }
1660 }
1661 Err(last_error.unwrap())
1662}
1663
1664#[allow(unused_variables)]
1677fn maybe_record_metainformation(path: &Path, downloader: &DashDownloader, mpd: &MPD) {
1678 #[cfg(target_family = "unix")]
1679 if downloader.record_metainformation && (downloader.fetch_audio || downloader.fetch_video) {
1680 if let Ok(origin_url) = Url::parse(&downloader.mpd_url) {
1681 #[allow(clippy::collapsible_if)]
1683 if origin_url.username().is_empty() && origin_url.password().is_none() {
1684 #[cfg(target_family = "unix")]
1685 if xattr::set(path, "user.xdg.origin.url", downloader.mpd_url.as_bytes()).is_err() {
1686 info!("Failed to set user.xdg.origin.url xattr on output file");
1687 }
1688 }
1689 for pi in &mpd.ProgramInformation {
1690 if let Some(t) = &pi.Title {
1691 if let Some(tc) = &t.content {
1692 if xattr::set(path, "user.dublincore.title", tc.as_bytes()).is_err() {
1693 info!("Failed to set user.dublincore.title xattr on output file");
1694 }
1695 }
1696 }
1697 if let Some(source) = &pi.Source {
1698 if let Some(sc) = &source.content {
1699 if xattr::set(path, "user.dublincore.source", sc.as_bytes()).is_err() {
1700 info!("Failed to set user.dublincore.source xattr on output file");
1701 }
1702 }
1703 }
1704 if let Some(copyright) = &pi.Copyright {
1705 if let Some(cc) = ©right.content {
1706 if xattr::set(path, "user.dublincore.rights", cc.as_bytes()).is_err() {
1707 info!("Failed to set user.dublincore.rights xattr on output file");
1708 }
1709 }
1710 }
1711 }
1712 }
1713 }
1714}
1715
1716fn fetchable_xlink_href(href: &str) -> bool {
1720 (!href.is_empty()) && href.ne("urn:mpeg:dash:resolve-to-zero:2013")
1721}
1722
1723fn element_resolves_to_zero(xot: &mut Xot, element: xot::Node) -> bool {
1724 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
1725 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
1726 if let Some(href) = xot.get_attribute(element, xlink_href_name.into()) {
1727 return href.eq("urn:mpeg:dash:resolve-to-zero:2013");
1728 }
1729 false
1730}
1731
1732fn skip_xml_preamble(input: &str) -> &str {
1733 if input.starts_with("<?xml") {
1734 if let Some(end_pos) = input.find("?>") {
1735 return &input[end_pos + 2..]; }
1738 }
1739 input
1741}
1742
1743async fn apply_xslt_stylesheets_xsltproc(
1747 downloader: &DashDownloader,
1748 xot: &mut Xot,
1749 doc: xot::Node) -> Result<String, DashMpdError> {
1750 let mut buf = Vec::new();
1751 xot.write(doc, &mut buf)
1752 .map_err(|e| parse_error("serializing rewritten manifest", e))?;
1753 for ss in &downloader.xslt_stylesheets {
1754 if downloader.verbosity > 0 {
1755 info!("Applying XSLT stylesheet {} with xsltproc", ss.display());
1756 }
1757 let tmpmpd = tmp_file_path("dashxslt", OsStr::new("xslt"))?;
1758 fs::write(&tmpmpd, &buf).await
1759 .map_err(|e| DashMpdError::Io(e, String::from("writing MPD")))?;
1760 let xsltproc = Command::new("xsltproc")
1761 .args([ss, &tmpmpd])
1762 .output()
1763 .map_err(|e| DashMpdError::Io(e, String::from("spawning xsltproc")))?;
1764 if !xsltproc.status.success() {
1765 let msg = format!("xsltproc returned {}", xsltproc.status);
1766 let out = partial_process_output(&xsltproc.stderr).to_string();
1767 return Err(DashMpdError::Io(std::io::Error::other(msg), out));
1768 }
1769 if env::var("DASHMPD_PERSIST_FILES").is_err() {
1770 if let Err(e) = fs::remove_file(&tmpmpd).await {
1771 warn!("Error removing temporary MPD after XSLT processing: {e:?}");
1772 }
1773 }
1774 buf.clone_from(&xsltproc.stdout);
1775 if downloader.verbosity > 2 {
1776 println!("Rewritten XSLT: {}", String::from_utf8_lossy(&buf));
1777 }
1778 }
1779 String::from_utf8(buf)
1780 .map_err(|e| parse_error("parsing UTF-8", e))
1781}
1782
1783async fn resolve_xlink_references(
1818 downloader: &DashDownloader,
1819 xot: &mut Xot,
1820 node: xot::Node) -> Result<(), DashMpdError>
1821{
1822 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
1823 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
1824 let xlinked = xot.descendants(node)
1825 .filter(|d| xot.get_attribute(*d, xlink_href_name.into()).is_some())
1826 .collect::<Vec<_>>();
1827 for xl in xlinked {
1828 if element_resolves_to_zero(xot, xl) {
1829 trace!("Removing node with resolve-to-zero xlink:href {xl:?}");
1830 if let Err(e) = xot.remove(xl) {
1831 return Err(parse_error("Failed to remove resolve-to-zero XML node", e));
1832 }
1833 } else if let Some(href) = xot.get_attribute(xl, xlink_href_name.into()) {
1834 if fetchable_xlink_href(href) {
1835 let xlink_url = if is_absolute_url(href) {
1836 Url::parse(href)
1837 .map_err(|e|
1838 if let Ok(ns) = xot.to_string(node) {
1839 parse_error(&format!("parsing XLink on {ns}"), e)
1840 } else {
1841 parse_error("parsing XLink", e)
1842 }
1843 )?
1844 } else {
1845 let mut merged = downloader.redirected_url.join(href)
1848 .map_err(|e|
1849 if let Ok(ns) = xot.to_string(node) {
1850 parse_error(&format!("parsing XLink on {ns}"), e)
1851 } else {
1852 parse_error("parsing XLink", e)
1853 }
1854 )?;
1855 merged.set_query(downloader.redirected_url.query());
1856 merged
1857 };
1858 let client = downloader.http_client.as_ref().unwrap();
1859 trace!("Fetching XLinked element {}", xlink_url.clone());
1860 let mut req = client.get(xlink_url.clone())
1861 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
1862 .header("Accept-Language", "en-US,en")
1863 .header("Sec-Fetch-Mode", "navigate");
1864 if let Some(referer) = &downloader.referer {
1865 req = req.header("Referer", referer);
1866 } else {
1867 req = req.header("Referer", downloader.redirected_url.to_string());
1868 }
1869 if let Some(username) = &downloader.auth_username {
1870 if let Some(password) = &downloader.auth_password {
1871 req = req.basic_auth(username, Some(password));
1872 }
1873 }
1874 if let Some(token) = &downloader.auth_bearer_token {
1875 req = req.bearer_auth(token);
1876 }
1877 let xml = req.send().await
1878 .map_err(|e|
1879 if let Ok(ns) = xot.to_string(node) {
1880 network_error(&format!("fetching XLink for {ns}"), &e)
1881 } else {
1882 network_error("fetching XLink", &e)
1883 }
1884 )?
1885 .error_for_status()
1886 .map_err(|e|
1887 if let Ok(ns) = xot.to_string(node) {
1888 network_error(&format!("fetching XLink for {ns}"), &e)
1889 } else {
1890 network_error("fetching XLink", &e)
1891 }
1892 )?
1893 .text().await
1894 .map_err(|e|
1895 if let Ok(ns) = xot.to_string(node) {
1896 network_error(&format!("resolving XLink for {ns}"), &e)
1897 } else {
1898 network_error("resolving XLink", &e)
1899 }
1900 )?;
1901 if downloader.verbosity > 2 {
1902 if let Ok(ns) = xot.to_string(node) {
1903 info!(" Resolved onLoad XLink {xlink_url} on {ns} -> {} octets", xml.len());
1904 } else {
1905 info!(" Resolved onLoad XLink {xlink_url} -> {} octets", xml.len());
1906 }
1907 }
1908 let wrapped_xml = r#"<?xml version="1.0" encoding="utf-8"?>"#.to_owned() +
1914 r#"<wrapper xmlns="urn:mpeg:dash:schema:mpd:2011" "# +
1915 r#"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" "# +
1916 r#"xmlns:cenc="urn:mpeg:cenc:2013" "# +
1917 r#"xmlns:mspr="urn:microsoft:playready" "# +
1918 r#"xmlns:xlink="http://www.w3.org/1999/xlink">"# +
1919 skip_xml_preamble(&xml) +
1920 r"</wrapper>";
1921 let wrapper_doc = xot.parse(&wrapped_xml)
1922 .map_err(|e| parse_error("parsing xlinked content", e))?;
1923 let wrapper_doc_el = xot.document_element(wrapper_doc)
1924 .map_err(|e| parse_error("extracting XML document element", e))?;
1925 for needs_insertion in xot.children(wrapper_doc_el).collect::<Vec<_>>() {
1926 xot.insert_after(xl, needs_insertion)
1928 .map_err(|e| parse_error("inserting XLinked content", e))?;
1929 }
1930 xot.remove(xl)
1931 .map_err(|e| parse_error("removing XLink node", e))?;
1932 }
1933 }
1934 }
1935 Ok(())
1936}
1937
1938#[tracing::instrument(level="trace", skip_all)]
1939pub async fn parse_resolving_xlinks(
1940 downloader: &DashDownloader,
1941 xml: &[u8]) -> Result<MPD, DashMpdError>
1942{
1943 use xot::xmlname::NameStrInfo;
1944
1945 let mut xot = Xot::new();
1946 let doc = xot.parse_bytes(xml)
1947 .map_err(|e| parse_error("XML parsing", e))?;
1948 let doc_el = xot.document_element(doc)
1949 .map_err(|e| parse_error("extracting XML document element", e))?;
1950 let doc_name = match xot.node_name(doc_el) {
1951 Some(n) => n,
1952 None => return Err(DashMpdError::Parsing(String::from("missing root node name"))),
1953 };
1954 let root_name = xot.name_ref(doc_name, doc_el)
1955 .map_err(|e| parse_error("extracting root node name", e))?;
1956 let root_local_name = root_name.local_name();
1957 if !root_local_name.eq("MPD") {
1958 return Err(DashMpdError::Parsing(format!("root element is {root_local_name}, expecting <MPD>")));
1959 }
1960 for _ in 1..5 {
1963 resolve_xlink_references(downloader, &mut xot, doc).await?;
1964 }
1965 let rewritten = apply_xslt_stylesheets_xsltproc(downloader, &mut xot, doc).await?;
1966 let mpd = parse(&rewritten)?;
1968 if downloader.conformity_checks {
1969 for emsg in check_conformity(&mpd) {
1970 warn!("DASH conformity error in manifest: {emsg}");
1971 }
1972 }
1973 Ok(mpd)
1974}
1975
1976async fn do_segmentbase_indexrange(
1977 downloader: &DashDownloader,
1978 period_counter: u8,
1979 base_url: Url,
1980 sb: &SegmentBase,
1981 dict: &HashMap<&str, String>
1982) -> Result<Vec<MediaFragment>, DashMpdError>
1983{
1984 let mut fragments = Vec::new();
2017 let mut start_byte: Option<u64> = None;
2018 let mut end_byte: Option<u64> = None;
2019 let mut indexable_segments = false;
2020 if downloader.use_index_range {
2021 if let Some(ir) = &sb.indexRange {
2022 let (s, e) = parse_range(ir)?;
2024 trace!("Fetching sidx for {}", base_url.clone());
2025 let mut req = downloader.http_client.as_ref()
2026 .unwrap()
2027 .get(base_url.clone())
2028 .header(RANGE, format!("bytes={s}-{e}"))
2029 .header("Referer", downloader.redirected_url.to_string())
2030 .header("Sec-Fetch-Mode", "navigate");
2031 if let Some(username) = &downloader.auth_username {
2032 if let Some(password) = &downloader.auth_password {
2033 req = req.basic_auth(username, Some(password));
2034 }
2035 }
2036 if let Some(token) = &downloader.auth_bearer_token {
2037 req = req.bearer_auth(token);
2038 }
2039 let mut resp = req.send().await
2040 .map_err(|e| network_error("fetching index data", &e))?
2041 .error_for_status()
2042 .map_err(|e| network_error("fetching index data", &e))?;
2043 let headers = std::mem::take(resp.headers_mut());
2044 if let Some(content_type) = headers.get(CONTENT_TYPE) {
2045 let idx = resp.bytes().await
2046 .map_err(|e| network_error("fetching index data", &e))?;
2047 if idx.len() as u64 != e - s + 1 {
2048 warn!(" HTTP server does not support Range requests; can't use indexRange addressing");
2049 } else {
2050 #[allow(clippy::collapsible_else_if)]
2051 if content_type.eq("video/mp4") ||
2052 content_type.eq("audio/mp4") {
2053 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2060 .with_range(Some(0), Some(e))
2061 .build();
2062 fragments.push(mf);
2063 let mut max_chunk_pos = 0;
2064 if let Ok(segment_chunks) = crate::sidx::from_isobmff_sidx(&idx, e+1) {
2065 trace!("Have {} segment chunks in sidx data", segment_chunks.len());
2066 for chunk in segment_chunks {
2067 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2068 .with_range(Some(chunk.start), Some(chunk.end))
2069 .build();
2070 fragments.push(mf);
2071 if chunk.end > max_chunk_pos {
2072 max_chunk_pos = chunk.end;
2073 }
2074 }
2075 indexable_segments = true;
2076 }
2077 }
2078 }
2085 }
2086 }
2087 }
2088 if indexable_segments {
2089 if let Some(init) = &sb.Initialization {
2090 if let Some(range) = &init.range {
2091 let (s, e) = parse_range(range)?;
2092 start_byte = Some(s);
2093 end_byte = Some(e);
2094 }
2095 if let Some(su) = &init.sourceURL {
2096 let path = resolve_url_template(su, dict);
2097 let u = merge_baseurls(&base_url, &path)?;
2098 let mf = MediaFragmentBuilder::new(period_counter, u)
2099 .with_range(start_byte, end_byte)
2100 .set_init()
2101 .build();
2102 fragments.push(mf);
2103 } else {
2104 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2106 .with_range(start_byte, end_byte)
2107 .set_init()
2108 .build();
2109 fragments.push(mf);
2110 }
2111 }
2112 } else {
2113 trace!("Falling back to retrieving full SegmentBase for {}", base_url.clone());
2118 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2119 .with_timeout(Duration::new(10_000, 0))
2120 .build();
2121 fragments.push(mf);
2122 }
2123 Ok(fragments)
2124}
2125
2126
2127#[tracing::instrument(level="trace", skip_all)]
2128async fn do_period_audio(
2129 downloader: &DashDownloader,
2130 mpd: &MPD,
2131 period: &Period,
2132 period_counter: u8,
2133 base_url: Url
2134) -> Result<PeriodOutputs, DashMpdError>
2135{
2136 let mut fragments = Vec::new();
2137 let mut diagnostics = Vec::new();
2138 let mut opt_init: Option<String> = None;
2139 let mut opt_media: Option<String> = None;
2140 let mut opt_duration: Option<f64> = None;
2141 let mut timescale = 1;
2142 let mut start_number = 1;
2143 let mut period_duration_secs: f64 = -1.0;
2146 if let Some(d) = mpd.mediaPresentationDuration {
2147 period_duration_secs = d.as_secs_f64();
2148 }
2149 if let Some(d) = period.duration {
2150 period_duration_secs = d.as_secs_f64();
2151 }
2152 if let Some(s) = downloader.force_duration {
2153 period_duration_secs = s;
2154 }
2155 if let Some(st) = &period.SegmentTemplate {
2159 if let Some(i) = &st.initialization {
2160 opt_init = Some(i.clone());
2161 }
2162 if let Some(m) = &st.media {
2163 opt_media = Some(m.clone());
2164 }
2165 if let Some(d) = st.duration {
2166 opt_duration = Some(d);
2167 }
2168 if let Some(ts) = st.timescale {
2169 timescale = ts;
2170 }
2171 if let Some(s) = st.startNumber {
2172 start_number = s;
2173 }
2174 }
2175 let mut selected_audio_language = "unk";
2176 let audio_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2179 .filter(is_audio_adaptation)
2180 .collect();
2181 let representations: Vec<&Representation> = select_preferred_adaptations(audio_adaptations, downloader)
2182 .iter()
2183 .flat_map(|a| a.representations.iter())
2184 .collect();
2185 if let Some(audio_repr) = select_preferred_representation(&representations, downloader) {
2186 let audio_adaptation = period.adaptations.iter()
2190 .find(|a| a.representations.iter().any(|r| r.eq(audio_repr)))
2191 .unwrap();
2192 if let Some(lang) = audio_repr.lang.as_ref().or(audio_adaptation.lang.as_ref()) {
2193 selected_audio_language = lang;
2194 }
2195 let mut base_url = base_url.clone();
2198 if let Some(bu) = &audio_adaptation.BaseURL.first() {
2199 base_url = merge_baseurls(&base_url, &bu.base)?;
2200 }
2201 if let Some(bu) = audio_repr.BaseURL.first() {
2202 base_url = merge_baseurls(&base_url, &bu.base)?;
2203 }
2204 if downloader.verbosity > 0 {
2205 let bw = if let Some(bw) = audio_repr.bandwidth {
2206 format!("bw={} Kbps ", bw / 1024)
2207 } else {
2208 String::from("")
2209 };
2210 let unknown = String::from("?");
2211 let lang = audio_repr.lang.as_ref()
2212 .unwrap_or(audio_adaptation.lang.as_ref()
2213 .unwrap_or(&unknown));
2214 let codec = audio_repr.codecs.as_ref()
2215 .unwrap_or(audio_adaptation.codecs.as_ref()
2216 .unwrap_or(&unknown));
2217 diagnostics.push(format!(" Audio stream selected: {bw}lang={lang} codec={codec}"));
2218 for cp in audio_repr.ContentProtection.iter()
2220 .chain(audio_adaptation.ContentProtection.iter())
2221 {
2222 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2223 if let Some(kid) = &cp.default_KID {
2224 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2225 }
2226 for pssh_element in &cp.cenc_pssh {
2227 if let Some(pssh_b64) = &pssh_element.content {
2228 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
2229 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
2230 diagnostics.push(format!(" {pssh}"));
2231 }
2232 }
2233 }
2234 }
2235 }
2236 if let Some(st) = &audio_adaptation.SegmentTemplate {
2241 if let Some(i) = &st.initialization {
2242 opt_init = Some(i.clone());
2243 }
2244 if let Some(m) = &st.media {
2245 opt_media = Some(m.clone());
2246 }
2247 if let Some(d) = st.duration {
2248 opt_duration = Some(d);
2249 }
2250 if let Some(ts) = st.timescale {
2251 timescale = ts;
2252 }
2253 if let Some(s) = st.startNumber {
2254 start_number = s;
2255 }
2256 }
2257 let mut dict = HashMap::new();
2258 if let Some(rid) = &audio_repr.id {
2259 dict.insert("RepresentationID", rid.clone());
2260 }
2261 if let Some(b) = &audio_repr.bandwidth {
2262 dict.insert("Bandwidth", b.to_string());
2263 }
2264 if let Some(sl) = &audio_adaptation.SegmentList {
2273 if downloader.verbosity > 1 {
2276 info!(" Using AdaptationSet>SegmentList addressing mode for audio representation");
2277 }
2278 let mut start_byte: Option<u64> = None;
2279 let mut end_byte: Option<u64> = None;
2280 if let Some(init) = &sl.Initialization {
2281 if let Some(range) = &init.range {
2282 let (s, e) = parse_range(range)?;
2283 start_byte = Some(s);
2284 end_byte = Some(e);
2285 }
2286 if let Some(su) = &init.sourceURL {
2287 let path = resolve_url_template(su, &dict);
2288 let init_url = merge_baseurls(&base_url, &path)?;
2289 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2290 .with_range(start_byte, end_byte)
2291 .set_init()
2292 .build();
2293 fragments.push(mf);
2294 } else {
2295 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2296 .with_range(start_byte, end_byte)
2297 .set_init()
2298 .build();
2299 fragments.push(mf);
2300 }
2301 }
2302 for su in &sl.segment_urls {
2303 start_byte = None;
2304 end_byte = None;
2305 if let Some(range) = &su.mediaRange {
2307 let (s, e) = parse_range(range)?;
2308 start_byte = Some(s);
2309 end_byte = Some(e);
2310 }
2311 if let Some(m) = &su.media {
2312 let u = merge_baseurls(&base_url, m)?;
2313 let mf = MediaFragmentBuilder::new(period_counter, u)
2314 .with_range(start_byte, end_byte)
2315 .build();
2316 fragments.push(mf);
2317 } else if let Some(bu) = audio_adaptation.BaseURL.first() {
2318 let u = merge_baseurls(&base_url, &bu.base)?;
2319 let mf = MediaFragmentBuilder::new(period_counter, u)
2320 .with_range(start_byte, end_byte)
2321 .build();
2322 fragments.push(mf);
2323 }
2324 }
2325 }
2326 if let Some(sl) = &audio_repr.SegmentList {
2327 if downloader.verbosity > 1 {
2329 info!(" Using Representation>SegmentList addressing mode for audio representation");
2330 }
2331 let mut start_byte: Option<u64> = None;
2332 let mut end_byte: Option<u64> = None;
2333 if let Some(init) = &sl.Initialization {
2334 if let Some(range) = &init.range {
2335 let (s, e) = parse_range(range)?;
2336 start_byte = Some(s);
2337 end_byte = Some(e);
2338 }
2339 if let Some(su) = &init.sourceURL {
2340 let path = resolve_url_template(su, &dict);
2341 let init_url = merge_baseurls(&base_url, &path)?;
2342 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2343 .with_range(start_byte, end_byte)
2344 .set_init()
2345 .build();
2346 fragments.push(mf);
2347 } else {
2348 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2349 .with_range(start_byte, end_byte)
2350 .set_init()
2351 .build();
2352 fragments.push(mf);
2353 }
2354 }
2355 for su in &sl.segment_urls {
2356 start_byte = None;
2357 end_byte = None;
2358 if let Some(range) = &su.mediaRange {
2360 let (s, e) = parse_range(range)?;
2361 start_byte = Some(s);
2362 end_byte = Some(e);
2363 }
2364 if let Some(m) = &su.media {
2365 let u = merge_baseurls(&base_url, m)?;
2366 let mf = MediaFragmentBuilder::new(period_counter, u)
2367 .with_range(start_byte, end_byte)
2368 .build();
2369 fragments.push(mf);
2370 } else if let Some(bu) = audio_repr.BaseURL.first() {
2371 let u = merge_baseurls(&base_url, &bu.base)?;
2372 let mf = MediaFragmentBuilder::new(period_counter, u)
2373 .with_range(start_byte, end_byte)
2374 .build();
2375 fragments.push(mf);
2376 }
2377 }
2378 } else if audio_repr.SegmentTemplate.is_some() ||
2379 audio_adaptation.SegmentTemplate.is_some()
2380 {
2381 let st;
2384 if let Some(it) = &audio_repr.SegmentTemplate {
2385 st = it;
2386 } else if let Some(it) = &audio_adaptation.SegmentTemplate {
2387 st = it;
2388 } else {
2389 panic!("unreachable");
2390 }
2391 if let Some(i) = &st.initialization {
2392 opt_init = Some(i.clone());
2393 }
2394 if let Some(m) = &st.media {
2395 opt_media = Some(m.clone());
2396 }
2397 if let Some(ts) = st.timescale {
2398 timescale = ts;
2399 }
2400 if let Some(sn) = st.startNumber {
2401 start_number = sn;
2402 }
2403 if let Some(stl) = &audio_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
2404 .or(audio_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
2405 {
2406 if downloader.verbosity > 1 {
2409 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for audio representation");
2410 }
2411 if let Some(init) = opt_init {
2412 let path = resolve_url_template(&init, &dict);
2413 let u = merge_baseurls(&base_url, &path)?;
2414 let mf = MediaFragmentBuilder::new(period_counter, u)
2415 .set_init()
2416 .build();
2417 fragments.push(mf);
2418 }
2419 let mut elapsed_seconds = 0.0;
2420 if let Some(media) = opt_media {
2421 let audio_path = resolve_url_template(&media, &dict);
2422 let mut segment_time = 0;
2423 let mut segment_duration;
2424 let mut number = start_number;
2425 let mut target_duration = period_duration_secs;
2426 if let Some(target) = downloader.force_duration {
2427 if target > period_duration_secs {
2428 warn!(" Requested forced duration exceeds available content");
2429 } else {
2430 target_duration = target;
2431 }
2432 }
2433 'segment_loop: for s in &stl.segments {
2434 if let Some(t) = s.t {
2435 segment_time = t;
2436 }
2437 segment_duration = s.d;
2438 let dict = HashMap::from([("Time", segment_time.to_string()),
2440 ("Number", number.to_string())]);
2441 let path = resolve_url_template(&audio_path, &dict);
2442 let u = merge_baseurls(&base_url, &path)?;
2443 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2444 number += 1;
2445 elapsed_seconds += segment_duration as f64 / timescale as f64;
2446 if downloader.force_duration.is_some() &&
2447 target_duration > 0.0 &&
2448 elapsed_seconds > target_duration {
2449 break 'segment_loop;
2450 }
2451 if let Some(r) = s.r {
2452 let mut count = 0i64;
2453 loop {
2454 count += 1;
2455 if r >= 0 && count > r {
2460 break;
2461 }
2462 if downloader.force_duration.is_some() &&
2463 target_duration > 0.0 &&
2464 elapsed_seconds > target_duration {
2465 break 'segment_loop;
2466 }
2467 segment_time += segment_duration;
2468 elapsed_seconds += segment_duration as f64 / timescale as f64;
2469 let dict = HashMap::from([("Time", segment_time.to_string()),
2470 ("Number", number.to_string())]);
2471 let path = resolve_url_template(&audio_path, &dict);
2472 let u = merge_baseurls(&base_url, &path)?;
2473 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2474 number += 1;
2475 }
2476 }
2477 segment_time += segment_duration;
2478 }
2479 } else {
2480 return Err(DashMpdError::UnhandledMediaStream(
2481 "SegmentTimeline without a media attribute".to_string()));
2482 }
2483 } else { if downloader.verbosity > 1 {
2488 info!(" Using SegmentTemplate addressing mode for audio representation");
2489 }
2490 let mut total_number = 0i64;
2491 if let Some(init) = opt_init {
2492 let path = resolve_url_template(&init, &dict);
2493 let u = merge_baseurls(&base_url, &path)?;
2494 let mf = MediaFragmentBuilder::new(period_counter, u)
2495 .set_init()
2496 .build();
2497 fragments.push(mf);
2498 }
2499 if let Some(media) = opt_media {
2500 let audio_path = resolve_url_template(&media, &dict);
2501 let timescale = st.timescale.unwrap_or(timescale);
2502 let mut segment_duration: f64 = -1.0;
2503 if let Some(d) = opt_duration {
2504 segment_duration = d;
2506 }
2507 if let Some(std) = st.duration {
2508 if timescale == 0 {
2509 return Err(DashMpdError::UnhandledMediaStream(
2510 "SegmentTemplate@duration attribute cannot be zero".to_string()));
2511 }
2512 segment_duration = std / timescale as f64;
2513 }
2514 if segment_duration < 0.0 {
2515 return Err(DashMpdError::UnhandledMediaStream(
2516 "Audio representation is missing SegmentTemplate@duration attribute".to_string()));
2517 }
2518 total_number += (period_duration_secs / segment_duration).round() as i64;
2519 let mut number = start_number;
2520 if mpd_is_dynamic(mpd) {
2523 if let Some(start_time) = mpd.availabilityStartTime {
2524 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
2525 number = (elapsed + number as f64 - 1f64).floor() as u64;
2526 } else {
2527 return Err(DashMpdError::UnhandledMediaStream(
2528 "dynamic manifest is missing @availabilityStartTime".to_string()));
2529 }
2530 }
2531 for _ in 1..=total_number {
2532 let dict = HashMap::from([("Number", number.to_string())]);
2533 let path = resolve_url_template(&audio_path, &dict);
2534 let u = merge_baseurls(&base_url, &path)?;
2535 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2536 number += 1;
2537 }
2538 }
2539 }
2540 } else if let Some(sb) = &audio_repr.SegmentBase {
2541 if downloader.verbosity > 1 {
2543 info!(" Using SegmentBase@indexRange addressing mode for audio representation");
2544 }
2545 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
2546 fragments.extend(mf);
2547 } else if fragments.is_empty() {
2548 if let Some(bu) = audio_repr.BaseURL.first() {
2549 if downloader.verbosity > 1 {
2551 info!(" Using BaseURL addressing mode for audio representation");
2552 }
2553 let u = merge_baseurls(&base_url, &bu.base)?;
2554 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2555 }
2556 }
2557 if fragments.is_empty() {
2558 return Err(DashMpdError::UnhandledMediaStream(
2559 "no usable addressing mode identified for audio representation".to_string()));
2560 }
2561 }
2562 Ok(PeriodOutputs {
2563 fragments, diagnostics, subtitle_formats: Vec::new(),
2564 selected_audio_language: String::from(selected_audio_language)
2565 })
2566}
2567
2568
2569#[tracing::instrument(level="trace", skip_all)]
2570async fn do_period_video(
2571 downloader: &DashDownloader,
2572 mpd: &MPD,
2573 period: &Period,
2574 period_counter: u8,
2575 base_url: Url
2576 ) -> Result<PeriodOutputs, DashMpdError>
2577{
2578 let mut fragments = Vec::new();
2579 let mut diagnostics = Vec::new();
2580 let mut period_duration_secs: f64 = 0.0;
2581 let mut opt_init: Option<String> = None;
2582 let mut opt_media: Option<String> = None;
2583 let mut opt_duration: Option<f64> = None;
2584 let mut timescale = 1;
2585 let mut start_number = 1;
2586 if let Some(d) = mpd.mediaPresentationDuration {
2587 period_duration_secs = d.as_secs_f64();
2588 }
2589 if let Some(d) = period.duration {
2590 period_duration_secs = d.as_secs_f64();
2591 }
2592 if let Some(s) = downloader.force_duration {
2593 period_duration_secs = s;
2594 }
2595 if let Some(st) = &period.SegmentTemplate {
2599 if let Some(i) = &st.initialization {
2600 opt_init = Some(i.clone());
2601 }
2602 if let Some(m) = &st.media {
2603 opt_media = Some(m.clone());
2604 }
2605 if let Some(d) = st.duration {
2606 opt_duration = Some(d);
2607 }
2608 if let Some(ts) = st.timescale {
2609 timescale = ts;
2610 }
2611 if let Some(s) = st.startNumber {
2612 start_number = s;
2613 }
2614 }
2615 let video_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2622 .filter(is_video_adaptation)
2623 .collect();
2624 let representations: Vec<&Representation> = select_preferred_adaptations(video_adaptations, downloader)
2625 .iter()
2626 .flat_map(|a| a.representations.iter())
2627 .collect();
2628 let maybe_video_repr = if let Some(want) = downloader.video_width_preference {
2629 representations.iter()
2630 .min_by_key(|x| if let Some(w) = x.width { want.abs_diff(w) } else { u64::MAX })
2631 .copied()
2632 } else if let Some(want) = downloader.video_height_preference {
2633 representations.iter()
2634 .min_by_key(|x| if let Some(h) = x.height { want.abs_diff(h) } else { u64::MAX })
2635 .copied()
2636 } else {
2637 select_preferred_representation(&representations, downloader)
2638 };
2639 if let Some(video_repr) = maybe_video_repr {
2640 let video_adaptation = period.adaptations.iter()
2644 .find(|a| a.representations.iter().any(|r| r.eq(video_repr)))
2645 .unwrap();
2646 let mut base_url = base_url.clone();
2649 if let Some(bu) = &video_adaptation.BaseURL.first() {
2650 base_url = merge_baseurls(&base_url, &bu.base)?;
2651 }
2652 if let Some(bu) = &video_repr.BaseURL.first() {
2653 base_url = merge_baseurls(&base_url, &bu.base)?;
2654 }
2655 if downloader.verbosity > 0 {
2656 let bw = if let Some(bw) = video_repr.bandwidth.or(video_adaptation.maxBandwidth) {
2657 format!("bw={} Kbps ", bw / 1024)
2658 } else {
2659 String::from("")
2660 };
2661 let unknown = String::from("?");
2662 let w = video_repr.width.unwrap_or(video_adaptation.width.unwrap_or(0));
2663 let h = video_repr.height.unwrap_or(video_adaptation.height.unwrap_or(0));
2664 let fmt = if w == 0 || h == 0 {
2665 String::from("")
2666 } else {
2667 format!("resolution={w}x{h} ")
2668 };
2669 let codec = video_repr.codecs.as_ref()
2670 .unwrap_or(video_adaptation.codecs.as_ref().unwrap_or(&unknown));
2671 diagnostics.push(format!(" Video stream selected: {bw}{fmt}codec={codec}"));
2672 for cp in video_repr.ContentProtection.iter()
2674 .chain(video_adaptation.ContentProtection.iter())
2675 {
2676 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2677 if let Some(kid) = &cp.default_KID {
2678 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2679 }
2680 for pssh_element in &cp.cenc_pssh {
2681 if let Some(pssh_b64) = &pssh_element.content {
2682 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
2683 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
2684 diagnostics.push(format!(" {pssh}"));
2685 }
2686 }
2687 }
2688 }
2689 }
2690 let mut dict = HashMap::new();
2691 if let Some(rid) = &video_repr.id {
2692 dict.insert("RepresentationID", rid.clone());
2693 }
2694 if let Some(b) = &video_repr.bandwidth {
2695 dict.insert("Bandwidth", b.to_string());
2696 }
2697 if let Some(st) = &video_adaptation.SegmentTemplate {
2702 if let Some(i) = &st.initialization {
2703 opt_init = Some(i.clone());
2704 }
2705 if let Some(m) = &st.media {
2706 opt_media = Some(m.clone());
2707 }
2708 if let Some(d) = st.duration {
2709 opt_duration = Some(d);
2710 }
2711 if let Some(ts) = st.timescale {
2712 timescale = ts;
2713 }
2714 if let Some(s) = st.startNumber {
2715 start_number = s;
2716 }
2717 }
2718 if let Some(sl) = &video_adaptation.SegmentList {
2722 if downloader.verbosity > 1 {
2724 info!(" Using AdaptationSet>SegmentList addressing mode for video representation");
2725 }
2726 let mut start_byte: Option<u64> = None;
2727 let mut end_byte: Option<u64> = None;
2728 if let Some(init) = &sl.Initialization {
2729 if let Some(range) = &init.range {
2730 let (s, e) = parse_range(range)?;
2731 start_byte = Some(s);
2732 end_byte = Some(e);
2733 }
2734 if let Some(su) = &init.sourceURL {
2735 let path = resolve_url_template(su, &dict);
2736 let u = merge_baseurls(&base_url, &path)?;
2737 let mf = MediaFragmentBuilder::new(period_counter, u)
2738 .with_range(start_byte, end_byte)
2739 .set_init()
2740 .build();
2741 fragments.push(mf);
2742 }
2743 } else {
2744 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2745 .with_range(start_byte, end_byte)
2746 .set_init()
2747 .build();
2748 fragments.push(mf);
2749 }
2750 for su in &sl.segment_urls {
2751 start_byte = None;
2752 end_byte = None;
2753 if let Some(range) = &su.mediaRange {
2755 let (s, e) = parse_range(range)?;
2756 start_byte = Some(s);
2757 end_byte = Some(e);
2758 }
2759 if let Some(m) = &su.media {
2760 let u = merge_baseurls(&base_url, m)?;
2761 let mf = MediaFragmentBuilder::new(period_counter, u)
2762 .with_range(start_byte, end_byte)
2763 .build();
2764 fragments.push(mf);
2765 } else if let Some(bu) = video_adaptation.BaseURL.first() {
2766 let u = merge_baseurls(&base_url, &bu.base)?;
2767 let mf = MediaFragmentBuilder::new(period_counter, u)
2768 .with_range(start_byte, end_byte)
2769 .build();
2770 fragments.push(mf);
2771 }
2772 }
2773 }
2774 if let Some(sl) = &video_repr.SegmentList {
2775 if downloader.verbosity > 1 {
2777 info!(" Using Representation>SegmentList addressing mode for video representation");
2778 }
2779 let mut start_byte: Option<u64> = None;
2780 let mut end_byte: Option<u64> = None;
2781 if let Some(init) = &sl.Initialization {
2782 if let Some(range) = &init.range {
2783 let (s, e) = parse_range(range)?;
2784 start_byte = Some(s);
2785 end_byte = Some(e);
2786 }
2787 if let Some(su) = &init.sourceURL {
2788 let path = resolve_url_template(su, &dict);
2789 let u = merge_baseurls(&base_url, &path)?;
2790 let mf = MediaFragmentBuilder::new(period_counter, u)
2791 .with_range(start_byte, end_byte)
2792 .set_init()
2793 .build();
2794 fragments.push(mf);
2795 } else {
2796 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2797 .with_range(start_byte, end_byte)
2798 .set_init()
2799 .build();
2800 fragments.push(mf);
2801 }
2802 }
2803 for su in sl.segment_urls.iter() {
2804 start_byte = None;
2805 end_byte = None;
2806 if let Some(range) = &su.mediaRange {
2808 let (s, e) = parse_range(range)?;
2809 start_byte = Some(s);
2810 end_byte = Some(e);
2811 }
2812 if let Some(m) = &su.media {
2813 let u = merge_baseurls(&base_url, m)?;
2814 let mf = MediaFragmentBuilder::new(period_counter, u)
2815 .with_range(start_byte, end_byte)
2816 .build();
2817 fragments.push(mf);
2818 } else if let Some(bu) = video_repr.BaseURL.first() {
2819 let u = merge_baseurls(&base_url, &bu.base)?;
2820 let mf = MediaFragmentBuilder::new(period_counter, u)
2821 .with_range(start_byte, end_byte)
2822 .build();
2823 fragments.push(mf);
2824 }
2825 }
2826 } else if video_repr.SegmentTemplate.is_some() ||
2827 video_adaptation.SegmentTemplate.is_some() {
2828 let st;
2831 if let Some(it) = &video_repr.SegmentTemplate {
2832 st = it;
2833 } else if let Some(it) = &video_adaptation.SegmentTemplate {
2834 st = it;
2835 } else {
2836 panic!("impossible");
2837 }
2838 if let Some(i) = &st.initialization {
2839 opt_init = Some(i.clone());
2840 }
2841 if let Some(m) = &st.media {
2842 opt_media = Some(m.clone());
2843 }
2844 if let Some(ts) = st.timescale {
2845 timescale = ts;
2846 }
2847 if let Some(sn) = st.startNumber {
2848 start_number = sn;
2849 }
2850 if let Some(stl) = &video_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
2851 .or(video_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
2852 {
2853 if downloader.verbosity > 1 {
2855 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for video representation");
2856 }
2857 if let Some(init) = opt_init {
2858 let path = resolve_url_template(&init, &dict);
2859 let u = merge_baseurls(&base_url, &path)?;
2860 let mf = MediaFragmentBuilder::new(period_counter, u)
2861 .set_init()
2862 .build();
2863 fragments.push(mf);
2864 }
2865 let mut elapsed_seconds = 0.0;
2866 if let Some(media) = opt_media {
2867 let video_path = resolve_url_template(&media, &dict);
2868 let mut segment_time = 0;
2869 let mut segment_duration;
2870 let mut number = start_number;
2871 let mut target_duration = period_duration_secs;
2872 if let Some(target) = downloader.force_duration {
2873 if target > period_duration_secs {
2874 warn!(" Requested forced duration exceeds available content");
2875 } else {
2876 target_duration = target;
2877 }
2878 }
2879 'segment_loop: for s in &stl.segments {
2880 if let Some(t) = s.t {
2881 segment_time = t;
2882 }
2883 segment_duration = s.d;
2884 let dict = HashMap::from([("Time", segment_time.to_string()),
2886 ("Number", number.to_string())]);
2887 let path = resolve_url_template(&video_path, &dict);
2888 let u = merge_baseurls(&base_url, &path)?;
2889 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2890 fragments.push(mf);
2891 number += 1;
2892 elapsed_seconds += segment_duration as f64 / timescale as f64;
2893 if downloader.force_duration.is_some() &&
2894 target_duration > 0.0 &&
2895 elapsed_seconds > target_duration
2896 {
2897 break 'segment_loop;
2898 }
2899 if let Some(r) = s.r {
2900 let mut count = 0i64;
2901 loop {
2902 count += 1;
2903 if r >= 0 && count > r {
2909 break;
2910 }
2911 if downloader.force_duration.is_some() &&
2912 target_duration > 0.0 &&
2913 elapsed_seconds > target_duration
2914 {
2915 break 'segment_loop;
2916 }
2917 segment_time += segment_duration;
2918 elapsed_seconds += segment_duration as f64 / timescale as f64;
2919 let dict = HashMap::from([("Time", segment_time.to_string()),
2920 ("Number", number.to_string())]);
2921 let path = resolve_url_template(&video_path, &dict);
2922 let u = merge_baseurls(&base_url, &path)?;
2923 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2924 fragments.push(mf);
2925 number += 1;
2926 }
2927 }
2928 segment_time += segment_duration;
2929 }
2930 } else {
2931 return Err(DashMpdError::UnhandledMediaStream(
2932 "SegmentTimeline without a media attribute".to_string()));
2933 }
2934 } else { if downloader.verbosity > 1 {
2937 info!(" Using SegmentTemplate addressing mode for video representation");
2938 }
2939 let mut total_number = 0i64;
2940 if let Some(init) = opt_init {
2941 let path = resolve_url_template(&init, &dict);
2942 let u = merge_baseurls(&base_url, &path)?;
2943 let mf = MediaFragmentBuilder::new(period_counter, u)
2944 .set_init()
2945 .build();
2946 fragments.push(mf);
2947 }
2948 if let Some(media) = opt_media {
2949 let video_path = resolve_url_template(&media, &dict);
2950 let timescale = st.timescale.unwrap_or(timescale);
2951 let mut segment_duration: f64 = -1.0;
2952 if let Some(d) = opt_duration {
2953 segment_duration = d;
2955 }
2956 if let Some(std) = st.duration {
2957 if timescale == 0 {
2958 return Err(DashMpdError::UnhandledMediaStream(
2959 "SegmentTemplate@duration attribute cannot be zero".to_string()));
2960 }
2961 segment_duration = std / timescale as f64;
2962 }
2963 if segment_duration < 0.0 {
2964 return Err(DashMpdError::UnhandledMediaStream(
2965 "Video representation is missing SegmentTemplate@duration attribute".to_string()));
2966 }
2967 total_number += (period_duration_secs / segment_duration).round() as i64;
2968 let mut number = start_number;
2969 if mpd_is_dynamic(mpd) {
2979 if let Some(start_time) = mpd.availabilityStartTime {
2980 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
2981 number = (elapsed + number as f64 - 1f64).floor() as u64;
2982 } else {
2983 return Err(DashMpdError::UnhandledMediaStream(
2984 "dynamic manifest is missing @availabilityStartTime".to_string()));
2985 }
2986 }
2987 for _ in 1..=total_number {
2988 let dict = HashMap::from([("Number", number.to_string())]);
2989 let path = resolve_url_template(&video_path, &dict);
2990 let u = merge_baseurls(&base_url, &path)?;
2991 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2992 fragments.push(mf);
2993 number += 1;
2994 }
2995 }
2996 }
2997 } else if let Some(sb) = &video_repr.SegmentBase {
2998 if downloader.verbosity > 1 {
3000 info!(" Using SegmentBase@indexRange addressing mode for video representation");
3001 }
3002 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
3003 fragments.extend(mf);
3004 } else if fragments.is_empty() {
3005 if let Some(bu) = video_repr.BaseURL.first() {
3006 if downloader.verbosity > 1 {
3008 info!(" Using BaseURL addressing mode for video representation");
3009 }
3010 let u = merge_baseurls(&base_url, &bu.base)?;
3011 let mf = MediaFragmentBuilder::new(period_counter, u)
3012 .with_timeout(Duration::new(10000, 0))
3013 .build();
3014 fragments.push(mf);
3015 }
3016 }
3017 if fragments.is_empty() {
3018 return Err(DashMpdError::UnhandledMediaStream(
3019 "no usable addressing mode identified for video representation".to_string()));
3020 }
3021 }
3022 Ok(PeriodOutputs {
3025 fragments,
3026 diagnostics,
3027 subtitle_formats: Vec::new(),
3028 selected_audio_language: String::from("unk")
3029 })
3030}
3031
3032#[tracing::instrument(level="trace", skip_all)]
3033async fn do_period_subtitles(
3034 downloader: &DashDownloader,
3035 mpd: &MPD,
3036 period: &Period,
3037 period_counter: u8,
3038 base_url: Url
3039 ) -> Result<PeriodOutputs, DashMpdError>
3040{
3041 let client = downloader.http_client.as_ref().unwrap();
3042 let output_path = &downloader.output_path.as_ref().unwrap().clone();
3043 let period_output_path = output_path_for_period(output_path, period_counter);
3044 let mut fragments = Vec::new();
3045 let mut subtitle_formats = Vec::new();
3046 let mut period_duration_secs: f64 = 0.0;
3047 if let Some(d) = mpd.mediaPresentationDuration {
3048 period_duration_secs = d.as_secs_f64();
3049 }
3050 if let Some(d) = period.duration {
3051 period_duration_secs = d.as_secs_f64();
3052 }
3053 let maybe_subtitle_adaptation = if let Some(ref lang) = downloader.language_preference {
3054 period.adaptations.iter().filter(is_subtitle_adaptation)
3055 .min_by_key(|a| adaptation_lang_distance(a, lang))
3056 } else {
3057 period.adaptations.iter().find(is_subtitle_adaptation)
3059 };
3060 if downloader.fetch_subtitles {
3061 if let Some(subtitle_adaptation) = maybe_subtitle_adaptation {
3062 let subtitle_format = subtitle_type(&subtitle_adaptation);
3063 subtitle_formats.push(subtitle_format);
3064 if downloader.verbosity > 1 && downloader.fetch_subtitles {
3065 info!(" Retrieving subtitles in format {subtitle_format:?}");
3066 }
3067 let mut base_url = base_url.clone();
3070 if let Some(bu) = &subtitle_adaptation.BaseURL.first() {
3071 base_url = merge_baseurls(&base_url, &bu.base)?;
3072 }
3073 if let Some(rep) = subtitle_adaptation.representations.first() {
3076 if !rep.BaseURL.is_empty() {
3077 for st_bu in &rep.BaseURL {
3078 let st_url = merge_baseurls(&base_url, &st_bu.base)?;
3079 let mut req = client.get(st_url.clone());
3080 if let Some(referer) = &downloader.referer {
3081 req = req.header("Referer", referer);
3082 } else {
3083 req = req.header("Referer", base_url.to_string());
3084 }
3085 let rqw = req.build()
3086 .map_err(|e| network_error("building request", &e))?;
3087 let subs = reqwest_bytes_with_retries(client, rqw, 5).await
3088 .map_err(|e| network_error("fetching subtitles", &e))?;
3089 let mut subs_path = period_output_path.clone();
3090 let subtitle_format = subtitle_type(&subtitle_adaptation);
3091 match subtitle_format {
3092 SubtitleType::Vtt => subs_path.set_extension("vtt"),
3093 SubtitleType::Srt => subs_path.set_extension("srt"),
3094 SubtitleType::Ttml => subs_path.set_extension("ttml"),
3095 SubtitleType::Sami => subs_path.set_extension("sami"),
3096 SubtitleType::Wvtt => subs_path.set_extension("wvtt"),
3097 SubtitleType::Stpp => subs_path.set_extension("stpp"),
3098 _ => subs_path.set_extension("sub"),
3099 };
3100 subtitle_formats.push(subtitle_format);
3101 let mut subs_file = File::create(subs_path.clone()).await
3102 .map_err(|e| DashMpdError::Io(e, String::from("creating subtitle file")))?;
3103 if downloader.verbosity > 2 {
3104 info!(" Subtitle {st_url} -> {} octets", subs.len());
3105 }
3106 match subs_file.write_all(&subs).await {
3107 Ok(()) => {
3108 if downloader.verbosity > 0 {
3109 info!(" Downloaded subtitles ({subtitle_format:?}) to {}",
3110 subs_path.display());
3111 }
3112 },
3113 Err(e) => {
3114 error!("Unable to write subtitle file: {e:?}");
3115 return Err(DashMpdError::Io(e, String::from("writing subtitle data")));
3116 },
3117 }
3118 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
3119 subtitle_formats.contains(&SubtitleType::Ttxt)
3120 {
3121 if downloader.verbosity > 0 {
3122 info!(" Converting subtitles to SRT format with MP4Box ");
3123 }
3124 let out = subs_path.with_extension("srt");
3125 let out_str = out.to_string_lossy();
3132 let subs_str = subs_path.to_string_lossy();
3133 let args = vec![
3134 "-srt", "1",
3135 "-out", &out_str,
3136 &subs_str];
3137 if downloader.verbosity > 0 {
3138 info!(" Running MPBox {}", args.join(" "));
3139 }
3140 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
3141 .args(args)
3142 .output()
3143 {
3144 let msg = partial_process_output(&mp4box.stdout);
3145 if !msg.is_empty() {
3146 info!("MP4Box stdout: {msg}");
3147 }
3148 let msg = partial_process_output(&mp4box.stderr);
3149 if !msg.is_empty() {
3150 info!("MP4Box stderr: {msg}");
3151 }
3152 if mp4box.status.success() {
3153 info!(" Converted subtitles to SRT");
3154 } else {
3155 warn!("Error running MP4Box to convert subtitles");
3156 }
3157 }
3158 }
3159 }
3160 } else if rep.SegmentTemplate.is_some() || subtitle_adaptation.SegmentTemplate.is_some() {
3161 let mut opt_init: Option<String> = None;
3162 let mut opt_media: Option<String> = None;
3163 let mut opt_duration: Option<f64> = None;
3164 let mut timescale = 1;
3165 let mut start_number = 1;
3166 if let Some(st) = &rep.SegmentTemplate {
3171 if let Some(i) = &st.initialization {
3172 opt_init = Some(i.clone());
3173 }
3174 if let Some(m) = &st.media {
3175 opt_media = Some(m.clone());
3176 }
3177 if let Some(d) = st.duration {
3178 opt_duration = Some(d);
3179 }
3180 if let Some(ts) = st.timescale {
3181 timescale = ts;
3182 }
3183 if let Some(s) = st.startNumber {
3184 start_number = s;
3185 }
3186 }
3187 let rid = match &rep.id {
3188 Some(id) => id,
3189 None => return Err(
3190 DashMpdError::UnhandledMediaStream(
3191 "Missing @id on Representation node".to_string())),
3192 };
3193 let mut dict = HashMap::from([("RepresentationID", rid.clone())]);
3194 if let Some(b) = &rep.bandwidth {
3195 dict.insert("Bandwidth", b.to_string());
3196 }
3197 if let Some(sl) = &rep.SegmentList {
3201 if downloader.verbosity > 1 {
3204 info!(" Using AdaptationSet>SegmentList addressing mode for subtitle representation");
3205 }
3206 let mut start_byte: Option<u64> = None;
3207 let mut end_byte: Option<u64> = None;
3208 if let Some(init) = &sl.Initialization {
3209 if let Some(range) = &init.range {
3210 let (s, e) = parse_range(range)?;
3211 start_byte = Some(s);
3212 end_byte = Some(e);
3213 }
3214 if let Some(su) = &init.sourceURL {
3215 let path = resolve_url_template(su, &dict);
3216 let u = merge_baseurls(&base_url, &path)?;
3217 let mf = MediaFragmentBuilder::new(period_counter, u)
3218 .with_range(start_byte, end_byte)
3219 .set_init()
3220 .build();
3221 fragments.push(mf);
3222 } else {
3223 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3224 .with_range(start_byte, end_byte)
3225 .set_init()
3226 .build();
3227 fragments.push(mf);
3228 }
3229 }
3230 for su in &sl.segment_urls {
3231 start_byte = None;
3232 end_byte = None;
3233 if let Some(range) = &su.mediaRange {
3235 let (s, e) = parse_range(range)?;
3236 start_byte = Some(s);
3237 end_byte = Some(e);
3238 }
3239 if let Some(m) = &su.media {
3240 let u = merge_baseurls(&base_url, m)?;
3241 let mf = MediaFragmentBuilder::new(period_counter, u)
3242 .with_range(start_byte, end_byte)
3243 .build();
3244 fragments.push(mf);
3245 } else if let Some(bu) = subtitle_adaptation.BaseURL.first() {
3246 let u = merge_baseurls(&base_url, &bu.base)?;
3247 let mf = MediaFragmentBuilder::new(period_counter, u)
3248 .with_range(start_byte, end_byte)
3249 .build();
3250 fragments.push(mf);
3251 }
3252 }
3253 }
3254 if let Some(sl) = &rep.SegmentList {
3255 if downloader.verbosity > 1 {
3257 info!(" Using Representation>SegmentList addressing mode for subtitle representation");
3258 }
3259 let mut start_byte: Option<u64> = None;
3260 let mut end_byte: Option<u64> = None;
3261 if let Some(init) = &sl.Initialization {
3262 if let Some(range) = &init.range {
3263 let (s, e) = parse_range(range)?;
3264 start_byte = Some(s);
3265 end_byte = Some(e);
3266 }
3267 if let Some(su) = &init.sourceURL {
3268 let path = resolve_url_template(su, &dict);
3269 let u = merge_baseurls(&base_url, &path)?;
3270 let mf = MediaFragmentBuilder::new(period_counter, u)
3271 .with_range(start_byte, end_byte)
3272 .set_init()
3273 .build();
3274 fragments.push(mf);
3275 } else {
3276 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3277 .with_range(start_byte, end_byte)
3278 .set_init()
3279 .build();
3280 fragments.push(mf);
3281 }
3282 }
3283 for su in sl.segment_urls.iter() {
3284 start_byte = None;
3285 end_byte = None;
3286 if let Some(range) = &su.mediaRange {
3288 let (s, e) = parse_range(range)?;
3289 start_byte = Some(s);
3290 end_byte = Some(e);
3291 }
3292 if let Some(m) = &su.media {
3293 let u = merge_baseurls(&base_url, m)?;
3294 let mf = MediaFragmentBuilder::new(period_counter, u)
3295 .with_range(start_byte, end_byte)
3296 .build();
3297 fragments.push(mf);
3298 } else if let Some(bu) = &rep.BaseURL.first() {
3299 let u = merge_baseurls(&base_url, &bu.base)?;
3300 let mf = MediaFragmentBuilder::new(period_counter, u)
3301 .with_range(start_byte, end_byte)
3302 .build();
3303 fragments.push(mf);
3304 };
3305 }
3306 } else if rep.SegmentTemplate.is_some() ||
3307 subtitle_adaptation.SegmentTemplate.is_some()
3308 {
3309 let st;
3312 if let Some(it) = &rep.SegmentTemplate {
3313 st = it;
3314 } else if let Some(it) = &subtitle_adaptation.SegmentTemplate {
3315 st = it;
3316 } else {
3317 panic!("unreachable");
3318 }
3319 if let Some(i) = &st.initialization {
3320 opt_init = Some(i.clone());
3321 }
3322 if let Some(m) = &st.media {
3323 opt_media = Some(m.clone());
3324 }
3325 if let Some(ts) = st.timescale {
3326 timescale = ts;
3327 }
3328 if let Some(sn) = st.startNumber {
3329 start_number = sn;
3330 }
3331 if let Some(stl) = &rep.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
3332 .or(subtitle_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
3333 {
3334 if downloader.verbosity > 1 {
3337 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for subtitle representation");
3338 }
3339 if let Some(init) = opt_init {
3340 let path = resolve_url_template(&init, &dict);
3341 let u = merge_baseurls(&base_url, &path)?;
3342 let mf = MediaFragmentBuilder::new(period_counter, u)
3343 .set_init()
3344 .build();
3345 fragments.push(mf);
3346 }
3347 if let Some(media) = opt_media {
3348 let sub_path = resolve_url_template(&media, &dict);
3349 let mut segment_time = 0;
3350 let mut segment_duration;
3351 let mut number = start_number;
3352 for s in &stl.segments {
3353 if let Some(t) = s.t {
3354 segment_time = t;
3355 }
3356 segment_duration = s.d;
3357 let dict = HashMap::from([("Time", segment_time.to_string()),
3359 ("Number", number.to_string())]);
3360 let path = resolve_url_template(&sub_path, &dict);
3361 let u = merge_baseurls(&base_url, &path)?;
3362 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3363 fragments.push(mf);
3364 number += 1;
3365 if let Some(r) = s.r {
3366 let mut count = 0i64;
3367 let end_time = period_duration_secs * timescale as f64;
3369 loop {
3370 count += 1;
3371 if r >= 0 {
3377 if count > r {
3378 break;
3379 }
3380 if downloader.force_duration.is_some() &&
3381 segment_time as f64 > end_time
3382 {
3383 break;
3384 }
3385 } else if segment_time as f64 > end_time {
3386 break;
3387 }
3388 segment_time += segment_duration;
3389 let dict = HashMap::from([("Time", segment_time.to_string()),
3390 ("Number", number.to_string())]);
3391 let path = resolve_url_template(&sub_path, &dict);
3392 let u = merge_baseurls(&base_url, &path)?;
3393 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3394 fragments.push(mf);
3395 number += 1;
3396 }
3397 }
3398 segment_time += segment_duration;
3399 }
3400 } else {
3401 return Err(DashMpdError::UnhandledMediaStream(
3402 "SegmentTimeline without a media attribute".to_string()));
3403 }
3404 } else { if downloader.verbosity > 0 {
3409 info!(" Using SegmentTemplate addressing mode for stpp subtitles");
3410 }
3411 if let Some(i) = &st.initialization {
3412 opt_init = Some(i.to_string());
3413 }
3414 if let Some(m) = &st.media {
3415 opt_media = Some(m.to_string());
3416 }
3417 if let Some(d) = st.duration {
3418 opt_duration = Some(d);
3419 }
3420 if let Some(ts) = st.timescale {
3421 timescale = ts;
3422 }
3423 if let Some(s) = st.startNumber {
3424 start_number = s;
3425 }
3426 let rid = match &rep.id {
3427 Some(id) => id,
3428 None => return Err(
3429 DashMpdError::UnhandledMediaStream(
3430 "Missing @id on Representation node".to_string())),
3431 };
3432 let mut dict = HashMap::from([("RepresentationID", rid.clone())]);
3433 if let Some(b) = &rep.bandwidth {
3434 dict.insert("Bandwidth", b.to_string());
3435 }
3436 let mut total_number = 0i64;
3437 if let Some(init) = opt_init {
3438 let path = resolve_url_template(&init, &dict);
3439 let u = merge_baseurls(&base_url, &path)?;
3440 let mf = MediaFragmentBuilder::new(period_counter, u)
3441 .set_init()
3442 .build();
3443 fragments.push(mf);
3444 }
3445 if let Some(media) = opt_media {
3446 let sub_path = resolve_url_template(&media, &dict);
3447 let mut segment_duration: f64 = -1.0;
3448 if let Some(d) = opt_duration {
3449 segment_duration = d;
3451 }
3452 if let Some(std) = st.duration {
3453 if timescale == 0 {
3454 return Err(DashMpdError::UnhandledMediaStream(
3455 "SegmentTemplate@duration attribute cannot be zero".to_string()));
3456 }
3457 segment_duration = std / timescale as f64;
3458 }
3459 if segment_duration < 0.0 {
3460 return Err(DashMpdError::UnhandledMediaStream(
3461 "Subtitle representation is missing SegmentTemplate@duration".to_string()));
3462 }
3463 total_number += (period_duration_secs / segment_duration).ceil() as i64;
3464 let mut number = start_number;
3465 for _ in 1..=total_number {
3466 let dict = HashMap::from([("Number", number.to_string())]);
3467 let path = resolve_url_template(&sub_path, &dict);
3468 let u = merge_baseurls(&base_url, &path)?;
3469 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3470 fragments.push(mf);
3471 number += 1;
3472 }
3473 }
3474 }
3475 } else if let Some(sb) = &rep.SegmentBase {
3476 info!(" Using SegmentBase@indexRange for subs");
3478 if downloader.verbosity > 1 {
3479 info!(" Using SegmentBase@indexRange addressing mode for subtitle representation");
3480 }
3481 let mut start_byte: Option<u64> = None;
3482 let mut end_byte: Option<u64> = None;
3483 if let Some(init) = &sb.Initialization {
3484 if let Some(range) = &init.range {
3485 let (s, e) = parse_range(range)?;
3486 start_byte = Some(s);
3487 end_byte = Some(e);
3488 }
3489 if let Some(su) = &init.sourceURL {
3490 let path = resolve_url_template(su, &dict);
3491 let u = merge_baseurls(&base_url, &path)?;
3492 let mf = MediaFragmentBuilder::new(period_counter, u)
3493 .with_range(start_byte, end_byte)
3494 .set_init()
3495 .build();
3496 fragments.push(mf);
3497 }
3498 }
3499 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3500 .set_init()
3501 .build();
3502 fragments.push(mf);
3503 }
3506 }
3507 }
3508 }
3509 }
3510 Ok(PeriodOutputs {
3511 fragments,
3512 diagnostics: Vec::new(),
3513 subtitle_formats,
3514 selected_audio_language: String::from("unk")
3515 })
3516}
3517
3518
3519struct DownloadState {
3522 period_counter: u8,
3523 segment_count: usize,
3524 segment_counter: usize,
3525 download_errors: u32
3526}
3527
3528#[tracing::instrument(level="trace", skip_all)]
3535async fn fetch_fragment(
3536 downloader: &mut DashDownloader,
3537 frag: &MediaFragment,
3538 fragment_type: &str,
3539 progress_percent: u32) -> Result<std::fs::File, DashMpdError>
3540{
3541 let send_request = || async {
3542 trace!("send_request {}", frag.url.clone());
3543 let mut req = downloader.http_client.as_ref().unwrap()
3546 .get(frag.url.clone())
3547 .header("Accept", format!("{fragment_type}/*;q=0.9,*/*;q=0.5"))
3548 .header("Sec-Fetch-Mode", "navigate");
3549 if let Some(sb) = &frag.start_byte {
3550 if let Some(eb) = &frag.end_byte {
3551 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
3552 }
3553 }
3554 if let Some(ts) = &frag.timeout {
3555 req = req.timeout(*ts);
3556 }
3557 if let Some(referer) = &downloader.referer {
3558 req = req.header("Referer", referer);
3559 } else {
3560 req = req.header("Referer", downloader.redirected_url.to_string());
3561 }
3562 if let Some(username) = &downloader.auth_username {
3563 if let Some(password) = &downloader.auth_password {
3564 req = req.basic_auth(username, Some(password));
3565 }
3566 }
3567 if let Some(token) = &downloader.auth_bearer_token {
3568 req = req.bearer_auth(token);
3569 }
3570 req.send().await?
3571 .error_for_status()
3572 };
3573 match send_request
3574 .retry(ExponentialBuilder::default())
3575 .when(reqwest_error_transient_p)
3576 .notify(notify_transient)
3577 .await
3578 {
3579 Ok(response) => {
3580 match response.error_for_status() {
3581 Ok(mut resp) => {
3582 let mut tmp_out = tempfile::tempfile()
3583 .map_err(|e| DashMpdError::Io(e, String::from("creating tmpfile for fragment")))?;
3584 let content_type_checker = if fragment_type.eq("audio") {
3585 content_type_audio_p
3586 } else if fragment_type.eq("video") {
3587 content_type_video_p
3588 } else {
3589 panic!("fragment_type not audio or video");
3590 };
3591 if !downloader.content_type_checks || content_type_checker(&resp) {
3592 let mut fragment_out: Option<File> = None;
3593 if let Some(ref fragment_path) = downloader.fragment_path {
3594 if let Some(path) = frag.url.path_segments()
3595 .unwrap_or_else(|| "".split(' '))
3596 .next_back()
3597 {
3598 let vf_file = fragment_path.clone().join(fragment_type).join(path);
3599 if let Ok(f) = File::create(vf_file).await {
3600 fragment_out = Some(f)
3601 }
3602 }
3603 }
3604 let mut segment_size = 0;
3605 while let Some(chunk) = resp.chunk().await
3611 .map_err(|e| network_error(&format!("fetching DASH {fragment_type} segment"), &e))?
3612 {
3613 segment_size += chunk.len();
3614 downloader.bw_estimator_bytes += chunk.len();
3615 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
3616 throttle_download_rate(downloader, size).await?;
3617 if let Err(e) = tmp_out.write_all(&chunk) {
3618 return Err(DashMpdError::Io(e, format!("writing DASH {fragment_type} data")));
3619 }
3620 if let Some(ref mut fout) = fragment_out {
3621 fout.write_all(&chunk)
3622 .map_err(|e| DashMpdError::Io(e, format!("writing {fragment_type} fragment")))
3623 .await?;
3624 }
3625 let elapsed = downloader.bw_estimator_started.elapsed().as_secs_f64();
3626 if (elapsed > 1.5) || (downloader.bw_estimator_bytes > 100_000) {
3627 let bw = downloader.bw_estimator_bytes as f64 / (1e6 * elapsed);
3628 let msg = if bw > 0.5 {
3629 format!("Fetching {fragment_type} segments ({bw:.1} MB/s)")
3630 } else {
3631 let kbs = (bw * 1000.0).round() as u64;
3632 format!("Fetching {fragment_type} segments ({kbs:3} kB/s)")
3633 };
3634 for observer in &downloader.progress_observers {
3635 observer.update(progress_percent, &msg);
3636 }
3637 downloader.bw_estimator_started = Instant::now();
3638 downloader.bw_estimator_bytes = 0;
3639 }
3640 }
3641 if downloader.verbosity > 2 {
3642 if let Some(sb) = &frag.start_byte {
3643 if let Some(eb) = &frag.end_byte {
3644 info!(" {fragment_type} segment {} range {sb}-{eb} -> {} octets",
3645 frag.url, segment_size);
3646 }
3647 } else {
3648 info!(" {fragment_type} segment {} -> {segment_size} octets", &frag.url);
3649 }
3650 }
3651 } else {
3652 warn!("Ignoring segment {} with non-{fragment_type} content-type", frag.url);
3653 };
3654 tmp_out.sync_all()
3655 .map_err(|e| DashMpdError::Io(e, format!("syncing {fragment_type} fragment")))?;
3656 Ok(tmp_out)
3657 },
3658 Err(e) => Err(network_error("HTTP error", &e)),
3659 }
3660 },
3661 Err(e) => Err(network_error(&format!("{e:?}"), &e)),
3662 }
3663}
3664
3665
3666#[tracing::instrument(level="trace", skip_all)]
3668async fn fetch_period_audio(
3669 downloader: &mut DashDownloader,
3670 tmppath: &Path,
3671 audio_fragments: &[MediaFragment],
3672 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3673{
3674 let start_download = Instant::now();
3675 let mut have_audio = false;
3676 {
3677 let tmpfile_audio = File::create(tmppath).await
3681 .map_err(|e| DashMpdError::Io(e, String::from("creating audio tmpfile")))?;
3682 ensure_permissions_readable(tmppath).await?;
3683 let mut tmpfile_audio = BufWriter::new(tmpfile_audio);
3684 if let Some(ref fragment_path) = downloader.fragment_path {
3686 let audio_fragment_dir = fragment_path.join("audio");
3687 if !audio_fragment_dir.exists() {
3688 fs::create_dir_all(audio_fragment_dir).await
3689 .map_err(|e| DashMpdError::Io(e, String::from("creating audio fragment dir")))?;
3690 }
3691 }
3692 for frag in audio_fragments.iter().filter(|f| f.period == ds.period_counter) {
3696 ds.segment_counter += 1;
3697 let progress_percent = (100.0 * ds.segment_counter as f32 / (2.0 + ds.segment_count as f32)).ceil() as u32;
3698 let url = &frag.url;
3699 if url.scheme() == "data" {
3703 let us = &url.to_string();
3704 let du = DataUrl::process(us)
3705 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
3706 if du.mime_type().type_ != "audio" {
3707 return Err(DashMpdError::UnhandledMediaStream(
3708 String::from("expecting audio content in data URL")));
3709 }
3710 let (body, _fragment) = du.decode_to_vec()
3711 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
3712 if downloader.verbosity > 2 {
3713 info!(" Audio segment data URL -> {} octets", body.len());
3714 }
3715 tmpfile_audio.write_all(&body)
3716 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH audio data")))
3717 .await?;
3718 have_audio = true;
3719 } else {
3720 'done: for _ in 0..downloader.fragment_retry_count {
3722 match fetch_fragment(downloader, frag, "audio", progress_percent).await {
3723 Ok(mut frag_file) => {
3724 frag_file.rewind()
3725 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
3726 let mut buf = Vec::new();
3727 frag_file.read_to_end(&mut buf)
3728 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
3729 tmpfile_audio.write_all(&buf)
3730 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH audio data")))
3731 .await?;
3732 have_audio = true;
3733 break 'done;
3734 },
3735 Err(e) => {
3736 if downloader.verbosity > 0 {
3737 error!("Error fetching audio segment {url}: {e:?}");
3738 }
3739 ds.download_errors += 1;
3740 if ds.download_errors > downloader.max_error_count {
3741 error!("max_error_count network errors encountered");
3742 return Err(DashMpdError::Network(
3743 String::from("more than max_error_count network errors")));
3744 }
3745 },
3746 }
3747 info!(" Retrying audio segment {url}");
3748 if downloader.sleep_between_requests > 0 {
3749 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
3750 }
3751 }
3752 }
3753 }
3754 tmpfile_audio.flush().map_err(|e| {
3755 error!("Couldn't flush DASH audio file: {e}");
3756 DashMpdError::Io(e, String::from("flushing DASH audio file"))
3757 }).await?;
3758 } if !downloader.decryption_keys.is_empty() {
3760 if downloader.verbosity > 0 {
3761 let metadata = fs::metadata(tmppath).await
3762 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted audio metadata")))?;
3763 info!(" Attempting to decrypt audio stream ({} kB) with {}",
3764 metadata.len() / 1024,
3765 downloader.decryptor_preference);
3766 }
3767 let out_ext = downloader.output_path.as_ref().unwrap()
3768 .extension()
3769 .unwrap_or(OsStr::new("mp4"));
3770 let decrypted = tmp_file_path("dashmpd-decrypted-audio", out_ext)?;
3771 if downloader.decryptor_preference.eq("mp4decrypt") {
3772 decrypt_mp4decrypt(downloader, tmppath, &decrypted, "audio").await?;
3773 } else if downloader.decryptor_preference.eq("shaka") {
3774 decrypt_shaka(downloader, tmppath, &decrypted, "audio").await?;
3775 } else if downloader.decryptor_preference.eq("shaka-container") {
3776 decrypt_shaka_container(downloader, tmppath, &decrypted, "audio").await?;
3777 } else if downloader.decryptor_preference.eq("mp4box") {
3778 decrypt_mp4box(downloader, tmppath, &decrypted, "audio").await?;
3779 } else if downloader.decryptor_preference.eq("mp4box-container") {
3780 decrypt_mp4box_container(downloader, tmppath, &decrypted, "audio").await?;
3781 } else {
3782 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
3783 }
3784 if let Err(e) = fs::metadata(&decrypted).await {
3785 return Err(DashMpdError::Decrypting(format!("missing decrypted audio file: {e:?}")));
3786 }
3787 fs::remove_file(&tmppath).await
3788 .map_err(|e| DashMpdError::Io(e, String::from("deleting encrypted audio tmpfile")))?;
3789 fs::rename(&decrypted, &tmppath).await
3790 .map_err(|e| {
3791 let dbg = Command::new("bash")
3792 .args(["-c", &format!("id;ls -l {}", decrypted.display())])
3793 .output()
3794 .unwrap();
3795 warn!("debugging ls: {}", String::from_utf8_lossy(&dbg.stdout));
3796 DashMpdError::Io(e, format!("renaming decrypted audio {}->{}", decrypted.display(), tmppath.display()))
3797 })?;
3798 }
3799 if let Ok(metadata) = fs::metadata(&tmppath).await {
3800 if downloader.verbosity > 1 {
3801 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
3802 let elapsed = start_download.elapsed();
3803 info!(" Wrote {mbytes:.1}MB to DASH audio file ({:.1} MB/s)",
3804 mbytes / elapsed.as_secs_f64());
3805 }
3806 }
3807 Ok(have_audio)
3808}
3809
3810
3811#[tracing::instrument(level="trace", skip_all)]
3813async fn fetch_period_video(
3814 downloader: &mut DashDownloader,
3815 tmppath: &Path,
3816 video_fragments: &[MediaFragment],
3817 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3818{
3819 let start_download = Instant::now();
3820 let mut have_video = false;
3821 {
3822 let tmpfile_video = File::create(tmppath).await
3826 .map_err(|e| DashMpdError::Io(e, String::from("creating video tmpfile")))?;
3827 ensure_permissions_readable(tmppath).await?;
3828 let mut tmpfile_video = BufWriter::new(tmpfile_video);
3829 if let Some(ref fragment_path) = downloader.fragment_path {
3831 let video_fragment_dir = fragment_path.join("video");
3832 if !video_fragment_dir.exists() {
3833 fs::create_dir_all(video_fragment_dir).await
3834 .map_err(|e| DashMpdError::Io(e, String::from("creating video fragment dir")))?;
3835 }
3836 }
3837 for frag in video_fragments.iter().filter(|f| f.period == ds.period_counter) {
3838 ds.segment_counter += 1;
3839 let progress_percent = (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32;
3840 if frag.url.scheme() == "data" {
3841 let us = &frag.url.to_string();
3842 let du = DataUrl::process(us)
3843 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
3844 if du.mime_type().type_ != "video" {
3845 return Err(DashMpdError::UnhandledMediaStream(
3846 String::from("expecting video content in data URL")));
3847 }
3848 let (body, _fragment) = du.decode_to_vec()
3849 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
3850 if downloader.verbosity > 2 {
3851 info!(" Video segment data URL -> {} octets", body.len());
3852 }
3853 tmpfile_video.write_all(&body)
3854 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH video data")))
3855 .await?;
3856 have_video = true;
3857 } else {
3858 'done: for _ in 0..downloader.fragment_retry_count {
3859 match fetch_fragment(downloader, frag, "video", progress_percent).await {
3860 Ok(mut frag_file) => {
3861 frag_file.rewind()
3862 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
3863 let mut buf = Vec::new();
3864 frag_file.read_to_end(&mut buf)
3865 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
3866 tmpfile_video.write_all(&buf)
3867 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH video data")))
3868 .await?;
3869 have_video = true;
3870 break 'done;
3871 },
3872 Err(e) => {
3873 if downloader.verbosity > 0 {
3874 error!(" Error fetching video segment {}: {e:?}", frag.url);
3875 }
3876 ds.download_errors += 1;
3877 if ds.download_errors > downloader.max_error_count {
3878 return Err(DashMpdError::Network(
3879 String::from("more than max_error_count network errors")));
3880 }
3881 },
3882 }
3883 info!(" Retrying video segment {}", frag.url);
3884 if downloader.sleep_between_requests > 0 {
3885 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
3886 }
3887 }
3888 }
3889 }
3890 tmpfile_video.flush().map_err(|e| {
3891 error!(" Couldn't flush video file: {e}");
3892 DashMpdError::Io(e, String::from("flushing video file"))
3893 }).await?;
3894 } if !downloader.decryption_keys.is_empty() {
3896 if downloader.verbosity > 0 {
3897 let metadata = fs::metadata(tmppath).await
3898 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted video metadata")))?;
3899 info!(" Attempting to decrypt video stream ({} kB) with {}",
3900 metadata.len() / 1024,
3901 downloader.decryptor_preference);
3902 }
3903 let out_ext = downloader.output_path.as_ref().unwrap()
3904 .extension()
3905 .unwrap_or(OsStr::new("mp4"));
3906 let decrypted = tmp_file_path("dashmpd-decrypted-video", out_ext)?;
3907 if downloader.decryptor_preference.eq("mp4decrypt") {
3908 decrypt_mp4decrypt(downloader, tmppath, &decrypted, "video").await?;
3909 } else if downloader.decryptor_preference.eq("shaka") {
3910 decrypt_shaka(downloader, tmppath, &decrypted, "video").await?;
3911 } else if downloader.decryptor_preference.eq("shaka-container") {
3912 decrypt_shaka_container(downloader, tmppath, &decrypted, "video").await?;
3913 } else if downloader.decryptor_preference.eq("mp4box") {
3914 decrypt_mp4box(downloader, tmppath, &decrypted, "video").await?;
3915 } else if downloader.decryptor_preference.eq("mp4box-container") {
3916 decrypt_mp4box_container(downloader, tmppath, &decrypted, "video").await?;
3917 } else {
3918 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
3919 }
3920 if let Err(e) = fs::metadata(&decrypted).await {
3921 return Err(DashMpdError::Decrypting(format!("missing decrypted video file: {e:?}")));
3922 }
3923 fs::remove_file(&tmppath).await
3924 .map_err(|e| DashMpdError::Io(e, String::from("deleting encrypted video tmpfile")))?;
3925 fs::rename(&decrypted, &tmppath).await
3926 .map_err(|e| DashMpdError::Io(e, String::from("renaming decrypted video")))?;
3927 }
3928 if let Ok(metadata) = fs::metadata(&tmppath).await {
3929 if downloader.verbosity > 1 {
3930 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
3931 let elapsed = start_download.elapsed();
3932 info!(" Wrote {mbytes:.1}MB to DASH video file ({:.1} MB/s)",
3933 mbytes / elapsed.as_secs_f64());
3934 }
3935 }
3936 Ok(have_video)
3937}
3938
3939
3940#[tracing::instrument(level="trace", skip_all)]
3942async fn fetch_period_subtitles(
3943 downloader: &DashDownloader,
3944 tmppath: &Path,
3945 subtitle_fragments: &[MediaFragment],
3946 subtitle_formats: &[SubtitleType],
3947 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3948{
3949 let client = downloader.http_client.clone().unwrap();
3950 let start_download = Instant::now();
3951 let mut have_subtitles = false;
3952 {
3953 let tmpfile_subs = File::create(tmppath).await
3954 .map_err(|e| DashMpdError::Io(e, String::from("creating subs tmpfile")))?;
3955 ensure_permissions_readable(tmppath).await?;
3956 let mut tmpfile_subs = BufWriter::new(tmpfile_subs);
3957 for frag in subtitle_fragments {
3958 ds.segment_counter += 1;
3960 let progress_percent = (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32;
3961 for observer in &downloader.progress_observers {
3962 observer.update(progress_percent, "Fetching subtitle segments");
3963 }
3964 if frag.url.scheme() == "data" {
3965 let us = &frag.url.to_string();
3966 let du = DataUrl::process(us)
3967 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
3968 if du.mime_type().type_ != "video" {
3969 return Err(DashMpdError::UnhandledMediaStream(
3970 String::from("expecting video content in data URL")));
3971 }
3972 let (body, _fragment) = du.decode_to_vec()
3973 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
3974 if downloader.verbosity > 2 {
3975 info!(" Subtitle segment data URL -> {} octets", body.len());
3976 }
3977 tmpfile_subs.write_all(&body)
3978 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH subtitle data")))
3979 .await?;
3980 have_subtitles = true;
3981 } else {
3982 let fetch = || async {
3983 let mut req = client.get(frag.url.clone())
3984 .header("Sec-Fetch-Mode", "navigate");
3985 if let Some(sb) = &frag.start_byte {
3986 if let Some(eb) = &frag.end_byte {
3987 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
3988 }
3989 }
3990 if let Some(referer) = &downloader.referer {
3991 req = req.header("Referer", referer);
3992 } else {
3993 req = req.header("Referer", downloader.redirected_url.to_string());
3994 }
3995 if let Some(username) = &downloader.auth_username {
3996 if let Some(password) = &downloader.auth_password {
3997 req = req.basic_auth(username, Some(password));
3998 }
3999 }
4000 if let Some(token) = &downloader.auth_bearer_token {
4001 req = req.bearer_auth(token);
4002 }
4003 req.send().await?
4004 .error_for_status()
4005 };
4006 let mut failure = None;
4007 match fetch
4008 .retry(ExponentialBuilder::default())
4009 .when(reqwest_error_transient_p)
4010 .notify(notify_transient)
4011 .await
4012 {
4013 Ok(response) => {
4014 if response.status().is_success() {
4015 let dash_bytes = response.bytes().await
4016 .map_err(|e| network_error("fetching DASH subtitle segment", &e))?;
4017 if downloader.verbosity > 2 {
4018 if let Some(sb) = &frag.start_byte {
4019 if let Some(eb) = &frag.end_byte {
4020 info!(" Subtitle segment {} range {sb}-{eb} -> {} octets",
4021 &frag.url, dash_bytes.len());
4022 }
4023 } else {
4024 info!(" Subtitle segment {} -> {} octets", &frag.url, dash_bytes.len());
4025 }
4026 }
4027 let size = min((dash_bytes.len()/1024 + 1) as u32, u32::MAX);
4028 throttle_download_rate(downloader, size).await?;
4029 tmpfile_subs.write_all(&dash_bytes)
4030 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH subtitle data")))
4031 .await?;
4032 have_subtitles = true;
4033 } else {
4034 failure = Some(format!("HTTP error {}", response.status().as_str()));
4035 }
4036 },
4037 Err(e) => failure = Some(format!("{e}")),
4038 }
4039 if let Some(f) = failure {
4040 if downloader.verbosity > 0 {
4041 error!("{f} fetching subtitle segment {}", &frag.url);
4042 }
4043 ds.download_errors += 1;
4044 if ds.download_errors > downloader.max_error_count {
4045 return Err(DashMpdError::Network(
4046 String::from("more than max_error_count network errors")));
4047 }
4048 }
4049 }
4050 if downloader.sleep_between_requests > 0 {
4051 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
4052 }
4053 }
4054 tmpfile_subs.flush().map_err(|e| {
4055 error!("Couldn't flush subs file: {e}");
4056 DashMpdError::Io(e, String::from("flushing subtitle file"))
4057 }).await?;
4058 } if have_subtitles {
4060 if let Ok(metadata) = fs::metadata(tmppath).await {
4061 if downloader.verbosity > 1 {
4062 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4063 let elapsed = start_download.elapsed();
4064 info!(" Wrote {mbytes:.1}MB to DASH subtitle file ({:.1} MB/s)",
4065 mbytes / elapsed.as_secs_f64());
4066 }
4067 }
4068 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
4071 subtitle_formats.contains(&SubtitleType::Ttxt)
4072 {
4073 if downloader.verbosity > 0 {
4075 if let Some(fmt) = subtitle_formats.first() {
4076 info!(" Downloaded media contains subtitles in {fmt:?} format");
4077 }
4078 info!(" Running MP4Box to extract subtitles");
4079 }
4080 let out = downloader.output_path.as_ref().unwrap()
4081 .with_extension("srt");
4082 let out_str = out.to_string_lossy();
4083 let tmp_str = tmppath.to_string_lossy();
4084 let args = vec![
4085 "-srt", "1",
4086 "-out", &out_str,
4087 &tmp_str];
4088 if downloader.verbosity > 0 {
4089 info!(" Running MP4Box {}", args.join(" "));
4090 }
4091 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4092 .args(args)
4093 .output()
4094 {
4095 let msg = partial_process_output(&mp4box.stdout);
4096 if !msg.is_empty() {
4097 info!(" MP4Box stdout: {msg}");
4098 }
4099 let msg = partial_process_output(&mp4box.stderr);
4100 if !msg.is_empty() {
4101 info!(" MP4Box stderr: {msg}");
4102 }
4103 if mp4box.status.success() {
4104 info!(" Extracted subtitles as SRT");
4105 } else {
4106 warn!(" Error running MP4Box to extract subtitles");
4107 }
4108 } else {
4109 warn!(" Failed to spawn MP4Box to extract subtitles");
4110 }
4111 }
4112 if subtitle_formats.contains(&SubtitleType::Stpp) {
4113 if downloader.verbosity > 0 {
4114 info!(" Converting STPP subtitles to TTML format with ffmpeg");
4115 }
4116 let out = downloader.output_path.as_ref().unwrap()
4117 .with_extension("ttml");
4118 let tmppath_arg = tmppath.to_string_lossy();
4119 let out_arg = &out.to_string_lossy();
4120 let ffmpeg_args = vec![
4121 "-hide_banner",
4122 "-nostats",
4123 "-loglevel", "error",
4124 "-y", "-nostdin",
4126 "-i", &tmppath_arg,
4127 "-f", "data",
4128 "-map", "0",
4129 "-c", "copy",
4130 out_arg];
4131 if downloader.verbosity > 0 {
4132 info!(" Running ffmpeg {}", ffmpeg_args.join(" "));
4133 }
4134 if let Ok(ffmpeg) = Command::new(downloader.ffmpeg_location.clone())
4135 .args(ffmpeg_args)
4136 .output()
4137 {
4138 let msg = partial_process_output(&ffmpeg.stdout);
4139 if !msg.is_empty() {
4140 info!(" ffmpeg stdout: {msg}");
4141 }
4142 let msg = partial_process_output(&ffmpeg.stderr);
4143 if !msg.is_empty() {
4144 info!(" ffmpeg stderr: {msg}");
4145 }
4146 if ffmpeg.status.success() {
4147 info!(" Converted STPP subtitles to TTML format");
4148 } else {
4149 warn!(" Error running ffmpeg to convert subtitles");
4150 }
4151 }
4152 }
4156
4157 }
4158 Ok(have_subtitles)
4159}
4160
4161
4162async fn fetch_mpd_http(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4164 let client = &downloader.http_client.clone().unwrap();
4165 let send_request = || async {
4166 let mut req = client.get(&downloader.mpd_url)
4167 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4168 .header("Accept-Language", "en-US,en")
4169 .header("Upgrade-Insecure-Requests", "1")
4170 .header("Sec-Fetch-Mode", "navigate");
4171 if let Some(referer) = &downloader.referer {
4172 req = req.header("Referer", referer);
4173 }
4174 if let Some(username) = &downloader.auth_username {
4175 if let Some(password) = &downloader.auth_password {
4176 req = req.basic_auth(username, Some(password));
4177 }
4178 }
4179 if let Some(token) = &downloader.auth_bearer_token {
4180 req = req.bearer_auth(token);
4181 }
4182 req.send().await?
4183 .error_for_status()
4184 };
4185 for observer in &downloader.progress_observers {
4186 observer.update(1, "Fetching DASH manifest");
4187 }
4188 if downloader.verbosity > 0 {
4189 if !downloader.fetch_audio && !downloader.fetch_video && !downloader.fetch_subtitles {
4190 info!("Only simulating media downloads");
4191 }
4192 info!("Fetching the DASH manifest");
4193 }
4194 let response = send_request
4195 .retry(ExponentialBuilder::default())
4196 .when(reqwest_error_transient_p)
4197 .notify(notify_transient)
4198 .await
4199 .map_err(|e| network_error("requesting DASH manifest", &e))?;
4200 if !response.status().is_success() {
4201 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4202 return Err(DashMpdError::Network(msg));
4203 }
4204 downloader.redirected_url = response.url().clone();
4205 response.bytes().await
4206 .map_err(|e| network_error("fetching DASH manifest", &e))
4207}
4208
4209async fn fetch_mpd_file(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4212 if ! &downloader.mpd_url.starts_with("file://") {
4213 return Err(DashMpdError::Other(String::from("expecting file:// URL scheme")));
4214 }
4215 let url = Url::parse(&downloader.mpd_url)
4216 .map_err(|_| DashMpdError::Other(String::from("parsing MPD URL")))?;
4217 let path = url.to_file_path()
4218 .map_err(|_| DashMpdError::Other(String::from("extracting path from file:// URL")))?;
4219 let octets = fs::read(path).await
4220 .map_err(|_| DashMpdError::Other(String::from("reading from file:// URL")))?;
4221 Ok(Bytes::from(octets))
4222}
4223
4224
4225#[tracing::instrument(level="trace", skip_all)]
4226async fn fetch_mpd(downloader: &mut DashDownloader) -> Result<PathBuf, DashMpdError> {
4227 #[cfg(all(feature = "sandbox", target_os = "linux"))]
4228 if downloader.sandbox {
4229 if let Err(e) = restrict_thread(downloader) {
4230 warn!("Sandboxing failed: {e:?}");
4231 }
4232 }
4233 let xml = if downloader.mpd_url.starts_with("file://") {
4234 fetch_mpd_file(downloader).await?
4235 } else {
4236 fetch_mpd_http(downloader).await?
4237 };
4238 let mut mpd: MPD = parse_resolving_xlinks(downloader, &xml).await
4239 .map_err(|e| parse_error("parsing DASH XML", e))?;
4240 let client = &downloader.http_client.clone().unwrap();
4243 if let Some(new_location) = &mpd.locations.first() {
4244 let new_url = &new_location.url;
4245 if downloader.verbosity > 0 {
4246 info!("Redirecting to new manifest <Location> {new_url}");
4247 }
4248 let send_request = || async {
4249 let mut req = client.get(new_url)
4250 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4251 .header("Accept-Language", "en-US,en")
4252 .header("Sec-Fetch-Mode", "navigate");
4253 if let Some(referer) = &downloader.referer {
4254 req = req.header("Referer", referer);
4255 } else {
4256 req = req.header("Referer", downloader.redirected_url.to_string());
4257 }
4258 if let Some(username) = &downloader.auth_username {
4259 if let Some(password) = &downloader.auth_password {
4260 req = req.basic_auth(username, Some(password));
4261 }
4262 }
4263 if let Some(token) = &downloader.auth_bearer_token {
4264 req = req.bearer_auth(token);
4265 }
4266 req.send().await?
4267 .error_for_status()
4268 };
4269 let response = send_request
4270 .retry(ExponentialBuilder::default())
4271 .when(reqwest_error_transient_p)
4272 .notify(notify_transient)
4273 .await
4274 .map_err(|e| network_error("requesting relocated DASH manifest", &e))?;
4275 if !response.status().is_success() {
4276 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4277 return Err(DashMpdError::Network(msg));
4278 }
4279 downloader.redirected_url = response.url().clone();
4280 let xml = response.bytes().await
4281 .map_err(|e| network_error("fetching relocated DASH manifest", &e))?;
4282 mpd = parse_resolving_xlinks(downloader, &xml).await
4283 .map_err(|e| parse_error("parsing relocated DASH XML", e))?;
4284 }
4285 if mpd_is_dynamic(&mpd) {
4286 if downloader.allow_live_streams {
4289 if downloader.verbosity > 0 {
4290 warn!("Attempting to download from live stream (this may not work).");
4291 }
4292 } else {
4293 return Err(DashMpdError::UnhandledMediaStream("Don't know how to download dynamic MPD".to_string()));
4294 }
4295 }
4296 let mut toplevel_base_url = downloader.redirected_url.clone();
4297 if let Some(bu) = &mpd.base_url.first() {
4299 toplevel_base_url = merge_baseurls(&downloader.redirected_url, &bu.base)?;
4300 }
4301 if let Some(base) = &downloader.base_url {
4304 toplevel_base_url = merge_baseurls(&downloader.redirected_url, base)?;
4305 }
4306 if downloader.verbosity > 0 {
4307 let pcount = mpd.periods.len();
4308 info!("DASH manifest has {pcount} period{}", if pcount > 1 { "s" } else { "" });
4309 print_available_streams(&mpd);
4310 }
4311 let mut pds: Vec<PeriodDownloads> = Vec::new();
4319 let mut period_counter = 0;
4320 for mpd_period in &mpd.periods {
4321 let period = mpd_period.clone();
4322 period_counter += 1;
4323 if let Some(min) = downloader.minimum_period_duration {
4324 if let Some(duration) = period.duration {
4325 if duration < min {
4326 if let Some(id) = period.id.as_ref() {
4327 info!("Skipping period {id} (#{period_counter}): duration is less than requested minimum");
4328 } else {
4329 info!("Skipping period #{period_counter}: duration is less than requested minimum");
4330 }
4331 continue;
4332 }
4333 }
4334 }
4335 let mut pd = PeriodDownloads { period_counter, ..Default::default() };
4336 if let Some(id) = period.id.as_ref() {
4337 pd.id = Some(id.clone());
4338 }
4339 if downloader.verbosity > 0 {
4340 if let Some(id) = period.id.as_ref() {
4341 info!("Preparing download for period {id} (#{period_counter})");
4342 } else {
4343 info!("Preparing download for period #{period_counter}");
4344 }
4345 }
4346 let mut base_url = toplevel_base_url.clone();
4347 if let Some(bu) = period.BaseURL.first() {
4349 base_url = merge_baseurls(&base_url, &bu.base)?;
4350 }
4351 let mut audio_outputs = PeriodOutputs::default();
4352 if downloader.fetch_audio {
4353 audio_outputs = do_period_audio(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4354 for f in audio_outputs.fragments {
4355 pd.audio_fragments.push(f);
4356 }
4357 pd.selected_audio_language = audio_outputs.selected_audio_language;
4358 }
4359 let mut video_outputs = PeriodOutputs::default();
4360 if downloader.fetch_video {
4361 video_outputs = do_period_video(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4362 for f in video_outputs.fragments {
4363 pd.video_fragments.push(f);
4364 }
4365 }
4366 match do_period_subtitles(downloader, &mpd, &period, period_counter, base_url.clone()).await {
4367 Ok(subtitle_outputs) => {
4368 for f in subtitle_outputs.fragments {
4369 pd.subtitle_fragments.push(f);
4370 }
4371 for f in subtitle_outputs.subtitle_formats {
4372 pd.subtitle_formats.push(f);
4373 }
4374 },
4375 Err(e) => warn!(" Ignoring error triggered while processing subtitles: {e}"),
4376 }
4377 if downloader.verbosity > 0 {
4379 use base64::prelude::{Engine as _, BASE64_STANDARD};
4380
4381 audio_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4382 for f in pd.audio_fragments.iter().filter(|f| f.is_init) {
4383 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4384 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4385 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4386 info!(" {}", pssh.to_string());
4387 }
4388 }
4389 }
4390 video_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4391 for f in pd.video_fragments.iter().filter(|f| f.is_init) {
4392 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4393 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4394 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4395 info!(" {}", pssh.to_string());
4396 }
4397 }
4398 }
4399 }
4400 pds.push(pd);
4401 } let output_path = &downloader.output_path.as_ref().unwrap().clone();
4406 let mut period_output_pathbufs: Vec<PathBuf> = Vec::new();
4407 let mut ds = DownloadState {
4408 period_counter: 0,
4409 segment_count: pds.iter().map(period_fragment_count).sum(),
4411 segment_counter: 0,
4412 download_errors: 0
4413 };
4414 for pd in pds {
4415 let mut have_audio = false;
4416 let mut have_video = false;
4417 let mut have_subtitles = false;
4418 ds.period_counter = pd.period_counter;
4419 let period_output_path = output_path_for_period(output_path, pd.period_counter);
4420 #[allow(clippy::collapsible_if)]
4421 if downloader.verbosity > 0 {
4422 if downloader.fetch_audio || downloader.fetch_video || downloader.fetch_subtitles {
4423 let idnum = if let Some(id) = pd.id {
4424 format!("id={} (#{})", id, pd.period_counter)
4425 } else {
4426 format!("#{}", pd.period_counter)
4427 };
4428 info!("Period {idnum}: fetching {} audio, {} video and {} subtitle segments",
4429 pd.audio_fragments.len(),
4430 pd.video_fragments.len(),
4431 pd.subtitle_fragments.len());
4432 }
4433 }
4434 let output_ext = downloader.output_path.as_ref().unwrap()
4435 .extension()
4436 .unwrap_or(OsStr::new("mp4"));
4437 let tmppath_audio = if let Some(ref path) = downloader.keep_audio {
4438 path.clone()
4439 } else {
4440 tmp_file_path("dashmpd-audio", output_ext)?
4441 };
4442 let tmppath_video = if let Some(ref path) = downloader.keep_video {
4443 path.clone()
4444 } else {
4445 tmp_file_path("dashmpd-video", output_ext)?
4446 };
4447 let tmppath_subs = tmp_file_path("dashmpd-subs", OsStr::new("sub"))?;
4448 if downloader.fetch_audio && !pd.audio_fragments.is_empty() {
4449 have_audio = fetch_period_audio(downloader,
4453 &tmppath_audio, &pd.audio_fragments,
4454 &mut ds).await?;
4455 }
4456 if downloader.fetch_video && !pd.video_fragments.is_empty() {
4457 have_video = fetch_period_video(downloader,
4458 &tmppath_video, &pd.video_fragments,
4459 &mut ds).await?;
4460 }
4461 if downloader.fetch_subtitles && !pd.subtitle_fragments.is_empty() {
4465 have_subtitles = fetch_period_subtitles(downloader,
4466 &tmppath_subs,
4467 &pd.subtitle_fragments,
4468 &pd.subtitle_formats,
4469 &mut ds).await?;
4470 }
4471
4472 if have_audio && have_video {
4475 for observer in &downloader.progress_observers {
4476 observer.update(99, "Muxing audio and video");
4477 }
4478 if downloader.verbosity > 1 {
4479 info!(" Muxing audio and video streams");
4480 }
4481 let audio_tracks = vec![
4482 AudioTrack {
4483 language: pd.selected_audio_language,
4484 path: tmppath_audio.clone()
4485 }];
4486 mux_audio_video(downloader, &period_output_path, &audio_tracks, &tmppath_video).await?;
4487 if pd.subtitle_formats.contains(&SubtitleType::Stpp) {
4488 let container = match &period_output_path.extension() {
4489 Some(ext) => ext.to_str().unwrap_or("mp4"),
4490 None => "mp4",
4491 };
4492 if container.eq("mp4") {
4493 if downloader.verbosity > 1 {
4494 if let Some(fmt) = &pd.subtitle_formats.first() {
4495 info!(" Downloaded media contains subtitles in {fmt:?} format");
4496 }
4497 info!(" Running MP4Box to merge subtitles with output MP4 container");
4498 }
4499 let tmp_str = tmppath_subs.to_string_lossy();
4502 let period_output_str = period_output_path.to_string_lossy();
4503 let args = vec!["-add", &tmp_str, &period_output_str];
4504 if downloader.verbosity > 0 {
4505 info!(" Running MP4Box {}", args.join(" "));
4506 }
4507 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4508 .args(args)
4509 .output()
4510 {
4511 let msg = partial_process_output(&mp4box.stdout);
4512 if !msg.is_empty() {
4513 info!(" MP4Box stdout: {msg}");
4514 }
4515 let msg = partial_process_output(&mp4box.stderr);
4516 if !msg.is_empty() {
4517 info!(" MP4Box stderr: {msg}");
4518 }
4519 if mp4box.status.success() {
4520 info!(" Merged subtitles with MP4 container");
4521 } else {
4522 warn!(" Error running MP4Box to merge subtitles");
4523 }
4524 } else {
4525 warn!(" Failed to spawn MP4Box to merge subtitles");
4526 }
4527 } else if container.eq("mkv") || container.eq("webm") {
4528 let srt = period_output_path.with_extension("srt");
4540 if srt.exists() {
4541 if downloader.verbosity > 0 {
4542 info!(" Running mkvmerge to merge subtitles with output Matroska container");
4543 }
4544 let tmppath = temporary_outpath(".mkv")?;
4545 let pop_arg = &period_output_path.to_string_lossy();
4546 let srt_arg = &srt.to_string_lossy();
4547 let mkvmerge_args = vec!["-o", &tmppath, pop_arg, srt_arg];
4548 if downloader.verbosity > 0 {
4549 info!(" Running mkvmerge {}", mkvmerge_args.join(" "));
4550 }
4551 if let Ok(mkvmerge) = Command::new(downloader.mkvmerge_location.clone())
4552 .args(mkvmerge_args)
4553 .output()
4554 {
4555 let msg = partial_process_output(&mkvmerge.stdout);
4556 if !msg.is_empty() {
4557 info!(" mkvmerge stdout: {msg}");
4558 }
4559 let msg = partial_process_output(&mkvmerge.stderr);
4560 if !msg.is_empty() {
4561 info!(" mkvmerge stderr: {msg}");
4562 }
4563 if mkvmerge.status.success() {
4564 info!(" Merged subtitles with Matroska container");
4565 {
4568 let tmpfile = File::open(tmppath.clone()).await
4569 .map_err(|e| DashMpdError::Io(
4570 e, String::from("opening mkvmerge output")))?;
4571 let mut merged = BufReader::new(tmpfile);
4572 let outfile = File::create(period_output_path.clone()).await
4574 .map_err(|e| DashMpdError::Io(
4575 e, String::from("creating output file")))?;
4576 let mut sink = BufWriter::new(outfile);
4577 io::copy(&mut merged, &mut sink).await
4578 .map_err(|e| DashMpdError::Io(
4579 e, String::from("copying mkvmerge output to output file")))?;
4580 }
4581 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4582 if let Err(e) = fs::remove_file(tmppath).await {
4583 warn!(" Error deleting temporary mkvmerge output: {e}");
4584 }
4585 }
4586 } else {
4587 warn!(" Error running mkvmerge to merge subtitles");
4588 }
4589 }
4590 }
4591 }
4592 }
4593 } else if have_audio {
4594 copy_audio_to_container(downloader, &period_output_path, &tmppath_audio).await?;
4595 } else if have_video {
4596 copy_video_to_container(downloader, &period_output_path, &tmppath_video).await?;
4597 } else if downloader.fetch_video && downloader.fetch_audio {
4598 return Err(DashMpdError::UnhandledMediaStream("no audio or video streams found".to_string()));
4599 } else if downloader.fetch_video {
4600 return Err(DashMpdError::UnhandledMediaStream("no video streams found".to_string()));
4601 } else if downloader.fetch_audio {
4602 return Err(DashMpdError::UnhandledMediaStream("no audio streams found".to_string()));
4603 }
4604 #[allow(clippy::collapsible_if)]
4605 if downloader.keep_audio.is_none() && downloader.fetch_audio {
4606 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4607 if tmppath_audio.exists() && fs::remove_file(tmppath_audio).await.is_err() {
4608 info!(" Failed to delete temporary file for audio stream");
4609 }
4610 }
4611 }
4612 #[allow(clippy::collapsible_if)]
4613 if downloader.keep_video.is_none() && downloader.fetch_video {
4614 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4615 if tmppath_video.exists() && fs::remove_file(tmppath_video).await.is_err() {
4616 info!(" Failed to delete temporary file for video stream");
4617 }
4618 }
4619 }
4620 #[allow(clippy::collapsible_if)]
4621 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4622 if downloader.fetch_subtitles && tmppath_subs.exists() &&
4623 fs::remove_file(tmppath_subs).await.is_err() {
4624 info!(" Failed to delete temporary file for subtitles");
4625 }
4626 }
4627 if downloader.verbosity > 1 && (downloader.fetch_audio || downloader.fetch_video || have_subtitles) {
4628 if let Ok(metadata) = fs::metadata(&period_output_path).await {
4629 info!(" Wrote {:.1}MB to media file", metadata.len() as f64 / (1024.0 * 1024.0));
4630 }
4631 }
4632 if have_audio || have_video {
4633 period_output_pathbufs.push(period_output_path);
4634 }
4635 } let period_output_paths: Vec<&Path> = period_output_pathbufs
4637 .iter()
4638 .map(PathBuf::as_path)
4639 .collect();
4640 #[allow(clippy::comparison_chain)]
4641 if period_output_paths.len() == 1 {
4642 maybe_record_metainformation(output_path, downloader, &mpd);
4644 } else if period_output_paths.len() > 1 {
4645 #[allow(unused_mut)]
4650 let mut concatenated = false;
4651 #[cfg(not(feature = "libav"))]
4652 if downloader.concatenate_periods && video_containers_concatable(downloader, &period_output_paths) {
4654 info!("Preparing to concatenate multiple Periods into one output file");
4655 concat_output_files(downloader, &period_output_paths).await?;
4656 for p in &period_output_paths[1..] {
4657 if fs::remove_file(p).await.is_err() {
4658 warn!(" Failed to delete temporary file {}", p.display());
4659 }
4660 }
4661 concatenated = true;
4662 if let Some(pop) = period_output_paths.first() {
4663 maybe_record_metainformation(pop, downloader, &mpd);
4664 }
4665 }
4666 if !concatenated {
4667 info!("Media content has been saved in a separate file for each period:");
4668 period_counter = 0;
4670 for p in period_output_paths {
4671 period_counter += 1;
4672 info!(" Period #{period_counter}: {}", p.display());
4673 maybe_record_metainformation(p, downloader, &mpd);
4674 }
4675 }
4676 }
4677 let have_content_protection = mpd.periods.iter().any(
4678 |p| p.adaptations.iter().any(
4679 |a| (!a.ContentProtection.is_empty()) ||
4680 a.representations.iter().any(
4681 |r| !r.ContentProtection.is_empty())));
4682 if have_content_protection && downloader.decryption_keys.is_empty() {
4683 warn!("Manifest seems to use ContentProtection (DRM), but you didn't provide decryption keys.");
4684 }
4685 for observer in &downloader.progress_observers {
4686 observer.update(100, "Done");
4687 }
4688 Ok(PathBuf::from(output_path))
4689}
4690
4691
4692#[cfg(test)]
4693mod tests {
4694 #[test]
4695 fn test_resolve_url_template() {
4696 use std::collections::HashMap;
4697 use super::resolve_url_template;
4698
4699 assert_eq!(resolve_url_template("AA$Time$BB", &HashMap::from([("Time", "ZZZ".to_string())])),
4700 "AAZZZBB");
4701 assert_eq!(resolve_url_template("AA$Number%06d$BB", &HashMap::from([("Number", "42".to_string())])),
4702 "AA000042BB");
4703 let dict = HashMap::from([("RepresentationID", "640x480".to_string()),
4704 ("Number", "42".to_string()),
4705 ("Time", "ZZZ".to_string())]);
4706 assert_eq!(resolve_url_template("AA/$RepresentationID$/segment-$Number%05d$.mp4", &dict),
4707 "AA/640x480/segment-00042.mp4");
4708 }
4709}