1use std::io;
4use std::env;
5use fs_err as fs;
6use fs::File;
7use std::io::{Read, Write, Seek, BufReader, BufWriter};
8use std::path::{Path, PathBuf};
9use std::process::Command;
10use std::time::Duration;
11use tokio::time::Instant;
12use chrono::Utc;
13use std::sync::Arc;
14use std::borrow::Cow;
15use std::collections::HashMap;
16use std::cmp::min;
17use std::ffi::OsStr;
18use std::num::NonZeroU32;
19use tracing::{trace, info, warn, error};
20use regex::Regex;
21use url::Url;
22use bytes::Bytes;
23use data_url::DataUrl;
24use reqwest::header::{RANGE, CONTENT_TYPE};
25use backoff::{future::retry_notify, ExponentialBackoff};
26use governor::{Quota, RateLimiter};
27use lazy_static::lazy_static;
28use xot::{xmlname, Xot};
29use crate::{MPD, Period, Representation, AdaptationSet, SegmentBase, DashMpdError};
30use crate::{parse, mux_audio_video, copy_video_to_container, copy_audio_to_container};
31use crate::{is_audio_adaptation, is_video_adaptation, is_subtitle_adaptation};
32use crate::{subtitle_type, content_protection_type, SubtitleType};
33use crate::check_conformity;
34#[cfg(not(feature = "libav"))]
35use crate::ffmpeg::concat_output_files;
36use crate::media::{temporary_outpath, AudioTrack};
37#[allow(unused_imports)]
38use crate::media::video_containers_concatable;
39
40#[cfg(all(feature = "sandbox", target_os = "linux"))]
41use crate::sandbox::{restrict_thread};
42
43
44pub type HttpClient = reqwest::Client;
46type DirectRateLimiter = RateLimiter<governor::state::direct::NotKeyed,
47 governor::state::InMemoryState,
48 governor::clock::DefaultClock,
49 governor::middleware::NoOpMiddleware>;
50
51
52pub fn partial_process_output(output: &[u8]) -> Cow<'_, str> {
55 let len = min(output.len(), 4096);
56 #[allow(clippy::indexing_slicing)]
57 String::from_utf8_lossy(&output[0..len])
58}
59
60
61fn tmp_file_path(prefix: &str, extension: &OsStr) -> Result<PathBuf, DashMpdError> {
64 if let Some(ext) = extension.to_str() {
65 let fmt = format!(".{}", extension.to_string_lossy());
67 let suffix = if ext.starts_with('.') {
68 extension
69 } else {
70 OsStr::new(&fmt)
71 };
72 let file = tempfile::Builder::new()
73 .prefix(prefix)
74 .suffix(suffix)
75 .rand_bytes(7)
76 .disable_cleanup(env::var("DASHMPD_PERSIST_FILES").is_ok())
77 .tempfile()
78 .map_err(|e| DashMpdError::Io(e, String::from("creating temporary file")))?;
79 Ok(file.path().to_path_buf())
80 } else {
81 Err(DashMpdError::Other(String::from("converting filename extension")))
82 }
83}
84
85
86#[cfg(unix)]
90fn ensure_permissions_readable(path: &PathBuf) -> Result<(), DashMpdError> {
91 use std::fs::Permissions;
92 use std::os::unix::fs::PermissionsExt;
93
94 let perms = Permissions::from_mode(0o644);
95 std::fs::set_permissions(path, perms)
96 .map_err(|e| DashMpdError::Io(e, String::from("setting file permissions")))?;
97 Ok(())
98}
99
100#[cfg(not(unix))]
101fn ensure_permissions_readable(path: &PathBuf) -> Result<(), DashMpdError> {
102 let mut perms = fs::metadata(path)
103 .map_err(|e| DashMpdError::Io(e, String::from("reading file permissions")))?
104 .permissions();
105 perms.set_readonly(false);
106 std::fs::set_permissions(path, perms)
107 .map_err(|e| DashMpdError::Io(e, String::from("setting file permissions")))?;
108 Ok(())
109}
110
111
112pub trait ProgressObserver: Send + Sync {
115 fn update(&self, percent: u32, message: &str);
116}
117
118
119#[derive(PartialEq, Eq, Clone, Copy, Default)]
122pub enum QualityPreference { #[default] Lowest, Intermediate, Highest }
123
124
125pub struct DashDownloader {
145 pub mpd_url: String,
146 pub redirected_url: Url,
147 base_url: Option<String>,
148 referer: Option<String>,
149 auth_username: Option<String>,
150 auth_password: Option<String>,
151 auth_bearer_token: Option<String>,
152 pub output_path: Option<PathBuf>,
153 http_client: Option<HttpClient>,
154 quality_preference: QualityPreference,
155 language_preference: Option<String>,
156 role_preference: Vec<String>,
157 video_width_preference: Option<u64>,
158 video_height_preference: Option<u64>,
159 fetch_video: bool,
160 fetch_audio: bool,
161 fetch_subtitles: bool,
162 keep_video: Option<PathBuf>,
163 keep_audio: Option<PathBuf>,
164 concatenate_periods: bool,
165 fragment_path: Option<PathBuf>,
166 decryption_keys: HashMap<String, String>,
167 xslt_stylesheets: Vec<PathBuf>,
168 minimum_period_duration: Option<Duration>,
169 content_type_checks: bool,
170 conformity_checks: bool,
171 use_index_range: bool,
172 fragment_retry_count: u32,
173 max_error_count: u32,
174 progress_observers: Vec<Arc<dyn ProgressObserver>>,
175 sleep_between_requests: u8,
176 allow_live_streams: bool,
177 force_duration: Option<f64>,
178 rate_limit: u64,
179 bw_limiter: Option<DirectRateLimiter>,
180 bw_estimator_started: Instant,
181 bw_estimator_bytes: usize,
182 pub sandbox: bool,
183 pub verbosity: u8,
184 record_metainformation: bool,
185 pub muxer_preference: HashMap<String, String>,
186 pub concat_preference: HashMap<String, String>,
187 pub decryptor_preference: String,
188 pub ffmpeg_location: String,
189 pub vlc_location: String,
190 pub mkvmerge_location: String,
191 pub mp4box_location: String,
192 pub mp4decrypt_location: String,
193 pub shaka_packager_location: String,
194}
195
196
197#[cfg(not(doctest))]
200impl DashDownloader {
219 pub fn new(mpd_url: &str) -> DashDownloader {
225 DashDownloader {
226 mpd_url: String::from(mpd_url),
227 redirected_url: Url::parse(mpd_url).unwrap(),
228 base_url: None,
229 referer: None,
230 auth_username: None,
231 auth_password: None,
232 auth_bearer_token: None,
233 output_path: None,
234 http_client: None,
235 quality_preference: QualityPreference::Lowest,
236 language_preference: None,
237 role_preference: vec!["main".to_string(), "alternate".to_string()],
238 video_width_preference: None,
239 video_height_preference: None,
240 fetch_video: true,
241 fetch_audio: true,
242 fetch_subtitles: false,
243 keep_video: None,
244 keep_audio: None,
245 concatenate_periods: true,
246 fragment_path: None,
247 decryption_keys: HashMap::new(),
248 xslt_stylesheets: Vec::new(),
249 minimum_period_duration: None,
250 content_type_checks: true,
251 conformity_checks: true,
252 use_index_range: true,
253 fragment_retry_count: 10,
254 max_error_count: 30,
255 progress_observers: Vec::new(),
256 sleep_between_requests: 0,
257 allow_live_streams: false,
258 force_duration: None,
259 rate_limit: 0,
260 bw_limiter: None,
261 bw_estimator_started: Instant::now(),
262 bw_estimator_bytes: 0,
263 sandbox: false,
264 verbosity: 0,
265 record_metainformation: true,
266 muxer_preference: HashMap::new(),
267 concat_preference: HashMap::new(),
268 decryptor_preference: String::from("mp4decrypt"),
269 ffmpeg_location: String::from("ffmpeg"),
270 vlc_location: if cfg!(target_os = "windows") {
271 String::from("c:/Program Files/VideoLAN/VLC/vlc.exe")
274 } else {
275 String::from("vlc")
276 },
277 mkvmerge_location: String::from("mkvmerge"),
278 mp4box_location: if cfg!(target_os = "windows") {
279 String::from("MP4Box.exe")
280 } else if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
281 String::from("MP4Box")
282 } else {
283 String::from("mp4box")
284 },
285 mp4decrypt_location: String::from("mp4decrypt"),
286 shaka_packager_location: String::from("shaka-packager"),
287 }
288 }
289
290 #[must_use]
293 pub fn with_base_url(mut self, base_url: String) -> DashDownloader {
294 self.base_url = Some(base_url);
295 self
296 }
297
298
299 #[must_use]
321 pub fn with_http_client(mut self, client: HttpClient) -> DashDownloader {
322 self.http_client = Some(client);
323 self
324 }
325
326 #[must_use]
330 pub fn with_referer(mut self, referer: String) -> DashDownloader {
331 self.referer = Some(referer);
332 self
333 }
334
335 #[must_use]
338 pub fn with_authentication(mut self, username: &str, password: &str) -> DashDownloader {
339 self.auth_username = Some(username.to_string());
340 self.auth_password = Some(password.to_string());
341 self
342 }
343
344 #[must_use]
347 pub fn with_auth_bearer(mut self, token: &str) -> DashDownloader {
348 self.auth_bearer_token = Some(token.to_string());
349 self
350 }
351
352 #[must_use]
355 pub fn add_progress_observer(mut self, observer: Arc<dyn ProgressObserver>) -> DashDownloader {
356 self.progress_observers.push(observer);
357 self
358 }
359
360 #[must_use]
363 pub fn best_quality(mut self) -> DashDownloader {
364 self.quality_preference = QualityPreference::Highest;
365 self
366 }
367
368 #[must_use]
371 pub fn intermediate_quality(mut self) -> DashDownloader {
372 self.quality_preference = QualityPreference::Intermediate;
373 self
374 }
375
376 #[must_use]
379 pub fn worst_quality(mut self) -> DashDownloader {
380 self.quality_preference = QualityPreference::Lowest;
381 self
382 }
383
384 #[must_use]
391 pub fn prefer_language(mut self, lang: String) -> DashDownloader {
392 self.language_preference = Some(lang);
393 self
394 }
395
396 #[must_use]
406 pub fn prefer_roles(mut self, role_preference: Vec<String>) -> DashDownloader {
407 if role_preference.len() < u8::MAX as usize {
408 self.role_preference = role_preference;
409 } else {
410 warn!("Ignoring role_preference ordering due to excessive length");
411 }
412 self
413 }
414
415 #[must_use]
418 pub fn prefer_video_width(mut self, width: u64) -> DashDownloader {
419 self.video_width_preference = Some(width);
420 self
421 }
422
423 #[must_use]
426 pub fn prefer_video_height(mut self, height: u64) -> DashDownloader {
427 self.video_height_preference = Some(height);
428 self
429 }
430
431 #[must_use]
433 pub fn video_only(mut self) -> DashDownloader {
434 self.fetch_audio = false;
435 self.fetch_video = true;
436 self
437 }
438
439 #[must_use]
441 pub fn audio_only(mut self) -> DashDownloader {
442 self.fetch_audio = true;
443 self.fetch_video = false;
444 self
445 }
446
447 #[must_use]
450 pub fn keep_video_as<P: Into<PathBuf>>(mut self, video_path: P) -> DashDownloader {
451 self.keep_video = Some(video_path.into());
452 self
453 }
454
455 #[must_use]
458 pub fn keep_audio_as<P: Into<PathBuf>>(mut self, audio_path: P) -> DashDownloader {
459 self.keep_audio = Some(audio_path.into());
460 self
461 }
462
463 #[must_use]
466 pub fn save_fragments_to<P: Into<PathBuf>>(mut self, fragment_path: P) -> DashDownloader {
467 self.fragment_path = Some(fragment_path.into());
468 self
469 }
470
471 #[must_use]
483 pub fn add_decryption_key(mut self, id: String, key: String) -> DashDownloader {
484 self.decryption_keys.insert(id, key);
485 self
486 }
487
488 #[must_use]
500 pub fn with_xslt_stylesheet<P: Into<PathBuf>>(mut self, stylesheet: P) -> DashDownloader {
501 self.xslt_stylesheets.push(stylesheet.into());
502 self
503 }
504
505 #[must_use]
508 pub fn minimum_period_duration(mut self, value: Duration) -> DashDownloader {
509 self.minimum_period_duration = Some(value);
510 self
511 }
512
513 #[must_use]
517 pub fn fetch_audio(mut self, value: bool) -> DashDownloader {
518 self.fetch_audio = value;
519 self
520 }
521
522 #[must_use]
526 pub fn fetch_video(mut self, value: bool) -> DashDownloader {
527 self.fetch_video = value;
528 self
529 }
530
531 #[must_use]
539 pub fn fetch_subtitles(mut self, value: bool) -> DashDownloader {
540 self.fetch_subtitles = value;
541 self
542 }
543
544 #[must_use]
548 pub fn concatenate_periods(mut self, value: bool) -> DashDownloader {
549 self.concatenate_periods = value;
550 self
551 }
552
553 #[must_use]
556 pub fn without_content_type_checks(mut self) -> DashDownloader {
557 self.content_type_checks = false;
558 self
559 }
560
561 #[must_use]
564 pub fn content_type_checks(mut self, value: bool) -> DashDownloader {
565 self.content_type_checks = value;
566 self
567 }
568
569 #[must_use]
572 pub fn conformity_checks(mut self, value: bool) -> DashDownloader {
573 self.conformity_checks = value;
574 self
575 }
576
577 #[must_use]
592 pub fn use_index_range(mut self, value: bool) -> DashDownloader {
593 self.use_index_range = value;
594 self
595 }
596
597 #[must_use]
601 pub fn fragment_retry_count(mut self, count: u32) -> DashDownloader {
602 self.fragment_retry_count = count;
603 self
604 }
605
606 #[must_use]
613 pub fn max_error_count(mut self, count: u32) -> DashDownloader {
614 self.max_error_count = count;
615 self
616 }
617
618 #[must_use]
620 pub fn sleep_between_requests(mut self, seconds: u8) -> DashDownloader {
621 self.sleep_between_requests = seconds;
622 self
623 }
624
625 #[must_use]
637 pub fn allow_live_streams(mut self, value: bool) -> DashDownloader {
638 self.allow_live_streams = value;
639 self
640 }
641
642 #[must_use]
648 pub fn force_duration(mut self, seconds: f64) -> DashDownloader {
649 self.force_duration = Some(seconds);
650 self
651 }
652
653 #[must_use]
659 pub fn with_rate_limit(mut self, bps: u64) -> DashDownloader {
660 if bps < 10 * 1024 {
661 warn!("Limiting bandwidth below 10kB/s is unlikely to be stable");
662 }
663 if self.verbosity > 1 {
664 info!("Limiting bandwidth to {} kB/s", bps/1024);
665 }
666 self.rate_limit = bps;
667 let mut kps = 1 + bps / 1024;
673 if kps > u64::from(u32::MAX) {
674 warn!("Throttling bandwidth limit");
675 kps = u32::MAX.into();
676 }
677 if let Some(bw_limit) = NonZeroU32::new(kps as u32) {
678 if let Some(burst) = NonZeroU32::new(10 * 1024) {
679 let bw_quota = Quota::per_second(bw_limit)
680 .allow_burst(burst);
681 self.bw_limiter = Some(RateLimiter::direct(bw_quota));
682 }
683 }
684 self
685 }
686
687 #[must_use]
697 pub fn verbosity(mut self, level: u8) -> DashDownloader {
698 self.verbosity = level;
699 self
700 }
701
702 #[must_use]
712 pub fn sandbox(mut self, enable: bool) -> DashDownloader {
713 #[cfg(not(all(feature = "sandbox", target_os = "linux")))]
714 if enable {
715 warn!("Sandboxing only available on Linux with crate feature sandbox enabled");
716 }
717 self.sandbox = enable;
718 self
719 }
720
721 #[must_use]
725 pub fn record_metainformation(mut self, record: bool) -> DashDownloader {
726 self.record_metainformation = record;
727 self
728 }
729
730 #[must_use]
752 pub fn with_muxer_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
753 self.muxer_preference.insert(container.to_string(), ordering.to_string());
754 self
755 }
756
757 #[must_use]
780 pub fn with_concat_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
781 self.concat_preference.insert(container.to_string(), ordering.to_string());
782 self
783 }
784
785 #[must_use]
792 pub fn with_decryptor_preference(mut self, decryption_tool: &str) -> DashDownloader {
793 self.decryptor_preference = decryption_tool.to_string();
794 self
795 }
796
797 #[must_use]
812 pub fn with_ffmpeg(mut self, ffmpeg_path: &str) -> DashDownloader {
813 self.ffmpeg_location = ffmpeg_path.to_string();
814 self
815 }
816
817 #[must_use]
832 pub fn with_vlc(mut self, vlc_path: &str) -> DashDownloader {
833 self.vlc_location = vlc_path.to_string();
834 self
835 }
836
837 #[must_use]
845 pub fn with_mkvmerge(mut self, path: &str) -> DashDownloader {
846 self.mkvmerge_location = path.to_string();
847 self
848 }
849
850 #[must_use]
858 pub fn with_mp4box(mut self, path: &str) -> DashDownloader {
859 self.mp4box_location = path.to_string();
860 self
861 }
862
863 #[must_use]
871 pub fn with_mp4decrypt(mut self, path: &str) -> DashDownloader {
872 self.mp4decrypt_location = path.to_string();
873 self
874 }
875
876 #[must_use]
884 pub fn with_shaka_packager(mut self, path: &str) -> DashDownloader {
885 self.shaka_packager_location = path.to_string();
886 self
887 }
888
889 pub async fn download_to<P: Into<PathBuf>>(mut self, out: P) -> Result<PathBuf, DashMpdError> {
899 self.output_path = Some(out.into());
900 if self.http_client.is_none() {
901 let client = reqwest::Client::builder()
902 .timeout(Duration::new(30, 0))
903 .cookie_store(true)
904 .build()
905 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
906 self.http_client = Some(client);
907 }
908 fetch_mpd(&mut self).await
909 }
910
911 pub async fn download(mut self) -> Result<PathBuf, DashMpdError> {
918 let cwd = env::current_dir()
919 .map_err(|e| DashMpdError::Io(e, String::from("obtaining current directory")))?;
920 let filename = generate_filename_from_url(&self.mpd_url);
921 let outpath = cwd.join(filename);
922 self.output_path = Some(outpath);
923 if self.http_client.is_none() {
924 let client = reqwest::Client::builder()
925 .timeout(Duration::new(30, 0))
926 .cookie_store(true)
927 .build()
928 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
929 self.http_client = Some(client);
930 }
931 fetch_mpd(&mut self).await
932 }
933}
934
935
936fn mpd_is_dynamic(mpd: &MPD) -> bool {
937 if let Some(mpdtype) = mpd.mpdtype.as_ref() {
938 return mpdtype.eq("dynamic");
939 }
940 false
941}
942
943fn parse_range(range: &str) -> Result<(u64, u64), DashMpdError> {
946 let v: Vec<&str> = range.split_terminator('-').collect();
947 if v.len() != 2 {
948 return Err(DashMpdError::Parsing(format!("invalid range specifier: {range}")));
949 }
950 #[allow(clippy::indexing_slicing)]
951 let start: u64 = v[0].parse()
952 .map_err(|_| DashMpdError::Parsing(String::from("invalid start for range specifier")))?;
953 #[allow(clippy::indexing_slicing)]
954 let end: u64 = v[1].parse()
955 .map_err(|_| DashMpdError::Parsing(String::from("invalid end for range specifier")))?;
956 Ok((start, end))
957}
958
959#[derive(Debug)]
960struct MediaFragment {
961 period: u8,
962 url: Url,
963 start_byte: Option<u64>,
964 end_byte: Option<u64>,
965 is_init: bool,
966 timeout: Option<Duration>,
967}
968
969#[derive(Debug)]
970struct MediaFragmentBuilder {
971 period: u8,
972 url: Url,
973 start_byte: Option<u64>,
974 end_byte: Option<u64>,
975 is_init: bool,
976 timeout: Option<Duration>,
977}
978
979impl MediaFragmentBuilder {
980 pub fn new(period: u8, url: Url) -> MediaFragmentBuilder {
981 MediaFragmentBuilder {
982 period, url, start_byte: None, end_byte: None, is_init: false, timeout: None
983 }
984 }
985
986 pub fn with_range(mut self, start_byte: Option<u64>, end_byte: Option<u64>) -> MediaFragmentBuilder {
987 self.start_byte = start_byte;
988 self.end_byte = end_byte;
989 self
990 }
991
992 pub fn with_timeout(mut self, timeout: Duration) -> MediaFragmentBuilder {
993 self.timeout = Some(timeout);
994 self
995 }
996
997 pub fn set_init(mut self) -> MediaFragmentBuilder {
998 self.is_init = true;
999 self
1000 }
1001
1002 pub fn build(self) -> MediaFragment {
1003 MediaFragment {
1004 period: self.period,
1005 url: self.url,
1006 start_byte: self.start_byte,
1007 end_byte: self.end_byte,
1008 is_init: self.is_init,
1009 timeout: self.timeout
1010 }
1011 }
1012}
1013
1014#[derive(Debug, Default)]
1018struct PeriodOutputs {
1019 fragments: Vec<MediaFragment>,
1020 diagnostics: Vec<String>,
1021 subtitle_formats: Vec<SubtitleType>,
1022 selected_audio_language: String,
1023}
1024
1025#[derive(Debug, Default)]
1026struct PeriodDownloads {
1027 audio_fragments: Vec<MediaFragment>,
1028 video_fragments: Vec<MediaFragment>,
1029 subtitle_fragments: Vec<MediaFragment>,
1030 subtitle_formats: Vec<SubtitleType>,
1031 period_counter: u8,
1032 id: Option<String>,
1033 selected_audio_language: String,
1034}
1035
1036fn period_fragment_count(pd: &PeriodDownloads) -> usize {
1037 pd.audio_fragments.len() +
1038 pd.video_fragments.len() +
1039 pd.subtitle_fragments.len()
1040}
1041
1042
1043
1044async fn throttle_download_rate(downloader: &DashDownloader, size: u32) -> Result<(), DashMpdError> {
1045 if downloader.rate_limit > 0 {
1046 if let Some(cells) = NonZeroU32::new(size) {
1047 if let Some(limiter) = downloader.bw_limiter.as_ref() {
1048 #[allow(clippy::redundant_pattern_matching)]
1049 if let Err(_) = limiter.until_n_ready(cells).await {
1050 return Err(DashMpdError::Other(
1051 "Bandwidth limit is too low".to_string()));
1052 }
1053 }
1054 }
1055 }
1056 Ok(())
1057}
1058
1059
1060fn generate_filename_from_url(url: &str) -> PathBuf {
1061 use sanitise_file_name::{sanitise_with_options, Options};
1062
1063 let mut path = url;
1064 if let Some(p) = path.strip_prefix("http://") {
1065 path = p;
1066 } else if let Some(p) = path.strip_prefix("https://") {
1067 path = p;
1068 } else if let Some(p) = path.strip_prefix("file://") {
1069 path = p;
1070 }
1071 if let Some(p) = path.strip_prefix("www.") {
1072 path = p;
1073 }
1074 if let Some(p) = path.strip_prefix("ftp.") {
1075 path = p;
1076 }
1077 if let Some(p) = path.strip_suffix(".mpd") {
1078 path = p;
1079 }
1080 let mut sanitize_opts = Options::DEFAULT;
1081 sanitize_opts.length_limit = 150;
1082 PathBuf::from(sanitise_with_options(path, &sanitize_opts) + ".mp4")
1087}
1088
1089fn output_path_for_period(base: &Path, period: u8) -> PathBuf {
1106 assert!(period > 0);
1107 if period == 1 {
1108 base.to_path_buf()
1109 } else {
1110 if let Some(stem) = base.file_stem() {
1111 if let Some(ext) = base.extension() {
1112 let fname = format!("{}-p{period}.{}", stem.to_string_lossy(), ext.to_string_lossy());
1113 return base.with_file_name(fname);
1114 }
1115 }
1116 let p = format!("dashmpd-p{period}");
1117 tmp_file_path(&p, base.extension().unwrap_or(OsStr::new("mp4")))
1118 .unwrap_or_else(|_| p.into())
1119 }
1120}
1121
1122fn is_absolute_url(s: &str) -> bool {
1123 s.starts_with("http://") ||
1124 s.starts_with("https://") ||
1125 s.starts_with("file://") ||
1126 s.starts_with("ftp://")
1127}
1128
1129fn merge_baseurls(current: &Url, new: &str) -> Result<Url, DashMpdError> {
1130 if is_absolute_url(new) {
1131 Url::parse(new)
1132 .map_err(|e| parse_error("parsing BaseURL", e))
1133 } else {
1134 let mut merged = current.join(new)
1147 .map_err(|e| parse_error("joining base with BaseURL", e))?;
1148 if merged.query().is_none() {
1149 merged.set_query(current.query());
1150 }
1151 Ok(merged)
1152 }
1153}
1154
1155fn content_type_audio_p(response: &reqwest::Response) -> bool {
1160 match response.headers().get("content-type") {
1161 Some(ct) => {
1162 let ctb = ct.as_bytes();
1163 ctb.starts_with(b"audio/") ||
1164 ctb.starts_with(b"video/") ||
1165 ctb.starts_with(b"application/octet-stream")
1166 },
1167 None => false,
1168 }
1169}
1170
1171fn content_type_video_p(response: &reqwest::Response) -> bool {
1173 match response.headers().get("content-type") {
1174 Some(ct) => {
1175 let ctb = ct.as_bytes();
1176 ctb.starts_with(b"video/") ||
1177 ctb.starts_with(b"application/octet-stream")
1178 },
1179 None => false,
1180 }
1181}
1182
1183
1184fn adaptation_lang_distance(a: &AdaptationSet, language_preference: &str) -> u8 {
1188 if let Some(lang) = &a.lang {
1189 if lang.eq(language_preference) {
1190 return 0;
1191 }
1192 if lang[0..2].eq(&language_preference[0..2]) {
1193 return 5;
1194 }
1195 100
1196 } else {
1197 100
1198 }
1199}
1200
1201fn adaptation_roles(a: &AdaptationSet) -> Vec<String> {
1204 let mut roles = Vec::new();
1205 for r in &a.Role {
1206 if let Some(rv) = &r.value {
1207 roles.push(String::from(rv));
1208 }
1209 }
1210 for cc in &a.ContentComponent {
1211 for r in &cc.Role {
1212 if let Some(rv) = &r.value {
1213 roles.push(String::from(rv));
1214 }
1215 }
1216 }
1217 roles
1218}
1219
1220fn adaptation_role_distance(a: &AdaptationSet, role_preference: &[String]) -> u8 {
1222 adaptation_roles(a).iter()
1223 .map(|r| role_preference.binary_search(r).unwrap_or(u8::MAX.into()))
1224 .map(|u| u8::try_from(u).unwrap_or(u8::MAX))
1225 .min()
1226 .unwrap_or(u8::MAX)
1227}
1228
1229
1230fn select_preferred_adaptations<'a>(
1238 adaptations: Vec<&'a AdaptationSet>,
1239 downloader: &DashDownloader) -> Vec<&'a AdaptationSet>
1240{
1241 let mut preferred: Vec<&'a AdaptationSet>;
1242 if let Some(ref lang) = downloader.language_preference {
1244 preferred = Vec::new();
1245 let distance: Vec<u8> = adaptations.iter()
1246 .map(|a| adaptation_lang_distance(a, lang))
1247 .collect();
1248 let min_distance = distance.iter().min().unwrap_or(&0);
1249 for (i, a) in adaptations.iter().enumerate() {
1250 if let Some(di) = distance.get(i) {
1251 if di == min_distance {
1252 preferred.push(a);
1253 }
1254 }
1255 }
1256 } else {
1257 preferred = adaptations;
1258 }
1259 let role_distance: Vec<u8> = preferred.iter()
1265 .map(|a| adaptation_role_distance(a, &downloader.role_preference))
1266 .collect();
1267 let role_distance_min = role_distance.iter().min().unwrap_or(&0);
1268 let mut best = Vec::new();
1269 for (i, a) in preferred.into_iter().enumerate() {
1270 if let Some(rdi) = role_distance.get(i) {
1271 if rdi == role_distance_min {
1272 best.push(a);
1273 }
1274 }
1275 }
1276 best
1277}
1278
1279
1280fn select_preferred_representation<'a>(
1286 representations: &[&'a Representation],
1287 downloader: &DashDownloader) -> Option<&'a Representation>
1288{
1289 if representations.iter().all(|x| x.qualityRanking.is_some()) {
1290 match downloader.quality_preference {
1293 QualityPreference::Lowest =>
1294 representations.iter()
1295 .max_by_key(|r| r.qualityRanking.unwrap_or(u8::MAX))
1296 .copied(),
1297 QualityPreference::Highest =>
1298 representations.iter().min_by_key(|r| r.qualityRanking.unwrap_or(0))
1299 .copied(),
1300 QualityPreference::Intermediate => {
1301 let count = representations.len();
1302 match count {
1303 0 => None,
1304 1 => Some(representations[0]),
1305 _ => {
1306 let mut ranking: Vec<u8> = representations.iter()
1307 .map(|r| r.qualityRanking.unwrap_or(u8::MAX))
1308 .collect();
1309 ranking.sort_unstable();
1310 if let Some(want_ranking) = ranking.get(count / 2) {
1311 representations.iter()
1312 .find(|r| r.qualityRanking.unwrap_or(u8::MAX) == *want_ranking)
1313 .copied()
1314 } else {
1315 representations.first().copied()
1316 }
1317 },
1318 }
1319 },
1320 }
1321 } else {
1322 match downloader.quality_preference {
1324 QualityPreference::Lowest => representations.iter()
1325 .min_by_key(|r| r.bandwidth.unwrap_or(1_000_000_000))
1326 .copied(),
1327 QualityPreference::Highest => representations.iter()
1328 .max_by_key(|r| r.bandwidth.unwrap_or(0))
1329 .copied(),
1330 QualityPreference::Intermediate => {
1331 let count = representations.len();
1332 match count {
1333 0 => None,
1334 1 => Some(representations[0]),
1335 _ => {
1336 let mut ranking: Vec<u64> = representations.iter()
1337 .map(|r| r.bandwidth.unwrap_or(100_000_000))
1338 .collect();
1339 ranking.sort_unstable();
1340 if let Some(want_ranking) = ranking.get(count / 2) {
1341 representations.iter()
1342 .find(|r| r.bandwidth.unwrap_or(100_000_000) == *want_ranking)
1343 .copied()
1344 } else {
1345 representations.first().copied()
1346 }
1347 },
1348 }
1349 },
1350 }
1351 }
1352}
1353
1354
1355fn print_available_subtitles_representation(r: &Representation, a: &AdaptationSet) {
1357 let unspecified = "<unspecified>".to_string();
1358 let empty = "".to_string();
1359 let lang = r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unspecified));
1360 let codecs = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&empty));
1361 let typ = subtitle_type(&a);
1362 let stype = if !codecs.is_empty() {
1363 format!("{typ:?}/{codecs}")
1364 } else {
1365 format!("{typ:?}")
1366 };
1367 let role = a.Role.first()
1368 .map_or_else(|| String::from(""),
1369 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1370 let label = a.Label.first()
1371 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1372 info!(" subs {stype:>18} | {lang:>10} |{role}{label}");
1373}
1374
1375fn print_available_subtitles_adaptation(a: &AdaptationSet) {
1376 a.representations.iter()
1377 .for_each(|r| print_available_subtitles_representation(r, a));
1378}
1379
1380fn print_available_streams_representation(r: &Representation, a: &AdaptationSet, typ: &str) {
1382 let unspecified = "<unspecified>".to_string();
1384 let w = r.width.unwrap_or(a.width.unwrap_or(0));
1385 let h = r.height.unwrap_or(a.height.unwrap_or(0));
1386 let codec = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&unspecified));
1387 let bw = r.bandwidth.unwrap_or(a.maxBandwidth.unwrap_or(0));
1388 let fmt = if typ.eq("audio") {
1389 let unknown = String::from("?");
1390 format!("lang={}", r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unknown)))
1391 } else if w == 0 || h == 0 {
1392 String::from("")
1395 } else {
1396 format!("{w}x{h}")
1397 };
1398 let role = a.Role.first()
1399 .map_or_else(|| String::from(""),
1400 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1401 let label = a.Label.first()
1402 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1403 info!(" {typ} {codec:17} | {:5} Kbps | {fmt:>9}{role}{label}", bw / 1024);
1404}
1405
1406fn print_available_streams_adaptation(a: &AdaptationSet, typ: &str) {
1407 a.representations.iter()
1408 .for_each(|r| print_available_streams_representation(r, a, typ));
1409}
1410
1411fn print_available_streams_period(p: &Period) {
1412 p.adaptations.iter()
1413 .filter(is_audio_adaptation)
1414 .for_each(|a| print_available_streams_adaptation(a, "audio"));
1415 p.adaptations.iter()
1416 .filter(is_video_adaptation)
1417 .for_each(|a| print_available_streams_adaptation(a, "video"));
1418 p.adaptations.iter()
1419 .filter(is_subtitle_adaptation)
1420 .for_each(print_available_subtitles_adaptation);
1421}
1422
1423#[tracing::instrument(level="trace", skip_all)]
1424fn print_available_streams(mpd: &MPD) {
1425 let mut counter = 0;
1426 for p in &mpd.periods {
1427 let mut period_duration_secs: f64 = 0.0;
1428 if let Some(d) = mpd.mediaPresentationDuration {
1429 period_duration_secs = d.as_secs_f64();
1430 }
1431 if let Some(d) = &p.duration {
1432 period_duration_secs = d.as_secs_f64();
1433 }
1434 counter += 1;
1435 if let Some(id) = p.id.as_ref() {
1436 info!("Streams in period {id} (#{counter}), duration {period_duration_secs:.3}s:");
1437 } else {
1438 info!("Streams in period #{counter}, duration {period_duration_secs:.3}s:");
1439 }
1440 print_available_streams_period(p);
1441 }
1442}
1443
1444async fn extract_init_pssh(downloader: &DashDownloader, init_url: Url) -> Option<Vec<u8>> {
1445 use bstr::ByteSlice;
1446 use hex_literal::hex;
1447
1448 if let Some(client) = downloader.http_client.as_ref() {
1449 let mut req = client.get(init_url);
1450 if let Some(referer) = &downloader.referer {
1451 req = req.header("Referer", referer);
1452 }
1453 if let Some(username) = &downloader.auth_username {
1454 if let Some(password) = &downloader.auth_password {
1455 req = req.basic_auth(username, Some(password));
1456 }
1457 }
1458 if let Some(token) = &downloader.auth_bearer_token {
1459 req = req.bearer_auth(token);
1460 }
1461 if let Ok(mut resp) = req.send().await {
1462 let mut chunk_counter = 0;
1465 let mut segment_first_bytes = Vec::<u8>::new();
1466 while let Ok(Some(chunk)) = resp.chunk().await {
1467 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
1468 #[allow(clippy::redundant_pattern_matching)]
1469 if let Err(_) = throttle_download_rate(downloader, size).await {
1470 return None;
1471 }
1472 segment_first_bytes.append(&mut chunk.to_vec());
1473 chunk_counter += 1;
1474 if chunk_counter > 20 {
1475 break;
1476 }
1477 }
1478 let needle = b"pssh";
1479 for offset in segment_first_bytes.find_iter(needle) {
1480 #[allow(clippy::needless_range_loop)]
1481 for i in offset-4..offset+2 {
1482 if let Some(b) = segment_first_bytes.get(i) {
1483 if *b != 0 {
1484 continue;
1485 }
1486 }
1487 }
1488 #[allow(clippy::needless_range_loop)]
1489 for i in offset+4..offset+8 {
1490 if let Some(b) = segment_first_bytes.get(i) {
1491 if *b != 0 {
1492 continue;
1493 }
1494 }
1495 }
1496 if offset+24 > segment_first_bytes.len() {
1497 continue;
1498 }
1499 const WIDEVINE_SYSID: [u8; 16] = hex!("edef8ba979d64acea3c827dcd51d21ed");
1501 if let Some(sysid) = segment_first_bytes.get((offset+8)..(offset+24)) {
1502 if !sysid.eq(&WIDEVINE_SYSID) {
1503 continue;
1504 }
1505 }
1506 if let Some(length) = segment_first_bytes.get(offset-1) {
1507 let start = offset - 4;
1508 let end = start + *length as usize;
1509 if let Some(pssh) = &segment_first_bytes.get(start..end) {
1510 return Some(pssh.to_vec());
1511 }
1512 }
1513 }
1514 }
1515 None
1516 } else {
1517 None
1518 }
1519}
1520
1521
1522lazy_static! {
1531 static ref URL_TEMPLATE_IDS: Vec<(&'static str, String, Regex)> = {
1532 vec!["RepresentationID", "Number", "Time", "Bandwidth"].into_iter()
1533 .map(|k| (k, format!("${k}$"), Regex::new(&format!("\\${k}%0([\\d])d\\$")).unwrap()))
1534 .collect()
1535 };
1536}
1537
1538fn resolve_url_template(template: &str, params: &HashMap<&str, String>) -> String {
1539 let mut result = template.to_string();
1540 for (k, ident, rx) in URL_TEMPLATE_IDS.iter() {
1541 if result.contains(ident) {
1543 if let Some(value) = params.get(k as &str) {
1544 result = result.replace(ident, value);
1545 }
1546 }
1547 if let Some(cap) = rx.captures(&result) {
1549 if let Some(value) = params.get(k as &str) {
1550 if let Ok(width) = cap[1].parse::<usize>() {
1551 if let Some(m) = rx.find(&result) {
1552 let count = format!("{value:0>width$}");
1553 result = result[..m.start()].to_owned() + &count + &result[m.end()..];
1554 }
1555 }
1556 }
1557 }
1558 }
1559 result
1560}
1561
1562
1563fn reqwest_error_transient_p(e: &reqwest::Error) -> bool {
1564 if e.is_timeout() {
1565 return true;
1566 }
1567 if let Some(s) = e.status() {
1568 if s == reqwest::StatusCode::REQUEST_TIMEOUT ||
1569 s == reqwest::StatusCode::TOO_MANY_REQUESTS ||
1570 s == reqwest::StatusCode::SERVICE_UNAVAILABLE ||
1571 s == reqwest::StatusCode::GATEWAY_TIMEOUT {
1572 return true;
1573 }
1574 }
1575 false
1576}
1577
1578fn categorize_reqwest_error(e: reqwest::Error) -> backoff::Error<reqwest::Error> {
1579 if reqwest_error_transient_p(&e) {
1580 backoff::Error::retry_after(e, Duration::new(5, 0))
1581 } else {
1582 backoff::Error::permanent(e)
1583 }
1584}
1585
1586fn notify_transient<E: std::fmt::Debug>(err: E, dur: Duration) {
1587 warn!("Transient error after {dur:?}: {err:?}");
1588}
1589
1590fn network_error(why: &str, e: &reqwest::Error) -> DashMpdError {
1591 if e.is_timeout() {
1592 DashMpdError::NetworkTimeout(format!("{why}: {e:?}"))
1593 } else if e.is_connect() {
1594 DashMpdError::NetworkConnect(format!("{why}: {e:?}"))
1595 } else {
1596 DashMpdError::Network(format!("{why}: {e:?}"))
1597 }
1598}
1599
1600fn parse_error(why: &str, e: impl std::error::Error) -> DashMpdError {
1601 DashMpdError::Parsing(format!("{why}: {e:#?}"))
1602}
1603
1604
1605async fn reqwest_bytes_with_retries(
1609 client: &reqwest::Client,
1610 req: reqwest::Request,
1611 retry_count: u32) -> Result<Bytes, reqwest::Error>
1612{
1613 let mut last_error = None;
1614 for _ in 0..retry_count {
1615 if let Some(rqw) = req.try_clone() {
1616 match client.execute(rqw).await {
1617 Ok(response) => {
1618 match response.error_for_status() {
1619 Ok(resp) => {
1620 match resp.bytes().await {
1621 Ok(bytes) => return Ok(bytes),
1622 Err(e) => {
1623 info!("Retrying after HTTP error {e:?}");
1624 last_error = Some(e);
1625 },
1626 }
1627 },
1628 Err(e) => {
1629 info!("Retrying after HTTP error {e:?}");
1630 last_error = Some(e);
1631 },
1632 }
1633 },
1634 Err(e) => {
1635 info!("Retrying after HTTP error {e:?}");
1636 last_error = Some(e);
1637 },
1638 }
1639 }
1640 }
1641 Err(last_error.unwrap())
1642}
1643
1644#[allow(unused_variables)]
1657fn maybe_record_metainformation(path: &Path, downloader: &DashDownloader, mpd: &MPD) {
1658 #[cfg(target_family = "unix")]
1659 if downloader.record_metainformation && (downloader.fetch_audio || downloader.fetch_video) {
1660 if let Ok(origin_url) = Url::parse(&downloader.mpd_url) {
1661 #[allow(clippy::collapsible_if)]
1663 if origin_url.username().is_empty() && origin_url.password().is_none() {
1664 #[cfg(target_family = "unix")]
1665 if xattr::set(path, "user.xdg.origin.url", downloader.mpd_url.as_bytes()).is_err() {
1666 info!("Failed to set user.xdg.origin.url xattr on output file");
1667 }
1668 }
1669 for pi in &mpd.ProgramInformation {
1670 if let Some(t) = &pi.Title {
1671 if let Some(tc) = &t.content {
1672 if xattr::set(path, "user.dublincore.title", tc.as_bytes()).is_err() {
1673 info!("Failed to set user.dublincore.title xattr on output file");
1674 }
1675 }
1676 }
1677 if let Some(source) = &pi.Source {
1678 if let Some(sc) = &source.content {
1679 if xattr::set(path, "user.dublincore.source", sc.as_bytes()).is_err() {
1680 info!("Failed to set user.dublincore.source xattr on output file");
1681 }
1682 }
1683 }
1684 if let Some(copyright) = &pi.Copyright {
1685 if let Some(cc) = ©right.content {
1686 if xattr::set(path, "user.dublincore.rights", cc.as_bytes()).is_err() {
1687 info!("Failed to set user.dublincore.rights xattr on output file");
1688 }
1689 }
1690 }
1691 }
1692 }
1693 }
1694}
1695
1696fn fetchable_xlink_href(href: &str) -> bool {
1700 (!href.is_empty()) && href.ne("urn:mpeg:dash:resolve-to-zero:2013")
1701}
1702
1703fn element_resolves_to_zero(xot: &mut Xot, element: xot::Node) -> bool {
1704 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
1705 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
1706 if let Some(href) = xot.get_attribute(element, xlink_href_name.into()) {
1707 return href.eq("urn:mpeg:dash:resolve-to-zero:2013");
1708 }
1709 false
1710}
1711
1712fn skip_xml_preamble(input: &str) -> &str {
1713 if input.starts_with("<?xml") {
1714 if let Some(end_pos) = input.find("?>") {
1715 return &input[end_pos + 2..]; }
1718 }
1719 input
1721}
1722
1723fn apply_xslt_stylesheets_xsltproc(
1727 downloader: &DashDownloader,
1728 xot: &mut Xot,
1729 doc: xot::Node) -> Result<String, DashMpdError> {
1730 let mut buf = Vec::new();
1731 xot.write(doc, &mut buf)
1732 .map_err(|e| parse_error("serializing rewritten manifest", e))?;
1733 for ss in &downloader.xslt_stylesheets {
1734 if downloader.verbosity > 0 {
1735 info!("Applying XSLT stylesheet {} with xsltproc", ss.display());
1736 }
1737 let tmpmpd = tmp_file_path("dashxslt", OsStr::new("xslt"))?;
1738 fs::write(&tmpmpd, &buf)
1739 .map_err(|e| DashMpdError::Io(e, String::from("writing MPD")))?;
1740 let xsltproc = Command::new("xsltproc")
1741 .args([ss, &tmpmpd])
1742 .output()
1743 .map_err(|e| DashMpdError::Io(e, String::from("spawning xsltproc")))?;
1744 if !xsltproc.status.success() {
1745 let msg = format!("xsltproc returned {}", xsltproc.status);
1746 let out = partial_process_output(&xsltproc.stderr).to_string();
1747 return Err(DashMpdError::Io(std::io::Error::other(msg), out));
1748 }
1749 if env::var("DASHMPD_PERSIST_FILES").is_err() {
1750 if let Err(e) = fs::remove_file(&tmpmpd) {
1751 warn!("Error removing temporary MPD after XSLT processing: {e:?}");
1752 }
1753 }
1754 buf.clone_from(&xsltproc.stdout);
1755 if downloader.verbosity > 2 {
1756 println!("Rewritten XSLT: {}", String::from_utf8_lossy(&buf));
1757 }
1758 }
1759 String::from_utf8(buf)
1760 .map_err(|e| parse_error("parsing UTF-8", e))
1761}
1762
1763async fn resolve_xlink_references(
1798 downloader: &DashDownloader,
1799 xot: &mut Xot,
1800 node: xot::Node) -> Result<(), DashMpdError>
1801{
1802 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
1803 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
1804 let xlinked = xot.descendants(node)
1805 .filter(|d| xot.get_attribute(*d, xlink_href_name.into()).is_some())
1806 .collect::<Vec<_>>();
1807 for xl in xlinked {
1808 if element_resolves_to_zero(xot, xl) {
1809 trace!("Removing node with resolve-to-zero xlink:href {xl:?}");
1810 if let Err(e) = xot.remove(xl) {
1811 return Err(parse_error("Failed to remove resolve-to-zero XML node", e));
1812 }
1813 } else if let Some(href) = xot.get_attribute(xl, xlink_href_name.into()) {
1814 if fetchable_xlink_href(href) {
1815 let xlink_url = if is_absolute_url(href) {
1816 Url::parse(href)
1817 .map_err(|e|
1818 if let Ok(ns) = xot.to_string(node) {
1819 parse_error(&format!("parsing XLink on {ns}"), e)
1820 } else {
1821 parse_error("parsing XLink", e)
1822 }
1823 )?
1824 } else {
1825 let mut merged = downloader.redirected_url.join(href)
1828 .map_err(|e|
1829 if let Ok(ns) = xot.to_string(node) {
1830 parse_error(&format!("parsing XLink on {ns}"), e)
1831 } else {
1832 parse_error("parsing XLink", e)
1833 }
1834 )?;
1835 merged.set_query(downloader.redirected_url.query());
1836 merged
1837 };
1838 let client = downloader.http_client.as_ref().unwrap();
1839 trace!("Fetching XLinked element {}", xlink_url.clone());
1840 let mut req = client.get(xlink_url.clone())
1841 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
1842 .header("Accept-Language", "en-US,en")
1843 .header("Sec-Fetch-Mode", "navigate");
1844 if let Some(referer) = &downloader.referer {
1845 req = req.header("Referer", referer);
1846 } else {
1847 req = req.header("Referer", downloader.redirected_url.to_string());
1848 }
1849 if let Some(username) = &downloader.auth_username {
1850 if let Some(password) = &downloader.auth_password {
1851 req = req.basic_auth(username, Some(password));
1852 }
1853 }
1854 if let Some(token) = &downloader.auth_bearer_token {
1855 req = req.bearer_auth(token);
1856 }
1857 let xml = req.send().await
1858 .map_err(|e|
1859 if let Ok(ns) = xot.to_string(node) {
1860 network_error(&format!("fetching XLink for {ns}"), &e)
1861 } else {
1862 network_error("fetching XLink", &e)
1863 }
1864 )?
1865 .error_for_status()
1866 .map_err(|e|
1867 if let Ok(ns) = xot.to_string(node) {
1868 network_error(&format!("fetching XLink for {ns}"), &e)
1869 } else {
1870 network_error("fetching XLink", &e)
1871 }
1872 )?
1873 .text().await
1874 .map_err(|e|
1875 if let Ok(ns) = xot.to_string(node) {
1876 network_error(&format!("resolving XLink for {ns}"), &e)
1877 } else {
1878 network_error("resolving XLink", &e)
1879 }
1880 )?;
1881 if downloader.verbosity > 2 {
1882 if let Ok(ns) = xot.to_string(node) {
1883 info!(" Resolved onLoad XLink {xlink_url} on {ns} -> {} octets", xml.len());
1884 } else {
1885 info!(" Resolved onLoad XLink {xlink_url} -> {} octets", xml.len());
1886 }
1887 }
1888 let wrapped_xml = r#"<?xml version="1.0" encoding="utf-8"?>"#.to_owned() +
1894 r#"<wrapper xmlns="urn:mpeg:dash:schema:mpd:2011" "# +
1895 r#"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" "# +
1896 r#"xmlns:cenc="urn:mpeg:cenc:2013" "# +
1897 r#"xmlns:mspr="urn:microsoft:playready" "# +
1898 r#"xmlns:xlink="http://www.w3.org/1999/xlink">"# +
1899 skip_xml_preamble(&xml) +
1900 r"</wrapper>";
1901 let wrapper_doc = xot.parse(&wrapped_xml)
1902 .map_err(|e| parse_error("parsing xlinked content", e))?;
1903 let wrapper_doc_el = xot.document_element(wrapper_doc)
1904 .map_err(|e| parse_error("extracting XML document element", e))?;
1905 for needs_insertion in xot.children(wrapper_doc_el).collect::<Vec<_>>() {
1906 xot.insert_after(xl, needs_insertion)
1908 .map_err(|e| parse_error("inserting XLinked content", e))?;
1909 }
1910 xot.remove(xl)
1911 .map_err(|e| parse_error("removing XLink node", e))?;
1912 }
1913 }
1914 }
1915 Ok(())
1916}
1917
1918#[tracing::instrument(level="trace", skip_all)]
1919pub async fn parse_resolving_xlinks(
1920 downloader: &DashDownloader,
1921 xml: &[u8]) -> Result<MPD, DashMpdError>
1922{
1923 use xot::xmlname::NameStrInfo;
1924
1925 let mut xot = Xot::new();
1926 let doc = xot.parse_bytes(xml)
1927 .map_err(|e| parse_error("XML parsing", e))?;
1928 let doc_el = xot.document_element(doc)
1929 .map_err(|e| parse_error("extracting XML document element", e))?;
1930 let doc_name = match xot.node_name(doc_el) {
1931 Some(n) => n,
1932 None => return Err(DashMpdError::Parsing(String::from("missing root node name"))),
1933 };
1934 let root_name = xot.name_ref(doc_name, doc_el)
1935 .map_err(|e| parse_error("extracting root node name", e))?;
1936 let root_local_name = root_name.local_name();
1937 if !root_local_name.eq("MPD") {
1938 return Err(DashMpdError::Parsing(format!("root element is {root_local_name}, expecting <MPD>")));
1939 }
1940 for _ in 1..5 {
1943 resolve_xlink_references(downloader, &mut xot, doc).await?;
1944 }
1945 let rewritten = apply_xslt_stylesheets_xsltproc(downloader, &mut xot, doc)?;
1946 let mpd = parse(&rewritten)?;
1948 if downloader.conformity_checks {
1949 for emsg in check_conformity(&mpd) {
1950 warn!("DASH conformity error in manifest: {emsg}");
1951 }
1952 }
1953 Ok(mpd)
1954}
1955
1956async fn do_segmentbase_indexrange(
1957 downloader: &DashDownloader,
1958 period_counter: u8,
1959 base_url: Url,
1960 sb: &SegmentBase,
1961 dict: &HashMap<&str, String>
1962) -> Result<Vec<MediaFragment>, DashMpdError>
1963{
1964 let mut fragments = Vec::new();
1997 let mut start_byte: Option<u64> = None;
1998 let mut end_byte: Option<u64> = None;
1999 let mut indexable_segments = false;
2000 if downloader.use_index_range {
2001 if let Some(ir) = &sb.indexRange {
2002 let (s, e) = parse_range(ir)?;
2004 trace!("Fetching sidx for {}", base_url.clone());
2005 let mut req = downloader.http_client.as_ref()
2006 .unwrap()
2007 .get(base_url.clone())
2008 .header(RANGE, format!("bytes={s}-{e}"))
2009 .header("Referer", downloader.redirected_url.to_string())
2010 .header("Sec-Fetch-Mode", "navigate");
2011 if let Some(username) = &downloader.auth_username {
2012 if let Some(password) = &downloader.auth_password {
2013 req = req.basic_auth(username, Some(password));
2014 }
2015 }
2016 if let Some(token) = &downloader.auth_bearer_token {
2017 req = req.bearer_auth(token);
2018 }
2019 let mut resp = req.send().await
2020 .map_err(|e| network_error("fetching index data", &e))?
2021 .error_for_status()
2022 .map_err(|e| network_error("fetching index data", &e))?;
2023 let headers = std::mem::take(resp.headers_mut());
2024 if let Some(content_type) = headers.get(CONTENT_TYPE) {
2025 let idx = resp.bytes().await
2026 .map_err(|e| network_error("fetching index data", &e))?;
2027 if idx.len() as u64 != e - s + 1 {
2028 warn!(" HTTP server does not support Range requests; can't use indexRange addressing");
2029 } else {
2030 #[allow(clippy::collapsible_else_if)]
2031 if content_type.eq("video/mp4") ||
2032 content_type.eq("audio/mp4") {
2033 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2040 .with_range(Some(0), Some(e))
2041 .build();
2042 fragments.push(mf);
2043 let mut max_chunk_pos = 0;
2044 if let Ok(segment_chunks) = crate::sidx::from_isobmff_sidx(&idx, e+1) {
2045 trace!("Have {} segment chunks in sidx data", segment_chunks.len());
2046 for chunk in segment_chunks {
2047 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2048 .with_range(Some(chunk.start), Some(chunk.end))
2049 .build();
2050 fragments.push(mf);
2051 if chunk.end > max_chunk_pos {
2052 max_chunk_pos = chunk.end;
2053 }
2054 }
2055 indexable_segments = true;
2056 }
2057 }
2058 }
2065 }
2066 }
2067 }
2068 if indexable_segments {
2069 if let Some(init) = &sb.Initialization {
2070 if let Some(range) = &init.range {
2071 let (s, e) = parse_range(range)?;
2072 start_byte = Some(s);
2073 end_byte = Some(e);
2074 }
2075 if let Some(su) = &init.sourceURL {
2076 let path = resolve_url_template(su, dict);
2077 let u = merge_baseurls(&base_url, &path)?;
2078 let mf = MediaFragmentBuilder::new(period_counter, u)
2079 .with_range(start_byte, end_byte)
2080 .set_init()
2081 .build();
2082 fragments.push(mf);
2083 } else {
2084 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2086 .with_range(start_byte, end_byte)
2087 .set_init()
2088 .build();
2089 fragments.push(mf);
2090 }
2091 }
2092 } else {
2093 trace!("Falling back to retrieving full SegmentBase for {}", base_url.clone());
2098 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2099 .with_timeout(Duration::new(10_000, 0))
2100 .build();
2101 fragments.push(mf);
2102 }
2103 Ok(fragments)
2104}
2105
2106
2107#[tracing::instrument(level="trace", skip_all)]
2108async fn do_period_audio(
2109 downloader: &DashDownloader,
2110 mpd: &MPD,
2111 period: &Period,
2112 period_counter: u8,
2113 base_url: Url
2114 ) -> Result<PeriodOutputs, DashMpdError>
2115{
2116 let mut fragments = Vec::new();
2117 let mut diagnostics = Vec::new();
2118 let mut opt_init: Option<String> = None;
2119 let mut opt_media: Option<String> = None;
2120 let mut opt_duration: Option<f64> = None;
2121 let mut timescale = 1;
2122 let mut start_number = 1;
2123 let mut period_duration_secs: f64 = 0.0;
2126 if let Some(d) = mpd.mediaPresentationDuration {
2127 period_duration_secs = d.as_secs_f64();
2128 }
2129 if let Some(d) = period.duration {
2130 period_duration_secs = d.as_secs_f64();
2131 }
2132 if let Some(s) = downloader.force_duration {
2133 period_duration_secs = s;
2134 }
2135 if let Some(st) = &period.SegmentTemplate {
2139 if let Some(i) = &st.initialization {
2140 opt_init = Some(i.clone());
2141 }
2142 if let Some(m) = &st.media {
2143 opt_media = Some(m.clone());
2144 }
2145 if let Some(d) = st.duration {
2146 opt_duration = Some(d);
2147 }
2148 if let Some(ts) = st.timescale {
2149 timescale = ts;
2150 }
2151 if let Some(s) = st.startNumber {
2152 start_number = s;
2153 }
2154 }
2155 let mut selected_audio_language = "unk";
2156 let audio_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2159 .filter(is_audio_adaptation)
2160 .collect();
2161 let representations: Vec<&Representation> = select_preferred_adaptations(audio_adaptations, downloader)
2162 .iter()
2163 .flat_map(|a| a.representations.iter())
2164 .collect();
2165 if let Some(audio_repr) = select_preferred_representation(&representations, downloader) {
2166 let audio_adaptation = period.adaptations.iter()
2170 .find(|a| a.representations.iter().any(|r| r.eq(audio_repr)))
2171 .unwrap();
2172 if let Some(lang) = audio_repr.lang.as_ref().or(audio_adaptation.lang.as_ref()) {
2173 selected_audio_language = lang;
2174 }
2175 let mut base_url = base_url.clone();
2178 if let Some(bu) = &audio_adaptation.BaseURL.first() {
2179 base_url = merge_baseurls(&base_url, &bu.base)?;
2180 }
2181 if let Some(bu) = audio_repr.BaseURL.first() {
2182 base_url = merge_baseurls(&base_url, &bu.base)?;
2183 }
2184 if downloader.verbosity > 0 {
2185 let bw = if let Some(bw) = audio_repr.bandwidth {
2186 format!("bw={} Kbps ", bw / 1024)
2187 } else {
2188 String::from("")
2189 };
2190 let unknown = String::from("?");
2191 let lang = audio_repr.lang.as_ref()
2192 .unwrap_or(audio_adaptation.lang.as_ref()
2193 .unwrap_or(&unknown));
2194 let codec = audio_repr.codecs.as_ref()
2195 .unwrap_or(audio_adaptation.codecs.as_ref()
2196 .unwrap_or(&unknown));
2197 diagnostics.push(format!(" Audio stream selected: {bw}lang={lang} codec={codec}"));
2198 for cp in audio_repr.ContentProtection.iter()
2200 .chain(audio_adaptation.ContentProtection.iter())
2201 {
2202 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2203 if let Some(kid) = &cp.default_KID {
2204 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2205 }
2206 for pssh_element in &cp.cenc_pssh {
2207 if let Some(pssh_b64) = &pssh_element.content {
2208 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
2209 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
2210 diagnostics.push(format!(" {pssh}"));
2211 }
2212 }
2213 }
2214 }
2215 }
2216 if let Some(st) = &audio_adaptation.SegmentTemplate {
2221 if let Some(i) = &st.initialization {
2222 opt_init = Some(i.clone());
2223 }
2224 if let Some(m) = &st.media {
2225 opt_media = Some(m.clone());
2226 }
2227 if let Some(d) = st.duration {
2228 opt_duration = Some(d);
2229 }
2230 if let Some(ts) = st.timescale {
2231 timescale = ts;
2232 }
2233 if let Some(s) = st.startNumber {
2234 start_number = s;
2235 }
2236 }
2237 let mut dict = HashMap::new();
2238 if let Some(rid) = &audio_repr.id {
2239 dict.insert("RepresentationID", rid.clone());
2240 }
2241 if let Some(b) = &audio_repr.bandwidth {
2242 dict.insert("Bandwidth", b.to_string());
2243 }
2244 if let Some(sl) = &audio_adaptation.SegmentList {
2253 if downloader.verbosity > 1 {
2256 info!(" Using AdaptationSet>SegmentList addressing mode for audio representation");
2257 }
2258 let mut start_byte: Option<u64> = None;
2259 let mut end_byte: Option<u64> = None;
2260 if let Some(init) = &sl.Initialization {
2261 if let Some(range) = &init.range {
2262 let (s, e) = parse_range(range)?;
2263 start_byte = Some(s);
2264 end_byte = Some(e);
2265 }
2266 if let Some(su) = &init.sourceURL {
2267 let path = resolve_url_template(su, &dict);
2268 let init_url = merge_baseurls(&base_url, &path)?;
2269 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2270 .with_range(start_byte, end_byte)
2271 .set_init()
2272 .build();
2273 fragments.push(mf);
2274 } else {
2275 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2276 .with_range(start_byte, end_byte)
2277 .set_init()
2278 .build();
2279 fragments.push(mf);
2280 }
2281 }
2282 for su in &sl.segment_urls {
2283 start_byte = None;
2284 end_byte = None;
2285 if let Some(range) = &su.mediaRange {
2287 let (s, e) = parse_range(range)?;
2288 start_byte = Some(s);
2289 end_byte = Some(e);
2290 }
2291 if let Some(m) = &su.media {
2292 let u = merge_baseurls(&base_url, m)?;
2293 let mf = MediaFragmentBuilder::new(period_counter, u)
2294 .with_range(start_byte, end_byte)
2295 .build();
2296 fragments.push(mf);
2297 } else if let Some(bu) = audio_adaptation.BaseURL.first() {
2298 let u = merge_baseurls(&base_url, &bu.base)?;
2299 let mf = MediaFragmentBuilder::new(period_counter, u)
2300 .with_range(start_byte, end_byte)
2301 .build();
2302 fragments.push(mf);
2303 }
2304 }
2305 }
2306 if let Some(sl) = &audio_repr.SegmentList {
2307 if downloader.verbosity > 1 {
2309 info!(" Using Representation>SegmentList addressing mode for audio representation");
2310 }
2311 let mut start_byte: Option<u64> = None;
2312 let mut end_byte: Option<u64> = None;
2313 if let Some(init) = &sl.Initialization {
2314 if let Some(range) = &init.range {
2315 let (s, e) = parse_range(range)?;
2316 start_byte = Some(s);
2317 end_byte = Some(e);
2318 }
2319 if let Some(su) = &init.sourceURL {
2320 let path = resolve_url_template(su, &dict);
2321 let init_url = merge_baseurls(&base_url, &path)?;
2322 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2323 .with_range(start_byte, end_byte)
2324 .set_init()
2325 .build();
2326 fragments.push(mf);
2327 } else {
2328 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2329 .with_range(start_byte, end_byte)
2330 .set_init()
2331 .build();
2332 fragments.push(mf);
2333 }
2334 }
2335 for su in &sl.segment_urls {
2336 start_byte = None;
2337 end_byte = None;
2338 if let Some(range) = &su.mediaRange {
2340 let (s, e) = parse_range(range)?;
2341 start_byte = Some(s);
2342 end_byte = Some(e);
2343 }
2344 if let Some(m) = &su.media {
2345 let u = merge_baseurls(&base_url, m)?;
2346 let mf = MediaFragmentBuilder::new(period_counter, u)
2347 .with_range(start_byte, end_byte)
2348 .build();
2349 fragments.push(mf);
2350 } else if let Some(bu) = audio_repr.BaseURL.first() {
2351 let u = merge_baseurls(&base_url, &bu.base)?;
2352 let mf = MediaFragmentBuilder::new(period_counter, u)
2353 .with_range(start_byte, end_byte)
2354 .build();
2355 fragments.push(mf);
2356 }
2357 }
2358 } else if audio_repr.SegmentTemplate.is_some() ||
2359 audio_adaptation.SegmentTemplate.is_some()
2360 {
2361 let st;
2364 if let Some(it) = &audio_repr.SegmentTemplate {
2365 st = it;
2366 } else if let Some(it) = &audio_adaptation.SegmentTemplate {
2367 st = it;
2368 } else {
2369 panic!("unreachable");
2370 }
2371 if let Some(i) = &st.initialization {
2372 opt_init = Some(i.clone());
2373 }
2374 if let Some(m) = &st.media {
2375 opt_media = Some(m.clone());
2376 }
2377 if let Some(ts) = st.timescale {
2378 timescale = ts;
2379 }
2380 if let Some(sn) = st.startNumber {
2381 start_number = sn;
2382 }
2383 if let Some(stl) = &audio_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
2384 .or(audio_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
2385 {
2386 if downloader.verbosity > 1 {
2389 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for audio representation");
2390 }
2391 if let Some(init) = opt_init {
2392 let path = resolve_url_template(&init, &dict);
2393 let u = merge_baseurls(&base_url, &path)?;
2394 let mf = MediaFragmentBuilder::new(period_counter, u)
2395 .set_init()
2396 .build();
2397 fragments.push(mf);
2398 }
2399 if let Some(media) = opt_media {
2400 let audio_path = resolve_url_template(&media, &dict);
2401 let mut segment_time = 0;
2402 let mut segment_duration;
2403 let mut number = start_number;
2404 for s in &stl.segments {
2405 if let Some(t) = s.t {
2406 segment_time = t;
2407 }
2408 segment_duration = s.d;
2409 let dict = HashMap::from([("Time", segment_time.to_string()),
2411 ("Number", number.to_string())]);
2412 let path = resolve_url_template(&audio_path, &dict);
2413 let u = merge_baseurls(&base_url, &path)?;
2414 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2415 number += 1;
2416 if let Some(r) = s.r {
2417 let mut count = 0i64;
2418 let end_time = period_duration_secs * timescale as f64;
2420 loop {
2421 count += 1;
2422 if r >= 0 {
2428 if count > r {
2429 break;
2430 }
2431 if downloader.force_duration.is_some() && segment_time as f64 > end_time {
2432 break;
2433 }
2434 } else if segment_time as f64 > end_time {
2435 break;
2436 }
2437 segment_time += segment_duration;
2438 let dict = HashMap::from([("Time", segment_time.to_string()),
2439 ("Number", number.to_string())]);
2440 let path = resolve_url_template(&audio_path, &dict);
2441 let u = merge_baseurls(&base_url, &path)?;
2442 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2443 number += 1;
2444 }
2445 }
2446 segment_time += segment_duration;
2447 }
2448 } else {
2449 return Err(DashMpdError::UnhandledMediaStream(
2450 "SegmentTimeline without a media attribute".to_string()));
2451 }
2452 } else { if downloader.verbosity > 1 {
2457 info!(" Using SegmentTemplate addressing mode for audio representation");
2458 }
2459 let mut total_number = 0i64;
2460 if let Some(init) = opt_init {
2461 let path = resolve_url_template(&init, &dict);
2462 let u = merge_baseurls(&base_url, &path)?;
2463 let mf = MediaFragmentBuilder::new(period_counter, u)
2464 .set_init()
2465 .build();
2466 fragments.push(mf);
2467 }
2468 if let Some(media) = opt_media {
2469 let audio_path = resolve_url_template(&media, &dict);
2470 let timescale = st.timescale.unwrap_or(timescale);
2471 let mut segment_duration: f64 = -1.0;
2472 if let Some(d) = opt_duration {
2473 segment_duration = d;
2475 }
2476 if let Some(std) = st.duration {
2477 if timescale == 0 {
2478 return Err(DashMpdError::UnhandledMediaStream(
2479 "SegmentTemplate@duration attribute cannot be zero".to_string()));
2480 }
2481 segment_duration = std / timescale as f64;
2482 }
2483 if segment_duration < 0.0 {
2484 return Err(DashMpdError::UnhandledMediaStream(
2485 "Audio representation is missing SegmentTemplate@duration attribute".to_string()));
2486 }
2487 total_number += (period_duration_secs / segment_duration).round() as i64;
2488 let mut number = start_number;
2489 if mpd_is_dynamic(mpd) {
2492 if let Some(start_time) = mpd.availabilityStartTime {
2493 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
2494 number = (elapsed + number as f64 - 1f64).floor() as u64;
2495 } else {
2496 return Err(DashMpdError::UnhandledMediaStream(
2497 "dynamic manifest is missing @availabilityStartTime".to_string()));
2498 }
2499 }
2500 for _ in 1..=total_number {
2501 let dict = HashMap::from([("Number", number.to_string())]);
2502 let path = resolve_url_template(&audio_path, &dict);
2503 let u = merge_baseurls(&base_url, &path)?;
2504 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2505 number += 1;
2506 }
2507 }
2508 }
2509 } else if let Some(sb) = &audio_repr.SegmentBase {
2510 if downloader.verbosity > 1 {
2512 info!(" Using SegmentBase@indexRange addressing mode for audio representation");
2513 }
2514 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
2515 fragments.extend(mf);
2516 } else if fragments.is_empty() {
2517 if let Some(bu) = audio_repr.BaseURL.first() {
2518 if downloader.verbosity > 1 {
2520 info!(" Using BaseURL addressing mode for audio representation");
2521 }
2522 let u = merge_baseurls(&base_url, &bu.base)?;
2523 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2524 }
2525 }
2526 if fragments.is_empty() {
2527 return Err(DashMpdError::UnhandledMediaStream(
2528 "no usable addressing mode identified for audio representation".to_string()));
2529 }
2530 }
2531 Ok(PeriodOutputs {
2532 fragments, diagnostics, subtitle_formats: Vec::new(),
2533 selected_audio_language: String::from(selected_audio_language)
2534 })
2535}
2536
2537
2538#[tracing::instrument(level="trace", skip_all)]
2539async fn do_period_video(
2540 downloader: &DashDownloader,
2541 mpd: &MPD,
2542 period: &Period,
2543 period_counter: u8,
2544 base_url: Url
2545 ) -> Result<PeriodOutputs, DashMpdError>
2546{
2547 let mut fragments = Vec::new();
2548 let mut diagnostics = Vec::new();
2549 let mut period_duration_secs: f64 = 0.0;
2550 let mut opt_init: Option<String> = None;
2551 let mut opt_media: Option<String> = None;
2552 let mut opt_duration: Option<f64> = None;
2553 let mut timescale = 1;
2554 let mut start_number = 1;
2555 if let Some(d) = mpd.mediaPresentationDuration {
2556 period_duration_secs = d.as_secs_f64();
2557 }
2558 if let Some(d) = period.duration {
2559 period_duration_secs = d.as_secs_f64();
2560 }
2561 if let Some(s) = downloader.force_duration {
2562 period_duration_secs = s;
2563 }
2564 if let Some(st) = &period.SegmentTemplate {
2568 if let Some(i) = &st.initialization {
2569 opt_init = Some(i.clone());
2570 }
2571 if let Some(m) = &st.media {
2572 opt_media = Some(m.clone());
2573 }
2574 if let Some(d) = st.duration {
2575 opt_duration = Some(d);
2576 }
2577 if let Some(ts) = st.timescale {
2578 timescale = ts;
2579 }
2580 if let Some(s) = st.startNumber {
2581 start_number = s;
2582 }
2583 }
2584 let video_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2591 .filter(is_video_adaptation)
2592 .collect();
2593 let representations: Vec<&Representation> = select_preferred_adaptations(video_adaptations, downloader)
2594 .iter()
2595 .flat_map(|a| a.representations.iter())
2596 .collect();
2597 let maybe_video_repr = if let Some(want) = downloader.video_width_preference {
2598 representations.iter()
2599 .min_by_key(|x| if let Some(w) = x.width { want.abs_diff(w) } else { u64::MAX })
2600 .copied()
2601 } else if let Some(want) = downloader.video_height_preference {
2602 representations.iter()
2603 .min_by_key(|x| if let Some(h) = x.height { want.abs_diff(h) } else { u64::MAX })
2604 .copied()
2605 } else {
2606 select_preferred_representation(&representations, downloader)
2607 };
2608 if let Some(video_repr) = maybe_video_repr {
2609 let video_adaptation = period.adaptations.iter()
2613 .find(|a| a.representations.iter().any(|r| r.eq(video_repr)))
2614 .unwrap();
2615 let mut base_url = base_url.clone();
2618 if let Some(bu) = &video_adaptation.BaseURL.first() {
2619 base_url = merge_baseurls(&base_url, &bu.base)?;
2620 }
2621 if let Some(bu) = &video_repr.BaseURL.first() {
2622 base_url = merge_baseurls(&base_url, &bu.base)?;
2623 }
2624 if downloader.verbosity > 0 {
2625 let bw = if let Some(bw) = video_repr.bandwidth.or(video_adaptation.maxBandwidth) {
2626 format!("bw={} Kbps ", bw / 1024)
2627 } else {
2628 String::from("")
2629 };
2630 let unknown = String::from("?");
2631 let w = video_repr.width.unwrap_or(video_adaptation.width.unwrap_or(0));
2632 let h = video_repr.height.unwrap_or(video_adaptation.height.unwrap_or(0));
2633 let fmt = if w == 0 || h == 0 {
2634 String::from("")
2635 } else {
2636 format!("resolution={w}x{h} ")
2637 };
2638 let codec = video_repr.codecs.as_ref()
2639 .unwrap_or(video_adaptation.codecs.as_ref().unwrap_or(&unknown));
2640 diagnostics.push(format!(" Video stream selected: {bw}{fmt}codec={codec}"));
2641 for cp in video_repr.ContentProtection.iter()
2643 .chain(video_adaptation.ContentProtection.iter())
2644 {
2645 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2646 if let Some(kid) = &cp.default_KID {
2647 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2648 }
2649 for pssh_element in &cp.cenc_pssh {
2650 if let Some(pssh_b64) = &pssh_element.content {
2651 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
2652 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
2653 diagnostics.push(format!(" {pssh}"));
2654 }
2655 }
2656 }
2657 }
2658 }
2659 let mut dict = HashMap::new();
2660 if let Some(rid) = &video_repr.id {
2661 dict.insert("RepresentationID", rid.clone());
2662 }
2663 if let Some(b) = &video_repr.bandwidth {
2664 dict.insert("Bandwidth", b.to_string());
2665 }
2666 if let Some(st) = &video_adaptation.SegmentTemplate {
2671 if let Some(i) = &st.initialization {
2672 opt_init = Some(i.clone());
2673 }
2674 if let Some(m) = &st.media {
2675 opt_media = Some(m.clone());
2676 }
2677 if let Some(d) = st.duration {
2678 opt_duration = Some(d);
2679 }
2680 if let Some(ts) = st.timescale {
2681 timescale = ts;
2682 }
2683 if let Some(s) = st.startNumber {
2684 start_number = s;
2685 }
2686 }
2687 if let Some(sl) = &video_adaptation.SegmentList {
2691 if downloader.verbosity > 1 {
2693 info!(" Using AdaptationSet>SegmentList addressing mode for video representation");
2694 }
2695 let mut start_byte: Option<u64> = None;
2696 let mut end_byte: Option<u64> = None;
2697 if let Some(init) = &sl.Initialization {
2698 if let Some(range) = &init.range {
2699 let (s, e) = parse_range(range)?;
2700 start_byte = Some(s);
2701 end_byte = Some(e);
2702 }
2703 if let Some(su) = &init.sourceURL {
2704 let path = resolve_url_template(su, &dict);
2705 let u = merge_baseurls(&base_url, &path)?;
2706 let mf = MediaFragmentBuilder::new(period_counter, u)
2707 .with_range(start_byte, end_byte)
2708 .set_init()
2709 .build();
2710 fragments.push(mf);
2711 }
2712 } else {
2713 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2714 .with_range(start_byte, end_byte)
2715 .set_init()
2716 .build();
2717 fragments.push(mf);
2718 }
2719 for su in &sl.segment_urls {
2720 start_byte = None;
2721 end_byte = None;
2722 if let Some(range) = &su.mediaRange {
2724 let (s, e) = parse_range(range)?;
2725 start_byte = Some(s);
2726 end_byte = Some(e);
2727 }
2728 if let Some(m) = &su.media {
2729 let u = merge_baseurls(&base_url, m)?;
2730 let mf = MediaFragmentBuilder::new(period_counter, u)
2731 .with_range(start_byte, end_byte)
2732 .build();
2733 fragments.push(mf);
2734 } else if let Some(bu) = video_adaptation.BaseURL.first() {
2735 let u = merge_baseurls(&base_url, &bu.base)?;
2736 let mf = MediaFragmentBuilder::new(period_counter, u)
2737 .with_range(start_byte, end_byte)
2738 .build();
2739 fragments.push(mf);
2740 }
2741 }
2742 }
2743 if let Some(sl) = &video_repr.SegmentList {
2744 if downloader.verbosity > 1 {
2746 info!(" Using Representation>SegmentList addressing mode for video representation");
2747 }
2748 let mut start_byte: Option<u64> = None;
2749 let mut end_byte: Option<u64> = None;
2750 if let Some(init) = &sl.Initialization {
2751 if let Some(range) = &init.range {
2752 let (s, e) = parse_range(range)?;
2753 start_byte = Some(s);
2754 end_byte = Some(e);
2755 }
2756 if let Some(su) = &init.sourceURL {
2757 let path = resolve_url_template(su, &dict);
2758 let u = merge_baseurls(&base_url, &path)?;
2759 let mf = MediaFragmentBuilder::new(period_counter, u)
2760 .with_range(start_byte, end_byte)
2761 .set_init()
2762 .build();
2763 fragments.push(mf);
2764 } else {
2765 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2766 .with_range(start_byte, end_byte)
2767 .set_init()
2768 .build();
2769 fragments.push(mf);
2770 }
2771 }
2772 for su in sl.segment_urls.iter() {
2773 start_byte = None;
2774 end_byte = None;
2775 if let Some(range) = &su.mediaRange {
2777 let (s, e) = parse_range(range)?;
2778 start_byte = Some(s);
2779 end_byte = Some(e);
2780 }
2781 if let Some(m) = &su.media {
2782 let u = merge_baseurls(&base_url, m)?;
2783 let mf = MediaFragmentBuilder::new(period_counter, u)
2784 .with_range(start_byte, end_byte)
2785 .build();
2786 fragments.push(mf);
2787 } else if let Some(bu) = video_repr.BaseURL.first() {
2788 let u = merge_baseurls(&base_url, &bu.base)?;
2789 let mf = MediaFragmentBuilder::new(period_counter, u)
2790 .with_range(start_byte, end_byte)
2791 .build();
2792 fragments.push(mf);
2793 }
2794 }
2795 } else if video_repr.SegmentTemplate.is_some() ||
2796 video_adaptation.SegmentTemplate.is_some() {
2797 let st;
2800 if let Some(it) = &video_repr.SegmentTemplate {
2801 st = it;
2802 } else if let Some(it) = &video_adaptation.SegmentTemplate {
2803 st = it;
2804 } else {
2805 panic!("impossible");
2806 }
2807 if let Some(i) = &st.initialization {
2808 opt_init = Some(i.clone());
2809 }
2810 if let Some(m) = &st.media {
2811 opt_media = Some(m.clone());
2812 }
2813 if let Some(ts) = st.timescale {
2814 timescale = ts;
2815 }
2816 if let Some(sn) = st.startNumber {
2817 start_number = sn;
2818 }
2819 if let Some(stl) = &video_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
2820 .or(video_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
2821 {
2822 if downloader.verbosity > 1 {
2824 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for video representation");
2825 }
2826 if let Some(init) = opt_init {
2827 let path = resolve_url_template(&init, &dict);
2828 let u = merge_baseurls(&base_url, &path)?;
2829 let mf = MediaFragmentBuilder::new(period_counter, u)
2830 .set_init()
2831 .build();
2832 fragments.push(mf);
2833 }
2834 if let Some(media) = opt_media {
2835 let video_path = resolve_url_template(&media, &dict);
2836 let mut segment_time = 0;
2837 let mut segment_duration;
2838 let mut number = start_number;
2839 for s in &stl.segments {
2840 if let Some(t) = s.t {
2841 segment_time = t;
2842 }
2843 segment_duration = s.d;
2844 let dict = HashMap::from([("Time", segment_time.to_string()),
2846 ("Number", number.to_string())]);
2847 let path = resolve_url_template(&video_path, &dict);
2848 let u = merge_baseurls(&base_url, &path)?;
2849 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2850 fragments.push(mf);
2851 number += 1;
2852 if let Some(r) = s.r {
2853 let mut count = 0i64;
2854 let end_time = period_duration_secs * timescale as f64;
2856 loop {
2857 count += 1;
2858 if r >= 0 {
2864 if count > r {
2865 break;
2866 }
2867 if downloader.force_duration.is_some() && segment_time as f64 > end_time {
2868 break;
2869 }
2870 } else if segment_time as f64 > end_time {
2871 break;
2872 }
2873 segment_time += segment_duration;
2874 let dict = HashMap::from([("Time", segment_time.to_string()),
2875 ("Number", number.to_string())]);
2876 let path = resolve_url_template(&video_path, &dict);
2877 let u = merge_baseurls(&base_url, &path)?;
2878 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2879 fragments.push(mf);
2880 number += 1;
2881 }
2882 }
2883 segment_time += segment_duration;
2884 }
2885 } else {
2886 return Err(DashMpdError::UnhandledMediaStream(
2887 "SegmentTimeline without a media attribute".to_string()));
2888 }
2889 } else { if downloader.verbosity > 1 {
2892 info!(" Using SegmentTemplate addressing mode for video representation");
2893 }
2894 let mut total_number = 0i64;
2895 if let Some(init) = opt_init {
2896 let path = resolve_url_template(&init, &dict);
2897 let u = merge_baseurls(&base_url, &path)?;
2898 let mf = MediaFragmentBuilder::new(period_counter, u)
2899 .set_init()
2900 .build();
2901 fragments.push(mf);
2902 }
2903 if let Some(media) = opt_media {
2904 let video_path = resolve_url_template(&media, &dict);
2905 let timescale = st.timescale.unwrap_or(timescale);
2906 let mut segment_duration: f64 = -1.0;
2907 if let Some(d) = opt_duration {
2908 segment_duration = d;
2910 }
2911 if let Some(std) = st.duration {
2912 if timescale == 0 {
2913 return Err(DashMpdError::UnhandledMediaStream(
2914 "SegmentTemplate@duration attribute cannot be zero".to_string()));
2915 }
2916 segment_duration = std / timescale as f64;
2917 }
2918 if segment_duration < 0.0 {
2919 return Err(DashMpdError::UnhandledMediaStream(
2920 "Video representation is missing SegmentTemplate@duration attribute".to_string()));
2921 }
2922 total_number += (period_duration_secs / segment_duration).round() as i64;
2923 let mut number = start_number;
2924 if mpd_is_dynamic(mpd) {
2934 if let Some(start_time) = mpd.availabilityStartTime {
2935 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
2936 number = (elapsed + number as f64 - 1f64).floor() as u64;
2937 } else {
2938 return Err(DashMpdError::UnhandledMediaStream(
2939 "dynamic manifest is missing @availabilityStartTime".to_string()));
2940 }
2941 }
2942 for _ in 1..=total_number {
2943 let dict = HashMap::from([("Number", number.to_string())]);
2944 let path = resolve_url_template(&video_path, &dict);
2945 let u = merge_baseurls(&base_url, &path)?;
2946 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2947 fragments.push(mf);
2948 number += 1;
2949 }
2950 }
2951 }
2952 } else if let Some(sb) = &video_repr.SegmentBase {
2953 if downloader.verbosity > 1 {
2955 info!(" Using SegmentBase@indexRange addressing mode for video representation");
2956 }
2957 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
2958 fragments.extend(mf);
2959 } else if fragments.is_empty() {
2960 if let Some(bu) = video_repr.BaseURL.first() {
2961 if downloader.verbosity > 1 {
2963 info!(" Using BaseURL addressing mode for video representation");
2964 }
2965 let u = merge_baseurls(&base_url, &bu.base)?;
2966 let mf = MediaFragmentBuilder::new(period_counter, u)
2967 .with_timeout(Duration::new(10000, 0))
2968 .build();
2969 fragments.push(mf);
2970 }
2971 }
2972 if fragments.is_empty() {
2973 return Err(DashMpdError::UnhandledMediaStream(
2974 "no usable addressing mode identified for video representation".to_string()));
2975 }
2976 }
2977 Ok(PeriodOutputs {
2980 fragments,
2981 diagnostics,
2982 subtitle_formats: Vec::new(),
2983 selected_audio_language: String::from("unk")
2984 })
2985}
2986
2987#[tracing::instrument(level="trace", skip_all)]
2988async fn do_period_subtitles(
2989 downloader: &DashDownloader,
2990 mpd: &MPD,
2991 period: &Period,
2992 period_counter: u8,
2993 base_url: Url
2994 ) -> Result<PeriodOutputs, DashMpdError>
2995{
2996 let client = downloader.http_client.as_ref().unwrap();
2997 let output_path = &downloader.output_path.as_ref().unwrap().clone();
2998 let period_output_path = output_path_for_period(output_path, period_counter);
2999 let mut fragments = Vec::new();
3000 let mut subtitle_formats = Vec::new();
3001 let mut period_duration_secs: f64 = 0.0;
3002 if let Some(d) = mpd.mediaPresentationDuration {
3003 period_duration_secs = d.as_secs_f64();
3004 }
3005 if let Some(d) = period.duration {
3006 period_duration_secs = d.as_secs_f64();
3007 }
3008 let maybe_subtitle_adaptation = if let Some(ref lang) = downloader.language_preference {
3009 period.adaptations.iter().filter(is_subtitle_adaptation)
3010 .min_by_key(|a| adaptation_lang_distance(a, lang))
3011 } else {
3012 period.adaptations.iter().find(is_subtitle_adaptation)
3014 };
3015 if downloader.fetch_subtitles {
3016 if let Some(subtitle_adaptation) = maybe_subtitle_adaptation {
3017 let subtitle_format = subtitle_type(&subtitle_adaptation);
3018 subtitle_formats.push(subtitle_format);
3019 if downloader.verbosity > 1 && downloader.fetch_subtitles {
3020 info!(" Retrieving subtitles in format {subtitle_format:?}");
3021 }
3022 let mut base_url = base_url.clone();
3025 if let Some(bu) = &subtitle_adaptation.BaseURL.first() {
3026 base_url = merge_baseurls(&base_url, &bu.base)?;
3027 }
3028 if let Some(rep) = subtitle_adaptation.representations.first() {
3031 if !rep.BaseURL.is_empty() {
3032 for st_bu in &rep.BaseURL {
3033 let st_url = merge_baseurls(&base_url, &st_bu.base)?;
3034 let mut req = client.get(st_url.clone());
3035 if let Some(referer) = &downloader.referer {
3036 req = req.header("Referer", referer);
3037 } else {
3038 req = req.header("Referer", base_url.to_string());
3039 }
3040 let rqw = req.build()
3041 .map_err(|e| network_error("building request", &e))?;
3042 let subs = reqwest_bytes_with_retries(client, rqw, 5).await
3043 .map_err(|e| network_error("fetching subtitles", &e))?;
3044 let mut subs_path = period_output_path.clone();
3045 let subtitle_format = subtitle_type(&subtitle_adaptation);
3046 match subtitle_format {
3047 SubtitleType::Vtt => subs_path.set_extension("vtt"),
3048 SubtitleType::Srt => subs_path.set_extension("srt"),
3049 SubtitleType::Ttml => subs_path.set_extension("ttml"),
3050 SubtitleType::Sami => subs_path.set_extension("sami"),
3051 SubtitleType::Wvtt => subs_path.set_extension("wvtt"),
3052 SubtitleType::Stpp => subs_path.set_extension("stpp"),
3053 _ => subs_path.set_extension("sub"),
3054 };
3055 subtitle_formats.push(subtitle_format);
3056 let mut subs_file = File::create(subs_path.clone())
3057 .map_err(|e| DashMpdError::Io(e, String::from("creating subtitle file")))?;
3058 if downloader.verbosity > 2 {
3059 info!(" Subtitle {st_url} -> {} octets", subs.len());
3060 }
3061 match subs_file.write_all(&subs) {
3062 Ok(()) => {
3063 if downloader.verbosity > 0 {
3064 info!(" Downloaded subtitles ({subtitle_format:?}) to {}",
3065 subs_path.display());
3066 }
3067 },
3068 Err(e) => {
3069 error!("Unable to write subtitle file: {e:?}");
3070 return Err(DashMpdError::Io(e, String::from("writing subtitle data")));
3071 },
3072 }
3073 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
3074 subtitle_formats.contains(&SubtitleType::Ttxt)
3075 {
3076 if downloader.verbosity > 0 {
3077 info!(" Converting subtitles to SRT format with MP4Box ");
3078 }
3079 let out = subs_path.with_extension("srt");
3080 let out_str = out.to_string_lossy();
3087 let subs_str = subs_path.to_string_lossy();
3088 let args = vec![
3089 "-srt", "1",
3090 "-out", &out_str,
3091 &subs_str];
3092 if downloader.verbosity > 0 {
3093 info!(" Running MPBox {}", args.join(" "));
3094 }
3095 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
3096 .args(args)
3097 .output()
3098 {
3099 let msg = partial_process_output(&mp4box.stdout);
3100 if !msg.is_empty() {
3101 info!("MP4Box stdout: {msg}");
3102 }
3103 let msg = partial_process_output(&mp4box.stderr);
3104 if !msg.is_empty() {
3105 info!("MP4Box stderr: {msg}");
3106 }
3107 if mp4box.status.success() {
3108 info!(" Converted subtitles to SRT");
3109 } else {
3110 warn!("Error running MP4Box to convert subtitles");
3111 }
3112 }
3113 }
3114 }
3115 } else if rep.SegmentTemplate.is_some() || subtitle_adaptation.SegmentTemplate.is_some() {
3116 let mut opt_init: Option<String> = None;
3117 let mut opt_media: Option<String> = None;
3118 let mut opt_duration: Option<f64> = None;
3119 let mut timescale = 1;
3120 let mut start_number = 1;
3121 if let Some(st) = &rep.SegmentTemplate {
3126 if let Some(i) = &st.initialization {
3127 opt_init = Some(i.clone());
3128 }
3129 if let Some(m) = &st.media {
3130 opt_media = Some(m.clone());
3131 }
3132 if let Some(d) = st.duration {
3133 opt_duration = Some(d);
3134 }
3135 if let Some(ts) = st.timescale {
3136 timescale = ts;
3137 }
3138 if let Some(s) = st.startNumber {
3139 start_number = s;
3140 }
3141 }
3142 let rid = match &rep.id {
3143 Some(id) => id,
3144 None => return Err(
3145 DashMpdError::UnhandledMediaStream(
3146 "Missing @id on Representation node".to_string())),
3147 };
3148 let mut dict = HashMap::from([("RepresentationID", rid.clone())]);
3149 if let Some(b) = &rep.bandwidth {
3150 dict.insert("Bandwidth", b.to_string());
3151 }
3152 if let Some(sl) = &rep.SegmentList {
3156 if downloader.verbosity > 1 {
3159 info!(" Using AdaptationSet>SegmentList addressing mode for subtitle representation");
3160 }
3161 let mut start_byte: Option<u64> = None;
3162 let mut end_byte: Option<u64> = None;
3163 if let Some(init) = &sl.Initialization {
3164 if let Some(range) = &init.range {
3165 let (s, e) = parse_range(range)?;
3166 start_byte = Some(s);
3167 end_byte = Some(e);
3168 }
3169 if let Some(su) = &init.sourceURL {
3170 let path = resolve_url_template(su, &dict);
3171 let u = merge_baseurls(&base_url, &path)?;
3172 let mf = MediaFragmentBuilder::new(period_counter, u)
3173 .with_range(start_byte, end_byte)
3174 .set_init()
3175 .build();
3176 fragments.push(mf);
3177 } else {
3178 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3179 .with_range(start_byte, end_byte)
3180 .set_init()
3181 .build();
3182 fragments.push(mf);
3183 }
3184 }
3185 for su in &sl.segment_urls {
3186 start_byte = None;
3187 end_byte = None;
3188 if let Some(range) = &su.mediaRange {
3190 let (s, e) = parse_range(range)?;
3191 start_byte = Some(s);
3192 end_byte = Some(e);
3193 }
3194 if let Some(m) = &su.media {
3195 let u = merge_baseurls(&base_url, m)?;
3196 let mf = MediaFragmentBuilder::new(period_counter, u)
3197 .with_range(start_byte, end_byte)
3198 .build();
3199 fragments.push(mf);
3200 } else if let Some(bu) = subtitle_adaptation.BaseURL.first() {
3201 let u = merge_baseurls(&base_url, &bu.base)?;
3202 let mf = MediaFragmentBuilder::new(period_counter, u)
3203 .with_range(start_byte, end_byte)
3204 .build();
3205 fragments.push(mf);
3206 }
3207 }
3208 }
3209 if let Some(sl) = &rep.SegmentList {
3210 if downloader.verbosity > 1 {
3212 info!(" Using Representation>SegmentList addressing mode for subtitle representation");
3213 }
3214 let mut start_byte: Option<u64> = None;
3215 let mut end_byte: Option<u64> = None;
3216 if let Some(init) = &sl.Initialization {
3217 if let Some(range) = &init.range {
3218 let (s, e) = parse_range(range)?;
3219 start_byte = Some(s);
3220 end_byte = Some(e);
3221 }
3222 if let Some(su) = &init.sourceURL {
3223 let path = resolve_url_template(su, &dict);
3224 let u = merge_baseurls(&base_url, &path)?;
3225 let mf = MediaFragmentBuilder::new(period_counter, u)
3226 .with_range(start_byte, end_byte)
3227 .set_init()
3228 .build();
3229 fragments.push(mf);
3230 } else {
3231 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3232 .with_range(start_byte, end_byte)
3233 .set_init()
3234 .build();
3235 fragments.push(mf);
3236 }
3237 }
3238 for su in sl.segment_urls.iter() {
3239 start_byte = None;
3240 end_byte = None;
3241 if let Some(range) = &su.mediaRange {
3243 let (s, e) = parse_range(range)?;
3244 start_byte = Some(s);
3245 end_byte = Some(e);
3246 }
3247 if let Some(m) = &su.media {
3248 let u = merge_baseurls(&base_url, m)?;
3249 let mf = MediaFragmentBuilder::new(period_counter, u)
3250 .with_range(start_byte, end_byte)
3251 .build();
3252 fragments.push(mf);
3253 } else if let Some(bu) = &rep.BaseURL.first() {
3254 let u = merge_baseurls(&base_url, &bu.base)?;
3255 let mf = MediaFragmentBuilder::new(period_counter, u)
3256 .with_range(start_byte, end_byte)
3257 .build();
3258 fragments.push(mf);
3259 };
3260 }
3261 } else if rep.SegmentTemplate.is_some() ||
3262 subtitle_adaptation.SegmentTemplate.is_some()
3263 {
3264 let st;
3267 if let Some(it) = &rep.SegmentTemplate {
3268 st = it;
3269 } else if let Some(it) = &subtitle_adaptation.SegmentTemplate {
3270 st = it;
3271 } else {
3272 panic!("unreachable");
3273 }
3274 if let Some(i) = &st.initialization {
3275 opt_init = Some(i.clone());
3276 }
3277 if let Some(m) = &st.media {
3278 opt_media = Some(m.clone());
3279 }
3280 if let Some(ts) = st.timescale {
3281 timescale = ts;
3282 }
3283 if let Some(sn) = st.startNumber {
3284 start_number = sn;
3285 }
3286 if let Some(stl) = &rep.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
3287 .or(subtitle_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
3288 {
3289 if downloader.verbosity > 1 {
3292 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for subtitle representation");
3293 }
3294 if let Some(init) = opt_init {
3295 let path = resolve_url_template(&init, &dict);
3296 let u = merge_baseurls(&base_url, &path)?;
3297 let mf = MediaFragmentBuilder::new(period_counter, u)
3298 .set_init()
3299 .build();
3300 fragments.push(mf);
3301 }
3302 if let Some(media) = opt_media {
3303 let sub_path = resolve_url_template(&media, &dict);
3304 let mut segment_time = 0;
3305 let mut segment_duration;
3306 let mut number = start_number;
3307 for s in &stl.segments {
3308 if let Some(t) = s.t {
3309 segment_time = t;
3310 }
3311 segment_duration = s.d;
3312 let dict = HashMap::from([("Time", segment_time.to_string()),
3314 ("Number", number.to_string())]);
3315 let path = resolve_url_template(&sub_path, &dict);
3316 let u = merge_baseurls(&base_url, &path)?;
3317 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3318 fragments.push(mf);
3319 number += 1;
3320 if let Some(r) = s.r {
3321 let mut count = 0i64;
3322 let end_time = period_duration_secs * timescale as f64;
3324 loop {
3325 count += 1;
3326 if r >= 0 {
3332 if count > r {
3333 break;
3334 }
3335 if downloader.force_duration.is_some() &&
3336 segment_time as f64 > end_time
3337 {
3338 break;
3339 }
3340 } else if segment_time as f64 > end_time {
3341 break;
3342 }
3343 segment_time += segment_duration;
3344 let dict = HashMap::from([("Time", segment_time.to_string()),
3345 ("Number", number.to_string())]);
3346 let path = resolve_url_template(&sub_path, &dict);
3347 let u = merge_baseurls(&base_url, &path)?;
3348 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3349 fragments.push(mf);
3350 number += 1;
3351 }
3352 }
3353 segment_time += segment_duration;
3354 }
3355 } else {
3356 return Err(DashMpdError::UnhandledMediaStream(
3357 "SegmentTimeline without a media attribute".to_string()));
3358 }
3359 } else { if downloader.verbosity > 0 {
3364 info!(" Using SegmentTemplate addressing mode for stpp subtitles");
3365 }
3366 if let Some(i) = &st.initialization {
3367 opt_init = Some(i.to_string());
3368 }
3369 if let Some(m) = &st.media {
3370 opt_media = Some(m.to_string());
3371 }
3372 if let Some(d) = st.duration {
3373 opt_duration = Some(d);
3374 }
3375 if let Some(ts) = st.timescale {
3376 timescale = ts;
3377 }
3378 if let Some(s) = st.startNumber {
3379 start_number = s;
3380 }
3381 let rid = match &rep.id {
3382 Some(id) => id,
3383 None => return Err(
3384 DashMpdError::UnhandledMediaStream(
3385 "Missing @id on Representation node".to_string())),
3386 };
3387 let mut dict = HashMap::from([("RepresentationID", rid.clone())]);
3388 if let Some(b) = &rep.bandwidth {
3389 dict.insert("Bandwidth", b.to_string());
3390 }
3391 let mut total_number = 0i64;
3392 if let Some(init) = opt_init {
3393 let path = resolve_url_template(&init, &dict);
3394 let u = merge_baseurls(&base_url, &path)?;
3395 let mf = MediaFragmentBuilder::new(period_counter, u)
3396 .set_init()
3397 .build();
3398 fragments.push(mf);
3399 }
3400 if let Some(media) = opt_media {
3401 let sub_path = resolve_url_template(&media, &dict);
3402 let mut segment_duration: f64 = -1.0;
3403 if let Some(d) = opt_duration {
3404 segment_duration = d;
3406 }
3407 if let Some(std) = st.duration {
3408 if timescale == 0 {
3409 return Err(DashMpdError::UnhandledMediaStream(
3410 "SegmentTemplate@duration attribute cannot be zero".to_string()));
3411 }
3412 segment_duration = std / timescale as f64;
3413 }
3414 if segment_duration < 0.0 {
3415 return Err(DashMpdError::UnhandledMediaStream(
3416 "Subtitle representation is missing SegmentTemplate@duration".to_string()));
3417 }
3418 total_number += (period_duration_secs / segment_duration).ceil() as i64;
3419 let mut number = start_number;
3420 for _ in 1..=total_number {
3421 let dict = HashMap::from([("Number", number.to_string())]);
3422 let path = resolve_url_template(&sub_path, &dict);
3423 let u = merge_baseurls(&base_url, &path)?;
3424 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3425 fragments.push(mf);
3426 number += 1;
3427 }
3428 }
3429 }
3430 } else if let Some(sb) = &rep.SegmentBase {
3431 info!(" Using SegmentBase@indexRange for subs");
3433 if downloader.verbosity > 1 {
3434 info!(" Using SegmentBase@indexRange addressing mode for subtitle representation");
3435 }
3436 let mut start_byte: Option<u64> = None;
3437 let mut end_byte: Option<u64> = None;
3438 if let Some(init) = &sb.Initialization {
3439 if let Some(range) = &init.range {
3440 let (s, e) = parse_range(range)?;
3441 start_byte = Some(s);
3442 end_byte = Some(e);
3443 }
3444 if let Some(su) = &init.sourceURL {
3445 let path = resolve_url_template(su, &dict);
3446 let u = merge_baseurls(&base_url, &path)?;
3447 let mf = MediaFragmentBuilder::new(period_counter, u)
3448 .with_range(start_byte, end_byte)
3449 .set_init()
3450 .build();
3451 fragments.push(mf);
3452 }
3453 }
3454 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3455 .set_init()
3456 .build();
3457 fragments.push(mf);
3458 }
3461 }
3462 }
3463 }
3464 }
3465 Ok(PeriodOutputs {
3466 fragments,
3467 diagnostics: Vec::new(),
3468 subtitle_formats,
3469 selected_audio_language: String::from("unk")
3470 })
3471}
3472
3473
3474struct DownloadState {
3477 period_counter: u8,
3478 segment_count: usize,
3479 segment_counter: usize,
3480 download_errors: u32
3481}
3482
3483#[tracing::instrument(level="trace", skip_all)]
3490async fn fetch_fragment(
3491 downloader: &mut DashDownloader,
3492 frag: &MediaFragment,
3493 fragment_type: &str,
3494 progress_percent: u32) -> Result<std::fs::File, DashMpdError>
3495{
3496 let send_request = || async {
3497 trace!("send_request {}", frag.url.clone());
3498 let mut req = downloader.http_client.as_ref().unwrap()
3501 .get(frag.url.clone())
3502 .header("Accept", format!("{fragment_type}/*;q=0.9,*/*;q=0.5"))
3503 .header("Sec-Fetch-Mode", "navigate");
3504 if let Some(sb) = &frag.start_byte {
3505 if let Some(eb) = &frag.end_byte {
3506 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
3507 }
3508 }
3509 if let Some(ts) = &frag.timeout {
3510 req = req.timeout(*ts);
3511 }
3512 if let Some(referer) = &downloader.referer {
3513 req = req.header("Referer", referer);
3514 } else {
3515 req = req.header("Referer", downloader.redirected_url.to_string());
3516 }
3517 if let Some(username) = &downloader.auth_username {
3518 if let Some(password) = &downloader.auth_password {
3519 req = req.basic_auth(username, Some(password));
3520 }
3521 }
3522 if let Some(token) = &downloader.auth_bearer_token {
3523 req = req.bearer_auth(token);
3524 }
3525 req.send().await
3526 .map_err(categorize_reqwest_error)?
3527 .error_for_status()
3528 .map_err(categorize_reqwest_error)
3529 };
3530 match retry_notify(ExponentialBackoff::default(), send_request, notify_transient).await {
3531 Ok(response) => {
3532 match response.error_for_status() {
3533 Ok(mut resp) => {
3534 let mut tmp_out = tempfile::tempfile()
3535 .map_err(|e| DashMpdError::Io(e, String::from("creating tmpfile for fragment")))?;
3536 let content_type_checker = if fragment_type.eq("audio") {
3537 content_type_audio_p
3538 } else if fragment_type.eq("video") {
3539 content_type_video_p
3540 } else {
3541 panic!("fragment_type not audio or video");
3542 };
3543 if !downloader.content_type_checks || content_type_checker(&resp) {
3544 let mut fragment_out: Option<File> = None;
3545 if let Some(ref fragment_path) = downloader.fragment_path {
3546 if let Some(path) = frag.url.path_segments()
3547 .unwrap_or_else(|| "".split(' '))
3548 .next_back()
3549 {
3550 let vf_file = fragment_path.clone().join(fragment_type).join(path);
3551 if let Ok(f) = File::create(vf_file) {
3552 fragment_out = Some(f)
3553 }
3554 }
3555 }
3556 let mut segment_size = 0;
3557 while let Some(chunk) = resp.chunk().await
3563 .map_err(|e| network_error(&format!("fetching DASH {fragment_type} segment"), &e))?
3564 {
3565 segment_size += chunk.len();
3566 downloader.bw_estimator_bytes += chunk.len();
3567 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
3568 throttle_download_rate(downloader, size).await?;
3569 if let Err(e) = tmp_out.write_all(&chunk) {
3570 return Err(DashMpdError::Io(e, format!("writing DASH {fragment_type} data")));
3571 }
3572 if let Some(ref mut fout) = fragment_out {
3573 fout.write_all(&chunk)
3574 .map_err(|e| DashMpdError::Io(e, format!("writing {fragment_type} fragment")))?;
3575 }
3576 let elapsed = downloader.bw_estimator_started.elapsed().as_secs_f64();
3577 if (elapsed > 1.5) || (downloader.bw_estimator_bytes > 100_000) {
3578 let bw = downloader.bw_estimator_bytes as f64 / (1e6 * elapsed);
3579 let msg = if bw > 0.5 {
3580 format!("Fetching {fragment_type} segments ({bw:.1} MB/s)")
3581 } else {
3582 let kbs = (bw * 1000.0).round() as u64;
3583 format!("Fetching {fragment_type} segments ({kbs:3} kB/s)")
3584 };
3585 for observer in &downloader.progress_observers {
3586 observer.update(progress_percent, &msg);
3587 }
3588 downloader.bw_estimator_started = Instant::now();
3589 downloader.bw_estimator_bytes = 0;
3590 }
3591 }
3592 if downloader.verbosity > 2 {
3593 if let Some(sb) = &frag.start_byte {
3594 if let Some(eb) = &frag.end_byte {
3595 info!(" {fragment_type} segment {} range {sb}-{eb} -> {} octets",
3596 frag.url, segment_size);
3597 }
3598 } else {
3599 info!(" {fragment_type} segment {} -> {segment_size} octets", &frag.url);
3600 }
3601 }
3602 } else {
3603 warn!("Ignoring segment {} with non-{fragment_type} content-type", frag.url);
3604 };
3605 tmp_out.sync_all()
3606 .map_err(|e| DashMpdError::Io(e, format!("syncing {fragment_type} fragment")))?;
3607 Ok(tmp_out)
3608 },
3609 Err(e) => Err(network_error("HTTP error", &e)),
3610 }
3611 },
3612 Err(e) => Err(network_error(&format!("{e:?}"), &e)),
3613 }
3614}
3615
3616
3617#[tracing::instrument(level="trace", skip_all)]
3619async fn fetch_period_audio(
3620 downloader: &mut DashDownloader,
3621 tmppath: PathBuf,
3622 audio_fragments: &[MediaFragment],
3623 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3624{
3625 let start_download = Instant::now();
3626 let mut have_audio = false;
3627 {
3628 let tmpfile_audio = File::create(tmppath.clone())
3632 .map_err(|e| DashMpdError::Io(e, String::from("creating audio tmpfile")))?;
3633 ensure_permissions_readable(&tmppath)?;
3634 let mut tmpfile_audio = BufWriter::new(tmpfile_audio);
3635 if let Some(ref fragment_path) = downloader.fragment_path {
3637 let audio_fragment_dir = fragment_path.join("audio");
3638 if !audio_fragment_dir.exists() {
3639 fs::create_dir_all(audio_fragment_dir)
3640 .map_err(|e| DashMpdError::Io(e, String::from("creating audio fragment dir")))?;
3641 }
3642 }
3643 for frag in audio_fragments.iter().filter(|f| f.period == ds.period_counter) {
3647 ds.segment_counter += 1;
3648 let progress_percent = (100.0 * ds.segment_counter as f32 / (2.0 + ds.segment_count as f32)).ceil() as u32;
3649 let url = &frag.url;
3650 if url.scheme() == "data" {
3654 let us = &url.to_string();
3655 let du = DataUrl::process(us)
3656 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
3657 if du.mime_type().type_ != "audio" {
3658 return Err(DashMpdError::UnhandledMediaStream(
3659 String::from("expecting audio content in data URL")));
3660 }
3661 let (body, _fragment) = du.decode_to_vec()
3662 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
3663 if downloader.verbosity > 2 {
3664 info!(" Audio segment data URL -> {} octets", body.len());
3665 }
3666 if let Err(e) = tmpfile_audio.write_all(&body) {
3667 error!("Unable to write DASH audio data: {e:?}");
3668 return Err(DashMpdError::Io(e, String::from("writing DASH audio data")));
3669 }
3670 have_audio = true;
3671 } else {
3672 'done: for _ in 0..downloader.fragment_retry_count {
3674 match fetch_fragment(downloader, frag, "audio", progress_percent).await {
3675 Ok(mut frag_file) => {
3676 frag_file.rewind()
3677 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
3678 let mut buf = Vec::new();
3679 frag_file.read_to_end(&mut buf)
3680 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
3681 if let Err(e) = tmpfile_audio.write_all(&buf) {
3682 error!("Unable to write DASH audio data: {e:?}");
3683 return Err(DashMpdError::Io(e, String::from("writing DASH audio data")));
3684 }
3685 have_audio = true;
3686 break 'done;
3687 },
3688 Err(e) => {
3689 if downloader.verbosity > 0 {
3690 error!("Error fetching audio segment {url}: {e:?}");
3691 }
3692 ds.download_errors += 1;
3693 if ds.download_errors > downloader.max_error_count {
3694 error!("max_error_count network errors encountered");
3695 return Err(DashMpdError::Network(
3696 String::from("more than max_error_count network errors")));
3697 }
3698 },
3699 }
3700 info!(" Retrying audio segment {url}");
3701 if downloader.sleep_between_requests > 0 {
3702 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
3703 }
3704 }
3705 }
3706 }
3707 tmpfile_audio.flush().map_err(|e| {
3708 error!("Couldn't flush DASH audio file: {e}");
3709 DashMpdError::Io(e, String::from("flushing DASH audio file"))
3710 })?;
3711 } if !downloader.decryption_keys.is_empty() {
3713 if downloader.verbosity > 0 {
3714 let metadata = fs::metadata(tmppath.clone())
3715 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted audio metadata")))?;
3716 info!(" Attempting to decrypt audio stream ({} kB) with {}",
3717 metadata.len() / 1024,
3718 downloader.decryptor_preference);
3719 }
3720 let out_ext = downloader.output_path.as_ref().unwrap()
3721 .extension()
3722 .unwrap_or(OsStr::new("mp4"));
3723 let decrypted = tmp_file_path("dashmpd-decrypted-audio", out_ext)?;
3724 if downloader.decryptor_preference.eq("mp4decrypt") {
3725 let mut args = Vec::new();
3726 for (k, v) in downloader.decryption_keys.iter() {
3727 args.push("--key".to_string());
3728 args.push(format!("{k}:{v}"));
3729 }
3730 args.push(String::from(tmppath.to_string_lossy()));
3731 args.push(String::from(decrypted.to_string_lossy()));
3732 if downloader.verbosity > 1 {
3733 info!(" Running mp4decrypt {}", args.join(" "));
3734 }
3735 let out = Command::new(downloader.mp4decrypt_location.clone())
3736 .args(args)
3737 .output()
3738 .map_err(|e| DashMpdError::Io(e, String::from("spawning mp4decrypt")))?;
3739 let mut no_output = true;
3740 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3741 if downloader.verbosity > 0 {
3742 info!(" Decrypted audio stream of size {} kB.", metadata.len() / 1024);
3743 }
3744 no_output = false;
3745 }
3746 if !out.status.success() || no_output {
3747 warn!(" mp4decrypt subprocess failed");
3748 let msg = partial_process_output(&out.stdout);
3749 if !msg.is_empty() {
3750 warn!(" mp4decrypt stdout: {msg}");
3751 }
3752 let msg = partial_process_output(&out.stderr);
3753 if !msg.is_empty() {
3754 warn!(" mp4decrypt stderr: {msg}");
3755 }
3756 }
3757 if no_output {
3758 error!(" Failed to decrypt audio stream with mp4decrypt");
3759 warn!(" Undecrypted audio left in {}", tmppath.display());
3760 return Err(DashMpdError::Decrypting(String::from("audio stream")));
3761 }
3762 } else if downloader.decryptor_preference.eq("shaka") {
3763 let mut args = Vec::new();
3764 let mut keys = Vec::new();
3765 if downloader.verbosity < 1 {
3766 args.push("--quiet".to_string());
3767 }
3768 args.push(format!("in={},stream=audio,output={}", tmppath.display(), decrypted.display()));
3769 let mut drm_label = 0;
3770 #[allow(clippy::explicit_counter_loop)]
3771 for (k, v) in downloader.decryption_keys.iter() {
3772 keys.push(format!("label=lbl{drm_label}:key_id={k}:key={v}"));
3773 drm_label += 1;
3774 }
3775 args.push("--enable_raw_key_decryption".to_string());
3776 args.push("--keys".to_string());
3777 args.push(keys.join(","));
3778 if downloader.verbosity > 1 {
3779 info!(" Running shaka-packager {}", args.join(" "));
3780 }
3781 let out = Command::new(downloader.shaka_packager_location.clone())
3782 .args(args)
3783 .output()
3784 .map_err(|e| DashMpdError::Io(e, String::from("spawning shaka-packager")))?;
3785 let mut no_output = false;
3786 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3787 if downloader.verbosity > 0 {
3788 info!(" Decrypted audio stream of size {} kB.", metadata.len() / 1024);
3789 }
3790 if metadata.len() == 0 {
3791 no_output = true;
3792 }
3793 } else {
3794 no_output = true;
3795 }
3796 if !out.status.success() || no_output {
3797 warn!(" shaka-packager subprocess failed");
3798 let msg = partial_process_output(&out.stdout);
3799 if !msg.is_empty() {
3800 warn!(" shaka-packager stdout: {msg}");
3801 }
3802 let msg = partial_process_output(&out.stderr);
3803 if !msg.is_empty() {
3804 warn!(" shaka-packager stderr: {msg}");
3805 }
3806 }
3807 if no_output {
3808 error!(" Failed to decrypt audio stream with shaka-packager");
3809 warn!(" Undecrypted audio stream left in {}", tmppath.display());
3810 return Err(DashMpdError::Decrypting(String::from("audio stream")));
3811 }
3812 } else if downloader.decryptor_preference.eq("mp4box") {
3815 let mut args = Vec::new();
3816 let drmfile = tmp_file_path("mp4boxcrypt", OsStr::new("xml"))?;
3817 let mut drmfile_contents = String::from("<GPACDRM>\n <CrypTrack>\n");
3818 for (k, v) in downloader.decryption_keys.iter() {
3819 drmfile_contents += &format!(" <key KID=\"0x{k}\" value=\"0x{v}\"/>\n");
3820 }
3821 drmfile_contents += " </CrypTrack>\n</GPACDRM>\n";
3822 fs::write(&drmfile, drmfile_contents)
3823 .map_err(|e| DashMpdError::Io(e, String::from("writing to MP4Box decrypt file")))?;
3824 args.push("-decrypt".to_string());
3825 args.push(drmfile.display().to_string());
3826 args.push(String::from(tmppath.to_string_lossy()));
3827 args.push("-out".to_string());
3828 args.push(String::from(decrypted.to_string_lossy()));
3829 if downloader.verbosity > 1 {
3830 info!(" Running decryption application MP4Box {}", args.join(" "));
3831 }
3832 let out = Command::new(downloader.mp4box_location.clone())
3833 .args(args)
3834 .output()
3835 .map_err(|e| DashMpdError::Io(e, String::from("spawning MP4Box")))?;
3836 let mut no_output = false;
3837 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3838 if downloader.verbosity > 0 {
3839 info!(" Decrypted audio stream of size {} kB.", metadata.len() / 1024);
3840 }
3841 if metadata.len() == 0 {
3842 no_output = true;
3843 }
3844 } else {
3845 no_output = true;
3846 }
3847 if !out.status.success() || no_output {
3848 warn!(" MP4Box decryption subprocess failed");
3849 let msg = partial_process_output(&out.stdout);
3850 if !msg.is_empty() {
3851 warn!(" MP4Box stdout: {msg}");
3852 }
3853 let msg = partial_process_output(&out.stderr);
3854 if !msg.is_empty() {
3855 warn!(" MP4Box stderr: {msg}");
3856 }
3857 }
3858 if let Err(e) = fs::remove_file(drmfile) {
3859 warn!(" Failed to delete temporary MP4Box crypt file: {e:?}");
3860 }
3861 if no_output {
3862 error!(" Failed to decrypt audio stream with MP4Box");
3863 warn!(" Undecrypted audio stream left in {}", tmppath.display());
3864 return Err(DashMpdError::Decrypting(String::from("audio stream")));
3865 }
3866 } else {
3867 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
3868 }
3869 fs::rename(decrypted, tmppath.clone())
3870 .map_err(|e| DashMpdError::Io(e, String::from("renaming decrypted audio")))?;
3871 }
3872 if let Ok(metadata) = fs::metadata(tmppath.clone()) {
3873 if downloader.verbosity > 1 {
3874 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
3875 let elapsed = start_download.elapsed();
3876 info!(" Wrote {mbytes:.1}MB to DASH audio file ({:.1} MB/s)",
3877 mbytes / elapsed.as_secs_f64());
3878 }
3879 }
3880 Ok(have_audio)
3881}
3882
3883
3884#[tracing::instrument(level="trace", skip_all)]
3886async fn fetch_period_video(
3887 downloader: &mut DashDownloader,
3888 tmppath: PathBuf,
3889 video_fragments: &[MediaFragment],
3890 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3891{
3892 let start_download = Instant::now();
3893 let mut have_video = false;
3894 {
3895 let tmpfile_video = File::create(tmppath.clone())
3898 .map_err(|e| DashMpdError::Io(e, String::from("creating video tmpfile")))?;
3899 ensure_permissions_readable(&tmppath)?;
3900 let mut tmpfile_video = BufWriter::new(tmpfile_video);
3901 if let Some(ref fragment_path) = downloader.fragment_path {
3903 let video_fragment_dir = fragment_path.join("video");
3904 if !video_fragment_dir.exists() {
3905 fs::create_dir_all(video_fragment_dir)
3906 .map_err(|e| DashMpdError::Io(e, String::from("creating video fragment dir")))?;
3907 }
3908 }
3909 for frag in video_fragments.iter().filter(|f| f.period == ds.period_counter) {
3910 ds.segment_counter += 1;
3911 let progress_percent = (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32;
3912 if frag.url.scheme() == "data" {
3913 let us = &frag.url.to_string();
3914 let du = DataUrl::process(us)
3915 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
3916 if du.mime_type().type_ != "video" {
3917 return Err(DashMpdError::UnhandledMediaStream(
3918 String::from("expecting video content in data URL")));
3919 }
3920 let (body, _fragment) = du.decode_to_vec()
3921 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
3922 if downloader.verbosity > 2 {
3923 info!(" Video segment data URL -> {} octets", body.len());
3924 }
3925 if let Err(e) = tmpfile_video.write_all(&body) {
3926 error!("Unable to write DASH video data: {e:?}");
3927 return Err(DashMpdError::Io(e, String::from("writing DASH video data")));
3928 }
3929 have_video = true;
3930 } else {
3931 'done: for _ in 0..downloader.fragment_retry_count {
3932 match fetch_fragment(downloader, frag, "video", progress_percent).await {
3933 Ok(mut frag_file) => {
3934 frag_file.rewind()
3935 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
3936 let mut buf = Vec::new();
3937 frag_file.read_to_end(&mut buf)
3938 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
3939 if let Err(e) = tmpfile_video.write_all(&buf) {
3940 error!("Unable to write DASH video data: {e:?}");
3941 return Err(DashMpdError::Io(e, String::from("writing DASH video data")));
3942 }
3943 have_video = true;
3944 break 'done;
3945 },
3946 Err(e) => {
3947 if downloader.verbosity > 0 {
3948 error!(" Error fetching video segment {}: {e:?}", frag.url);
3949 }
3950 ds.download_errors += 1;
3951 if ds.download_errors > downloader.max_error_count {
3952 return Err(DashMpdError::Network(
3953 String::from("more than max_error_count network errors")));
3954 }
3955 },
3956 }
3957 info!(" Retrying video segment {}", frag.url);
3958 if downloader.sleep_between_requests > 0 {
3959 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
3960 }
3961 }
3962 }
3963 }
3964 tmpfile_video.flush().map_err(|e| {
3965 error!(" Couldn't flush video file: {e}");
3966 DashMpdError::Io(e, String::from("flushing video file"))
3967 })?;
3968 } if !downloader.decryption_keys.is_empty() {
3970 if downloader.verbosity > 0 {
3971 let metadata = fs::metadata(tmppath.clone())
3972 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted video metadata")))?;
3973 info!(" Attempting to decrypt video stream ({} kB) with {}",
3974 metadata.len() / 1024,
3975 downloader.decryptor_preference);
3976 }
3977 let out_ext = downloader.output_path.as_ref().unwrap()
3978 .extension()
3979 .unwrap_or(OsStr::new("mp4"));
3980 let decrypted = tmp_file_path("dashmpd-decrypted-video", out_ext)?;
3981 if downloader.decryptor_preference.eq("mp4decrypt") {
3982 let mut args = Vec::new();
3983 for (k, v) in downloader.decryption_keys.iter() {
3984 args.push("--key".to_string());
3985 args.push(format!("{k}:{v}"));
3986 }
3987 args.push(tmppath.to_string_lossy().to_string());
3988 args.push(decrypted.to_string_lossy().to_string());
3989 if downloader.verbosity > 1 {
3990 info!(" Running mp4decrypt {}", args.join(" "));
3991 }
3992 let out = Command::new(downloader.mp4decrypt_location.clone())
3993 .args(args)
3994 .output()
3995 .map_err(|e| DashMpdError::Io(e, String::from("spawning mp4decrypt")))?;
3996 let mut no_output = false;
3997 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3998 if downloader.verbosity > 0 {
3999 info!(" Decrypted video stream of size {} kB.", metadata.len() / 1024);
4000 }
4001 if metadata.len() == 0 {
4002 no_output = true;
4003 }
4004 } else {
4005 no_output = true;
4006 }
4007 if !out.status.success() || no_output {
4008 error!(" mp4decrypt subprocess failed");
4009 let msg = partial_process_output(&out.stdout);
4010 if !msg.is_empty() {
4011 warn!(" mp4decrypt stdout: {msg}");
4012 }
4013 let msg = partial_process_output(&out.stderr);
4014 if !msg.is_empty() {
4015 warn!(" mp4decrypt stderr: {msg}");
4016 }
4017 }
4018 if no_output {
4019 error!(" Failed to decrypt video stream with mp4decrypt");
4020 warn!(" Undecrypted video stream left in {}", tmppath.display());
4021 return Err(DashMpdError::Decrypting(String::from("video stream")));
4022 }
4023 } else if downloader.decryptor_preference.eq("shaka") {
4024 let mut args = Vec::new();
4025 let mut keys = Vec::new();
4026 if downloader.verbosity < 1 {
4027 args.push("--quiet".to_string());
4028 }
4029 args.push(format!("in={},stream=video,output={}", tmppath.display(), decrypted.display()));
4030 let mut drm_label = 0;
4031 #[allow(clippy::explicit_counter_loop)]
4032 for (k, v) in downloader.decryption_keys.iter() {
4033 keys.push(format!("label=lbl{drm_label}:key_id={k}:key={v}"));
4034 drm_label += 1;
4035 }
4036 args.push("--enable_raw_key_decryption".to_string());
4037 args.push("--keys".to_string());
4038 args.push(keys.join(","));
4039 if downloader.verbosity > 1 {
4040 info!(" Running shaka-packager {}", args.join(" "));
4041 }
4042 let out = Command::new(downloader.shaka_packager_location.clone())
4043 .args(args)
4044 .output()
4045 .map_err(|e| DashMpdError::Io(e, String::from("spawning shaka-packager")))?;
4046 let mut no_output = true;
4047 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
4048 if downloader.verbosity > 0 {
4049 info!(" Decrypted video stream of size {} kB.", metadata.len() / 1024);
4050 }
4051 no_output = false;
4052 }
4053 if !out.status.success() || no_output {
4054 warn!(" shaka-packager subprocess failed");
4055 let msg = partial_process_output(&out.stdout);
4056 if !msg.is_empty() {
4057 warn!(" shaka-packager stdout: {msg}");
4058 }
4059 let msg = partial_process_output(&out.stderr);
4060 if !msg.is_empty() {
4061 warn!(" shaka-packager stderr: {msg}");
4062 }
4063 }
4064 if no_output {
4065 error!(" Failed to decrypt video stream with shaka-packager");
4066 warn!(" Undecrypted video left in {}", tmppath.display());
4067 return Err(DashMpdError::Decrypting(String::from("video stream")));
4068 }
4069 } else if downloader.decryptor_preference.eq("mp4box") {
4070 let mut args = Vec::new();
4071 let drmfile = tmp_file_path("mp4boxcrypt", OsStr::new("xml"))?;
4072 let mut drmfile_contents = String::from("<GPACDRM>\n <CrypTrack>\n");
4073 for (k, v) in downloader.decryption_keys.iter() {
4074 drmfile_contents += &format!(" <key KID=\"0x{k}\" value=\"0x{v}\"/>\n");
4075 }
4076 drmfile_contents += " </CrypTrack>\n</GPACDRM>\n";
4077 fs::write(&drmfile, drmfile_contents)
4078 .map_err(|e| DashMpdError::Io(e, String::from("writing to MP4Box decrypt file")))?;
4079 args.push("-decrypt".to_string());
4080 args.push(drmfile.display().to_string());
4081 args.push(String::from(tmppath.to_string_lossy()));
4082 args.push("-out".to_string());
4083 args.push(String::from(decrypted.to_string_lossy()));
4084 if downloader.verbosity > 1 {
4085 info!(" Running decryption application MP4Box {}", args.join(" "));
4086 }
4087 let out = Command::new(downloader.mp4box_location.clone())
4088 .args(args)
4089 .output()
4090 .map_err(|e| DashMpdError::Io(e, String::from("spawning MP4Box")))?;
4091 let mut no_output = false;
4092 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
4093 if downloader.verbosity > 0 {
4094 info!(" Decrypted video stream of size {} kB.", metadata.len() / 1024);
4095 }
4096 if metadata.len() == 0 {
4097 no_output = true;
4098 }
4099 } else {
4100 no_output = true;
4101 }
4102 if !out.status.success() || no_output {
4103 warn!(" MP4Box decryption subprocess failed");
4104 let msg = partial_process_output(&out.stdout);
4105 if !msg.is_empty() {
4106 warn!(" MP4Box stdout: {msg}");
4107 }
4108 let msg = partial_process_output(&out.stderr);
4109 if !msg.is_empty() {
4110 warn!(" MP4Box stderr: {msg}");
4111 }
4112 }
4113 if no_output {
4114 error!(" Failed to decrypt video stream with MP4Box");
4115 warn!(" Undecrypted video stream left in {}", tmppath.display());
4116 return Err(DashMpdError::Decrypting(String::from("video stream")));
4117 }
4118 } else {
4119 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
4120 }
4121 fs::rename(decrypted, tmppath.clone())
4122 .map_err(|e| DashMpdError::Io(e, String::from("renaming decrypted video")))?;
4123 }
4124 if let Ok(metadata) = fs::metadata(tmppath.clone()) {
4125 if downloader.verbosity > 1 {
4126 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4127 let elapsed = start_download.elapsed();
4128 info!(" Wrote {mbytes:.1}MB to DASH video file ({:.1} MB/s)",
4129 mbytes / elapsed.as_secs_f64());
4130 }
4131 }
4132 Ok(have_video)
4133}
4134
4135
4136#[tracing::instrument(level="trace", skip_all)]
4138async fn fetch_period_subtitles(
4139 downloader: &DashDownloader,
4140 tmppath: PathBuf,
4141 subtitle_fragments: &[MediaFragment],
4142 subtitle_formats: &[SubtitleType],
4143 ds: &mut DownloadState) -> Result<bool, DashMpdError>
4144{
4145 let client = downloader.http_client.clone().unwrap();
4146 let start_download = Instant::now();
4147 let mut have_subtitles = false;
4148 {
4149 let tmpfile_subs = File::create(tmppath.clone())
4150 .map_err(|e| DashMpdError::Io(e, String::from("creating subs tmpfile")))?;
4151 ensure_permissions_readable(&tmppath)?;
4152 let mut tmpfile_subs = BufWriter::new(tmpfile_subs);
4153 for frag in subtitle_fragments {
4154 ds.segment_counter += 1;
4156 let progress_percent = (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32;
4157 for observer in &downloader.progress_observers {
4158 observer.update(progress_percent, "Fetching subtitle segments");
4159 }
4160 if frag.url.scheme() == "data" {
4161 let us = &frag.url.to_string();
4162 let du = DataUrl::process(us)
4163 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
4164 if du.mime_type().type_ != "video" {
4165 return Err(DashMpdError::UnhandledMediaStream(
4166 String::from("expecting video content in data URL")));
4167 }
4168 let (body, _fragment) = du.decode_to_vec()
4169 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
4170 if downloader.verbosity > 2 {
4171 info!(" Subtitle segment data URL -> {} octets", body.len());
4172 }
4173 if let Err(e) = tmpfile_subs.write_all(&body) {
4174 error!("Unable to write DASH subtitle data: {e:?}");
4175 return Err(DashMpdError::Io(e, String::from("writing DASH subtitle data")));
4176 }
4177 have_subtitles = true;
4178 } else {
4179 let fetch = || async {
4180 let mut req = client.get(frag.url.clone())
4181 .header("Sec-Fetch-Mode", "navigate");
4182 if let Some(sb) = &frag.start_byte {
4183 if let Some(eb) = &frag.end_byte {
4184 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
4185 }
4186 }
4187 if let Some(referer) = &downloader.referer {
4188 req = req.header("Referer", referer);
4189 } else {
4190 req = req.header("Referer", downloader.redirected_url.to_string());
4191 }
4192 if let Some(username) = &downloader.auth_username {
4193 if let Some(password) = &downloader.auth_password {
4194 req = req.basic_auth(username, Some(password));
4195 }
4196 }
4197 if let Some(token) = &downloader.auth_bearer_token {
4198 req = req.bearer_auth(token);
4199 }
4200 req.send().await
4201 .map_err(categorize_reqwest_error)?
4202 .error_for_status()
4203 .map_err(categorize_reqwest_error)
4204 };
4205 let mut failure = None;
4206 match retry_notify(ExponentialBackoff::default(), fetch, notify_transient).await {
4207 Ok(response) => {
4208 if response.status().is_success() {
4209 let dash_bytes = response.bytes().await
4210 .map_err(|e| network_error("fetching DASH subtitle segment", &e))?;
4211 if downloader.verbosity > 2 {
4212 if let Some(sb) = &frag.start_byte {
4213 if let Some(eb) = &frag.end_byte {
4214 info!(" Subtitle segment {} range {sb}-{eb} -> {} octets",
4215 &frag.url, dash_bytes.len());
4216 }
4217 } else {
4218 info!(" Subtitle segment {} -> {} octets", &frag.url, dash_bytes.len());
4219 }
4220 }
4221 let size = min((dash_bytes.len()/1024 + 1) as u32, u32::MAX);
4222 throttle_download_rate(downloader, size).await?;
4223 if let Err(e) = tmpfile_subs.write_all(&dash_bytes) {
4224 return Err(DashMpdError::Io(e, String::from("writing DASH subtitle data")));
4225 }
4226 have_subtitles = true;
4227 } else {
4228 failure = Some(format!("HTTP error {}", response.status().as_str()));
4229 }
4230 },
4231 Err(e) => failure = Some(format!("{e}")),
4232 }
4233 if let Some(f) = failure {
4234 if downloader.verbosity > 0 {
4235 error!("{f} fetching subtitle segment {}", &frag.url);
4236 }
4237 ds.download_errors += 1;
4238 if ds.download_errors > downloader.max_error_count {
4239 return Err(DashMpdError::Network(
4240 String::from("more than max_error_count network errors")));
4241 }
4242 }
4243 }
4244 if downloader.sleep_between_requests > 0 {
4245 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
4246 }
4247 }
4248 tmpfile_subs.flush().map_err(|e| {
4249 error!("Couldn't flush subs file: {e}");
4250 DashMpdError::Io(e, String::from("flushing subtitle file"))
4251 })?;
4252 } if have_subtitles {
4254 if let Ok(metadata) = fs::metadata(tmppath.clone()) {
4255 if downloader.verbosity > 1 {
4256 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4257 let elapsed = start_download.elapsed();
4258 info!(" Wrote {mbytes:.1}MB to DASH subtitle file ({:.1} MB/s)",
4259 mbytes / elapsed.as_secs_f64());
4260 }
4261 }
4262 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
4265 subtitle_formats.contains(&SubtitleType::Ttxt)
4266 {
4267 if downloader.verbosity > 0 {
4269 if let Some(fmt) = subtitle_formats.first() {
4270 info!(" Downloaded media contains subtitles in {fmt:?} format");
4271 }
4272 info!(" Running MP4Box to extract subtitles");
4273 }
4274 let out = downloader.output_path.as_ref().unwrap()
4275 .with_extension("srt");
4276 let out_str = out.to_string_lossy();
4277 let tmp_str = tmppath.to_string_lossy();
4278 let args = vec![
4279 "-srt", "1",
4280 "-out", &out_str,
4281 &tmp_str];
4282 if downloader.verbosity > 0 {
4283 info!(" Running MP4Box {}", args.join(" "));
4284 }
4285 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4286 .args(args)
4287 .output()
4288 {
4289 let msg = partial_process_output(&mp4box.stdout);
4290 if !msg.is_empty() {
4291 info!(" MP4Box stdout: {msg}");
4292 }
4293 let msg = partial_process_output(&mp4box.stderr);
4294 if !msg.is_empty() {
4295 info!(" MP4Box stderr: {msg}");
4296 }
4297 if mp4box.status.success() {
4298 info!(" Extracted subtitles as SRT");
4299 } else {
4300 warn!(" Error running MP4Box to extract subtitles");
4301 }
4302 } else {
4303 warn!(" Failed to spawn MP4Box to extract subtitles");
4304 }
4305 }
4306 if subtitle_formats.contains(&SubtitleType::Stpp) {
4307 if downloader.verbosity > 0 {
4308 info!(" Converting STPP subtitles to TTML format with ffmpeg");
4309 }
4310 let out = downloader.output_path.as_ref().unwrap()
4311 .with_extension("ttml");
4312 let tmppath_arg = &tmppath.to_string_lossy();
4313 let out_arg = &out.to_string_lossy();
4314 let ffmpeg_args = vec![
4315 "-hide_banner",
4316 "-nostats",
4317 "-loglevel", "error",
4318 "-y", "-nostdin",
4320 "-i", tmppath_arg,
4321 "-f", "data",
4322 "-map", "0",
4323 "-c", "copy",
4324 out_arg];
4325 if downloader.verbosity > 0 {
4326 info!(" Running ffmpeg {}", ffmpeg_args.join(" "));
4327 }
4328 if let Ok(ffmpeg) = Command::new(downloader.ffmpeg_location.clone())
4329 .args(ffmpeg_args)
4330 .output()
4331 {
4332 let msg = partial_process_output(&ffmpeg.stdout);
4333 if !msg.is_empty() {
4334 info!(" ffmpeg stdout: {msg}");
4335 }
4336 let msg = partial_process_output(&ffmpeg.stderr);
4337 if !msg.is_empty() {
4338 info!(" ffmpeg stderr: {msg}");
4339 }
4340 if ffmpeg.status.success() {
4341 info!(" Converted STPP subtitles to TTML format");
4342 } else {
4343 warn!(" Error running ffmpeg to convert subtitles");
4344 }
4345 }
4346 }
4350
4351 }
4352 Ok(have_subtitles)
4353}
4354
4355
4356async fn fetch_mpd_http(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4358 let client = &downloader.http_client.clone().unwrap();
4359 let send_request = || async {
4360 let mut req = client.get(&downloader.mpd_url)
4361 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4362 .header("Accept-Language", "en-US,en")
4363 .header("Upgrade-Insecure-Requests", "1")
4364 .header("Sec-Fetch-Mode", "navigate");
4365 if let Some(referer) = &downloader.referer {
4366 req = req.header("Referer", referer);
4367 }
4368 if let Some(username) = &downloader.auth_username {
4369 if let Some(password) = &downloader.auth_password {
4370 req = req.basic_auth(username, Some(password));
4371 }
4372 }
4373 if let Some(token) = &downloader.auth_bearer_token {
4374 req = req.bearer_auth(token);
4375 }
4376 req.send().await
4377 .map_err(categorize_reqwest_error)?
4378 .error_for_status()
4379 .map_err(categorize_reqwest_error)
4380 };
4381 for observer in &downloader.progress_observers {
4382 observer.update(1, "Fetching DASH manifest");
4383 }
4384 if downloader.verbosity > 0 {
4385 if !downloader.fetch_audio && !downloader.fetch_video && !downloader.fetch_subtitles {
4386 info!("Only simulating media downloads");
4387 }
4388 info!("Fetching the DASH manifest");
4389 }
4390 let response = retry_notify(ExponentialBackoff::default(), send_request, notify_transient)
4391 .await
4392 .map_err(|e| network_error("requesting DASH manifest", &e))?;
4393 if !response.status().is_success() {
4394 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4395 return Err(DashMpdError::Network(msg));
4396 }
4397 downloader.redirected_url = response.url().clone();
4398 response.bytes().await
4399 .map_err(|e| network_error("fetching DASH manifest", &e))
4400}
4401
4402async fn fetch_mpd_file(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4405 if ! &downloader.mpd_url.starts_with("file://") {
4406 return Err(DashMpdError::Other(String::from("expecting file:// URL scheme")));
4407 }
4408 let url = Url::parse(&downloader.mpd_url)
4409 .map_err(|_| DashMpdError::Other(String::from("parsing MPD URL")))?;
4410 let path = url.to_file_path()
4411 .map_err(|_| DashMpdError::Other(String::from("extracting path from file:// URL")))?;
4412 let octets = fs::read(path)
4413 .map_err(|_| DashMpdError::Other(String::from("reading from file:// URL")))?;
4414 Ok(Bytes::from(octets))
4415}
4416
4417
4418#[tracing::instrument(level="trace", skip_all)]
4419async fn fetch_mpd(downloader: &mut DashDownloader) -> Result<PathBuf, DashMpdError> {
4420 #[cfg(all(feature = "sandbox", target_os = "linux"))]
4421 if downloader.sandbox {
4422 if let Err(e) = restrict_thread(downloader) {
4423 warn!("Sandboxing failed: {e:?}");
4424 }
4425 }
4426 let xml = if downloader.mpd_url.starts_with("file://") {
4427 fetch_mpd_file(downloader).await?
4428 } else {
4429 fetch_mpd_http(downloader).await?
4430 };
4431 let mut mpd: MPD = parse_resolving_xlinks(downloader, &xml).await
4432 .map_err(|e| parse_error("parsing DASH XML", e))?;
4433 let client = &downloader.http_client.clone().unwrap();
4436 if let Some(new_location) = &mpd.locations.first() {
4437 let new_url = &new_location.url;
4438 if downloader.verbosity > 0 {
4439 info!("Redirecting to new manifest <Location> {new_url}");
4440 }
4441 let send_request = || async {
4442 let mut req = client.get(new_url)
4443 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4444 .header("Accept-Language", "en-US,en")
4445 .header("Sec-Fetch-Mode", "navigate");
4446 if let Some(referer) = &downloader.referer {
4447 req = req.header("Referer", referer);
4448 } else {
4449 req = req.header("Referer", downloader.redirected_url.to_string());
4450 }
4451 if let Some(username) = &downloader.auth_username {
4452 if let Some(password) = &downloader.auth_password {
4453 req = req.basic_auth(username, Some(password));
4454 }
4455 }
4456 if let Some(token) = &downloader.auth_bearer_token {
4457 req = req.bearer_auth(token);
4458 }
4459 req.send().await
4460 .map_err(categorize_reqwest_error)?
4461 .error_for_status()
4462 .map_err(categorize_reqwest_error)
4463 };
4464 let response = retry_notify(ExponentialBackoff::default(), send_request, notify_transient)
4465 .await
4466 .map_err(|e| network_error("requesting relocated DASH manifest", &e))?;
4467 if !response.status().is_success() {
4468 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4469 return Err(DashMpdError::Network(msg));
4470 }
4471 downloader.redirected_url = response.url().clone();
4472 let xml = response.bytes().await
4473 .map_err(|e| network_error("fetching relocated DASH manifest", &e))?;
4474 mpd = parse_resolving_xlinks(downloader, &xml).await
4475 .map_err(|e| parse_error("parsing relocated DASH XML", e))?;
4476 }
4477 if mpd_is_dynamic(&mpd) {
4478 if downloader.allow_live_streams {
4481 if downloader.verbosity > 0 {
4482 warn!("Attempting to download from live stream (this may not work).");
4483 }
4484 } else {
4485 return Err(DashMpdError::UnhandledMediaStream("Don't know how to download dynamic MPD".to_string()));
4486 }
4487 }
4488 let mut toplevel_base_url = downloader.redirected_url.clone();
4489 if let Some(bu) = &mpd.base_url.first() {
4491 toplevel_base_url = merge_baseurls(&downloader.redirected_url, &bu.base)?;
4492 }
4493 if let Some(base) = &downloader.base_url {
4496 toplevel_base_url = merge_baseurls(&downloader.redirected_url, base)?;
4497 }
4498 if downloader.verbosity > 0 {
4499 let pcount = mpd.periods.len();
4500 info!("DASH manifest has {pcount} period{}", if pcount > 1 { "s" } else { "" });
4501 print_available_streams(&mpd);
4502 }
4503 let mut pds: Vec<PeriodDownloads> = Vec::new();
4511 let mut period_counter = 0;
4512 for mpd_period in &mpd.periods {
4513 let period = mpd_period.clone();
4514 period_counter += 1;
4515 if let Some(min) = downloader.minimum_period_duration {
4516 if let Some(duration) = period.duration {
4517 if duration < min {
4518 if let Some(id) = period.id.as_ref() {
4519 info!("Skipping period {id} (#{period_counter}): duration is less than requested minimum");
4520 } else {
4521 info!("Skipping period #{period_counter}: duration is less than requested minimum");
4522 }
4523 continue;
4524 }
4525 }
4526 }
4527 let mut pd = PeriodDownloads { period_counter, ..Default::default() };
4528 if let Some(id) = period.id.as_ref() {
4529 pd.id = Some(id.clone());
4530 }
4531 if downloader.verbosity > 0 {
4532 if let Some(id) = period.id.as_ref() {
4533 info!("Preparing download for period {id} (#{period_counter})");
4534 } else {
4535 info!("Preparing download for period #{period_counter}");
4536 }
4537 }
4538 let mut base_url = toplevel_base_url.clone();
4539 if let Some(bu) = period.BaseURL.first() {
4541 base_url = merge_baseurls(&base_url, &bu.base)?;
4542 }
4543 let mut audio_outputs = PeriodOutputs::default();
4544 if downloader.fetch_audio {
4545 audio_outputs = do_period_audio(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4546 for f in audio_outputs.fragments {
4547 pd.audio_fragments.push(f);
4548 }
4549 pd.selected_audio_language = audio_outputs.selected_audio_language;
4550 }
4551 let mut video_outputs = PeriodOutputs::default();
4552 if downloader.fetch_video {
4553 video_outputs = do_period_video(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4554 for f in video_outputs.fragments {
4555 pd.video_fragments.push(f);
4556 }
4557 }
4558 match do_period_subtitles(downloader, &mpd, &period, period_counter, base_url.clone()).await {
4559 Ok(subtitle_outputs) => {
4560 for f in subtitle_outputs.fragments {
4561 pd.subtitle_fragments.push(f);
4562 }
4563 for f in subtitle_outputs.subtitle_formats {
4564 pd.subtitle_formats.push(f);
4565 }
4566 },
4567 Err(e) => warn!(" Ignoring error triggered while processing subtitles: {e}"),
4568 }
4569 if downloader.verbosity > 0 {
4571 use base64::prelude::{Engine as _, BASE64_STANDARD};
4572
4573 audio_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4574 for f in pd.audio_fragments.iter().filter(|f| f.is_init) {
4575 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4576 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4577 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4578 info!(" {}", pssh.to_string());
4579 }
4580 }
4581 }
4582 video_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4583 for f in pd.video_fragments.iter().filter(|f| f.is_init) {
4584 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4585 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4586 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4587 info!(" {}", pssh.to_string());
4588 }
4589 }
4590 }
4591 }
4592 pds.push(pd);
4593 } let output_path = &downloader.output_path.as_ref().unwrap().clone();
4598 let mut period_output_paths: Vec<PathBuf> = Vec::new();
4599 let mut ds = DownloadState {
4600 period_counter: 0,
4601 segment_count: pds.iter().map(period_fragment_count).sum(),
4603 segment_counter: 0,
4604 download_errors: 0
4605 };
4606 for pd in pds {
4607 let mut have_audio = false;
4608 let mut have_video = false;
4609 let mut have_subtitles = false;
4610 ds.period_counter = pd.period_counter;
4611 let period_output_path = output_path_for_period(output_path, pd.period_counter);
4612 #[allow(clippy::collapsible_if)]
4613 if downloader.verbosity > 0 {
4614 if downloader.fetch_audio || downloader.fetch_video || downloader.fetch_subtitles {
4615 let idnum = if let Some(id) = pd.id {
4616 format!("id={} (#{})", id, pd.period_counter)
4617 } else {
4618 format!("#{}", pd.period_counter)
4619 };
4620 info!("Period {idnum}: fetching {} audio, {} video and {} subtitle segments",
4621 pd.audio_fragments.len(),
4622 pd.video_fragments.len(),
4623 pd.subtitle_fragments.len());
4624 }
4625 }
4626 let output_ext = downloader.output_path.as_ref().unwrap()
4627 .extension()
4628 .unwrap_or(OsStr::new("mp4"));
4629 let tmppath_audio = if let Some(ref path) = downloader.keep_audio {
4630 path.clone()
4631 } else {
4632 tmp_file_path("dashmpd-audio", output_ext)?
4633 };
4634 let tmppath_video = if let Some(ref path) = downloader.keep_video {
4635 path.clone()
4636 } else {
4637 tmp_file_path("dashmpd-video", output_ext)?
4638 };
4639 let tmppath_subs = tmp_file_path("dashmpd-subs", OsStr::new("sub"))?;
4640 if downloader.fetch_audio && !pd.audio_fragments.is_empty() {
4641 have_audio = fetch_period_audio(downloader,
4645 tmppath_audio.clone(), &pd.audio_fragments,
4646 &mut ds).await?;
4647 }
4648 if downloader.fetch_video && !pd.video_fragments.is_empty() {
4649 have_video = fetch_period_video(downloader,
4650 tmppath_video.clone(), &pd.video_fragments,
4651 &mut ds).await?;
4652 }
4653 if downloader.fetch_subtitles && !pd.subtitle_fragments.is_empty() {
4657 have_subtitles = fetch_period_subtitles(downloader,
4658 tmppath_subs.clone(),
4659 &pd.subtitle_fragments,
4660 &pd.subtitle_formats,
4661 &mut ds).await?;
4662 }
4663
4664 if have_audio && have_video {
4667 for observer in &downloader.progress_observers {
4668 observer.update(99, "Muxing audio and video");
4669 }
4670 if downloader.verbosity > 1 {
4671 info!(" Muxing audio and video streams");
4672 }
4673 let audio_tracks = vec![
4674 AudioTrack {
4675 language: pd.selected_audio_language,
4676 path: tmppath_audio.clone()
4677 }];
4678 mux_audio_video(downloader, &period_output_path, &audio_tracks, &tmppath_video)?;
4679 if pd.subtitle_formats.contains(&SubtitleType::Stpp) {
4680 let container = match &period_output_path.extension() {
4681 Some(ext) => ext.to_str().unwrap_or("mp4"),
4682 None => "mp4",
4683 };
4684 if container.eq("mp4") {
4685 if downloader.verbosity > 1 {
4686 if let Some(fmt) = &pd.subtitle_formats.first() {
4687 info!(" Downloaded media contains subtitles in {fmt:?} format");
4688 }
4689 info!(" Running MP4Box to merge subtitles with output MP4 container");
4690 }
4691 let tmp_str = tmppath_subs.to_string_lossy();
4694 let period_output_str = period_output_path.to_string_lossy();
4695 let args = vec!["-add", &tmp_str, &period_output_str];
4696 if downloader.verbosity > 0 {
4697 info!(" Running MP4Box {}", args.join(" "));
4698 }
4699 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4700 .args(args)
4701 .output()
4702 {
4703 let msg = partial_process_output(&mp4box.stdout);
4704 if !msg.is_empty() {
4705 info!(" MP4Box stdout: {msg}");
4706 }
4707 let msg = partial_process_output(&mp4box.stderr);
4708 if !msg.is_empty() {
4709 info!(" MP4Box stderr: {msg}");
4710 }
4711 if mp4box.status.success() {
4712 info!(" Merged subtitles with MP4 container");
4713 } else {
4714 warn!(" Error running MP4Box to merge subtitles");
4715 }
4716 } else {
4717 warn!(" Failed to spawn MP4Box to merge subtitles");
4718 }
4719 } else if container.eq("mkv") || container.eq("webm") {
4720 let srt = period_output_path.with_extension("srt");
4732 if srt.exists() {
4733 if downloader.verbosity > 0 {
4734 info!(" Running mkvmerge to merge subtitles with output Matroska container");
4735 }
4736 let tmppath = temporary_outpath(".mkv")?;
4737 let pop_arg = &period_output_path.to_string_lossy();
4738 let srt_arg = &srt.to_string_lossy();
4739 let mkvmerge_args = vec!["-o", &tmppath, pop_arg, srt_arg];
4740 if downloader.verbosity > 0 {
4741 info!(" Running mkvmerge {}", mkvmerge_args.join(" "));
4742 }
4743 if let Ok(mkvmerge) = Command::new(downloader.mkvmerge_location.clone())
4744 .args(mkvmerge_args)
4745 .output()
4746 {
4747 let msg = partial_process_output(&mkvmerge.stdout);
4748 if !msg.is_empty() {
4749 info!(" mkvmerge stdout: {msg}");
4750 }
4751 let msg = partial_process_output(&mkvmerge.stderr);
4752 if !msg.is_empty() {
4753 info!(" mkvmerge stderr: {msg}");
4754 }
4755 if mkvmerge.status.success() {
4756 info!(" Merged subtitles with Matroska container");
4757 {
4760 let tmpfile = File::open(tmppath.clone())
4761 .map_err(|e| DashMpdError::Io(
4762 e, String::from("opening mkvmerge output")))?;
4763 let mut merged = BufReader::new(tmpfile);
4764 let outfile = File::create(period_output_path.clone())
4766 .map_err(|e| DashMpdError::Io(
4767 e, String::from("creating output file")))?;
4768 let mut sink = BufWriter::new(outfile);
4769 io::copy(&mut merged, &mut sink)
4770 .map_err(|e| DashMpdError::Io(
4771 e, String::from("copying mkvmerge output to output file")))?;
4772 }
4773 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4774 if let Err(e) = fs::remove_file(tmppath) {
4775 warn!(" Error deleting temporary mkvmerge output: {e}");
4776 }
4777 }
4778 } else {
4779 warn!(" Error running mkvmerge to merge subtitles");
4780 }
4781 }
4782 }
4783 }
4784 }
4785 } else if have_audio {
4786 copy_audio_to_container(downloader, &period_output_path, &tmppath_audio)?;
4787 } else if have_video {
4788 copy_video_to_container(downloader, &period_output_path, &tmppath_video)?;
4789 } else if downloader.fetch_video && downloader.fetch_audio {
4790 return Err(DashMpdError::UnhandledMediaStream("no audio or video streams found".to_string()));
4791 } else if downloader.fetch_video {
4792 return Err(DashMpdError::UnhandledMediaStream("no video streams found".to_string()));
4793 } else if downloader.fetch_audio {
4794 return Err(DashMpdError::UnhandledMediaStream("no audio streams found".to_string()));
4795 }
4796 #[allow(clippy::collapsible_if)]
4797 if downloader.keep_audio.is_none() && downloader.fetch_audio {
4798 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4799 if tmppath_audio.exists() && fs::remove_file(tmppath_audio).is_err() {
4800 info!(" Failed to delete temporary file for audio stream");
4801 }
4802 }
4803 }
4804 #[allow(clippy::collapsible_if)]
4805 if downloader.keep_video.is_none() && downloader.fetch_video {
4806 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4807 if tmppath_video.exists() && fs::remove_file(tmppath_video).is_err() {
4808 info!(" Failed to delete temporary file for video stream");
4809 }
4810 }
4811 }
4812 #[allow(clippy::collapsible_if)]
4813 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4814 if downloader.fetch_subtitles && tmppath_subs.exists() && fs::remove_file(tmppath_subs).is_err() {
4815 info!(" Failed to delete temporary file for subtitles");
4816 }
4817 }
4818 if downloader.verbosity > 1 && (downloader.fetch_audio || downloader.fetch_video || have_subtitles) {
4819 if let Ok(metadata) = fs::metadata(period_output_path.clone()) {
4820 info!(" Wrote {:.1}MB to media file", metadata.len() as f64 / (1024.0 * 1024.0));
4821 }
4822 }
4823 if have_audio || have_video {
4824 period_output_paths.push(period_output_path);
4825 }
4826 } #[allow(clippy::comparison_chain)]
4828 if period_output_paths.len() == 1 {
4829 maybe_record_metainformation(output_path, downloader, &mpd);
4831 } else if period_output_paths.len() > 1 {
4832 #[allow(unused_mut)]
4837 let mut concatenated = false;
4838 #[cfg(not(feature = "libav"))]
4839 if downloader.concatenate_periods && video_containers_concatable(downloader, &period_output_paths) {
4840 info!("Preparing to concatenate multiple Periods into one output file");
4841 concat_output_files(downloader, &period_output_paths)?;
4842 for p in &period_output_paths[1..] {
4843 if fs::remove_file(p).is_err() {
4844 warn!(" Failed to delete temporary file {}", p.display());
4845 }
4846 }
4847 concatenated = true;
4848 if let Some(pop) = period_output_paths.first() {
4849 maybe_record_metainformation(pop, downloader, &mpd);
4850 }
4851 }
4852 if !concatenated {
4853 info!("Media content has been saved in a separate file for each period:");
4854 period_counter = 0;
4856 for p in period_output_paths {
4857 period_counter += 1;
4858 info!(" Period #{period_counter}: {}", p.display());
4859 maybe_record_metainformation(&p, downloader, &mpd);
4860 }
4861 }
4862 }
4863 let have_content_protection = mpd.periods.iter().any(
4864 |p| p.adaptations.iter().any(
4865 |a| (!a.ContentProtection.is_empty()) ||
4866 a.representations.iter().any(
4867 |r| !r.ContentProtection.is_empty())));
4868 if have_content_protection && downloader.decryption_keys.is_empty() {
4869 warn!("Manifest seems to use ContentProtection (DRM), but you didn't provide decryption keys.");
4870 }
4871 for observer in &downloader.progress_observers {
4872 observer.update(100, "Done");
4873 }
4874 Ok(PathBuf::from(output_path))
4875}
4876
4877
4878#[cfg(test)]
4879mod tests {
4880 #[test]
4881 fn test_resolve_url_template() {
4882 use std::collections::HashMap;
4883 use super::resolve_url_template;
4884
4885 assert_eq!(resolve_url_template("AA$Time$BB", &HashMap::from([("Time", "ZZZ".to_string())])),
4886 "AAZZZBB");
4887 assert_eq!(resolve_url_template("AA$Number%06d$BB", &HashMap::from([("Number", "42".to_string())])),
4888 "AA000042BB");
4889 let dict = HashMap::from([("RepresentationID", "640x480".to_string()),
4890 ("Number", "42".to_string()),
4891 ("Time", "ZZZ".to_string())]);
4892 assert_eq!(resolve_url_template("AA/$RepresentationID$/segment-$Number%05d$.mp4", &dict),
4893 "AA/640x480/segment-00042.mp4");
4894 }
4895}