1use std::io;
4use std::env;
5use fs_err as fs;
6use fs::File;
7use std::io::{Read, Write, Seek, BufReader, BufWriter};
8use std::path::{Path, PathBuf};
9use std::process::Command;
10use std::time::Duration;
11use tokio::time::Instant;
12use chrono::Utc;
13use std::sync::Arc;
14use std::borrow::Cow;
15use std::collections::HashMap;
16use std::cmp::min;
17use std::ffi::OsStr;
18use std::num::NonZeroU32;
19use tracing::{trace, info, warn, error};
20use regex::Regex;
21use url::Url;
22use bytes::Bytes;
23use data_url::DataUrl;
24use reqwest::header::{RANGE, CONTENT_TYPE};
25use backoff::{future::retry_notify, ExponentialBackoff};
26use governor::{Quota, RateLimiter};
27use lazy_static::lazy_static;
28use xot::{xmlname, Xot};
29use crate::{MPD, Period, Representation, AdaptationSet, SegmentBase, DashMpdError};
30use crate::{parse, mux_audio_video, copy_video_to_container, copy_audio_to_container};
31use crate::{is_audio_adaptation, is_video_adaptation, is_subtitle_adaptation};
32use crate::{subtitle_type, content_protection_type, SubtitleType};
33use crate::check_conformity;
34#[cfg(not(feature = "libav"))]
35use crate::ffmpeg::concat_output_files;
36use crate::media::{temporary_outpath, AudioTrack};
37#[allow(unused_imports)]
38use crate::media::video_containers_concatable;
39
40
41pub type HttpClient = reqwest::Client;
43type DirectRateLimiter = RateLimiter<governor::state::direct::NotKeyed,
44 governor::state::InMemoryState,
45 governor::clock::DefaultClock,
46 governor::middleware::NoOpMiddleware>;
47
48
49pub fn partial_process_output(output: &[u8]) -> Cow<'_, str> {
52 let len = min(output.len(), 4096);
53 #[allow(clippy::indexing_slicing)]
54 String::from_utf8_lossy(&output[0..len])
55}
56
57
58fn tmp_file_path(prefix: &str, extension: &OsStr) -> Result<PathBuf, DashMpdError> {
61 if let Some(ext) = extension.to_str() {
62 let fmt = format!(".{}", extension.to_string_lossy());
64 let suffix = if ext.starts_with('.') {
65 extension
66 } else {
67 OsStr::new(&fmt)
68 };
69 let file = tempfile::Builder::new()
70 .prefix(prefix)
71 .suffix(suffix)
72 .rand_bytes(7)
73 .disable_cleanup(env::var("DASHMPD_PERSIST_FILES").is_ok())
74 .tempfile()
75 .map_err(|e| DashMpdError::Io(e, String::from("creating temporary file")))?;
76 Ok(file.path().to_path_buf())
77 } else {
78 Err(DashMpdError::Other(String::from("converting filename extension")))
79 }
80}
81
82
83#[cfg(unix)]
87fn ensure_permissions_readable(path: &PathBuf) -> Result<(), DashMpdError> {
88 use std::fs::Permissions;
89 use std::os::unix::fs::PermissionsExt;
90
91 let perms = Permissions::from_mode(0o644);
92 std::fs::set_permissions(path, perms)
93 .map_err(|e| DashMpdError::Io(e, String::from("setting file permissions")))?;
94 Ok(())
95}
96
97#[cfg(not(unix))]
98fn ensure_permissions_readable(path: &PathBuf) -> Result<(), DashMpdError> {
99 let mut perms = fs::metadata(path)
100 .map_err(|e| DashMpdError::Io(e, String::from("reading file permissions")))?
101 .permissions();
102 perms.set_readonly(false);
103 std::fs::set_permissions(path, perms)
104 .map_err(|e| DashMpdError::Io(e, String::from("setting file permissions")))?;
105 Ok(())
106}
107
108
109pub trait ProgressObserver: Send + Sync {
112 fn update(&self, percent: u32, message: &str);
113}
114
115
116#[derive(PartialEq, Eq, Clone, Copy, Default)]
119pub enum QualityPreference { #[default] Lowest, Intermediate, Highest }
120
121
122pub struct DashDownloader {
142 pub mpd_url: String,
143 pub redirected_url: Url,
144 base_url: Option<String>,
145 referer: Option<String>,
146 auth_username: Option<String>,
147 auth_password: Option<String>,
148 auth_bearer_token: Option<String>,
149 pub output_path: Option<PathBuf>,
150 http_client: Option<HttpClient>,
151 quality_preference: QualityPreference,
152 language_preference: Option<String>,
153 role_preference: Vec<String>,
154 video_width_preference: Option<u64>,
155 video_height_preference: Option<u64>,
156 fetch_video: bool,
157 fetch_audio: bool,
158 fetch_subtitles: bool,
159 keep_video: Option<PathBuf>,
160 keep_audio: Option<PathBuf>,
161 concatenate_periods: bool,
162 fragment_path: Option<PathBuf>,
163 decryption_keys: HashMap<String, String>,
164 xslt_stylesheets: Vec<PathBuf>,
165 minimum_period_duration: Option<Duration>,
166 content_type_checks: bool,
167 conformity_checks: bool,
168 use_index_range: bool,
169 fragment_retry_count: u32,
170 max_error_count: u32,
171 progress_observers: Vec<Arc<dyn ProgressObserver>>,
172 sleep_between_requests: u8,
173 allow_live_streams: bool,
174 force_duration: Option<f64>,
175 rate_limit: u64,
176 bw_limiter: Option<DirectRateLimiter>,
177 bw_estimator_started: Instant,
178 bw_estimator_bytes: usize,
179 pub verbosity: u8,
180 record_metainformation: bool,
181 pub muxer_preference: HashMap<String, String>,
182 pub concat_preference: HashMap<String, String>,
183 pub decryptor_preference: String,
184 pub ffmpeg_location: String,
185 pub vlc_location: String,
186 pub mkvmerge_location: String,
187 pub mp4box_location: String,
188 pub mp4decrypt_location: String,
189 pub shaka_packager_location: String,
190}
191
192
193#[cfg(not(doctest))]
196impl DashDownloader {
215 pub fn new(mpd_url: &str) -> DashDownloader {
221 DashDownloader {
222 mpd_url: String::from(mpd_url),
223 redirected_url: Url::parse(mpd_url).unwrap(),
224 base_url: None,
225 referer: None,
226 auth_username: None,
227 auth_password: None,
228 auth_bearer_token: None,
229 output_path: None,
230 http_client: None,
231 quality_preference: QualityPreference::Lowest,
232 language_preference: None,
233 role_preference: vec!["main".to_string(), "alternate".to_string()],
234 video_width_preference: None,
235 video_height_preference: None,
236 fetch_video: true,
237 fetch_audio: true,
238 fetch_subtitles: false,
239 keep_video: None,
240 keep_audio: None,
241 concatenate_periods: true,
242 fragment_path: None,
243 decryption_keys: HashMap::new(),
244 xslt_stylesheets: Vec::new(),
245 minimum_period_duration: None,
246 content_type_checks: true,
247 conformity_checks: true,
248 use_index_range: true,
249 fragment_retry_count: 10,
250 max_error_count: 30,
251 progress_observers: Vec::new(),
252 sleep_between_requests: 0,
253 allow_live_streams: false,
254 force_duration: None,
255 rate_limit: 0,
256 bw_limiter: None,
257 bw_estimator_started: Instant::now(),
258 bw_estimator_bytes: 0,
259 verbosity: 0,
260 record_metainformation: true,
261 muxer_preference: HashMap::new(),
262 concat_preference: HashMap::new(),
263 decryptor_preference: String::from("mp4decrypt"),
264 ffmpeg_location: String::from("ffmpeg"),
265 vlc_location: if cfg!(target_os = "windows") {
266 String::from("c:/Program Files/VideoLAN/VLC/vlc.exe")
269 } else {
270 String::from("vlc")
271 },
272 mkvmerge_location: String::from("mkvmerge"),
273 mp4box_location: if cfg!(target_os = "windows") {
274 String::from("MP4Box.exe")
275 } else if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
276 String::from("MP4Box")
277 } else {
278 String::from("mp4box")
279 },
280 mp4decrypt_location: String::from("mp4decrypt"),
281 shaka_packager_location: String::from("shaka-packager"),
282 }
283 }
284
285 #[must_use]
288 pub fn with_base_url(mut self, base_url: String) -> DashDownloader {
289 self.base_url = Some(base_url);
290 self
291 }
292
293
294 #[must_use]
316 pub fn with_http_client(mut self, client: HttpClient) -> DashDownloader {
317 self.http_client = Some(client);
318 self
319 }
320
321 #[must_use]
325 pub fn with_referer(mut self, referer: String) -> DashDownloader {
326 self.referer = Some(referer);
327 self
328 }
329
330 #[must_use]
333 pub fn with_authentication(mut self, username: &str, password: &str) -> DashDownloader {
334 self.auth_username = Some(username.to_string());
335 self.auth_password = Some(password.to_string());
336 self
337 }
338
339 #[must_use]
342 pub fn with_auth_bearer(mut self, token: &str) -> DashDownloader {
343 self.auth_bearer_token = Some(token.to_string());
344 self
345 }
346
347 #[must_use]
350 pub fn add_progress_observer(mut self, observer: Arc<dyn ProgressObserver>) -> DashDownloader {
351 self.progress_observers.push(observer);
352 self
353 }
354
355 #[must_use]
358 pub fn best_quality(mut self) -> DashDownloader {
359 self.quality_preference = QualityPreference::Highest;
360 self
361 }
362
363 #[must_use]
366 pub fn intermediate_quality(mut self) -> DashDownloader {
367 self.quality_preference = QualityPreference::Intermediate;
368 self
369 }
370
371 #[must_use]
374 pub fn worst_quality(mut self) -> DashDownloader {
375 self.quality_preference = QualityPreference::Lowest;
376 self
377 }
378
379 #[must_use]
386 pub fn prefer_language(mut self, lang: String) -> DashDownloader {
387 self.language_preference = Some(lang);
388 self
389 }
390
391 #[must_use]
401 pub fn prefer_roles(mut self, role_preference: Vec<String>) -> DashDownloader {
402 if role_preference.len() < u8::MAX as usize {
403 self.role_preference = role_preference;
404 } else {
405 warn!("Ignoring role_preference ordering due to excessive length");
406 }
407 self
408 }
409
410 #[must_use]
413 pub fn prefer_video_width(mut self, width: u64) -> DashDownloader {
414 self.video_width_preference = Some(width);
415 self
416 }
417
418 #[must_use]
421 pub fn prefer_video_height(mut self, height: u64) -> DashDownloader {
422 self.video_height_preference = Some(height);
423 self
424 }
425
426 #[must_use]
428 pub fn video_only(mut self) -> DashDownloader {
429 self.fetch_audio = false;
430 self.fetch_video = true;
431 self
432 }
433
434 #[must_use]
436 pub fn audio_only(mut self) -> DashDownloader {
437 self.fetch_audio = true;
438 self.fetch_video = false;
439 self
440 }
441
442 #[must_use]
445 pub fn keep_video_as<P: Into<PathBuf>>(mut self, video_path: P) -> DashDownloader {
446 self.keep_video = Some(video_path.into());
447 self
448 }
449
450 #[must_use]
453 pub fn keep_audio_as<P: Into<PathBuf>>(mut self, audio_path: P) -> DashDownloader {
454 self.keep_audio = Some(audio_path.into());
455 self
456 }
457
458 #[must_use]
461 pub fn save_fragments_to<P: Into<PathBuf>>(mut self, fragment_path: P) -> DashDownloader {
462 self.fragment_path = Some(fragment_path.into());
463 self
464 }
465
466 #[must_use]
478 pub fn add_decryption_key(mut self, id: String, key: String) -> DashDownloader {
479 self.decryption_keys.insert(id, key);
480 self
481 }
482
483 #[must_use]
495 pub fn with_xslt_stylesheet<P: Into<PathBuf>>(mut self, stylesheet: P) -> DashDownloader {
496 self.xslt_stylesheets.push(stylesheet.into());
497 self
498 }
499
500 #[must_use]
503 pub fn minimum_period_duration(mut self, value: Duration) -> DashDownloader {
504 self.minimum_period_duration = Some(value);
505 self
506 }
507
508 #[must_use]
512 pub fn fetch_audio(mut self, value: bool) -> DashDownloader {
513 self.fetch_audio = value;
514 self
515 }
516
517 #[must_use]
521 pub fn fetch_video(mut self, value: bool) -> DashDownloader {
522 self.fetch_video = value;
523 self
524 }
525
526 #[must_use]
534 pub fn fetch_subtitles(mut self, value: bool) -> DashDownloader {
535 self.fetch_subtitles = value;
536 self
537 }
538
539 #[must_use]
543 pub fn concatenate_periods(mut self, value: bool) -> DashDownloader {
544 self.concatenate_periods = value;
545 self
546 }
547
548 #[must_use]
551 pub fn without_content_type_checks(mut self) -> DashDownloader {
552 self.content_type_checks = false;
553 self
554 }
555
556 #[must_use]
559 pub fn content_type_checks(mut self, value: bool) -> DashDownloader {
560 self.content_type_checks = value;
561 self
562 }
563
564 #[must_use]
567 pub fn conformity_checks(mut self, value: bool) -> DashDownloader {
568 self.conformity_checks = value;
569 self
570 }
571
572 #[must_use]
587 pub fn use_index_range(mut self, value: bool) -> DashDownloader {
588 self.use_index_range = value;
589 self
590 }
591
592 #[must_use]
596 pub fn fragment_retry_count(mut self, count: u32) -> DashDownloader {
597 self.fragment_retry_count = count;
598 self
599 }
600
601 #[must_use]
608 pub fn max_error_count(mut self, count: u32) -> DashDownloader {
609 self.max_error_count = count;
610 self
611 }
612
613 #[must_use]
615 pub fn sleep_between_requests(mut self, seconds: u8) -> DashDownloader {
616 self.sleep_between_requests = seconds;
617 self
618 }
619
620 #[must_use]
632 pub fn allow_live_streams(mut self, value: bool) -> DashDownloader {
633 self.allow_live_streams = value;
634 self
635 }
636
637 #[must_use]
643 pub fn force_duration(mut self, seconds: f64) -> DashDownloader {
644 self.force_duration = Some(seconds);
645 self
646 }
647
648 #[must_use]
654 pub fn with_rate_limit(mut self, bps: u64) -> DashDownloader {
655 if bps < 10 * 1024 {
656 warn!("Limiting bandwidth below 10kB/s is unlikely to be stable");
657 }
658 if self.verbosity > 1 {
659 info!("Limiting bandwidth to {} kB/s", bps/1024);
660 }
661 self.rate_limit = bps;
662 let mut kps = 1 + bps / 1024;
668 if kps > u64::from(u32::MAX) {
669 warn!("Throttling bandwidth limit");
670 kps = u32::MAX.into();
671 }
672 if let Some(bw_limit) = NonZeroU32::new(kps as u32) {
673 if let Some(burst) = NonZeroU32::new(10 * 1024) {
674 let bw_quota = Quota::per_second(bw_limit)
675 .allow_burst(burst);
676 self.bw_limiter = Some(RateLimiter::direct(bw_quota));
677 }
678 }
679 self
680 }
681
682 #[must_use]
692 pub fn verbosity(mut self, level: u8) -> DashDownloader {
693 self.verbosity = level;
694 self
695 }
696
697 #[must_use]
701 pub fn record_metainformation(mut self, record: bool) -> DashDownloader {
702 self.record_metainformation = record;
703 self
704 }
705
706 #[must_use]
728 pub fn with_muxer_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
729 self.muxer_preference.insert(container.to_string(), ordering.to_string());
730 self
731 }
732
733 #[must_use]
756 pub fn with_concat_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
757 self.concat_preference.insert(container.to_string(), ordering.to_string());
758 self
759 }
760
761 #[must_use]
768 pub fn with_decryptor_preference(mut self, decryption_tool: &str) -> DashDownloader {
769 self.decryptor_preference = decryption_tool.to_string();
770 self
771 }
772
773 #[must_use]
788 pub fn with_ffmpeg(mut self, ffmpeg_path: &str) -> DashDownloader {
789 self.ffmpeg_location = ffmpeg_path.to_string();
790 self
791 }
792
793 #[must_use]
808 pub fn with_vlc(mut self, vlc_path: &str) -> DashDownloader {
809 self.vlc_location = vlc_path.to_string();
810 self
811 }
812
813 #[must_use]
821 pub fn with_mkvmerge(mut self, path: &str) -> DashDownloader {
822 self.mkvmerge_location = path.to_string();
823 self
824 }
825
826 #[must_use]
834 pub fn with_mp4box(mut self, path: &str) -> DashDownloader {
835 self.mp4box_location = path.to_string();
836 self
837 }
838
839 #[must_use]
847 pub fn with_mp4decrypt(mut self, path: &str) -> DashDownloader {
848 self.mp4decrypt_location = path.to_string();
849 self
850 }
851
852 #[must_use]
860 pub fn with_shaka_packager(mut self, path: &str) -> DashDownloader {
861 self.shaka_packager_location = path.to_string();
862 self
863 }
864
865 pub async fn download_to<P: Into<PathBuf>>(mut self, out: P) -> Result<PathBuf, DashMpdError> {
875 self.output_path = Some(out.into());
876 if self.http_client.is_none() {
877 let client = reqwest::Client::builder()
878 .timeout(Duration::new(30, 0))
879 .cookie_store(true)
880 .build()
881 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
882 self.http_client = Some(client);
883 }
884 fetch_mpd(&mut self).await
885 }
886
887 pub async fn download(mut self) -> Result<PathBuf, DashMpdError> {
894 let cwd = env::current_dir()
895 .map_err(|e| DashMpdError::Io(e, String::from("obtaining current directory")))?;
896 let filename = generate_filename_from_url(&self.mpd_url);
897 let outpath = cwd.join(filename);
898 self.output_path = Some(outpath);
899 if self.http_client.is_none() {
900 let client = reqwest::Client::builder()
901 .timeout(Duration::new(30, 0))
902 .cookie_store(true)
903 .build()
904 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
905 self.http_client = Some(client);
906 }
907 fetch_mpd(&mut self).await
908 }
909}
910
911
912fn mpd_is_dynamic(mpd: &MPD) -> bool {
913 if let Some(mpdtype) = mpd.mpdtype.as_ref() {
914 return mpdtype.eq("dynamic");
915 }
916 false
917}
918
919fn parse_range(range: &str) -> Result<(u64, u64), DashMpdError> {
922 let v: Vec<&str> = range.split_terminator('-').collect();
923 if v.len() != 2 {
924 return Err(DashMpdError::Parsing(format!("invalid range specifier: {range}")));
925 }
926 #[allow(clippy::indexing_slicing)]
927 let start: u64 = v[0].parse()
928 .map_err(|_| DashMpdError::Parsing(String::from("invalid start for range specifier")))?;
929 #[allow(clippy::indexing_slicing)]
930 let end: u64 = v[1].parse()
931 .map_err(|_| DashMpdError::Parsing(String::from("invalid end for range specifier")))?;
932 Ok((start, end))
933}
934
935#[derive(Debug)]
936struct MediaFragment {
937 period: u8,
938 url: Url,
939 start_byte: Option<u64>,
940 end_byte: Option<u64>,
941 is_init: bool,
942 timeout: Option<Duration>,
943}
944
945#[derive(Debug)]
946struct MediaFragmentBuilder {
947 period: u8,
948 url: Url,
949 start_byte: Option<u64>,
950 end_byte: Option<u64>,
951 is_init: bool,
952 timeout: Option<Duration>,
953}
954
955impl MediaFragmentBuilder {
956 pub fn new(period: u8, url: Url) -> MediaFragmentBuilder {
957 MediaFragmentBuilder {
958 period, url, start_byte: None, end_byte: None, is_init: false, timeout: None
959 }
960 }
961
962 pub fn with_range(mut self, start_byte: Option<u64>, end_byte: Option<u64>) -> MediaFragmentBuilder {
963 self.start_byte = start_byte;
964 self.end_byte = end_byte;
965 self
966 }
967
968 pub fn with_timeout(mut self, timeout: Duration) -> MediaFragmentBuilder {
969 self.timeout = Some(timeout);
970 self
971 }
972
973 pub fn set_init(mut self) -> MediaFragmentBuilder {
974 self.is_init = true;
975 self
976 }
977
978 pub fn build(self) -> MediaFragment {
979 MediaFragment {
980 period: self.period,
981 url: self.url,
982 start_byte: self.start_byte,
983 end_byte: self.end_byte,
984 is_init: self.is_init,
985 timeout: self.timeout
986 }
987 }
988}
989
990#[derive(Debug, Default)]
994struct PeriodOutputs {
995 fragments: Vec<MediaFragment>,
996 diagnostics: Vec<String>,
997 subtitle_formats: Vec<SubtitleType>,
998 selected_audio_language: String,
999}
1000
1001#[derive(Debug, Default)]
1002struct PeriodDownloads {
1003 audio_fragments: Vec<MediaFragment>,
1004 video_fragments: Vec<MediaFragment>,
1005 subtitle_fragments: Vec<MediaFragment>,
1006 subtitle_formats: Vec<SubtitleType>,
1007 period_counter: u8,
1008 id: Option<String>,
1009 selected_audio_language: String,
1010}
1011
1012fn period_fragment_count(pd: &PeriodDownloads) -> usize {
1013 pd.audio_fragments.len() +
1014 pd.video_fragments.len() +
1015 pd.subtitle_fragments.len()
1016}
1017
1018
1019
1020async fn throttle_download_rate(downloader: &DashDownloader, size: u32) -> Result<(), DashMpdError> {
1021 if downloader.rate_limit > 0 {
1022 if let Some(cells) = NonZeroU32::new(size) {
1023 if let Some(limiter) = downloader.bw_limiter.as_ref() {
1024 #[allow(clippy::redundant_pattern_matching)]
1025 if let Err(_) = limiter.until_n_ready(cells).await {
1026 return Err(DashMpdError::Other(
1027 "Bandwidth limit is too low".to_string()));
1028 }
1029 }
1030 }
1031 }
1032 Ok(())
1033}
1034
1035
1036fn generate_filename_from_url(url: &str) -> PathBuf {
1037 use sanitise_file_name::{sanitise_with_options, Options};
1038
1039 let mut path = url;
1040 if let Some(p) = path.strip_prefix("http://") {
1041 path = p;
1042 } else if let Some(p) = path.strip_prefix("https://") {
1043 path = p;
1044 } else if let Some(p) = path.strip_prefix("file://") {
1045 path = p;
1046 }
1047 if let Some(p) = path.strip_prefix("www.") {
1048 path = p;
1049 }
1050 if let Some(p) = path.strip_prefix("ftp.") {
1051 path = p;
1052 }
1053 if let Some(p) = path.strip_suffix(".mpd") {
1054 path = p;
1055 }
1056 let mut sanitize_opts = Options::DEFAULT;
1057 sanitize_opts.length_limit = 150;
1058 PathBuf::from(sanitise_with_options(path, &sanitize_opts) + ".mp4")
1063}
1064
1065fn output_path_for_period(base: &Path, period: u8) -> PathBuf {
1082 assert!(period > 0);
1083 if period == 1 {
1084 base.to_path_buf()
1085 } else {
1086 if let Some(stem) = base.file_stem() {
1087 if let Some(ext) = base.extension() {
1088 let fname = format!("{}-p{period}.{}", stem.to_string_lossy(), ext.to_string_lossy());
1089 return base.with_file_name(fname);
1090 }
1091 }
1092 let p = format!("dashmpd-p{period}");
1093 tmp_file_path(&p, base.extension().unwrap_or(OsStr::new("mp4")))
1094 .unwrap_or_else(|_| p.into())
1095 }
1096}
1097
1098fn is_absolute_url(s: &str) -> bool {
1099 s.starts_with("http://") ||
1100 s.starts_with("https://") ||
1101 s.starts_with("file://") ||
1102 s.starts_with("ftp://")
1103}
1104
1105fn merge_baseurls(current: &Url, new: &str) -> Result<Url, DashMpdError> {
1106 if is_absolute_url(new) {
1107 Url::parse(new)
1108 .map_err(|e| parse_error("parsing BaseURL", e))
1109 } else {
1110 let mut merged = current.join(new)
1123 .map_err(|e| parse_error("joining base with BaseURL", e))?;
1124 if merged.query().is_none() {
1125 merged.set_query(current.query());
1126 }
1127 Ok(merged)
1128 }
1129}
1130
1131fn content_type_audio_p(response: &reqwest::Response) -> bool {
1136 match response.headers().get("content-type") {
1137 Some(ct) => {
1138 let ctb = ct.as_bytes();
1139 ctb.starts_with(b"audio/") ||
1140 ctb.starts_with(b"video/") ||
1141 ctb.starts_with(b"application/octet-stream")
1142 },
1143 None => false,
1144 }
1145}
1146
1147fn content_type_video_p(response: &reqwest::Response) -> bool {
1149 match response.headers().get("content-type") {
1150 Some(ct) => {
1151 let ctb = ct.as_bytes();
1152 ctb.starts_with(b"video/") ||
1153 ctb.starts_with(b"application/octet-stream")
1154 },
1155 None => false,
1156 }
1157}
1158
1159
1160fn adaptation_lang_distance(a: &AdaptationSet, language_preference: &str) -> u8 {
1164 if let Some(lang) = &a.lang {
1165 if lang.eq(language_preference) {
1166 return 0;
1167 }
1168 if lang[0..2].eq(&language_preference[0..2]) {
1169 return 5;
1170 }
1171 100
1172 } else {
1173 100
1174 }
1175}
1176
1177fn adaptation_roles(a: &AdaptationSet) -> Vec<String> {
1180 let mut roles = Vec::new();
1181 for r in &a.Role {
1182 if let Some(rv) = &r.value {
1183 roles.push(String::from(rv));
1184 }
1185 }
1186 for cc in &a.ContentComponent {
1187 for r in &cc.Role {
1188 if let Some(rv) = &r.value {
1189 roles.push(String::from(rv));
1190 }
1191 }
1192 }
1193 roles
1194}
1195
1196fn adaptation_role_distance(a: &AdaptationSet, role_preference: &[String]) -> u8 {
1198 adaptation_roles(a).iter()
1199 .map(|r| role_preference.binary_search(r).unwrap_or(u8::MAX.into()))
1200 .map(|u| u8::try_from(u).unwrap_or(u8::MAX))
1201 .min()
1202 .unwrap_or(u8::MAX)
1203}
1204
1205
1206fn select_preferred_adaptations<'a>(
1214 adaptations: Vec<&'a AdaptationSet>,
1215 downloader: &DashDownloader) -> Vec<&'a AdaptationSet>
1216{
1217 let mut preferred: Vec<&'a AdaptationSet>;
1218 if let Some(ref lang) = downloader.language_preference {
1220 preferred = Vec::new();
1221 let distance: Vec<u8> = adaptations.iter()
1222 .map(|a| adaptation_lang_distance(a, lang))
1223 .collect();
1224 let min_distance = distance.iter().min().unwrap_or(&0);
1225 for (i, a) in adaptations.iter().enumerate() {
1226 if let Some(di) = distance.get(i) {
1227 if di == min_distance {
1228 preferred.push(a);
1229 }
1230 }
1231 }
1232 } else {
1233 preferred = adaptations;
1234 }
1235 let role_distance: Vec<u8> = preferred.iter()
1241 .map(|a| adaptation_role_distance(a, &downloader.role_preference))
1242 .collect();
1243 let role_distance_min = role_distance.iter().min().unwrap_or(&0);
1244 let mut best = Vec::new();
1245 for (i, a) in preferred.into_iter().enumerate() {
1246 if let Some(rdi) = role_distance.get(i) {
1247 if rdi == role_distance_min {
1248 best.push(a);
1249 }
1250 }
1251 }
1252 best
1253}
1254
1255
1256fn select_preferred_representation<'a>(
1262 representations: &[&'a Representation],
1263 downloader: &DashDownloader) -> Option<&'a Representation>
1264{
1265 if representations.iter().all(|x| x.qualityRanking.is_some()) {
1266 match downloader.quality_preference {
1269 QualityPreference::Lowest =>
1270 representations.iter()
1271 .max_by_key(|r| r.qualityRanking.unwrap_or(u8::MAX))
1272 .copied(),
1273 QualityPreference::Highest =>
1274 representations.iter().min_by_key(|r| r.qualityRanking.unwrap_or(0))
1275 .copied(),
1276 QualityPreference::Intermediate => {
1277 let count = representations.len();
1278 match count {
1279 0 => None,
1280 1 => Some(representations[0]),
1281 _ => {
1282 let mut ranking: Vec<u8> = representations.iter()
1283 .map(|r| r.qualityRanking.unwrap_or(u8::MAX))
1284 .collect();
1285 ranking.sort_unstable();
1286 if let Some(want_ranking) = ranking.get(count / 2) {
1287 representations.iter()
1288 .find(|r| r.qualityRanking.unwrap_or(u8::MAX) == *want_ranking)
1289 .copied()
1290 } else {
1291 representations.first().copied()
1292 }
1293 },
1294 }
1295 },
1296 }
1297 } else {
1298 match downloader.quality_preference {
1300 QualityPreference::Lowest => representations.iter()
1301 .min_by_key(|r| r.bandwidth.unwrap_or(1_000_000_000))
1302 .copied(),
1303 QualityPreference::Highest => representations.iter()
1304 .max_by_key(|r| r.bandwidth.unwrap_or(0))
1305 .copied(),
1306 QualityPreference::Intermediate => {
1307 let count = representations.len();
1308 match count {
1309 0 => None,
1310 1 => Some(representations[0]),
1311 _ => {
1312 let mut ranking: Vec<u64> = representations.iter()
1313 .map(|r| r.bandwidth.unwrap_or(100_000_000))
1314 .collect();
1315 ranking.sort_unstable();
1316 if let Some(want_ranking) = ranking.get(count / 2) {
1317 representations.iter()
1318 .find(|r| r.bandwidth.unwrap_or(100_000_000) == *want_ranking)
1319 .copied()
1320 } else {
1321 representations.first().copied()
1322 }
1323 },
1324 }
1325 },
1326 }
1327 }
1328}
1329
1330
1331fn print_available_subtitles_representation(r: &Representation, a: &AdaptationSet) {
1333 let unspecified = "<unspecified>".to_string();
1334 let empty = "".to_string();
1335 let lang = r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unspecified));
1336 let codecs = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&empty));
1337 let typ = subtitle_type(&a);
1338 let stype = if !codecs.is_empty() {
1339 format!("{typ:?}/{codecs}")
1340 } else {
1341 format!("{typ:?}")
1342 };
1343 let role = a.Role.first()
1344 .map_or_else(|| String::from(""),
1345 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1346 let label = a.Label.first()
1347 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1348 info!(" subs {stype:>18} | {lang:>10} |{role}{label}");
1349}
1350
1351fn print_available_subtitles_adaptation(a: &AdaptationSet) {
1352 a.representations.iter()
1353 .for_each(|r| print_available_subtitles_representation(r, a));
1354}
1355
1356fn print_available_streams_representation(r: &Representation, a: &AdaptationSet, typ: &str) {
1358 let unspecified = "<unspecified>".to_string();
1360 let w = r.width.unwrap_or(a.width.unwrap_or(0));
1361 let h = r.height.unwrap_or(a.height.unwrap_or(0));
1362 let codec = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&unspecified));
1363 let bw = r.bandwidth.unwrap_or(a.maxBandwidth.unwrap_or(0));
1364 let fmt = if typ.eq("audio") {
1365 let unknown = String::from("?");
1366 format!("lang={}", r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unknown)))
1367 } else if w == 0 || h == 0 {
1368 String::from("")
1371 } else {
1372 format!("{w}x{h}")
1373 };
1374 let role = a.Role.first()
1375 .map_or_else(|| String::from(""),
1376 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1377 let label = a.Label.first()
1378 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1379 info!(" {typ} {codec:17} | {:5} Kbps | {fmt:>9}{role}{label}", bw / 1024);
1380}
1381
1382fn print_available_streams_adaptation(a: &AdaptationSet, typ: &str) {
1383 a.representations.iter()
1384 .for_each(|r| print_available_streams_representation(r, a, typ));
1385}
1386
1387fn print_available_streams_period(p: &Period) {
1388 p.adaptations.iter()
1389 .filter(is_audio_adaptation)
1390 .for_each(|a| print_available_streams_adaptation(a, "audio"));
1391 p.adaptations.iter()
1392 .filter(is_video_adaptation)
1393 .for_each(|a| print_available_streams_adaptation(a, "video"));
1394 p.adaptations.iter()
1395 .filter(is_subtitle_adaptation)
1396 .for_each(print_available_subtitles_adaptation);
1397}
1398
1399#[tracing::instrument(level="trace", skip_all)]
1400fn print_available_streams(mpd: &MPD) {
1401 let mut counter = 0;
1402 for p in &mpd.periods {
1403 let mut period_duration_secs: f64 = 0.0;
1404 if let Some(d) = mpd.mediaPresentationDuration {
1405 period_duration_secs = d.as_secs_f64();
1406 }
1407 if let Some(d) = &p.duration {
1408 period_duration_secs = d.as_secs_f64();
1409 }
1410 counter += 1;
1411 if let Some(id) = p.id.as_ref() {
1412 info!("Streams in period {id} (#{counter}), duration {period_duration_secs:.3}s:");
1413 } else {
1414 info!("Streams in period #{counter}, duration {period_duration_secs:.3}s:");
1415 }
1416 print_available_streams_period(p);
1417 }
1418}
1419
1420async fn extract_init_pssh(downloader: &DashDownloader, init_url: Url) -> Option<Vec<u8>> {
1421 use bstr::ByteSlice;
1422 use hex_literal::hex;
1423
1424 if let Some(client) = downloader.http_client.as_ref() {
1425 let mut req = client.get(init_url);
1426 if let Some(referer) = &downloader.referer {
1427 req = req.header("Referer", referer);
1428 }
1429 if let Some(username) = &downloader.auth_username {
1430 if let Some(password) = &downloader.auth_password {
1431 req = req.basic_auth(username, Some(password));
1432 }
1433 }
1434 if let Some(token) = &downloader.auth_bearer_token {
1435 req = req.bearer_auth(token);
1436 }
1437 if let Ok(mut resp) = req.send().await {
1438 let mut chunk_counter = 0;
1441 let mut segment_first_bytes = Vec::<u8>::new();
1442 while let Ok(Some(chunk)) = resp.chunk().await {
1443 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
1444 #[allow(clippy::redundant_pattern_matching)]
1445 if let Err(_) = throttle_download_rate(downloader, size).await {
1446 return None;
1447 }
1448 segment_first_bytes.append(&mut chunk.to_vec());
1449 chunk_counter += 1;
1450 if chunk_counter > 20 {
1451 break;
1452 }
1453 }
1454 let needle = b"pssh";
1455 for offset in segment_first_bytes.find_iter(needle) {
1456 #[allow(clippy::needless_range_loop)]
1457 for i in offset-4..offset+2 {
1458 if let Some(b) = segment_first_bytes.get(i) {
1459 if *b != 0 {
1460 continue;
1461 }
1462 }
1463 }
1464 #[allow(clippy::needless_range_loop)]
1465 for i in offset+4..offset+8 {
1466 if let Some(b) = segment_first_bytes.get(i) {
1467 if *b != 0 {
1468 continue;
1469 }
1470 }
1471 }
1472 if offset+24 > segment_first_bytes.len() {
1473 continue;
1474 }
1475 const WIDEVINE_SYSID: [u8; 16] = hex!("edef8ba979d64acea3c827dcd51d21ed");
1477 if let Some(sysid) = segment_first_bytes.get((offset+8)..(offset+24)) {
1478 if !sysid.eq(&WIDEVINE_SYSID) {
1479 continue;
1480 }
1481 }
1482 if let Some(length) = segment_first_bytes.get(offset-1) {
1483 let start = offset - 4;
1484 let end = start + *length as usize;
1485 if let Some(pssh) = &segment_first_bytes.get(start..end) {
1486 return Some(pssh.to_vec());
1487 }
1488 }
1489 }
1490 }
1491 None
1492 } else {
1493 None
1494 }
1495}
1496
1497
1498lazy_static! {
1507 static ref URL_TEMPLATE_IDS: Vec<(&'static str, String, Regex)> = {
1508 vec!["RepresentationID", "Number", "Time", "Bandwidth"].into_iter()
1509 .map(|k| (k, format!("${k}$"), Regex::new(&format!("\\${k}%0([\\d])d\\$")).unwrap()))
1510 .collect()
1511 };
1512}
1513
1514fn resolve_url_template(template: &str, params: &HashMap<&str, String>) -> String {
1515 let mut result = template.to_string();
1516 for (k, ident, rx) in URL_TEMPLATE_IDS.iter() {
1517 if result.contains(ident) {
1519 if let Some(value) = params.get(k as &str) {
1520 result = result.replace(ident, value);
1521 }
1522 }
1523 if let Some(cap) = rx.captures(&result) {
1525 if let Some(value) = params.get(k as &str) {
1526 if let Ok(width) = cap[1].parse::<usize>() {
1527 if let Some(m) = rx.find(&result) {
1528 let count = format!("{value:0>width$}");
1529 result = result[..m.start()].to_owned() + &count + &result[m.end()..];
1530 }
1531 }
1532 }
1533 }
1534 }
1535 result
1536}
1537
1538
1539fn reqwest_error_transient_p(e: &reqwest::Error) -> bool {
1540 if e.is_timeout() {
1541 return true;
1542 }
1543 if let Some(s) = e.status() {
1544 if s == reqwest::StatusCode::REQUEST_TIMEOUT ||
1545 s == reqwest::StatusCode::TOO_MANY_REQUESTS ||
1546 s == reqwest::StatusCode::SERVICE_UNAVAILABLE ||
1547 s == reqwest::StatusCode::GATEWAY_TIMEOUT {
1548 return true;
1549 }
1550 }
1551 false
1552}
1553
1554fn categorize_reqwest_error(e: reqwest::Error) -> backoff::Error<reqwest::Error> {
1555 if reqwest_error_transient_p(&e) {
1556 backoff::Error::retry_after(e, Duration::new(5, 0))
1557 } else {
1558 backoff::Error::permanent(e)
1559 }
1560}
1561
1562fn notify_transient<E: std::fmt::Debug>(err: E, dur: Duration) {
1563 warn!("Transient error after {dur:?}: {err:?}");
1564}
1565
1566fn network_error(why: &str, e: &reqwest::Error) -> DashMpdError {
1567 if e.is_timeout() {
1568 DashMpdError::NetworkTimeout(format!("{why}: {e:?}"))
1569 } else if e.is_connect() {
1570 DashMpdError::NetworkConnect(format!("{why}: {e:?}"))
1571 } else {
1572 DashMpdError::Network(format!("{why}: {e:?}"))
1573 }
1574}
1575
1576fn parse_error(why: &str, e: impl std::error::Error) -> DashMpdError {
1577 DashMpdError::Parsing(format!("{why}: {e:#?}"))
1578}
1579
1580
1581async fn reqwest_bytes_with_retries(
1585 client: &reqwest::Client,
1586 req: reqwest::Request,
1587 retry_count: u32) -> Result<Bytes, reqwest::Error>
1588{
1589 let mut last_error = None;
1590 for _ in 0..retry_count {
1591 if let Some(rqw) = req.try_clone() {
1592 match client.execute(rqw).await {
1593 Ok(response) => {
1594 match response.error_for_status() {
1595 Ok(resp) => {
1596 match resp.bytes().await {
1597 Ok(bytes) => return Ok(bytes),
1598 Err(e) => {
1599 info!("Retrying after HTTP error {e:?}");
1600 last_error = Some(e);
1601 },
1602 }
1603 },
1604 Err(e) => {
1605 info!("Retrying after HTTP error {e:?}");
1606 last_error = Some(e);
1607 },
1608 }
1609 },
1610 Err(e) => {
1611 info!("Retrying after HTTP error {e:?}");
1612 last_error = Some(e);
1613 },
1614 }
1615 }
1616 }
1617 Err(last_error.unwrap())
1618}
1619
1620#[allow(unused_variables)]
1633fn maybe_record_metainformation(path: &Path, downloader: &DashDownloader, mpd: &MPD) {
1634 #[cfg(target_family = "unix")]
1635 if downloader.record_metainformation && (downloader.fetch_audio || downloader.fetch_video) {
1636 if let Ok(origin_url) = Url::parse(&downloader.mpd_url) {
1637 #[allow(clippy::collapsible_if)]
1639 if origin_url.username().is_empty() && origin_url.password().is_none() {
1640 #[cfg(target_family = "unix")]
1641 if xattr::set(path, "user.xdg.origin.url", downloader.mpd_url.as_bytes()).is_err() {
1642 info!("Failed to set user.xdg.origin.url xattr on output file");
1643 }
1644 }
1645 for pi in &mpd.ProgramInformation {
1646 if let Some(t) = &pi.Title {
1647 if let Some(tc) = &t.content {
1648 if xattr::set(path, "user.dublincore.title", tc.as_bytes()).is_err() {
1649 info!("Failed to set user.dublincore.title xattr on output file");
1650 }
1651 }
1652 }
1653 if let Some(source) = &pi.Source {
1654 if let Some(sc) = &source.content {
1655 if xattr::set(path, "user.dublincore.source", sc.as_bytes()).is_err() {
1656 info!("Failed to set user.dublincore.source xattr on output file");
1657 }
1658 }
1659 }
1660 if let Some(copyright) = &pi.Copyright {
1661 if let Some(cc) = ©right.content {
1662 if xattr::set(path, "user.dublincore.rights", cc.as_bytes()).is_err() {
1663 info!("Failed to set user.dublincore.rights xattr on output file");
1664 }
1665 }
1666 }
1667 }
1668 }
1669 }
1670}
1671
1672fn fetchable_xlink_href(href: &str) -> bool {
1676 (!href.is_empty()) && href.ne("urn:mpeg:dash:resolve-to-zero:2013")
1677}
1678
1679fn element_resolves_to_zero(xot: &mut Xot, element: xot::Node) -> bool {
1680 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
1681 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
1682 if let Some(href) = xot.get_attribute(element, xlink_href_name.into()) {
1683 return href.eq("urn:mpeg:dash:resolve-to-zero:2013");
1684 }
1685 false
1686}
1687
1688fn skip_xml_preamble(input: &str) -> &str {
1689 if input.starts_with("<?xml") {
1690 if let Some(end_pos) = input.find("?>") {
1691 return &input[end_pos + 2..]; }
1694 }
1695 input
1697}
1698
1699fn apply_xslt_stylesheets_xsltproc(
1703 downloader: &DashDownloader,
1704 xot: &mut Xot,
1705 doc: xot::Node) -> Result<String, DashMpdError> {
1706 let mut buf = Vec::new();
1707 xot.write(doc, &mut buf)
1708 .map_err(|e| parse_error("serializing rewritten manifest", e))?;
1709 for ss in &downloader.xslt_stylesheets {
1710 if downloader.verbosity > 0 {
1711 info!("Applying XSLT stylesheet {} with xsltproc", ss.display());
1712 }
1713 let tmpmpd = tmp_file_path("dashxslt", OsStr::new("xslt"))?;
1714 fs::write(&tmpmpd, &buf)
1715 .map_err(|e| DashMpdError::Io(e, String::from("writing MPD")))?;
1716 let xsltproc = Command::new("xsltproc")
1717 .args([ss, &tmpmpd])
1718 .output()
1719 .map_err(|e| DashMpdError::Io(e, String::from("spawning xsltproc")))?;
1720 if !xsltproc.status.success() {
1721 let msg = format!("xsltproc returned {}", xsltproc.status);
1722 let out = partial_process_output(&xsltproc.stderr).to_string();
1723 return Err(DashMpdError::Io(std::io::Error::other(msg), out));
1724 }
1725 if env::var("DASHMPD_PERSIST_FILES").is_err() {
1726 if let Err(e) = fs::remove_file(&tmpmpd) {
1727 warn!("Error removing temporary MPD after XSLT processing: {e:?}");
1728 }
1729 }
1730 buf.clone_from(&xsltproc.stdout);
1731 if downloader.verbosity > 2 {
1732 println!("Rewritten XSLT: {}", String::from_utf8_lossy(&buf));
1733 }
1734 }
1735 String::from_utf8(buf)
1736 .map_err(|e| parse_error("parsing UTF-8", e))
1737}
1738
1739async fn resolve_xlink_references(
1774 downloader: &DashDownloader,
1775 xot: &mut Xot,
1776 node: xot::Node) -> Result<(), DashMpdError>
1777{
1778 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
1779 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
1780 let xlinked = xot.descendants(node)
1781 .filter(|d| xot.get_attribute(*d, xlink_href_name.into()).is_some())
1782 .collect::<Vec<_>>();
1783 for xl in xlinked {
1784 if element_resolves_to_zero(xot, xl) {
1785 trace!("Removing node with resolve-to-zero xlink:href {xl:?}");
1786 if let Err(e) = xot.remove(xl) {
1787 return Err(parse_error("Failed to remove resolve-to-zero XML node", e));
1788 }
1789 } else if let Some(href) = xot.get_attribute(xl, xlink_href_name.into()) {
1790 if fetchable_xlink_href(href) {
1791 let xlink_url = if is_absolute_url(href) {
1792 Url::parse(href)
1793 .map_err(|e|
1794 if let Ok(ns) = xot.to_string(node) {
1795 parse_error(&format!("parsing XLink on {ns}"), e)
1796 } else {
1797 parse_error("parsing XLink", e)
1798 }
1799 )?
1800 } else {
1801 let mut merged = downloader.redirected_url.join(href)
1804 .map_err(|e|
1805 if let Ok(ns) = xot.to_string(node) {
1806 parse_error(&format!("parsing XLink on {ns}"), e)
1807 } else {
1808 parse_error("parsing XLink", e)
1809 }
1810 )?;
1811 merged.set_query(downloader.redirected_url.query());
1812 merged
1813 };
1814 let client = downloader.http_client.as_ref().unwrap();
1815 trace!("Fetching XLinked element {}", xlink_url.clone());
1816 let mut req = client.get(xlink_url.clone())
1817 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
1818 .header("Accept-Language", "en-US,en")
1819 .header("Sec-Fetch-Mode", "navigate");
1820 if let Some(referer) = &downloader.referer {
1821 req = req.header("Referer", referer);
1822 } else {
1823 req = req.header("Referer", downloader.redirected_url.to_string());
1824 }
1825 if let Some(username) = &downloader.auth_username {
1826 if let Some(password) = &downloader.auth_password {
1827 req = req.basic_auth(username, Some(password));
1828 }
1829 }
1830 if let Some(token) = &downloader.auth_bearer_token {
1831 req = req.bearer_auth(token);
1832 }
1833 let xml = req.send().await
1834 .map_err(|e|
1835 if let Ok(ns) = xot.to_string(node) {
1836 network_error(&format!("fetching XLink for {ns}"), &e)
1837 } else {
1838 network_error("fetching XLink", &e)
1839 }
1840 )?
1841 .error_for_status()
1842 .map_err(|e|
1843 if let Ok(ns) = xot.to_string(node) {
1844 network_error(&format!("fetching XLink for {ns}"), &e)
1845 } else {
1846 network_error("fetching XLink", &e)
1847 }
1848 )?
1849 .text().await
1850 .map_err(|e|
1851 if let Ok(ns) = xot.to_string(node) {
1852 network_error(&format!("resolving XLink for {ns}"), &e)
1853 } else {
1854 network_error("resolving XLink", &e)
1855 }
1856 )?;
1857 if downloader.verbosity > 2 {
1858 if let Ok(ns) = xot.to_string(node) {
1859 info!(" Resolved onLoad XLink {xlink_url} on {ns} -> {} octets", xml.len());
1860 } else {
1861 info!(" Resolved onLoad XLink {xlink_url} -> {} octets", xml.len());
1862 }
1863 }
1864 let wrapped_xml = r#"<?xml version="1.0" encoding="utf-8"?>"#.to_owned() +
1870 r#"<wrapper xmlns="urn:mpeg:dash:schema:mpd:2011" "# +
1871 r#"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" "# +
1872 r#"xmlns:cenc="urn:mpeg:cenc:2013" "# +
1873 r#"xmlns:mspr="urn:microsoft:playready" "# +
1874 r#"xmlns:xlink="http://www.w3.org/1999/xlink">"# +
1875 skip_xml_preamble(&xml) +
1876 r"</wrapper>";
1877 let wrapper_doc = xot.parse(&wrapped_xml)
1878 .map_err(|e| parse_error("parsing xlinked content", e))?;
1879 let wrapper_doc_el = xot.document_element(wrapper_doc)
1880 .map_err(|e| parse_error("extracting XML document element", e))?;
1881 for needs_insertion in xot.children(wrapper_doc_el).collect::<Vec<_>>() {
1882 xot.insert_after(xl, needs_insertion)
1884 .map_err(|e| parse_error("inserting XLinked content", e))?;
1885 }
1886 xot.remove(xl)
1887 .map_err(|e| parse_error("removing XLink node", e))?;
1888 }
1889 }
1890 }
1891 Ok(())
1892}
1893
1894#[tracing::instrument(level="trace", skip_all)]
1895pub async fn parse_resolving_xlinks(
1896 downloader: &DashDownloader,
1897 xml: &[u8]) -> Result<MPD, DashMpdError>
1898{
1899 use xot::xmlname::NameStrInfo;
1900
1901 let mut xot = Xot::new();
1902 let doc = xot.parse_bytes(xml)
1903 .map_err(|e| parse_error("XML parsing", e))?;
1904 let doc_el = xot.document_element(doc)
1905 .map_err(|e| parse_error("extracting XML document element", e))?;
1906 let doc_name = match xot.node_name(doc_el) {
1907 Some(n) => n,
1908 None => return Err(DashMpdError::Parsing(String::from("missing root node name"))),
1909 };
1910 let root_name = xot.name_ref(doc_name, doc_el)
1911 .map_err(|e| parse_error("extracting root node name", e))?;
1912 let root_local_name = root_name.local_name();
1913 if !root_local_name.eq("MPD") {
1914 return Err(DashMpdError::Parsing(format!("root element is {root_local_name}, expecting <MPD>")));
1915 }
1916 for _ in 1..5 {
1919 resolve_xlink_references(downloader, &mut xot, doc).await?;
1920 }
1921 let rewritten = apply_xslt_stylesheets_xsltproc(downloader, &mut xot, doc)?;
1922 let mpd = parse(&rewritten)?;
1924 if downloader.conformity_checks {
1925 for emsg in check_conformity(&mpd) {
1926 warn!("DASH conformity error in manifest: {emsg}");
1927 }
1928 }
1929 Ok(mpd)
1930}
1931
1932async fn do_segmentbase_indexrange(
1933 downloader: &DashDownloader,
1934 period_counter: u8,
1935 base_url: Url,
1936 sb: &SegmentBase,
1937 dict: &HashMap<&str, String>
1938) -> Result<Vec<MediaFragment>, DashMpdError>
1939{
1940 let mut fragments = Vec::new();
1973 let mut start_byte: Option<u64> = None;
1974 let mut end_byte: Option<u64> = None;
1975 let mut indexable_segments = false;
1976 if downloader.use_index_range {
1977 if let Some(ir) = &sb.indexRange {
1978 let (s, e) = parse_range(ir)?;
1980 trace!("Fetching sidx for {}", base_url.clone());
1981 let mut req = downloader.http_client.as_ref()
1982 .unwrap()
1983 .get(base_url.clone())
1984 .header(RANGE, format!("bytes={s}-{e}"))
1985 .header("Referer", downloader.redirected_url.to_string())
1986 .header("Sec-Fetch-Mode", "navigate");
1987 if let Some(username) = &downloader.auth_username {
1988 if let Some(password) = &downloader.auth_password {
1989 req = req.basic_auth(username, Some(password));
1990 }
1991 }
1992 if let Some(token) = &downloader.auth_bearer_token {
1993 req = req.bearer_auth(token);
1994 }
1995 let mut resp = req.send().await
1996 .map_err(|e| network_error("fetching index data", &e))?
1997 .error_for_status()
1998 .map_err(|e| network_error("fetching index data", &e))?;
1999 let headers = std::mem::take(resp.headers_mut());
2000 if let Some(content_type) = headers.get(CONTENT_TYPE) {
2001 let idx = resp.bytes().await
2002 .map_err(|e| network_error("fetching index data", &e))?;
2003 if idx.len() as u64 != e - s + 1 {
2004 warn!(" HTTP server does not support Range requests; can't use indexRange addressing");
2005 } else {
2006 #[allow(clippy::collapsible_else_if)]
2007 if content_type.eq("video/mp4") ||
2008 content_type.eq("audio/mp4") {
2009 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2016 .with_range(Some(0), Some(e))
2017 .build();
2018 fragments.push(mf);
2019 let mut max_chunk_pos = 0;
2020 if let Ok(segment_chunks) = crate::sidx::from_isobmff_sidx(&idx, e+1) {
2021 trace!("Have {} segment chunks in sidx data", segment_chunks.len());
2022 for chunk in segment_chunks {
2023 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2024 .with_range(Some(chunk.start), Some(chunk.end))
2025 .build();
2026 fragments.push(mf);
2027 if chunk.end > max_chunk_pos {
2028 max_chunk_pos = chunk.end;
2029 }
2030 }
2031 indexable_segments = true;
2032 }
2033 }
2034 }
2041 }
2042 }
2043 }
2044 if indexable_segments {
2045 if let Some(init) = &sb.Initialization {
2046 if let Some(range) = &init.range {
2047 let (s, e) = parse_range(range)?;
2048 start_byte = Some(s);
2049 end_byte = Some(e);
2050 }
2051 if let Some(su) = &init.sourceURL {
2052 let path = resolve_url_template(su, dict);
2053 let u = merge_baseurls(&base_url, &path)?;
2054 let mf = MediaFragmentBuilder::new(period_counter, u)
2055 .with_range(start_byte, end_byte)
2056 .set_init()
2057 .build();
2058 fragments.push(mf);
2059 } else {
2060 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2062 .with_range(start_byte, end_byte)
2063 .set_init()
2064 .build();
2065 fragments.push(mf);
2066 }
2067 }
2068 } else {
2069 trace!("Falling back to retrieving full SegmentBase for {}", base_url.clone());
2074 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2075 .with_timeout(Duration::new(10_000, 0))
2076 .build();
2077 fragments.push(mf);
2078 }
2079 Ok(fragments)
2080}
2081
2082
2083#[tracing::instrument(level="trace", skip_all)]
2084async fn do_period_audio(
2085 downloader: &DashDownloader,
2086 mpd: &MPD,
2087 period: &Period,
2088 period_counter: u8,
2089 base_url: Url
2090 ) -> Result<PeriodOutputs, DashMpdError>
2091{
2092 let mut fragments = Vec::new();
2093 let mut diagnostics = Vec::new();
2094 let mut opt_init: Option<String> = None;
2095 let mut opt_media: Option<String> = None;
2096 let mut opt_duration: Option<f64> = None;
2097 let mut timescale = 1;
2098 let mut start_number = 1;
2099 let mut period_duration_secs: f64 = 0.0;
2102 if let Some(d) = mpd.mediaPresentationDuration {
2103 period_duration_secs = d.as_secs_f64();
2104 }
2105 if let Some(d) = period.duration {
2106 period_duration_secs = d.as_secs_f64();
2107 }
2108 if let Some(s) = downloader.force_duration {
2109 period_duration_secs = s;
2110 }
2111 if let Some(st) = &period.SegmentTemplate {
2115 if let Some(i) = &st.initialization {
2116 opt_init = Some(i.clone());
2117 }
2118 if let Some(m) = &st.media {
2119 opt_media = Some(m.clone());
2120 }
2121 if let Some(d) = st.duration {
2122 opt_duration = Some(d);
2123 }
2124 if let Some(ts) = st.timescale {
2125 timescale = ts;
2126 }
2127 if let Some(s) = st.startNumber {
2128 start_number = s;
2129 }
2130 }
2131 let mut selected_audio_language = "unk";
2132 let audio_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2135 .filter(is_audio_adaptation)
2136 .collect();
2137 let representations: Vec<&Representation> = select_preferred_adaptations(audio_adaptations, downloader)
2138 .iter()
2139 .flat_map(|a| a.representations.iter())
2140 .collect();
2141 if let Some(audio_repr) = select_preferred_representation(&representations, downloader) {
2142 let audio_adaptation = period.adaptations.iter()
2146 .find(|a| a.representations.iter().any(|r| r.eq(audio_repr)))
2147 .unwrap();
2148 if let Some(lang) = audio_repr.lang.as_ref().or(audio_adaptation.lang.as_ref()) {
2149 selected_audio_language = lang;
2150 }
2151 let mut base_url = base_url.clone();
2154 if let Some(bu) = &audio_adaptation.BaseURL.first() {
2155 base_url = merge_baseurls(&base_url, &bu.base)?;
2156 }
2157 if let Some(bu) = audio_repr.BaseURL.first() {
2158 base_url = merge_baseurls(&base_url, &bu.base)?;
2159 }
2160 if downloader.verbosity > 0 {
2161 let bw = if let Some(bw) = audio_repr.bandwidth {
2162 format!("bw={} Kbps ", bw / 1024)
2163 } else {
2164 String::from("")
2165 };
2166 let unknown = String::from("?");
2167 let lang = audio_repr.lang.as_ref()
2168 .unwrap_or(audio_adaptation.lang.as_ref()
2169 .unwrap_or(&unknown));
2170 let codec = audio_repr.codecs.as_ref()
2171 .unwrap_or(audio_adaptation.codecs.as_ref()
2172 .unwrap_or(&unknown));
2173 diagnostics.push(format!(" Audio stream selected: {bw}lang={lang} codec={codec}"));
2174 for cp in audio_repr.ContentProtection.iter()
2176 .chain(audio_adaptation.ContentProtection.iter())
2177 {
2178 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2179 if let Some(kid) = &cp.default_KID {
2180 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2181 }
2182 for pssh_element in &cp.cenc_pssh {
2183 if let Some(pssh_b64) = &pssh_element.content {
2184 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
2185 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
2186 diagnostics.push(format!(" {pssh}"));
2187 }
2188 }
2189 }
2190 }
2191 }
2192 if let Some(st) = &audio_adaptation.SegmentTemplate {
2197 if let Some(i) = &st.initialization {
2198 opt_init = Some(i.clone());
2199 }
2200 if let Some(m) = &st.media {
2201 opt_media = Some(m.clone());
2202 }
2203 if let Some(d) = st.duration {
2204 opt_duration = Some(d);
2205 }
2206 if let Some(ts) = st.timescale {
2207 timescale = ts;
2208 }
2209 if let Some(s) = st.startNumber {
2210 start_number = s;
2211 }
2212 }
2213 let mut dict = HashMap::new();
2214 if let Some(rid) = &audio_repr.id {
2215 dict.insert("RepresentationID", rid.clone());
2216 }
2217 if let Some(b) = &audio_repr.bandwidth {
2218 dict.insert("Bandwidth", b.to_string());
2219 }
2220 if let Some(sl) = &audio_adaptation.SegmentList {
2229 if downloader.verbosity > 1 {
2232 info!(" Using AdaptationSet>SegmentList addressing mode for audio representation");
2233 }
2234 let mut start_byte: Option<u64> = None;
2235 let mut end_byte: Option<u64> = None;
2236 if let Some(init) = &sl.Initialization {
2237 if let Some(range) = &init.range {
2238 let (s, e) = parse_range(range)?;
2239 start_byte = Some(s);
2240 end_byte = Some(e);
2241 }
2242 if let Some(su) = &init.sourceURL {
2243 let path = resolve_url_template(su, &dict);
2244 let init_url = merge_baseurls(&base_url, &path)?;
2245 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2246 .with_range(start_byte, end_byte)
2247 .set_init()
2248 .build();
2249 fragments.push(mf);
2250 } else {
2251 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2252 .with_range(start_byte, end_byte)
2253 .set_init()
2254 .build();
2255 fragments.push(mf);
2256 }
2257 }
2258 for su in &sl.segment_urls {
2259 start_byte = None;
2260 end_byte = None;
2261 if let Some(range) = &su.mediaRange {
2263 let (s, e) = parse_range(range)?;
2264 start_byte = Some(s);
2265 end_byte = Some(e);
2266 }
2267 if let Some(m) = &su.media {
2268 let u = merge_baseurls(&base_url, m)?;
2269 let mf = MediaFragmentBuilder::new(period_counter, u)
2270 .with_range(start_byte, end_byte)
2271 .build();
2272 fragments.push(mf);
2273 } else if let Some(bu) = audio_adaptation.BaseURL.first() {
2274 let u = merge_baseurls(&base_url, &bu.base)?;
2275 let mf = MediaFragmentBuilder::new(period_counter, u)
2276 .with_range(start_byte, end_byte)
2277 .build();
2278 fragments.push(mf);
2279 }
2280 }
2281 }
2282 if let Some(sl) = &audio_repr.SegmentList {
2283 if downloader.verbosity > 1 {
2285 info!(" Using Representation>SegmentList addressing mode for audio representation");
2286 }
2287 let mut start_byte: Option<u64> = None;
2288 let mut end_byte: Option<u64> = None;
2289 if let Some(init) = &sl.Initialization {
2290 if let Some(range) = &init.range {
2291 let (s, e) = parse_range(range)?;
2292 start_byte = Some(s);
2293 end_byte = Some(e);
2294 }
2295 if let Some(su) = &init.sourceURL {
2296 let path = resolve_url_template(su, &dict);
2297 let init_url = merge_baseurls(&base_url, &path)?;
2298 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2299 .with_range(start_byte, end_byte)
2300 .set_init()
2301 .build();
2302 fragments.push(mf);
2303 } else {
2304 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2305 .with_range(start_byte, end_byte)
2306 .set_init()
2307 .build();
2308 fragments.push(mf);
2309 }
2310 }
2311 for su in &sl.segment_urls {
2312 start_byte = None;
2313 end_byte = None;
2314 if let Some(range) = &su.mediaRange {
2316 let (s, e) = parse_range(range)?;
2317 start_byte = Some(s);
2318 end_byte = Some(e);
2319 }
2320 if let Some(m) = &su.media {
2321 let u = merge_baseurls(&base_url, m)?;
2322 let mf = MediaFragmentBuilder::new(period_counter, u)
2323 .with_range(start_byte, end_byte)
2324 .build();
2325 fragments.push(mf);
2326 } else if let Some(bu) = audio_repr.BaseURL.first() {
2327 let u = merge_baseurls(&base_url, &bu.base)?;
2328 let mf = MediaFragmentBuilder::new(period_counter, u)
2329 .with_range(start_byte, end_byte)
2330 .build();
2331 fragments.push(mf);
2332 }
2333 }
2334 } else if audio_repr.SegmentTemplate.is_some() ||
2335 audio_adaptation.SegmentTemplate.is_some()
2336 {
2337 let st;
2340 if let Some(it) = &audio_repr.SegmentTemplate {
2341 st = it;
2342 } else if let Some(it) = &audio_adaptation.SegmentTemplate {
2343 st = it;
2344 } else {
2345 panic!("unreachable");
2346 }
2347 if let Some(i) = &st.initialization {
2348 opt_init = Some(i.clone());
2349 }
2350 if let Some(m) = &st.media {
2351 opt_media = Some(m.clone());
2352 }
2353 if let Some(ts) = st.timescale {
2354 timescale = ts;
2355 }
2356 if let Some(sn) = st.startNumber {
2357 start_number = sn;
2358 }
2359 if let Some(stl) = &audio_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
2360 .or(audio_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
2361 {
2362 if downloader.verbosity > 1 {
2365 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for audio representation");
2366 }
2367 if let Some(init) = opt_init {
2368 let path = resolve_url_template(&init, &dict);
2369 let u = merge_baseurls(&base_url, &path)?;
2370 let mf = MediaFragmentBuilder::new(period_counter, u)
2371 .set_init()
2372 .build();
2373 fragments.push(mf);
2374 }
2375 if let Some(media) = opt_media {
2376 let audio_path = resolve_url_template(&media, &dict);
2377 let mut segment_time = 0;
2378 let mut segment_duration;
2379 let mut number = start_number;
2380 for s in &stl.segments {
2381 if let Some(t) = s.t {
2382 segment_time = t;
2383 }
2384 segment_duration = s.d;
2385 let dict = HashMap::from([("Time", segment_time.to_string()),
2387 ("Number", number.to_string())]);
2388 let path = resolve_url_template(&audio_path, &dict);
2389 let u = merge_baseurls(&base_url, &path)?;
2390 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2391 number += 1;
2392 if let Some(r) = s.r {
2393 let mut count = 0i64;
2394 let end_time = period_duration_secs * timescale as f64;
2396 loop {
2397 count += 1;
2398 if r >= 0 {
2404 if count > r {
2405 break;
2406 }
2407 if downloader.force_duration.is_some() && segment_time as f64 > end_time {
2408 break;
2409 }
2410 } else if segment_time as f64 > end_time {
2411 break;
2412 }
2413 segment_time += segment_duration;
2414 let dict = HashMap::from([("Time", segment_time.to_string()),
2415 ("Number", number.to_string())]);
2416 let path = resolve_url_template(&audio_path, &dict);
2417 let u = merge_baseurls(&base_url, &path)?;
2418 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2419 number += 1;
2420 }
2421 }
2422 segment_time += segment_duration;
2423 }
2424 } else {
2425 return Err(DashMpdError::UnhandledMediaStream(
2426 "SegmentTimeline without a media attribute".to_string()));
2427 }
2428 } else { if downloader.verbosity > 1 {
2433 info!(" Using SegmentTemplate addressing mode for audio representation");
2434 }
2435 let mut total_number = 0i64;
2436 if let Some(init) = opt_init {
2437 let path = resolve_url_template(&init, &dict);
2438 let u = merge_baseurls(&base_url, &path)?;
2439 let mf = MediaFragmentBuilder::new(period_counter, u)
2440 .set_init()
2441 .build();
2442 fragments.push(mf);
2443 }
2444 if let Some(media) = opt_media {
2445 let audio_path = resolve_url_template(&media, &dict);
2446 let timescale = st.timescale.unwrap_or(timescale);
2447 let mut segment_duration: f64 = -1.0;
2448 if let Some(d) = opt_duration {
2449 segment_duration = d;
2451 }
2452 if let Some(std) = st.duration {
2453 if timescale == 0 {
2454 return Err(DashMpdError::UnhandledMediaStream(
2455 "SegmentTemplate@duration attribute cannot be zero".to_string()));
2456 }
2457 segment_duration = std / timescale as f64;
2458 }
2459 if segment_duration < 0.0 {
2460 return Err(DashMpdError::UnhandledMediaStream(
2461 "Audio representation is missing SegmentTemplate@duration attribute".to_string()));
2462 }
2463 total_number += (period_duration_secs / segment_duration).round() as i64;
2464 let mut number = start_number;
2465 if mpd_is_dynamic(mpd) {
2468 if let Some(start_time) = mpd.availabilityStartTime {
2469 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
2470 number = (elapsed + number as f64 - 1f64).floor() as u64;
2471 } else {
2472 return Err(DashMpdError::UnhandledMediaStream(
2473 "dynamic manifest is missing @availabilityStartTime".to_string()));
2474 }
2475 }
2476 for _ in 1..=total_number {
2477 let dict = HashMap::from([("Number", number.to_string())]);
2478 let path = resolve_url_template(&audio_path, &dict);
2479 let u = merge_baseurls(&base_url, &path)?;
2480 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2481 number += 1;
2482 }
2483 }
2484 }
2485 } else if let Some(sb) = &audio_repr.SegmentBase {
2486 if downloader.verbosity > 1 {
2488 info!(" Using SegmentBase@indexRange addressing mode for audio representation");
2489 }
2490 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
2491 fragments.extend(mf);
2492 } else if fragments.is_empty() {
2493 if let Some(bu) = audio_repr.BaseURL.first() {
2494 if downloader.verbosity > 1 {
2496 info!(" Using BaseURL addressing mode for audio representation");
2497 }
2498 let u = merge_baseurls(&base_url, &bu.base)?;
2499 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2500 }
2501 }
2502 if fragments.is_empty() {
2503 return Err(DashMpdError::UnhandledMediaStream(
2504 "no usable addressing mode identified for audio representation".to_string()));
2505 }
2506 }
2507 Ok(PeriodOutputs {
2508 fragments, diagnostics, subtitle_formats: Vec::new(),
2509 selected_audio_language: String::from(selected_audio_language)
2510 })
2511}
2512
2513
2514#[tracing::instrument(level="trace", skip_all)]
2515async fn do_period_video(
2516 downloader: &DashDownloader,
2517 mpd: &MPD,
2518 period: &Period,
2519 period_counter: u8,
2520 base_url: Url
2521 ) -> Result<PeriodOutputs, DashMpdError>
2522{
2523 let mut fragments = Vec::new();
2524 let mut diagnostics = Vec::new();
2525 let mut period_duration_secs: f64 = 0.0;
2526 let mut opt_init: Option<String> = None;
2527 let mut opt_media: Option<String> = None;
2528 let mut opt_duration: Option<f64> = None;
2529 let mut timescale = 1;
2530 let mut start_number = 1;
2531 if let Some(d) = mpd.mediaPresentationDuration {
2532 period_duration_secs = d.as_secs_f64();
2533 }
2534 if let Some(d) = period.duration {
2535 period_duration_secs = d.as_secs_f64();
2536 }
2537 if let Some(s) = downloader.force_duration {
2538 period_duration_secs = s;
2539 }
2540 if let Some(st) = &period.SegmentTemplate {
2544 if let Some(i) = &st.initialization {
2545 opt_init = Some(i.clone());
2546 }
2547 if let Some(m) = &st.media {
2548 opt_media = Some(m.clone());
2549 }
2550 if let Some(d) = st.duration {
2551 opt_duration = Some(d);
2552 }
2553 if let Some(ts) = st.timescale {
2554 timescale = ts;
2555 }
2556 if let Some(s) = st.startNumber {
2557 start_number = s;
2558 }
2559 }
2560 let video_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2567 .filter(is_video_adaptation)
2568 .collect();
2569 let representations: Vec<&Representation> = select_preferred_adaptations(video_adaptations, downloader)
2570 .iter()
2571 .flat_map(|a| a.representations.iter())
2572 .collect();
2573 let maybe_video_repr = if let Some(want) = downloader.video_width_preference {
2574 representations.iter()
2575 .min_by_key(|x| if let Some(w) = x.width { want.abs_diff(w) } else { u64::MAX })
2576 .copied()
2577 } else if let Some(want) = downloader.video_height_preference {
2578 representations.iter()
2579 .min_by_key(|x| if let Some(h) = x.height { want.abs_diff(h) } else { u64::MAX })
2580 .copied()
2581 } else {
2582 select_preferred_representation(&representations, downloader)
2583 };
2584 if let Some(video_repr) = maybe_video_repr {
2585 let video_adaptation = period.adaptations.iter()
2589 .find(|a| a.representations.iter().any(|r| r.eq(video_repr)))
2590 .unwrap();
2591 let mut base_url = base_url.clone();
2594 if let Some(bu) = &video_adaptation.BaseURL.first() {
2595 base_url = merge_baseurls(&base_url, &bu.base)?;
2596 }
2597 if let Some(bu) = &video_repr.BaseURL.first() {
2598 base_url = merge_baseurls(&base_url, &bu.base)?;
2599 }
2600 if downloader.verbosity > 0 {
2601 let bw = if let Some(bw) = video_repr.bandwidth.or(video_adaptation.maxBandwidth) {
2602 format!("bw={} Kbps ", bw / 1024)
2603 } else {
2604 String::from("")
2605 };
2606 let unknown = String::from("?");
2607 let w = video_repr.width.unwrap_or(video_adaptation.width.unwrap_or(0));
2608 let h = video_repr.height.unwrap_or(video_adaptation.height.unwrap_or(0));
2609 let fmt = if w == 0 || h == 0 {
2610 String::from("")
2611 } else {
2612 format!("resolution={w}x{h} ")
2613 };
2614 let codec = video_repr.codecs.as_ref()
2615 .unwrap_or(video_adaptation.codecs.as_ref().unwrap_or(&unknown));
2616 diagnostics.push(format!(" Video stream selected: {bw}{fmt}codec={codec}"));
2617 for cp in video_repr.ContentProtection.iter()
2619 .chain(video_adaptation.ContentProtection.iter())
2620 {
2621 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2622 if let Some(kid) = &cp.default_KID {
2623 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2624 }
2625 for pssh_element in &cp.cenc_pssh {
2626 if let Some(pssh_b64) = &pssh_element.content {
2627 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
2628 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
2629 diagnostics.push(format!(" {pssh}"));
2630 }
2631 }
2632 }
2633 }
2634 }
2635 let mut dict = HashMap::new();
2636 if let Some(rid) = &video_repr.id {
2637 dict.insert("RepresentationID", rid.clone());
2638 }
2639 if let Some(b) = &video_repr.bandwidth {
2640 dict.insert("Bandwidth", b.to_string());
2641 }
2642 if let Some(st) = &video_adaptation.SegmentTemplate {
2647 if let Some(i) = &st.initialization {
2648 opt_init = Some(i.clone());
2649 }
2650 if let Some(m) = &st.media {
2651 opt_media = Some(m.clone());
2652 }
2653 if let Some(d) = st.duration {
2654 opt_duration = Some(d);
2655 }
2656 if let Some(ts) = st.timescale {
2657 timescale = ts;
2658 }
2659 if let Some(s) = st.startNumber {
2660 start_number = s;
2661 }
2662 }
2663 if let Some(sl) = &video_adaptation.SegmentList {
2667 if downloader.verbosity > 1 {
2669 info!(" Using AdaptationSet>SegmentList addressing mode for video representation");
2670 }
2671 let mut start_byte: Option<u64> = None;
2672 let mut end_byte: Option<u64> = None;
2673 if let Some(init) = &sl.Initialization {
2674 if let Some(range) = &init.range {
2675 let (s, e) = parse_range(range)?;
2676 start_byte = Some(s);
2677 end_byte = Some(e);
2678 }
2679 if let Some(su) = &init.sourceURL {
2680 let path = resolve_url_template(su, &dict);
2681 let u = merge_baseurls(&base_url, &path)?;
2682 let mf = MediaFragmentBuilder::new(period_counter, u)
2683 .with_range(start_byte, end_byte)
2684 .set_init()
2685 .build();
2686 fragments.push(mf);
2687 }
2688 } else {
2689 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2690 .with_range(start_byte, end_byte)
2691 .set_init()
2692 .build();
2693 fragments.push(mf);
2694 }
2695 for su in &sl.segment_urls {
2696 start_byte = None;
2697 end_byte = None;
2698 if let Some(range) = &su.mediaRange {
2700 let (s, e) = parse_range(range)?;
2701 start_byte = Some(s);
2702 end_byte = Some(e);
2703 }
2704 if let Some(m) = &su.media {
2705 let u = merge_baseurls(&base_url, m)?;
2706 let mf = MediaFragmentBuilder::new(period_counter, u)
2707 .with_range(start_byte, end_byte)
2708 .build();
2709 fragments.push(mf);
2710 } else if let Some(bu) = video_adaptation.BaseURL.first() {
2711 let u = merge_baseurls(&base_url, &bu.base)?;
2712 let mf = MediaFragmentBuilder::new(period_counter, u)
2713 .with_range(start_byte, end_byte)
2714 .build();
2715 fragments.push(mf);
2716 }
2717 }
2718 }
2719 if let Some(sl) = &video_repr.SegmentList {
2720 if downloader.verbosity > 1 {
2722 info!(" Using Representation>SegmentList addressing mode for video representation");
2723 }
2724 let mut start_byte: Option<u64> = None;
2725 let mut end_byte: Option<u64> = None;
2726 if let Some(init) = &sl.Initialization {
2727 if let Some(range) = &init.range {
2728 let (s, e) = parse_range(range)?;
2729 start_byte = Some(s);
2730 end_byte = Some(e);
2731 }
2732 if let Some(su) = &init.sourceURL {
2733 let path = resolve_url_template(su, &dict);
2734 let u = merge_baseurls(&base_url, &path)?;
2735 let mf = MediaFragmentBuilder::new(period_counter, u)
2736 .with_range(start_byte, end_byte)
2737 .set_init()
2738 .build();
2739 fragments.push(mf);
2740 } else {
2741 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2742 .with_range(start_byte, end_byte)
2743 .set_init()
2744 .build();
2745 fragments.push(mf);
2746 }
2747 }
2748 for su in sl.segment_urls.iter() {
2749 start_byte = None;
2750 end_byte = None;
2751 if let Some(range) = &su.mediaRange {
2753 let (s, e) = parse_range(range)?;
2754 start_byte = Some(s);
2755 end_byte = Some(e);
2756 }
2757 if let Some(m) = &su.media {
2758 let u = merge_baseurls(&base_url, m)?;
2759 let mf = MediaFragmentBuilder::new(period_counter, u)
2760 .with_range(start_byte, end_byte)
2761 .build();
2762 fragments.push(mf);
2763 } else if let Some(bu) = video_repr.BaseURL.first() {
2764 let u = merge_baseurls(&base_url, &bu.base)?;
2765 let mf = MediaFragmentBuilder::new(period_counter, u)
2766 .with_range(start_byte, end_byte)
2767 .build();
2768 fragments.push(mf);
2769 }
2770 }
2771 } else if video_repr.SegmentTemplate.is_some() ||
2772 video_adaptation.SegmentTemplate.is_some() {
2773 let st;
2776 if let Some(it) = &video_repr.SegmentTemplate {
2777 st = it;
2778 } else if let Some(it) = &video_adaptation.SegmentTemplate {
2779 st = it;
2780 } else {
2781 panic!("impossible");
2782 }
2783 if let Some(i) = &st.initialization {
2784 opt_init = Some(i.clone());
2785 }
2786 if let Some(m) = &st.media {
2787 opt_media = Some(m.clone());
2788 }
2789 if let Some(ts) = st.timescale {
2790 timescale = ts;
2791 }
2792 if let Some(sn) = st.startNumber {
2793 start_number = sn;
2794 }
2795 if let Some(stl) = &video_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
2796 .or(video_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
2797 {
2798 if downloader.verbosity > 1 {
2800 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for video representation");
2801 }
2802 if let Some(init) = opt_init {
2803 let path = resolve_url_template(&init, &dict);
2804 let u = merge_baseurls(&base_url, &path)?;
2805 let mf = MediaFragmentBuilder::new(period_counter, u)
2806 .set_init()
2807 .build();
2808 fragments.push(mf);
2809 }
2810 if let Some(media) = opt_media {
2811 let video_path = resolve_url_template(&media, &dict);
2812 let mut segment_time = 0;
2813 let mut segment_duration;
2814 let mut number = start_number;
2815 for s in &stl.segments {
2816 if let Some(t) = s.t {
2817 segment_time = t;
2818 }
2819 segment_duration = s.d;
2820 let dict = HashMap::from([("Time", segment_time.to_string()),
2822 ("Number", number.to_string())]);
2823 let path = resolve_url_template(&video_path, &dict);
2824 let u = merge_baseurls(&base_url, &path)?;
2825 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2826 fragments.push(mf);
2827 number += 1;
2828 if let Some(r) = s.r {
2829 let mut count = 0i64;
2830 let end_time = period_duration_secs * timescale as f64;
2832 loop {
2833 count += 1;
2834 if r >= 0 {
2840 if count > r {
2841 break;
2842 }
2843 if downloader.force_duration.is_some() && segment_time as f64 > end_time {
2844 break;
2845 }
2846 } else if segment_time as f64 > end_time {
2847 break;
2848 }
2849 segment_time += segment_duration;
2850 let dict = HashMap::from([("Time", segment_time.to_string()),
2851 ("Number", number.to_string())]);
2852 let path = resolve_url_template(&video_path, &dict);
2853 let u = merge_baseurls(&base_url, &path)?;
2854 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2855 fragments.push(mf);
2856 number += 1;
2857 }
2858 }
2859 segment_time += segment_duration;
2860 }
2861 } else {
2862 return Err(DashMpdError::UnhandledMediaStream(
2863 "SegmentTimeline without a media attribute".to_string()));
2864 }
2865 } else { if downloader.verbosity > 1 {
2868 info!(" Using SegmentTemplate addressing mode for video representation");
2869 }
2870 let mut total_number = 0i64;
2871 if let Some(init) = opt_init {
2872 let path = resolve_url_template(&init, &dict);
2873 let u = merge_baseurls(&base_url, &path)?;
2874 let mf = MediaFragmentBuilder::new(period_counter, u)
2875 .set_init()
2876 .build();
2877 fragments.push(mf);
2878 }
2879 if let Some(media) = opt_media {
2880 let video_path = resolve_url_template(&media, &dict);
2881 let timescale = st.timescale.unwrap_or(timescale);
2882 let mut segment_duration: f64 = -1.0;
2883 if let Some(d) = opt_duration {
2884 segment_duration = d;
2886 }
2887 if let Some(std) = st.duration {
2888 if timescale == 0 {
2889 return Err(DashMpdError::UnhandledMediaStream(
2890 "SegmentTemplate@duration attribute cannot be zero".to_string()));
2891 }
2892 segment_duration = std / timescale as f64;
2893 }
2894 if segment_duration < 0.0 {
2895 return Err(DashMpdError::UnhandledMediaStream(
2896 "Video representation is missing SegmentTemplate@duration attribute".to_string()));
2897 }
2898 total_number += (period_duration_secs / segment_duration).round() as i64;
2899 let mut number = start_number;
2900 if mpd_is_dynamic(mpd) {
2910 if let Some(start_time) = mpd.availabilityStartTime {
2911 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
2912 number = (elapsed + number as f64 - 1f64).floor() as u64;
2913 } else {
2914 return Err(DashMpdError::UnhandledMediaStream(
2915 "dynamic manifest is missing @availabilityStartTime".to_string()));
2916 }
2917 }
2918 for _ in 1..=total_number {
2919 let dict = HashMap::from([("Number", number.to_string())]);
2920 let path = resolve_url_template(&video_path, &dict);
2921 let u = merge_baseurls(&base_url, &path)?;
2922 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2923 fragments.push(mf);
2924 number += 1;
2925 }
2926 }
2927 }
2928 } else if let Some(sb) = &video_repr.SegmentBase {
2929 if downloader.verbosity > 1 {
2931 info!(" Using SegmentBase@indexRange addressing mode for video representation");
2932 }
2933 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
2934 fragments.extend(mf);
2935 } else if fragments.is_empty() {
2936 if let Some(bu) = video_repr.BaseURL.first() {
2937 if downloader.verbosity > 1 {
2939 info!(" Using BaseURL addressing mode for video representation");
2940 }
2941 let u = merge_baseurls(&base_url, &bu.base)?;
2942 let mf = MediaFragmentBuilder::new(period_counter, u)
2943 .with_timeout(Duration::new(10000, 0))
2944 .build();
2945 fragments.push(mf);
2946 }
2947 }
2948 if fragments.is_empty() {
2949 return Err(DashMpdError::UnhandledMediaStream(
2950 "no usable addressing mode identified for video representation".to_string()));
2951 }
2952 }
2953 Ok(PeriodOutputs {
2956 fragments,
2957 diagnostics,
2958 subtitle_formats: Vec::new(),
2959 selected_audio_language: String::from("unk")
2960 })
2961}
2962
2963#[tracing::instrument(level="trace", skip_all)]
2964async fn do_period_subtitles(
2965 downloader: &DashDownloader,
2966 mpd: &MPD,
2967 period: &Period,
2968 period_counter: u8,
2969 base_url: Url
2970 ) -> Result<PeriodOutputs, DashMpdError>
2971{
2972 let client = downloader.http_client.as_ref().unwrap();
2973 let output_path = &downloader.output_path.as_ref().unwrap().clone();
2974 let period_output_path = output_path_for_period(output_path, period_counter);
2975 let mut fragments = Vec::new();
2976 let mut subtitle_formats = Vec::new();
2977 let mut period_duration_secs: f64 = 0.0;
2978 if let Some(d) = mpd.mediaPresentationDuration {
2979 period_duration_secs = d.as_secs_f64();
2980 }
2981 if let Some(d) = period.duration {
2982 period_duration_secs = d.as_secs_f64();
2983 }
2984 let maybe_subtitle_adaptation = if let Some(ref lang) = downloader.language_preference {
2985 period.adaptations.iter().filter(is_subtitle_adaptation)
2986 .min_by_key(|a| adaptation_lang_distance(a, lang))
2987 } else {
2988 period.adaptations.iter().find(is_subtitle_adaptation)
2990 };
2991 if downloader.fetch_subtitles {
2992 if let Some(subtitle_adaptation) = maybe_subtitle_adaptation {
2993 let subtitle_format = subtitle_type(&subtitle_adaptation);
2994 subtitle_formats.push(subtitle_format);
2995 if downloader.verbosity > 1 && downloader.fetch_subtitles {
2996 info!(" Retrieving subtitles in format {subtitle_format:?}");
2997 }
2998 let mut base_url = base_url.clone();
3001 if let Some(bu) = &subtitle_adaptation.BaseURL.first() {
3002 base_url = merge_baseurls(&base_url, &bu.base)?;
3003 }
3004 if let Some(rep) = subtitle_adaptation.representations.first() {
3007 if !rep.BaseURL.is_empty() {
3008 for st_bu in &rep.BaseURL {
3009 let st_url = merge_baseurls(&base_url, &st_bu.base)?;
3010 let mut req = client.get(st_url.clone());
3011 if let Some(referer) = &downloader.referer {
3012 req = req.header("Referer", referer);
3013 } else {
3014 req = req.header("Referer", base_url.to_string());
3015 }
3016 let rqw = req.build()
3017 .map_err(|e| network_error("building request", &e))?;
3018 let subs = reqwest_bytes_with_retries(client, rqw, 5).await
3019 .map_err(|e| network_error("fetching subtitles", &e))?;
3020 let mut subs_path = period_output_path.clone();
3021 let subtitle_format = subtitle_type(&subtitle_adaptation);
3022 match subtitle_format {
3023 SubtitleType::Vtt => subs_path.set_extension("vtt"),
3024 SubtitleType::Srt => subs_path.set_extension("srt"),
3025 SubtitleType::Ttml => subs_path.set_extension("ttml"),
3026 SubtitleType::Sami => subs_path.set_extension("sami"),
3027 SubtitleType::Wvtt => subs_path.set_extension("wvtt"),
3028 SubtitleType::Stpp => subs_path.set_extension("stpp"),
3029 _ => subs_path.set_extension("sub"),
3030 };
3031 subtitle_formats.push(subtitle_format);
3032 let mut subs_file = File::create(subs_path.clone())
3033 .map_err(|e| DashMpdError::Io(e, String::from("creating subtitle file")))?;
3034 if downloader.verbosity > 2 {
3035 info!(" Subtitle {st_url} -> {} octets", subs.len());
3036 }
3037 match subs_file.write_all(&subs) {
3038 Ok(()) => {
3039 if downloader.verbosity > 0 {
3040 info!(" Downloaded subtitles ({subtitle_format:?}) to {}",
3041 subs_path.display());
3042 }
3043 },
3044 Err(e) => {
3045 error!("Unable to write subtitle file: {e:?}");
3046 return Err(DashMpdError::Io(e, String::from("writing subtitle data")));
3047 },
3048 }
3049 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
3050 subtitle_formats.contains(&SubtitleType::Ttxt)
3051 {
3052 if downloader.verbosity > 0 {
3053 info!(" Converting subtitles to SRT format with MP4Box ");
3054 }
3055 let out = subs_path.with_extension("srt");
3056 let out_str = out.to_string_lossy();
3063 let subs_str = subs_path.to_string_lossy();
3064 let args = vec![
3065 "-srt", "1",
3066 "-out", &out_str,
3067 &subs_str];
3068 if downloader.verbosity > 0 {
3069 info!(" Running MPBox {}", args.join(" "));
3070 }
3071 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
3072 .args(args)
3073 .output()
3074 {
3075 let msg = partial_process_output(&mp4box.stdout);
3076 if !msg.is_empty() {
3077 info!("MP4Box stdout: {msg}");
3078 }
3079 let msg = partial_process_output(&mp4box.stderr);
3080 if !msg.is_empty() {
3081 info!("MP4Box stderr: {msg}");
3082 }
3083 if mp4box.status.success() {
3084 info!(" Converted subtitles to SRT");
3085 } else {
3086 warn!("Error running MP4Box to convert subtitles");
3087 }
3088 }
3089 }
3090 }
3091 } else if rep.SegmentTemplate.is_some() || subtitle_adaptation.SegmentTemplate.is_some() {
3092 let mut opt_init: Option<String> = None;
3093 let mut opt_media: Option<String> = None;
3094 let mut opt_duration: Option<f64> = None;
3095 let mut timescale = 1;
3096 let mut start_number = 1;
3097 if let Some(st) = &rep.SegmentTemplate {
3102 if let Some(i) = &st.initialization {
3103 opt_init = Some(i.clone());
3104 }
3105 if let Some(m) = &st.media {
3106 opt_media = Some(m.clone());
3107 }
3108 if let Some(d) = st.duration {
3109 opt_duration = Some(d);
3110 }
3111 if let Some(ts) = st.timescale {
3112 timescale = ts;
3113 }
3114 if let Some(s) = st.startNumber {
3115 start_number = s;
3116 }
3117 }
3118 let rid = match &rep.id {
3119 Some(id) => id,
3120 None => return Err(
3121 DashMpdError::UnhandledMediaStream(
3122 "Missing @id on Representation node".to_string())),
3123 };
3124 let mut dict = HashMap::from([("RepresentationID", rid.clone())]);
3125 if let Some(b) = &rep.bandwidth {
3126 dict.insert("Bandwidth", b.to_string());
3127 }
3128 if let Some(sl) = &rep.SegmentList {
3132 if downloader.verbosity > 1 {
3135 info!(" Using AdaptationSet>SegmentList addressing mode for subtitle representation");
3136 }
3137 let mut start_byte: Option<u64> = None;
3138 let mut end_byte: Option<u64> = None;
3139 if let Some(init) = &sl.Initialization {
3140 if let Some(range) = &init.range {
3141 let (s, e) = parse_range(range)?;
3142 start_byte = Some(s);
3143 end_byte = Some(e);
3144 }
3145 if let Some(su) = &init.sourceURL {
3146 let path = resolve_url_template(su, &dict);
3147 let u = merge_baseurls(&base_url, &path)?;
3148 let mf = MediaFragmentBuilder::new(period_counter, u)
3149 .with_range(start_byte, end_byte)
3150 .set_init()
3151 .build();
3152 fragments.push(mf);
3153 } else {
3154 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3155 .with_range(start_byte, end_byte)
3156 .set_init()
3157 .build();
3158 fragments.push(mf);
3159 }
3160 }
3161 for su in &sl.segment_urls {
3162 start_byte = None;
3163 end_byte = None;
3164 if let Some(range) = &su.mediaRange {
3166 let (s, e) = parse_range(range)?;
3167 start_byte = Some(s);
3168 end_byte = Some(e);
3169 }
3170 if let Some(m) = &su.media {
3171 let u = merge_baseurls(&base_url, m)?;
3172 let mf = MediaFragmentBuilder::new(period_counter, u)
3173 .with_range(start_byte, end_byte)
3174 .build();
3175 fragments.push(mf);
3176 } else if let Some(bu) = subtitle_adaptation.BaseURL.first() {
3177 let u = merge_baseurls(&base_url, &bu.base)?;
3178 let mf = MediaFragmentBuilder::new(period_counter, u)
3179 .with_range(start_byte, end_byte)
3180 .build();
3181 fragments.push(mf);
3182 }
3183 }
3184 }
3185 if let Some(sl) = &rep.SegmentList {
3186 if downloader.verbosity > 1 {
3188 info!(" Using Representation>SegmentList addressing mode for subtitle representation");
3189 }
3190 let mut start_byte: Option<u64> = None;
3191 let mut end_byte: Option<u64> = None;
3192 if let Some(init) = &sl.Initialization {
3193 if let Some(range) = &init.range {
3194 let (s, e) = parse_range(range)?;
3195 start_byte = Some(s);
3196 end_byte = Some(e);
3197 }
3198 if let Some(su) = &init.sourceURL {
3199 let path = resolve_url_template(su, &dict);
3200 let u = merge_baseurls(&base_url, &path)?;
3201 let mf = MediaFragmentBuilder::new(period_counter, u)
3202 .with_range(start_byte, end_byte)
3203 .set_init()
3204 .build();
3205 fragments.push(mf);
3206 } else {
3207 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3208 .with_range(start_byte, end_byte)
3209 .set_init()
3210 .build();
3211 fragments.push(mf);
3212 }
3213 }
3214 for su in sl.segment_urls.iter() {
3215 start_byte = None;
3216 end_byte = None;
3217 if let Some(range) = &su.mediaRange {
3219 let (s, e) = parse_range(range)?;
3220 start_byte = Some(s);
3221 end_byte = Some(e);
3222 }
3223 if let Some(m) = &su.media {
3224 let u = merge_baseurls(&base_url, m)?;
3225 let mf = MediaFragmentBuilder::new(period_counter, u)
3226 .with_range(start_byte, end_byte)
3227 .build();
3228 fragments.push(mf);
3229 } else if let Some(bu) = &rep.BaseURL.first() {
3230 let u = merge_baseurls(&base_url, &bu.base)?;
3231 let mf = MediaFragmentBuilder::new(period_counter, u)
3232 .with_range(start_byte, end_byte)
3233 .build();
3234 fragments.push(mf);
3235 };
3236 }
3237 } else if rep.SegmentTemplate.is_some() ||
3238 subtitle_adaptation.SegmentTemplate.is_some()
3239 {
3240 let st;
3243 if let Some(it) = &rep.SegmentTemplate {
3244 st = it;
3245 } else if let Some(it) = &subtitle_adaptation.SegmentTemplate {
3246 st = it;
3247 } else {
3248 panic!("unreachable");
3249 }
3250 if let Some(i) = &st.initialization {
3251 opt_init = Some(i.clone());
3252 }
3253 if let Some(m) = &st.media {
3254 opt_media = Some(m.clone());
3255 }
3256 if let Some(ts) = st.timescale {
3257 timescale = ts;
3258 }
3259 if let Some(sn) = st.startNumber {
3260 start_number = sn;
3261 }
3262 if let Some(stl) = &rep.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
3263 .or(subtitle_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
3264 {
3265 if downloader.verbosity > 1 {
3268 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for subtitle representation");
3269 }
3270 if let Some(init) = opt_init {
3271 let path = resolve_url_template(&init, &dict);
3272 let u = merge_baseurls(&base_url, &path)?;
3273 let mf = MediaFragmentBuilder::new(period_counter, u)
3274 .set_init()
3275 .build();
3276 fragments.push(mf);
3277 }
3278 if let Some(media) = opt_media {
3279 let sub_path = resolve_url_template(&media, &dict);
3280 let mut segment_time = 0;
3281 let mut segment_duration;
3282 let mut number = start_number;
3283 for s in &stl.segments {
3284 if let Some(t) = s.t {
3285 segment_time = t;
3286 }
3287 segment_duration = s.d;
3288 let dict = HashMap::from([("Time", segment_time.to_string()),
3290 ("Number", number.to_string())]);
3291 let path = resolve_url_template(&sub_path, &dict);
3292 let u = merge_baseurls(&base_url, &path)?;
3293 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3294 fragments.push(mf);
3295 number += 1;
3296 if let Some(r) = s.r {
3297 let mut count = 0i64;
3298 let end_time = period_duration_secs * timescale as f64;
3300 loop {
3301 count += 1;
3302 if r >= 0 {
3308 if count > r {
3309 break;
3310 }
3311 if downloader.force_duration.is_some() &&
3312 segment_time as f64 > end_time
3313 {
3314 break;
3315 }
3316 } else if segment_time as f64 > end_time {
3317 break;
3318 }
3319 segment_time += segment_duration;
3320 let dict = HashMap::from([("Time", segment_time.to_string()),
3321 ("Number", number.to_string())]);
3322 let path = resolve_url_template(&sub_path, &dict);
3323 let u = merge_baseurls(&base_url, &path)?;
3324 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3325 fragments.push(mf);
3326 number += 1;
3327 }
3328 }
3329 segment_time += segment_duration;
3330 }
3331 } else {
3332 return Err(DashMpdError::UnhandledMediaStream(
3333 "SegmentTimeline without a media attribute".to_string()));
3334 }
3335 } else { if downloader.verbosity > 0 {
3340 info!(" Using SegmentTemplate addressing mode for stpp subtitles");
3341 }
3342 if let Some(i) = &st.initialization {
3343 opt_init = Some(i.to_string());
3344 }
3345 if let Some(m) = &st.media {
3346 opt_media = Some(m.to_string());
3347 }
3348 if let Some(d) = st.duration {
3349 opt_duration = Some(d);
3350 }
3351 if let Some(ts) = st.timescale {
3352 timescale = ts;
3353 }
3354 if let Some(s) = st.startNumber {
3355 start_number = s;
3356 }
3357 let rid = match &rep.id {
3358 Some(id) => id,
3359 None => return Err(
3360 DashMpdError::UnhandledMediaStream(
3361 "Missing @id on Representation node".to_string())),
3362 };
3363 let mut dict = HashMap::from([("RepresentationID", rid.clone())]);
3364 if let Some(b) = &rep.bandwidth {
3365 dict.insert("Bandwidth", b.to_string());
3366 }
3367 let mut total_number = 0i64;
3368 if let Some(init) = opt_init {
3369 let path = resolve_url_template(&init, &dict);
3370 let u = merge_baseurls(&base_url, &path)?;
3371 let mf = MediaFragmentBuilder::new(period_counter, u)
3372 .set_init()
3373 .build();
3374 fragments.push(mf);
3375 }
3376 if let Some(media) = opt_media {
3377 let sub_path = resolve_url_template(&media, &dict);
3378 let mut segment_duration: f64 = -1.0;
3379 if let Some(d) = opt_duration {
3380 segment_duration = d;
3382 }
3383 if let Some(std) = st.duration {
3384 if timescale == 0 {
3385 return Err(DashMpdError::UnhandledMediaStream(
3386 "SegmentTemplate@duration attribute cannot be zero".to_string()));
3387 }
3388 segment_duration = std / timescale as f64;
3389 }
3390 if segment_duration < 0.0 {
3391 return Err(DashMpdError::UnhandledMediaStream(
3392 "Subtitle representation is missing SegmentTemplate@duration".to_string()));
3393 }
3394 total_number += (period_duration_secs / segment_duration).ceil() as i64;
3395 let mut number = start_number;
3396 for _ in 1..=total_number {
3397 let dict = HashMap::from([("Number", number.to_string())]);
3398 let path = resolve_url_template(&sub_path, &dict);
3399 let u = merge_baseurls(&base_url, &path)?;
3400 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3401 fragments.push(mf);
3402 number += 1;
3403 }
3404 }
3405 }
3406 } else if let Some(sb) = &rep.SegmentBase {
3407 info!(" Using SegmentBase@indexRange for subs");
3409 if downloader.verbosity > 1 {
3410 info!(" Using SegmentBase@indexRange addressing mode for subtitle representation");
3411 }
3412 let mut start_byte: Option<u64> = None;
3413 let mut end_byte: Option<u64> = None;
3414 if let Some(init) = &sb.Initialization {
3415 if let Some(range) = &init.range {
3416 let (s, e) = parse_range(range)?;
3417 start_byte = Some(s);
3418 end_byte = Some(e);
3419 }
3420 if let Some(su) = &init.sourceURL {
3421 let path = resolve_url_template(su, &dict);
3422 let u = merge_baseurls(&base_url, &path)?;
3423 let mf = MediaFragmentBuilder::new(period_counter, u)
3424 .with_range(start_byte, end_byte)
3425 .set_init()
3426 .build();
3427 fragments.push(mf);
3428 }
3429 }
3430 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3431 .set_init()
3432 .build();
3433 fragments.push(mf);
3434 }
3437 }
3438 }
3439 }
3440 }
3441 Ok(PeriodOutputs {
3442 fragments,
3443 diagnostics: Vec::new(),
3444 subtitle_formats,
3445 selected_audio_language: String::from("unk")
3446 })
3447}
3448
3449
3450struct DownloadState {
3453 period_counter: u8,
3454 segment_count: usize,
3455 segment_counter: usize,
3456 download_errors: u32
3457}
3458
3459#[tracing::instrument(level="trace", skip_all)]
3466async fn fetch_fragment(
3467 downloader: &mut DashDownloader,
3468 frag: &MediaFragment,
3469 fragment_type: &str,
3470 progress_percent: u32) -> Result<std::fs::File, DashMpdError>
3471{
3472 let send_request = || async {
3473 trace!("send_request {}", frag.url.clone());
3474 let mut req = downloader.http_client.as_ref().unwrap()
3477 .get(frag.url.clone())
3478 .header("Accept", format!("{fragment_type}/*;q=0.9,*/*;q=0.5"))
3479 .header("Sec-Fetch-Mode", "navigate");
3480 if let Some(sb) = &frag.start_byte {
3481 if let Some(eb) = &frag.end_byte {
3482 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
3483 }
3484 }
3485 if let Some(ts) = &frag.timeout {
3486 req = req.timeout(*ts);
3487 }
3488 if let Some(referer) = &downloader.referer {
3489 req = req.header("Referer", referer);
3490 } else {
3491 req = req.header("Referer", downloader.redirected_url.to_string());
3492 }
3493 if let Some(username) = &downloader.auth_username {
3494 if let Some(password) = &downloader.auth_password {
3495 req = req.basic_auth(username, Some(password));
3496 }
3497 }
3498 if let Some(token) = &downloader.auth_bearer_token {
3499 req = req.bearer_auth(token);
3500 }
3501 req.send().await
3502 .map_err(categorize_reqwest_error)?
3503 .error_for_status()
3504 .map_err(categorize_reqwest_error)
3505 };
3506 match retry_notify(ExponentialBackoff::default(), send_request, notify_transient).await {
3507 Ok(response) => {
3508 match response.error_for_status() {
3509 Ok(mut resp) => {
3510 let mut tmp_out = tempfile::tempfile()
3511 .map_err(|e| DashMpdError::Io(e, String::from("creating tmpfile for fragment")))?;
3512 let content_type_checker = if fragment_type.eq("audio") {
3513 content_type_audio_p
3514 } else if fragment_type.eq("video") {
3515 content_type_video_p
3516 } else {
3517 panic!("fragment_type not audio or video");
3518 };
3519 if !downloader.content_type_checks || content_type_checker(&resp) {
3520 let mut fragment_out: Option<File> = None;
3521 if let Some(ref fragment_path) = downloader.fragment_path {
3522 if let Some(path) = frag.url.path_segments()
3523 .unwrap_or_else(|| "".split(' '))
3524 .next_back()
3525 {
3526 let vf_file = fragment_path.clone().join(fragment_type).join(path);
3527 if let Ok(f) = File::create(vf_file) {
3528 fragment_out = Some(f)
3529 }
3530 }
3531 }
3532 let mut segment_size = 0;
3533 while let Some(chunk) = resp.chunk().await
3539 .map_err(|e| network_error(&format!("fetching DASH {fragment_type} segment"), &e))?
3540 {
3541 segment_size += chunk.len();
3542 downloader.bw_estimator_bytes += chunk.len();
3543 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
3544 throttle_download_rate(downloader, size).await?;
3545 if let Err(e) = tmp_out.write_all(&chunk) {
3546 return Err(DashMpdError::Io(e, format!("writing DASH {fragment_type} data")));
3547 }
3548 if let Some(ref mut fout) = fragment_out {
3549 fout.write_all(&chunk)
3550 .map_err(|e| DashMpdError::Io(e, format!("writing {fragment_type} fragment")))?;
3551 }
3552 let elapsed = downloader.bw_estimator_started.elapsed().as_secs_f64();
3553 if (elapsed > 1.5) || (downloader.bw_estimator_bytes > 100_000) {
3554 let bw = downloader.bw_estimator_bytes as f64 / (1e6 * elapsed);
3555 let msg = if bw > 0.5 {
3556 format!("Fetching {fragment_type} segments ({bw:.1} MB/s)")
3557 } else {
3558 let kbs = (bw * 1000.0).round() as u64;
3559 format!("Fetching {fragment_type} segments ({kbs:3} kB/s)")
3560 };
3561 for observer in &downloader.progress_observers {
3562 observer.update(progress_percent, &msg);
3563 }
3564 downloader.bw_estimator_started = Instant::now();
3565 downloader.bw_estimator_bytes = 0;
3566 }
3567 }
3568 if downloader.verbosity > 2 {
3569 if let Some(sb) = &frag.start_byte {
3570 if let Some(eb) = &frag.end_byte {
3571 info!(" {fragment_type} segment {} range {sb}-{eb} -> {} octets",
3572 frag.url, segment_size);
3573 }
3574 } else {
3575 info!(" {fragment_type} segment {} -> {segment_size} octets", &frag.url);
3576 }
3577 }
3578 } else {
3579 warn!("Ignoring segment {} with non-{fragment_type} content-type", frag.url);
3580 };
3581 tmp_out.sync_all()
3582 .map_err(|e| DashMpdError::Io(e, format!("syncing {fragment_type} fragment")))?;
3583 Ok(tmp_out)
3584 },
3585 Err(e) => Err(network_error("HTTP error", &e)),
3586 }
3587 },
3588 Err(e) => Err(network_error(&format!("{e:?}"), &e)),
3589 }
3590}
3591
3592
3593#[tracing::instrument(level="trace", skip_all)]
3595async fn fetch_period_audio(
3596 downloader: &mut DashDownloader,
3597 tmppath: PathBuf,
3598 audio_fragments: &[MediaFragment],
3599 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3600{
3601 let start_download = Instant::now();
3602 let mut have_audio = false;
3603 {
3604 let tmpfile_audio = File::create(tmppath.clone())
3608 .map_err(|e| DashMpdError::Io(e, String::from("creating audio tmpfile")))?;
3609 ensure_permissions_readable(&tmppath)?;
3610 let mut tmpfile_audio = BufWriter::new(tmpfile_audio);
3611 if let Some(ref fragment_path) = downloader.fragment_path {
3613 let audio_fragment_dir = fragment_path.join("audio");
3614 if !audio_fragment_dir.exists() {
3615 fs::create_dir_all(audio_fragment_dir)
3616 .map_err(|e| DashMpdError::Io(e, String::from("creating audio fragment dir")))?;
3617 }
3618 }
3619 for frag in audio_fragments.iter().filter(|f| f.period == ds.period_counter) {
3623 ds.segment_counter += 1;
3624 let progress_percent = (100.0 * ds.segment_counter as f32 / (2.0 + ds.segment_count as f32)).ceil() as u32;
3625 let url = &frag.url;
3626 if url.scheme() == "data" {
3630 let us = &url.to_string();
3631 let du = DataUrl::process(us)
3632 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
3633 if du.mime_type().type_ != "audio" {
3634 return Err(DashMpdError::UnhandledMediaStream(
3635 String::from("expecting audio content in data URL")));
3636 }
3637 let (body, _fragment) = du.decode_to_vec()
3638 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
3639 if downloader.verbosity > 2 {
3640 info!(" Audio segment data URL -> {} octets", body.len());
3641 }
3642 if let Err(e) = tmpfile_audio.write_all(&body) {
3643 error!("Unable to write DASH audio data: {e:?}");
3644 return Err(DashMpdError::Io(e, String::from("writing DASH audio data")));
3645 }
3646 have_audio = true;
3647 } else {
3648 'done: for _ in 0..downloader.fragment_retry_count {
3650 match fetch_fragment(downloader, frag, "audio", progress_percent).await {
3651 Ok(mut frag_file) => {
3652 frag_file.rewind()
3653 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
3654 let mut buf = Vec::new();
3655 frag_file.read_to_end(&mut buf)
3656 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
3657 if let Err(e) = tmpfile_audio.write_all(&buf) {
3658 error!("Unable to write DASH audio data: {e:?}");
3659 return Err(DashMpdError::Io(e, String::from("writing DASH audio data")));
3660 }
3661 have_audio = true;
3662 break 'done;
3663 },
3664 Err(e) => {
3665 if downloader.verbosity > 0 {
3666 error!("Error fetching audio segment {url}: {e:?}");
3667 }
3668 ds.download_errors += 1;
3669 if ds.download_errors > downloader.max_error_count {
3670 error!("max_error_count network errors encountered");
3671 return Err(DashMpdError::Network(
3672 String::from("more than max_error_count network errors")));
3673 }
3674 },
3675 }
3676 info!(" Retrying audio segment {url}");
3677 if downloader.sleep_between_requests > 0 {
3678 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
3679 }
3680 }
3681 }
3682 }
3683 tmpfile_audio.flush().map_err(|e| {
3684 error!("Couldn't flush DASH audio file: {e}");
3685 DashMpdError::Io(e, String::from("flushing DASH audio file"))
3686 })?;
3687 } if !downloader.decryption_keys.is_empty() {
3689 if downloader.verbosity > 0 {
3690 let metadata = fs::metadata(tmppath.clone())
3691 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted audio metadata")))?;
3692 info!(" Attempting to decrypt audio stream ({} kB) with {}",
3693 metadata.len() / 1024,
3694 downloader.decryptor_preference);
3695 }
3696 let out_ext = downloader.output_path.as_ref().unwrap()
3697 .extension()
3698 .unwrap_or(OsStr::new("mp4"));
3699 let decrypted = tmp_file_path("dashmpd-decrypted-audio", out_ext)?;
3700 if downloader.decryptor_preference.eq("mp4decrypt") {
3701 let mut args = Vec::new();
3702 for (k, v) in downloader.decryption_keys.iter() {
3703 args.push("--key".to_string());
3704 args.push(format!("{k}:{v}"));
3705 }
3706 args.push(String::from(tmppath.to_string_lossy()));
3707 args.push(String::from(decrypted.to_string_lossy()));
3708 if downloader.verbosity > 1 {
3709 info!(" Running mp4decrypt {}", args.join(" "));
3710 }
3711 let out = Command::new(downloader.mp4decrypt_location.clone())
3712 .args(args)
3713 .output()
3714 .map_err(|e| DashMpdError::Io(e, String::from("spawning mp4decrypt")))?;
3715 let mut no_output = true;
3716 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3717 if downloader.verbosity > 0 {
3718 info!(" Decrypted audio stream of size {} kB.", metadata.len() / 1024);
3719 }
3720 no_output = false;
3721 }
3722 if !out.status.success() || no_output {
3723 warn!(" mp4decrypt subprocess failed");
3724 let msg = partial_process_output(&out.stdout);
3725 if !msg.is_empty() {
3726 warn!(" mp4decrypt stdout: {msg}");
3727 }
3728 let msg = partial_process_output(&out.stderr);
3729 if !msg.is_empty() {
3730 warn!(" mp4decrypt stderr: {msg}");
3731 }
3732 }
3733 if no_output {
3734 error!(" Failed to decrypt audio stream with mp4decrypt");
3735 warn!(" Undecrypted audio left in {}", tmppath.display());
3736 return Err(DashMpdError::Decrypting(String::from("audio stream")));
3737 }
3738 } else if downloader.decryptor_preference.eq("shaka") {
3739 let mut args = Vec::new();
3740 let mut keys = Vec::new();
3741 if downloader.verbosity < 1 {
3742 args.push("--quiet".to_string());
3743 }
3744 args.push(format!("in={},stream=audio,output={}", tmppath.display(), decrypted.display()));
3745 let mut drm_label = 0;
3746 #[allow(clippy::explicit_counter_loop)]
3747 for (k, v) in downloader.decryption_keys.iter() {
3748 keys.push(format!("label=lbl{drm_label}:key_id={k}:key={v}"));
3749 drm_label += 1;
3750 }
3751 args.push("--enable_raw_key_decryption".to_string());
3752 args.push("--keys".to_string());
3753 args.push(keys.join(","));
3754 if downloader.verbosity > 1 {
3755 info!(" Running shaka-packager {}", args.join(" "));
3756 }
3757 let out = Command::new(downloader.shaka_packager_location.clone())
3758 .args(args)
3759 .output()
3760 .map_err(|e| DashMpdError::Io(e, String::from("spawning shaka-packager")))?;
3761 let mut no_output = false;
3762 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3763 if downloader.verbosity > 0 {
3764 info!(" Decrypted audio stream of size {} kB.", metadata.len() / 1024);
3765 }
3766 if metadata.len() == 0 {
3767 no_output = true;
3768 }
3769 } else {
3770 no_output = true;
3771 }
3772 if !out.status.success() || no_output {
3773 warn!(" shaka-packager subprocess failed");
3774 let msg = partial_process_output(&out.stdout);
3775 if !msg.is_empty() {
3776 warn!(" shaka-packager stdout: {msg}");
3777 }
3778 let msg = partial_process_output(&out.stderr);
3779 if !msg.is_empty() {
3780 warn!(" shaka-packager stderr: {msg}");
3781 }
3782 }
3783 if no_output {
3784 error!(" Failed to decrypt audio stream with shaka-packager");
3785 warn!(" Undecrypted audio stream left in {}", tmppath.display());
3786 return Err(DashMpdError::Decrypting(String::from("audio stream")));
3787 }
3788 } else if downloader.decryptor_preference.eq("mp4box") {
3791 let mut args = Vec::new();
3792 let drmfile = tmp_file_path("mp4boxcrypt", OsStr::new("xml"))?;
3793 let mut drmfile_contents = String::from("<GPACDRM>\n <CrypTrack>\n");
3794 for (k, v) in downloader.decryption_keys.iter() {
3795 drmfile_contents += &format!(" <key KID=\"0x{k}\" value=\"0x{v}\"/>\n");
3796 }
3797 drmfile_contents += " </CrypTrack>\n</GPACDRM>\n";
3798 fs::write(&drmfile, drmfile_contents)
3799 .map_err(|e| DashMpdError::Io(e, String::from("writing to MP4Box decrypt file")))?;
3800 args.push("-decrypt".to_string());
3801 args.push(drmfile.display().to_string());
3802 args.push(String::from(tmppath.to_string_lossy()));
3803 args.push("-out".to_string());
3804 args.push(String::from(decrypted.to_string_lossy()));
3805 if downloader.verbosity > 1 {
3806 info!(" Running decryption application MP4Box {}", args.join(" "));
3807 }
3808 let out = Command::new(downloader.mp4box_location.clone())
3809 .args(args)
3810 .output()
3811 .map_err(|e| DashMpdError::Io(e, String::from("spawning MP4Box")))?;
3812 let mut no_output = false;
3813 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3814 if downloader.verbosity > 0 {
3815 info!(" Decrypted audio stream of size {} kB.", metadata.len() / 1024);
3816 }
3817 if metadata.len() == 0 {
3818 no_output = true;
3819 }
3820 } else {
3821 no_output = true;
3822 }
3823 if !out.status.success() || no_output {
3824 warn!(" MP4Box decryption subprocess failed");
3825 let msg = partial_process_output(&out.stdout);
3826 if !msg.is_empty() {
3827 warn!(" MP4Box stdout: {msg}");
3828 }
3829 let msg = partial_process_output(&out.stderr);
3830 if !msg.is_empty() {
3831 warn!(" MP4Box stderr: {msg}");
3832 }
3833 }
3834 if let Err(e) = fs::remove_file(drmfile) {
3835 warn!(" Failed to delete temporary MP4Box crypt file: {e:?}");
3836 }
3837 if no_output {
3838 error!(" Failed to decrypt audio stream with MP4Box");
3839 warn!(" Undecrypted audio stream left in {}", tmppath.display());
3840 return Err(DashMpdError::Decrypting(String::from("audio stream")));
3841 }
3842 } else {
3843 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
3844 }
3845 fs::rename(decrypted, tmppath.clone())
3846 .map_err(|e| DashMpdError::Io(e, String::from("renaming decrypted audio")))?;
3847 }
3848 if let Ok(metadata) = fs::metadata(tmppath.clone()) {
3849 if downloader.verbosity > 1 {
3850 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
3851 let elapsed = start_download.elapsed();
3852 info!(" Wrote {mbytes:.1}MB to DASH audio file ({:.1} MB/s)",
3853 mbytes / elapsed.as_secs_f64());
3854 }
3855 }
3856 Ok(have_audio)
3857}
3858
3859
3860#[tracing::instrument(level="trace", skip_all)]
3862async fn fetch_period_video(
3863 downloader: &mut DashDownloader,
3864 tmppath: PathBuf,
3865 video_fragments: &[MediaFragment],
3866 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3867{
3868 let start_download = Instant::now();
3869 let mut have_video = false;
3870 {
3871 let tmpfile_video = File::create(tmppath.clone())
3874 .map_err(|e| DashMpdError::Io(e, String::from("creating video tmpfile")))?;
3875 ensure_permissions_readable(&tmppath)?;
3876 let mut tmpfile_video = BufWriter::new(tmpfile_video);
3877 if let Some(ref fragment_path) = downloader.fragment_path {
3879 let video_fragment_dir = fragment_path.join("video");
3880 if !video_fragment_dir.exists() {
3881 fs::create_dir_all(video_fragment_dir)
3882 .map_err(|e| DashMpdError::Io(e, String::from("creating video fragment dir")))?;
3883 }
3884 }
3885 for frag in video_fragments.iter().filter(|f| f.period == ds.period_counter) {
3886 ds.segment_counter += 1;
3887 let progress_percent = (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32;
3888 if frag.url.scheme() == "data" {
3889 let us = &frag.url.to_string();
3890 let du = DataUrl::process(us)
3891 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
3892 if du.mime_type().type_ != "video" {
3893 return Err(DashMpdError::UnhandledMediaStream(
3894 String::from("expecting video content in data URL")));
3895 }
3896 let (body, _fragment) = du.decode_to_vec()
3897 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
3898 if downloader.verbosity > 2 {
3899 info!(" Video segment data URL -> {} octets", body.len());
3900 }
3901 if let Err(e) = tmpfile_video.write_all(&body) {
3902 error!("Unable to write DASH video data: {e:?}");
3903 return Err(DashMpdError::Io(e, String::from("writing DASH video data")));
3904 }
3905 have_video = true;
3906 } else {
3907 'done: for _ in 0..downloader.fragment_retry_count {
3908 match fetch_fragment(downloader, frag, "video", progress_percent).await {
3909 Ok(mut frag_file) => {
3910 frag_file.rewind()
3911 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
3912 let mut buf = Vec::new();
3913 frag_file.read_to_end(&mut buf)
3914 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
3915 if let Err(e) = tmpfile_video.write_all(&buf) {
3916 error!("Unable to write DASH video data: {e:?}");
3917 return Err(DashMpdError::Io(e, String::from("writing DASH video data")));
3918 }
3919 have_video = true;
3920 break 'done;
3921 },
3922 Err(e) => {
3923 if downloader.verbosity > 0 {
3924 error!(" Error fetching video segment {}: {e:?}", frag.url);
3925 }
3926 ds.download_errors += 1;
3927 if ds.download_errors > downloader.max_error_count {
3928 return Err(DashMpdError::Network(
3929 String::from("more than max_error_count network errors")));
3930 }
3931 },
3932 }
3933 info!(" Retrying video segment {}", frag.url);
3934 if downloader.sleep_between_requests > 0 {
3935 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
3936 }
3937 }
3938 }
3939 }
3940 tmpfile_video.flush().map_err(|e| {
3941 error!(" Couldn't flush video file: {e}");
3942 DashMpdError::Io(e, String::from("flushing video file"))
3943 })?;
3944 } if !downloader.decryption_keys.is_empty() {
3946 if downloader.verbosity > 0 {
3947 let metadata = fs::metadata(tmppath.clone())
3948 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted video metadata")))?;
3949 info!(" Attempting to decrypt video stream ({} kB) with {}",
3950 metadata.len() / 1024,
3951 downloader.decryptor_preference);
3952 }
3953 let out_ext = downloader.output_path.as_ref().unwrap()
3954 .extension()
3955 .unwrap_or(OsStr::new("mp4"));
3956 let decrypted = tmp_file_path("dashmpd-decrypted-video", out_ext)?;
3957 if downloader.decryptor_preference.eq("mp4decrypt") {
3958 let mut args = Vec::new();
3959 for (k, v) in downloader.decryption_keys.iter() {
3960 args.push("--key".to_string());
3961 args.push(format!("{k}:{v}"));
3962 }
3963 args.push(tmppath.to_string_lossy().to_string());
3964 args.push(decrypted.to_string_lossy().to_string());
3965 if downloader.verbosity > 1 {
3966 info!(" Running mp4decrypt {}", args.join(" "));
3967 }
3968 let out = Command::new(downloader.mp4decrypt_location.clone())
3969 .args(args)
3970 .output()
3971 .map_err(|e| DashMpdError::Io(e, String::from("spawning mp4decrypt")))?;
3972 let mut no_output = false;
3973 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3974 if downloader.verbosity > 0 {
3975 info!(" Decrypted video stream of size {} kB.", metadata.len() / 1024);
3976 }
3977 if metadata.len() == 0 {
3978 no_output = true;
3979 }
3980 } else {
3981 no_output = true;
3982 }
3983 if !out.status.success() || no_output {
3984 error!(" mp4decrypt subprocess failed");
3985 let msg = partial_process_output(&out.stdout);
3986 if !msg.is_empty() {
3987 warn!(" mp4decrypt stdout: {msg}");
3988 }
3989 let msg = partial_process_output(&out.stderr);
3990 if !msg.is_empty() {
3991 warn!(" mp4decrypt stderr: {msg}");
3992 }
3993 }
3994 if no_output {
3995 error!(" Failed to decrypt video stream with mp4decrypt");
3996 warn!(" Undecrypted video stream left in {}", tmppath.display());
3997 return Err(DashMpdError::Decrypting(String::from("video stream")));
3998 }
3999 } else if downloader.decryptor_preference.eq("shaka") {
4000 let mut args = Vec::new();
4001 let mut keys = Vec::new();
4002 if downloader.verbosity < 1 {
4003 args.push("--quiet".to_string());
4004 }
4005 args.push(format!("in={},stream=video,output={}", tmppath.display(), decrypted.display()));
4006 let mut drm_label = 0;
4007 #[allow(clippy::explicit_counter_loop)]
4008 for (k, v) in downloader.decryption_keys.iter() {
4009 keys.push(format!("label=lbl{drm_label}:key_id={k}:key={v}"));
4010 drm_label += 1;
4011 }
4012 args.push("--enable_raw_key_decryption".to_string());
4013 args.push("--keys".to_string());
4014 args.push(keys.join(","));
4015 if downloader.verbosity > 1 {
4016 info!(" Running shaka-packager {}", args.join(" "));
4017 }
4018 let out = Command::new(downloader.shaka_packager_location.clone())
4019 .args(args)
4020 .output()
4021 .map_err(|e| DashMpdError::Io(e, String::from("spawning shaka-packager")))?;
4022 let mut no_output = true;
4023 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
4024 if downloader.verbosity > 0 {
4025 info!(" Decrypted video stream of size {} kB.", metadata.len() / 1024);
4026 }
4027 no_output = false;
4028 }
4029 if !out.status.success() || no_output {
4030 warn!(" shaka-packager subprocess failed");
4031 let msg = partial_process_output(&out.stdout);
4032 if !msg.is_empty() {
4033 warn!(" shaka-packager stdout: {msg}");
4034 }
4035 let msg = partial_process_output(&out.stderr);
4036 if !msg.is_empty() {
4037 warn!(" shaka-packager stderr: {msg}");
4038 }
4039 }
4040 if no_output {
4041 error!(" Failed to decrypt video stream with shaka-packager");
4042 warn!(" Undecrypted video left in {}", tmppath.display());
4043 return Err(DashMpdError::Decrypting(String::from("video stream")));
4044 }
4045 } else if downloader.decryptor_preference.eq("mp4box") {
4046 let mut args = Vec::new();
4047 let drmfile = tmp_file_path("mp4boxcrypt", OsStr::new("xml"))?;
4048 let mut drmfile_contents = String::from("<GPACDRM>\n <CrypTrack>\n");
4049 for (k, v) in downloader.decryption_keys.iter() {
4050 drmfile_contents += &format!(" <key KID=\"0x{k}\" value=\"0x{v}\"/>\n");
4051 }
4052 drmfile_contents += " </CrypTrack>\n</GPACDRM>\n";
4053 fs::write(&drmfile, drmfile_contents)
4054 .map_err(|e| DashMpdError::Io(e, String::from("writing to MP4Box decrypt file")))?;
4055 args.push("-decrypt".to_string());
4056 args.push(drmfile.display().to_string());
4057 args.push(String::from(tmppath.to_string_lossy()));
4058 args.push("-out".to_string());
4059 args.push(String::from(decrypted.to_string_lossy()));
4060 if downloader.verbosity > 1 {
4061 info!(" Running decryption application MP4Box {}", args.join(" "));
4062 }
4063 let out = Command::new(downloader.mp4box_location.clone())
4064 .args(args)
4065 .output()
4066 .map_err(|e| DashMpdError::Io(e, String::from("spawning MP4Box")))?;
4067 let mut no_output = false;
4068 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
4069 if downloader.verbosity > 0 {
4070 info!(" Decrypted video stream of size {} kB.", metadata.len() / 1024);
4071 }
4072 if metadata.len() == 0 {
4073 no_output = true;
4074 }
4075 } else {
4076 no_output = true;
4077 }
4078 if !out.status.success() || no_output {
4079 warn!(" MP4Box decryption subprocess failed");
4080 let msg = partial_process_output(&out.stdout);
4081 if !msg.is_empty() {
4082 warn!(" MP4Box stdout: {msg}");
4083 }
4084 let msg = partial_process_output(&out.stderr);
4085 if !msg.is_empty() {
4086 warn!(" MP4Box stderr: {msg}");
4087 }
4088 }
4089 if no_output {
4090 error!(" Failed to decrypt video stream with MP4Box");
4091 warn!(" Undecrypted video stream left in {}", tmppath.display());
4092 return Err(DashMpdError::Decrypting(String::from("video stream")));
4093 }
4094 } else {
4095 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
4096 }
4097 fs::rename(decrypted, tmppath.clone())
4098 .map_err(|e| DashMpdError::Io(e, String::from("renaming decrypted video")))?;
4099 }
4100 if let Ok(metadata) = fs::metadata(tmppath.clone()) {
4101 if downloader.verbosity > 1 {
4102 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4103 let elapsed = start_download.elapsed();
4104 info!(" Wrote {mbytes:.1}MB to DASH video file ({:.1} MB/s)",
4105 mbytes / elapsed.as_secs_f64());
4106 }
4107 }
4108 Ok(have_video)
4109}
4110
4111
4112#[tracing::instrument(level="trace", skip_all)]
4114async fn fetch_period_subtitles(
4115 downloader: &DashDownloader,
4116 tmppath: PathBuf,
4117 subtitle_fragments: &[MediaFragment],
4118 subtitle_formats: &[SubtitleType],
4119 ds: &mut DownloadState) -> Result<bool, DashMpdError>
4120{
4121 let client = downloader.http_client.clone().unwrap();
4122 let start_download = Instant::now();
4123 let mut have_subtitles = false;
4124 {
4125 let tmpfile_subs = File::create(tmppath.clone())
4126 .map_err(|e| DashMpdError::Io(e, String::from("creating subs tmpfile")))?;
4127 ensure_permissions_readable(&tmppath)?;
4128 let mut tmpfile_subs = BufWriter::new(tmpfile_subs);
4129 for frag in subtitle_fragments {
4130 ds.segment_counter += 1;
4132 let progress_percent = (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32;
4133 for observer in &downloader.progress_observers {
4134 observer.update(progress_percent, "Fetching subtitle segments");
4135 }
4136 if frag.url.scheme() == "data" {
4137 let us = &frag.url.to_string();
4138 let du = DataUrl::process(us)
4139 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
4140 if du.mime_type().type_ != "video" {
4141 return Err(DashMpdError::UnhandledMediaStream(
4142 String::from("expecting video content in data URL")));
4143 }
4144 let (body, _fragment) = du.decode_to_vec()
4145 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
4146 if downloader.verbosity > 2 {
4147 info!(" Subtitle segment data URL -> {} octets", body.len());
4148 }
4149 if let Err(e) = tmpfile_subs.write_all(&body) {
4150 error!("Unable to write DASH subtitle data: {e:?}");
4151 return Err(DashMpdError::Io(e, String::from("writing DASH subtitle data")));
4152 }
4153 have_subtitles = true;
4154 } else {
4155 let fetch = || async {
4156 let mut req = client.get(frag.url.clone())
4157 .header("Sec-Fetch-Mode", "navigate");
4158 if let Some(sb) = &frag.start_byte {
4159 if let Some(eb) = &frag.end_byte {
4160 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
4161 }
4162 }
4163 if let Some(referer) = &downloader.referer {
4164 req = req.header("Referer", referer);
4165 } else {
4166 req = req.header("Referer", downloader.redirected_url.to_string());
4167 }
4168 if let Some(username) = &downloader.auth_username {
4169 if let Some(password) = &downloader.auth_password {
4170 req = req.basic_auth(username, Some(password));
4171 }
4172 }
4173 if let Some(token) = &downloader.auth_bearer_token {
4174 req = req.bearer_auth(token);
4175 }
4176 req.send().await
4177 .map_err(categorize_reqwest_error)?
4178 .error_for_status()
4179 .map_err(categorize_reqwest_error)
4180 };
4181 let mut failure = None;
4182 match retry_notify(ExponentialBackoff::default(), fetch, notify_transient).await {
4183 Ok(response) => {
4184 if response.status().is_success() {
4185 let dash_bytes = response.bytes().await
4186 .map_err(|e| network_error("fetching DASH subtitle segment", &e))?;
4187 if downloader.verbosity > 2 {
4188 if let Some(sb) = &frag.start_byte {
4189 if let Some(eb) = &frag.end_byte {
4190 info!(" Subtitle segment {} range {sb}-{eb} -> {} octets",
4191 &frag.url, dash_bytes.len());
4192 }
4193 } else {
4194 info!(" Subtitle segment {} -> {} octets", &frag.url, dash_bytes.len());
4195 }
4196 }
4197 let size = min((dash_bytes.len()/1024 + 1) as u32, u32::MAX);
4198 throttle_download_rate(downloader, size).await?;
4199 if let Err(e) = tmpfile_subs.write_all(&dash_bytes) {
4200 return Err(DashMpdError::Io(e, String::from("writing DASH subtitle data")));
4201 }
4202 have_subtitles = true;
4203 } else {
4204 failure = Some(format!("HTTP error {}", response.status().as_str()));
4205 }
4206 },
4207 Err(e) => failure = Some(format!("{e}")),
4208 }
4209 if let Some(f) = failure {
4210 if downloader.verbosity > 0 {
4211 error!("{f} fetching subtitle segment {}", &frag.url);
4212 }
4213 ds.download_errors += 1;
4214 if ds.download_errors > downloader.max_error_count {
4215 return Err(DashMpdError::Network(
4216 String::from("more than max_error_count network errors")));
4217 }
4218 }
4219 }
4220 if downloader.sleep_between_requests > 0 {
4221 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
4222 }
4223 }
4224 tmpfile_subs.flush().map_err(|e| {
4225 error!("Couldn't flush subs file: {e}");
4226 DashMpdError::Io(e, String::from("flushing subtitle file"))
4227 })?;
4228 } if have_subtitles {
4230 if let Ok(metadata) = fs::metadata(tmppath.clone()) {
4231 if downloader.verbosity > 1 {
4232 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4233 let elapsed = start_download.elapsed();
4234 info!(" Wrote {mbytes:.1}MB to DASH subtitle file ({:.1} MB/s)",
4235 mbytes / elapsed.as_secs_f64());
4236 }
4237 }
4238 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
4241 subtitle_formats.contains(&SubtitleType::Ttxt)
4242 {
4243 if downloader.verbosity > 0 {
4245 if let Some(fmt) = subtitle_formats.first() {
4246 info!(" Downloaded media contains subtitles in {fmt:?} format");
4247 }
4248 info!(" Running MP4Box to extract subtitles");
4249 }
4250 let out = downloader.output_path.as_ref().unwrap()
4251 .with_extension("srt");
4252 let out_str = out.to_string_lossy();
4253 let tmp_str = tmppath.to_string_lossy();
4254 let args = vec![
4255 "-srt", "1",
4256 "-out", &out_str,
4257 &tmp_str];
4258 if downloader.verbosity > 0 {
4259 info!(" Running MP4Box {}", args.join(" "));
4260 }
4261 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4262 .args(args)
4263 .output()
4264 {
4265 let msg = partial_process_output(&mp4box.stdout);
4266 if !msg.is_empty() {
4267 info!(" MP4Box stdout: {msg}");
4268 }
4269 let msg = partial_process_output(&mp4box.stderr);
4270 if !msg.is_empty() {
4271 info!(" MP4Box stderr: {msg}");
4272 }
4273 if mp4box.status.success() {
4274 info!(" Extracted subtitles as SRT");
4275 } else {
4276 warn!(" Error running MP4Box to extract subtitles");
4277 }
4278 } else {
4279 warn!(" Failed to spawn MP4Box to extract subtitles");
4280 }
4281 }
4282 if subtitle_formats.contains(&SubtitleType::Stpp) {
4283 if downloader.verbosity > 0 {
4284 info!(" Converting STPP subtitles to TTML format with ffmpeg");
4285 }
4286 let out = downloader.output_path.as_ref().unwrap()
4287 .with_extension("ttml");
4288 let tmppath_arg = &tmppath.to_string_lossy();
4289 let out_arg = &out.to_string_lossy();
4290 let ffmpeg_args = vec![
4291 "-hide_banner",
4292 "-nostats",
4293 "-loglevel", "error",
4294 "-y", "-nostdin",
4296 "-i", tmppath_arg,
4297 "-f", "data",
4298 "-map", "0",
4299 "-c", "copy",
4300 out_arg];
4301 if downloader.verbosity > 0 {
4302 info!(" Running ffmpeg {}", ffmpeg_args.join(" "));
4303 }
4304 if let Ok(ffmpeg) = Command::new(downloader.ffmpeg_location.clone())
4305 .args(ffmpeg_args)
4306 .output()
4307 {
4308 let msg = partial_process_output(&ffmpeg.stdout);
4309 if !msg.is_empty() {
4310 info!(" ffmpeg stdout: {msg}");
4311 }
4312 let msg = partial_process_output(&ffmpeg.stderr);
4313 if !msg.is_empty() {
4314 info!(" ffmpeg stderr: {msg}");
4315 }
4316 if ffmpeg.status.success() {
4317 info!(" Converted STPP subtitles to TTML format");
4318 } else {
4319 warn!(" Error running ffmpeg to convert subtitles");
4320 }
4321 }
4322 }
4326
4327 }
4328 Ok(have_subtitles)
4329}
4330
4331
4332async fn fetch_mpd_http(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4334 let client = &downloader.http_client.clone().unwrap();
4335 let send_request = || async {
4336 let mut req = client.get(&downloader.mpd_url)
4337 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4338 .header("Accept-Language", "en-US,en")
4339 .header("Upgrade-Insecure-Requests", "1")
4340 .header("Sec-Fetch-Mode", "navigate");
4341 if let Some(referer) = &downloader.referer {
4342 req = req.header("Referer", referer);
4343 }
4344 if let Some(username) = &downloader.auth_username {
4345 if let Some(password) = &downloader.auth_password {
4346 req = req.basic_auth(username, Some(password));
4347 }
4348 }
4349 if let Some(token) = &downloader.auth_bearer_token {
4350 req = req.bearer_auth(token);
4351 }
4352 req.send().await
4353 .map_err(categorize_reqwest_error)?
4354 .error_for_status()
4355 .map_err(categorize_reqwest_error)
4356 };
4357 for observer in &downloader.progress_observers {
4358 observer.update(1, "Fetching DASH manifest");
4359 }
4360 if downloader.verbosity > 0 {
4361 if !downloader.fetch_audio && !downloader.fetch_video && !downloader.fetch_subtitles {
4362 info!("Only simulating media downloads");
4363 }
4364 info!("Fetching the DASH manifest");
4365 }
4366 let response = retry_notify(ExponentialBackoff::default(), send_request, notify_transient)
4367 .await
4368 .map_err(|e| network_error("requesting DASH manifest", &e))?;
4369 if !response.status().is_success() {
4370 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4371 return Err(DashMpdError::Network(msg));
4372 }
4373 downloader.redirected_url = response.url().clone();
4374 response.bytes().await
4375 .map_err(|e| network_error("fetching DASH manifest", &e))
4376}
4377
4378async fn fetch_mpd_file(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4381 if ! &downloader.mpd_url.starts_with("file://") {
4382 return Err(DashMpdError::Other(String::from("expecting file:// URL scheme")));
4383 }
4384 let url = Url::parse(&downloader.mpd_url)
4385 .map_err(|_| DashMpdError::Other(String::from("parsing MPD URL")))?;
4386 let path = url.to_file_path()
4387 .map_err(|_| DashMpdError::Other(String::from("extracting path from file:// URL")))?;
4388 let octets = fs::read(path)
4389 .map_err(|_| DashMpdError::Other(String::from("reading from file:// URL")))?;
4390 Ok(Bytes::from(octets))
4391}
4392
4393
4394#[tracing::instrument(level="trace", skip_all)]
4395async fn fetch_mpd(downloader: &mut DashDownloader) -> Result<PathBuf, DashMpdError> {
4396 let xml = if downloader.mpd_url.starts_with("file://") {
4397 fetch_mpd_file(downloader).await?
4398 } else {
4399 fetch_mpd_http(downloader).await?
4400 };
4401 let mut mpd: MPD = parse_resolving_xlinks(downloader, &xml).await
4402 .map_err(|e| parse_error("parsing DASH XML", e))?;
4403 let client = &downloader.http_client.clone().unwrap();
4406 if let Some(new_location) = &mpd.locations.first() {
4407 let new_url = &new_location.url;
4408 if downloader.verbosity > 0 {
4409 info!("Redirecting to new manifest <Location> {new_url}");
4410 }
4411 let send_request = || async {
4412 let mut req = client.get(new_url)
4413 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4414 .header("Accept-Language", "en-US,en")
4415 .header("Sec-Fetch-Mode", "navigate");
4416 if let Some(referer) = &downloader.referer {
4417 req = req.header("Referer", referer);
4418 } else {
4419 req = req.header("Referer", downloader.redirected_url.to_string());
4420 }
4421 if let Some(username) = &downloader.auth_username {
4422 if let Some(password) = &downloader.auth_password {
4423 req = req.basic_auth(username, Some(password));
4424 }
4425 }
4426 if let Some(token) = &downloader.auth_bearer_token {
4427 req = req.bearer_auth(token);
4428 }
4429 req.send().await
4430 .map_err(categorize_reqwest_error)?
4431 .error_for_status()
4432 .map_err(categorize_reqwest_error)
4433 };
4434 let response = retry_notify(ExponentialBackoff::default(), send_request, notify_transient)
4435 .await
4436 .map_err(|e| network_error("requesting relocated DASH manifest", &e))?;
4437 if !response.status().is_success() {
4438 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4439 return Err(DashMpdError::Network(msg));
4440 }
4441 downloader.redirected_url = response.url().clone();
4442 let xml = response.bytes().await
4443 .map_err(|e| network_error("fetching relocated DASH manifest", &e))?;
4444 mpd = parse_resolving_xlinks(downloader, &xml).await
4445 .map_err(|e| parse_error("parsing relocated DASH XML", e))?;
4446 }
4447 if mpd_is_dynamic(&mpd) {
4448 if downloader.allow_live_streams {
4451 if downloader.verbosity > 0 {
4452 warn!("Attempting to download from live stream (this may not work).");
4453 }
4454 } else {
4455 return Err(DashMpdError::UnhandledMediaStream("Don't know how to download dynamic MPD".to_string()));
4456 }
4457 }
4458 let mut toplevel_base_url = downloader.redirected_url.clone();
4459 if let Some(bu) = &mpd.base_url.first() {
4461 toplevel_base_url = merge_baseurls(&downloader.redirected_url, &bu.base)?;
4462 }
4463 if let Some(base) = &downloader.base_url {
4466 toplevel_base_url = merge_baseurls(&downloader.redirected_url, base)?;
4467 }
4468 if downloader.verbosity > 0 {
4469 let pcount = mpd.periods.len();
4470 info!("DASH manifest has {pcount} period{}", if pcount > 1 { "s" } else { "" });
4471 print_available_streams(&mpd);
4472 }
4473 let mut pds: Vec<PeriodDownloads> = Vec::new();
4481 let mut period_counter = 0;
4482 for mpd_period in &mpd.periods {
4483 let period = mpd_period.clone();
4484 period_counter += 1;
4485 if let Some(min) = downloader.minimum_period_duration {
4486 if let Some(duration) = period.duration {
4487 if duration < min {
4488 if let Some(id) = period.id.as_ref() {
4489 info!("Skipping period {id} (#{period_counter}): duration is less than requested minimum");
4490 } else {
4491 info!("Skipping period #{period_counter}: duration is less than requested minimum");
4492 }
4493 continue;
4494 }
4495 }
4496 }
4497 let mut pd = PeriodDownloads { period_counter, ..Default::default() };
4498 if let Some(id) = period.id.as_ref() {
4499 pd.id = Some(id.clone());
4500 }
4501 if downloader.verbosity > 0 {
4502 if let Some(id) = period.id.as_ref() {
4503 info!("Preparing download for period {id} (#{period_counter})");
4504 } else {
4505 info!("Preparing download for period #{period_counter}");
4506 }
4507 }
4508 let mut base_url = toplevel_base_url.clone();
4509 if let Some(bu) = period.BaseURL.first() {
4511 base_url = merge_baseurls(&base_url, &bu.base)?;
4512 }
4513 let mut audio_outputs = PeriodOutputs::default();
4514 if downloader.fetch_audio {
4515 audio_outputs = do_period_audio(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4516 for f in audio_outputs.fragments {
4517 pd.audio_fragments.push(f);
4518 }
4519 pd.selected_audio_language = audio_outputs.selected_audio_language;
4520 }
4521 let mut video_outputs = PeriodOutputs::default();
4522 if downloader.fetch_video {
4523 video_outputs = do_period_video(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4524 for f in video_outputs.fragments {
4525 pd.video_fragments.push(f);
4526 }
4527 }
4528 match do_period_subtitles(downloader, &mpd, &period, period_counter, base_url.clone()).await {
4529 Ok(subtitle_outputs) => {
4530 for f in subtitle_outputs.fragments {
4531 pd.subtitle_fragments.push(f);
4532 }
4533 for f in subtitle_outputs.subtitle_formats {
4534 pd.subtitle_formats.push(f);
4535 }
4536 },
4537 Err(e) => warn!(" Ignoring error triggered while processing subtitles: {e}"),
4538 }
4539 if downloader.verbosity > 0 {
4541 use base64::prelude::{Engine as _, BASE64_STANDARD};
4542
4543 audio_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4544 for f in pd.audio_fragments.iter().filter(|f| f.is_init) {
4545 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4546 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4547 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4548 info!(" {}", pssh.to_string());
4549 }
4550 }
4551 }
4552 video_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4553 for f in pd.video_fragments.iter().filter(|f| f.is_init) {
4554 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4555 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4556 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4557 info!(" {}", pssh.to_string());
4558 }
4559 }
4560 }
4561 }
4562 pds.push(pd);
4563 } let output_path = &downloader.output_path.as_ref().unwrap().clone();
4568 let mut period_output_paths: Vec<PathBuf> = Vec::new();
4569 let mut ds = DownloadState {
4570 period_counter: 0,
4571 segment_count: pds.iter().map(period_fragment_count).sum(),
4573 segment_counter: 0,
4574 download_errors: 0
4575 };
4576 for pd in pds {
4577 let mut have_audio = false;
4578 let mut have_video = false;
4579 let mut have_subtitles = false;
4580 ds.period_counter = pd.period_counter;
4581 let period_output_path = output_path_for_period(output_path, pd.period_counter);
4582 #[allow(clippy::collapsible_if)]
4583 if downloader.verbosity > 0 {
4584 if downloader.fetch_audio || downloader.fetch_video || downloader.fetch_subtitles {
4585 let idnum = if let Some(id) = pd.id {
4586 format!("id={} (#{})", id, pd.period_counter)
4587 } else {
4588 format!("#{}", pd.period_counter)
4589 };
4590 info!("Period {idnum}: fetching {} audio, {} video and {} subtitle segments",
4591 pd.audio_fragments.len(),
4592 pd.video_fragments.len(),
4593 pd.subtitle_fragments.len());
4594 }
4595 }
4596 let output_ext = downloader.output_path.as_ref().unwrap()
4597 .extension()
4598 .unwrap_or(OsStr::new("mp4"));
4599 let tmppath_audio = if let Some(ref path) = downloader.keep_audio {
4600 path.clone()
4601 } else {
4602 tmp_file_path("dashmpd-audio", output_ext)?
4603 };
4604 let tmppath_video = if let Some(ref path) = downloader.keep_video {
4605 path.clone()
4606 } else {
4607 tmp_file_path("dashmpd-video", output_ext)?
4608 };
4609 let tmppath_subs = tmp_file_path("dashmpd-subs", OsStr::new("sub"))?;
4610 if downloader.fetch_audio && !pd.audio_fragments.is_empty() {
4611 have_audio = fetch_period_audio(downloader,
4615 tmppath_audio.clone(), &pd.audio_fragments,
4616 &mut ds).await?;
4617 }
4618 if downloader.fetch_video && !pd.video_fragments.is_empty() {
4619 have_video = fetch_period_video(downloader,
4620 tmppath_video.clone(), &pd.video_fragments,
4621 &mut ds).await?;
4622 }
4623 if downloader.fetch_subtitles && !pd.subtitle_fragments.is_empty() {
4627 have_subtitles = fetch_period_subtitles(downloader,
4628 tmppath_subs.clone(),
4629 &pd.subtitle_fragments,
4630 &pd.subtitle_formats,
4631 &mut ds).await?;
4632 }
4633
4634 if have_audio && have_video {
4637 for observer in &downloader.progress_observers {
4638 observer.update(99, "Muxing audio and video");
4639 }
4640 if downloader.verbosity > 1 {
4641 info!(" Muxing audio and video streams");
4642 }
4643 let audio_tracks = vec![
4644 AudioTrack {
4645 language: pd.selected_audio_language,
4646 path: tmppath_audio.clone()
4647 }];
4648 mux_audio_video(downloader, &period_output_path, &audio_tracks, &tmppath_video)?;
4649 if pd.subtitle_formats.contains(&SubtitleType::Stpp) {
4650 let container = match &period_output_path.extension() {
4651 Some(ext) => ext.to_str().unwrap_or("mp4"),
4652 None => "mp4",
4653 };
4654 if container.eq("mp4") {
4655 if downloader.verbosity > 1 {
4656 if let Some(fmt) = &pd.subtitle_formats.first() {
4657 info!(" Downloaded media contains subtitles in {fmt:?} format");
4658 }
4659 info!(" Running MP4Box to merge subtitles with output MP4 container");
4660 }
4661 let tmp_str = tmppath_subs.to_string_lossy();
4664 let period_output_str = period_output_path.to_string_lossy();
4665 let args = vec!["-add", &tmp_str, &period_output_str];
4666 if downloader.verbosity > 0 {
4667 info!(" Running MP4Box {}", args.join(" "));
4668 }
4669 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4670 .args(args)
4671 .output()
4672 {
4673 let msg = partial_process_output(&mp4box.stdout);
4674 if !msg.is_empty() {
4675 info!(" MP4Box stdout: {msg}");
4676 }
4677 let msg = partial_process_output(&mp4box.stderr);
4678 if !msg.is_empty() {
4679 info!(" MP4Box stderr: {msg}");
4680 }
4681 if mp4box.status.success() {
4682 info!(" Merged subtitles with MP4 container");
4683 } else {
4684 warn!(" Error running MP4Box to merge subtitles");
4685 }
4686 } else {
4687 warn!(" Failed to spawn MP4Box to merge subtitles");
4688 }
4689 } else if container.eq("mkv") || container.eq("webm") {
4690 let srt = period_output_path.with_extension("srt");
4702 if srt.exists() {
4703 if downloader.verbosity > 0 {
4704 info!(" Running mkvmerge to merge subtitles with output Matroska container");
4705 }
4706 let tmppath = temporary_outpath(".mkv")?;
4707 let pop_arg = &period_output_path.to_string_lossy();
4708 let srt_arg = &srt.to_string_lossy();
4709 let mkvmerge_args = vec!["-o", &tmppath, pop_arg, srt_arg];
4710 if downloader.verbosity > 0 {
4711 info!(" Running mkvmerge {}", mkvmerge_args.join(" "));
4712 }
4713 if let Ok(mkvmerge) = Command::new(downloader.mkvmerge_location.clone())
4714 .args(mkvmerge_args)
4715 .output()
4716 {
4717 let msg = partial_process_output(&mkvmerge.stdout);
4718 if !msg.is_empty() {
4719 info!(" mkvmerge stdout: {msg}");
4720 }
4721 let msg = partial_process_output(&mkvmerge.stderr);
4722 if !msg.is_empty() {
4723 info!(" mkvmerge stderr: {msg}");
4724 }
4725 if mkvmerge.status.success() {
4726 info!(" Merged subtitles with Matroska container");
4727 {
4730 let tmpfile = File::open(tmppath.clone())
4731 .map_err(|e| DashMpdError::Io(
4732 e, String::from("opening mkvmerge output")))?;
4733 let mut merged = BufReader::new(tmpfile);
4734 let outfile = File::create(period_output_path.clone())
4736 .map_err(|e| DashMpdError::Io(
4737 e, String::from("creating output file")))?;
4738 let mut sink = BufWriter::new(outfile);
4739 io::copy(&mut merged, &mut sink)
4740 .map_err(|e| DashMpdError::Io(
4741 e, String::from("copying mkvmerge output to output file")))?;
4742 }
4743 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4744 if let Err(e) = fs::remove_file(tmppath) {
4745 warn!(" Error deleting temporary mkvmerge output: {e}");
4746 }
4747 }
4748 } else {
4749 warn!(" Error running mkvmerge to merge subtitles");
4750 }
4751 }
4752 }
4753 }
4754 }
4755 } else if have_audio {
4756 copy_audio_to_container(downloader, &period_output_path, &tmppath_audio)?;
4757 } else if have_video {
4758 copy_video_to_container(downloader, &period_output_path, &tmppath_video)?;
4759 } else if downloader.fetch_video && downloader.fetch_audio {
4760 return Err(DashMpdError::UnhandledMediaStream("no audio or video streams found".to_string()));
4761 } else if downloader.fetch_video {
4762 return Err(DashMpdError::UnhandledMediaStream("no video streams found".to_string()));
4763 } else if downloader.fetch_audio {
4764 return Err(DashMpdError::UnhandledMediaStream("no audio streams found".to_string()));
4765 }
4766 #[allow(clippy::collapsible_if)]
4767 if downloader.keep_audio.is_none() && downloader.fetch_audio {
4768 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4769 if tmppath_audio.exists() && fs::remove_file(tmppath_audio).is_err() {
4770 info!(" Failed to delete temporary file for audio stream");
4771 }
4772 }
4773 }
4774 #[allow(clippy::collapsible_if)]
4775 if downloader.keep_video.is_none() && downloader.fetch_video {
4776 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4777 if tmppath_video.exists() && fs::remove_file(tmppath_video).is_err() {
4778 info!(" Failed to delete temporary file for video stream");
4779 }
4780 }
4781 }
4782 #[allow(clippy::collapsible_if)]
4783 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4784 if downloader.fetch_subtitles && tmppath_subs.exists() && fs::remove_file(tmppath_subs).is_err() {
4785 info!(" Failed to delete temporary file for subtitles");
4786 }
4787 }
4788 if downloader.verbosity > 1 && (downloader.fetch_audio || downloader.fetch_video || have_subtitles) {
4789 if let Ok(metadata) = fs::metadata(period_output_path.clone()) {
4790 info!(" Wrote {:.1}MB to media file", metadata.len() as f64 / (1024.0 * 1024.0));
4791 }
4792 }
4793 if have_audio || have_video {
4794 period_output_paths.push(period_output_path);
4795 }
4796 } #[allow(clippy::comparison_chain)]
4798 if period_output_paths.len() == 1 {
4799 maybe_record_metainformation(output_path, downloader, &mpd);
4801 } else if period_output_paths.len() > 1 {
4802 #[allow(unused_mut)]
4807 let mut concatenated = false;
4808 #[cfg(not(feature = "libav"))]
4809 if downloader.concatenate_periods && video_containers_concatable(downloader, &period_output_paths) {
4810 info!("Preparing to concatenate multiple Periods into one output file");
4811 concat_output_files(downloader, &period_output_paths)?;
4812 for p in &period_output_paths[1..] {
4813 if fs::remove_file(p).is_err() {
4814 warn!(" Failed to delete temporary file {}", p.display());
4815 }
4816 }
4817 concatenated = true;
4818 if let Some(pop) = period_output_paths.first() {
4819 maybe_record_metainformation(pop, downloader, &mpd);
4820 }
4821 }
4822 if !concatenated {
4823 info!("Media content has been saved in a separate file for each period:");
4824 period_counter = 0;
4826 for p in period_output_paths {
4827 period_counter += 1;
4828 info!(" Period #{period_counter}: {}", p.display());
4829 maybe_record_metainformation(&p, downloader, &mpd);
4830 }
4831 }
4832 }
4833 let have_content_protection = mpd.periods.iter().any(
4834 |p| p.adaptations.iter().any(
4835 |a| (!a.ContentProtection.is_empty()) ||
4836 a.representations.iter().any(
4837 |r| !r.ContentProtection.is_empty())));
4838 if have_content_protection && downloader.decryption_keys.is_empty() {
4839 warn!("Manifest seems to use ContentProtection (DRM), but you didn't provide decryption keys.");
4840 }
4841 for observer in &downloader.progress_observers {
4842 observer.update(100, "Done");
4843 }
4844 Ok(PathBuf::from(output_path))
4845}
4846
4847
4848#[cfg(test)]
4849mod tests {
4850 #[test]
4851 fn test_resolve_url_template() {
4852 use std::collections::HashMap;
4853 use super::resolve_url_template;
4854
4855 assert_eq!(resolve_url_template("AA$Time$BB", &HashMap::from([("Time", "ZZZ".to_string())])),
4856 "AAZZZBB");
4857 assert_eq!(resolve_url_template("AA$Number%06d$BB", &HashMap::from([("Number", "42".to_string())])),
4858 "AA000042BB");
4859 let dict = HashMap::from([("RepresentationID", "640x480".to_string()),
4860 ("Number", "42".to_string()),
4861 ("Time", "ZZZ".to_string())]);
4862 assert_eq!(resolve_url_template("AA/$RepresentationID$/segment-$Number%05d$.mp4", &dict),
4863 "AA/640x480/segment-00042.mp4");
4864 }
4865}