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