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