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 colored::*;
21use regex::Regex;
22use url::Url;
23use bytes::Bytes;
24use data_url::DataUrl;
25use reqwest::header::{RANGE, CONTENT_TYPE};
26use backoff::{future::retry_notify, ExponentialBackoff};
27use governor::{Quota, RateLimiter};
28use lazy_static::lazy_static;
29use xot::{xmlname, Xot};
30use crate::{MPD, Period, Representation, AdaptationSet, SegmentBase, DashMpdError};
31use crate::{parse, mux_audio_video, copy_video_to_container, copy_audio_to_container};
32use crate::{is_audio_adaptation, is_video_adaptation, is_subtitle_adaptation};
33use crate::{subtitle_type, content_protection_type, SubtitleType};
34use crate::check_conformity;
35#[cfg(not(feature = "libav"))]
36use crate::ffmpeg::concat_output_files;
37use crate::media::{temporary_outpath, AudioTrack};
38#[allow(unused_imports)]
39use crate::media::video_containers_concatable;
40
41
42pub type HttpClient = reqwest::Client;
44type DirectRateLimiter = RateLimiter<governor::state::direct::NotKeyed,
45 governor::state::InMemoryState,
46 governor::clock::DefaultClock,
47 governor::middleware::NoOpMiddleware>;
48
49
50pub fn partial_process_output(output: &[u8]) -> Cow<'_, str> {
53 let len = min(output.len(), 4096);
54 #[allow(clippy::indexing_slicing)]
55 String::from_utf8_lossy(&output[0..len])
56}
57
58
59fn tmp_file_path(prefix: &str, extension: &OsStr) -> Result<PathBuf, DashMpdError> {
62 if let Some(ext) = extension.to_str() {
63 let fmt = format!(".{}", extension.to_string_lossy());
65 let suffix = if ext.starts_with('.') {
66 extension
67 } else {
68 OsStr::new(&fmt)
69 };
70 let file = tempfile::Builder::new()
71 .prefix(prefix)
72 .suffix(suffix)
73 .rand_bytes(7)
74 .disable_cleanup(env::var("DASHMPD_PERSIST_FILES").is_ok())
75 .tempfile()
76 .map_err(|e| DashMpdError::Io(e, String::from("creating temporary file")))?;
77 Ok(file.path().to_path_buf())
78 } else {
79 Err(DashMpdError::Other(String::from("converting filename extension")))
80 }
81}
82
83
84
85pub trait ProgressObserver: Send + Sync {
88 fn update(&self, percent: u32, message: &str);
89}
90
91
92#[derive(PartialEq, Eq, Clone, Copy, Default)]
95pub enum QualityPreference { #[default] Lowest, Intermediate, Highest }
96
97
98pub struct DashDownloader {
118 pub mpd_url: String,
119 pub redirected_url: Url,
120 base_url: Option<String>,
121 referer: Option<String>,
122 auth_username: Option<String>,
123 auth_password: Option<String>,
124 auth_bearer_token: Option<String>,
125 pub output_path: Option<PathBuf>,
126 http_client: Option<HttpClient>,
127 quality_preference: QualityPreference,
128 language_preference: Option<String>,
129 role_preference: Vec<String>,
130 video_width_preference: Option<u64>,
131 video_height_preference: Option<u64>,
132 fetch_video: bool,
133 fetch_audio: bool,
134 fetch_subtitles: bool,
135 keep_video: Option<PathBuf>,
136 keep_audio: Option<PathBuf>,
137 concatenate_periods: bool,
138 fragment_path: Option<PathBuf>,
139 decryption_keys: HashMap<String, String>,
140 xslt_stylesheets: Vec<PathBuf>,
141 minimum_period_duration: Option<Duration>,
142 content_type_checks: bool,
143 conformity_checks: bool,
144 use_index_range: bool,
145 fragment_retry_count: u32,
146 max_error_count: u32,
147 progress_observers: Vec<Arc<dyn ProgressObserver>>,
148 sleep_between_requests: u8,
149 allow_live_streams: bool,
150 force_duration: Option<f64>,
151 rate_limit: u64,
152 bw_limiter: Option<DirectRateLimiter>,
153 bw_estimator_started: Instant,
154 bw_estimator_bytes: usize,
155 pub verbosity: u8,
156 record_metainformation: bool,
157 pub muxer_preference: HashMap<String, String>,
158 pub concat_preference: HashMap<String, String>,
159 pub decryptor_preference: String,
160 pub ffmpeg_location: String,
161 pub vlc_location: String,
162 pub mkvmerge_location: String,
163 pub mp4box_location: String,
164 pub mp4decrypt_location: String,
165 pub shaka_packager_location: String,
166}
167
168
169#[cfg(not(doctest))]
172impl DashDownloader {
191 pub fn new(mpd_url: &str) -> DashDownloader {
193 DashDownloader {
194 mpd_url: String::from(mpd_url),
195 redirected_url: Url::parse(mpd_url).unwrap(),
196 base_url: None,
197 referer: None,
198 auth_username: None,
199 auth_password: None,
200 auth_bearer_token: None,
201 output_path: None,
202 http_client: None,
203 quality_preference: QualityPreference::Lowest,
204 language_preference: None,
205 role_preference: vec!["main".to_string(), "alternate".to_string()],
206 video_width_preference: None,
207 video_height_preference: None,
208 fetch_video: true,
209 fetch_audio: true,
210 fetch_subtitles: false,
211 keep_video: None,
212 keep_audio: None,
213 concatenate_periods: true,
214 fragment_path: None,
215 decryption_keys: HashMap::new(),
216 xslt_stylesheets: Vec::new(),
217 minimum_period_duration: None,
218 content_type_checks: true,
219 conformity_checks: true,
220 use_index_range: true,
221 fragment_retry_count: 10,
222 max_error_count: 30,
223 progress_observers: Vec::new(),
224 sleep_between_requests: 0,
225 allow_live_streams: false,
226 force_duration: None,
227 rate_limit: 0,
228 bw_limiter: None,
229 bw_estimator_started: Instant::now(),
230 bw_estimator_bytes: 0,
231 verbosity: 0,
232 record_metainformation: true,
233 muxer_preference: HashMap::new(),
234 concat_preference: HashMap::new(),
235 decryptor_preference: String::from("mp4decrypt"),
236 ffmpeg_location: String::from("ffmpeg"),
237 vlc_location: if cfg!(target_os = "windows") {
238 String::from("c:/Program Files/VideoLAN/VLC/vlc.exe")
241 } else {
242 String::from("vlc")
243 },
244 mkvmerge_location: String::from("mkvmerge"),
245 mp4box_location: if cfg!(target_os = "windows") {
246 String::from("MP4Box.exe")
247 } else if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
248 String::from("MP4Box")
249 } else {
250 String::from("mp4box")
251 },
252 mp4decrypt_location: String::from("mp4decrypt"),
253 shaka_packager_location: String::from("shaka-packager"),
254 }
255 }
256
257 pub fn with_base_url(mut self, base_url: String) -> DashDownloader {
260 self.base_url = Some(base_url);
261 self
262 }
263
264
265 pub fn with_http_client(mut self, client: HttpClient) -> DashDownloader {
287 self.http_client = Some(client);
288 self
289 }
290
291 pub fn with_referer(mut self, referer: String) -> DashDownloader {
295 self.referer = Some(referer);
296 self
297 }
298
299 pub fn with_authentication(mut self, username: String, password: String) -> DashDownloader {
302 self.auth_username = Some(username.clone());
303 self.auth_password = Some(password.clone());
304 self
305 }
306
307 pub fn with_auth_bearer(mut self, token: String) -> DashDownloader {
310 self.auth_bearer_token = Some(token.clone());
311 self
312 }
313
314 pub fn add_progress_observer(mut self, observer: Arc<dyn ProgressObserver>) -> DashDownloader {
317 self.progress_observers.push(observer);
318 self
319 }
320
321 pub fn best_quality(mut self) -> DashDownloader {
324 self.quality_preference = QualityPreference::Highest;
325 self
326 }
327
328 pub fn intermediate_quality(mut self) -> DashDownloader {
331 self.quality_preference = QualityPreference::Intermediate;
332 self
333 }
334
335 pub fn worst_quality(mut self) -> DashDownloader {
338 self.quality_preference = QualityPreference::Lowest;
339 self
340 }
341
342 pub fn prefer_language(mut self, lang: String) -> DashDownloader {
347 self.language_preference = Some(lang);
348 self
349 }
350
351 pub fn prefer_roles(mut self, role_preference: Vec<String>) -> DashDownloader {
361 if role_preference.len() < u8::MAX as usize {
362 self.role_preference = role_preference;
363 } else {
364 warn!("Ignoring role_preference ordering due to excessive length");
365 }
366 self
367 }
368
369 pub fn prefer_video_width(mut self, width: u64) -> DashDownloader {
372 self.video_width_preference = Some(width);
373 self
374 }
375
376 pub fn prefer_video_height(mut self, height: u64) -> DashDownloader {
379 self.video_height_preference = Some(height);
380 self
381 }
382
383 pub fn video_only(mut self) -> DashDownloader {
385 self.fetch_audio = false;
386 self.fetch_video = true;
387 self
388 }
389
390 pub fn audio_only(mut self) -> DashDownloader {
392 self.fetch_audio = true;
393 self.fetch_video = false;
394 self
395 }
396
397 pub fn keep_video_as<P: Into<PathBuf>>(mut self, video_path: P) -> DashDownloader {
400 self.keep_video = Some(video_path.into());
401 self
402 }
403
404 pub fn keep_audio_as<P: Into<PathBuf>>(mut self, audio_path: P) -> DashDownloader {
407 self.keep_audio = Some(audio_path.into());
408 self
409 }
410
411 pub fn save_fragments_to<P: Into<PathBuf>>(mut self, fragment_path: P) -> DashDownloader {
414 self.fragment_path = Some(fragment_path.into());
415 self
416 }
417
418 pub fn add_decryption_key(mut self, id: String, key: String) -> DashDownloader {
430 self.decryption_keys.insert(id, key);
431 self
432 }
433
434 pub fn with_xslt_stylesheet<P: Into<PathBuf>>(mut self, stylesheet: P) -> DashDownloader {
446 self.xslt_stylesheets.push(stylesheet.into());
447 self
448 }
449
450 pub fn minimum_period_duration(mut self, value: Duration) -> DashDownloader {
453 self.minimum_period_duration = Some(value);
454 self
455 }
456
457 pub fn fetch_audio(mut self, value: bool) -> DashDownloader {
461 self.fetch_audio = value;
462 self
463 }
464
465 pub fn fetch_video(mut self, value: bool) -> DashDownloader {
469 self.fetch_video = value;
470 self
471 }
472
473 pub fn fetch_subtitles(mut self, value: bool) -> DashDownloader {
481 self.fetch_subtitles = value;
482 self
483 }
484
485 pub fn concatenate_periods(mut self, value: bool) -> DashDownloader {
489 self.concatenate_periods = value;
490 self
491 }
492
493 pub fn without_content_type_checks(mut self) -> DashDownloader {
496 self.content_type_checks = false;
497 self
498 }
499
500 pub fn content_type_checks(mut self, value: bool) -> DashDownloader {
503 self.content_type_checks = value;
504 self
505 }
506
507 pub fn conformity_checks(mut self, value: bool) -> DashDownloader {
510 self.conformity_checks = value;
511 self
512 }
513
514 pub fn use_index_range(mut self, value: bool) -> DashDownloader {
529 self.use_index_range = value;
530 self
531 }
532
533 pub fn fragment_retry_count(mut self, count: u32) -> DashDownloader {
537 self.fragment_retry_count = count;
538 self
539 }
540
541 pub fn max_error_count(mut self, count: u32) -> DashDownloader {
548 self.max_error_count = count;
549 self
550 }
551
552 pub fn sleep_between_requests(mut self, seconds: u8) -> DashDownloader {
554 self.sleep_between_requests = seconds;
555 self
556 }
557
558 pub fn allow_live_streams(mut self, value: bool) -> DashDownloader {
570 self.allow_live_streams = value;
571 self
572 }
573
574 pub fn force_duration(mut self, seconds: f64) -> DashDownloader {
580 self.force_duration = Some(seconds);
581 self
582 }
583
584 pub fn with_rate_limit(mut self, bps: u64) -> DashDownloader {
590 if bps < 10 * 1024 {
591 warn!("Limiting bandwidth below 10kB/s is unlikely to be stable");
592 }
593 if self.verbosity > 1 {
594 info!("Limiting bandwidth to {} kB/s", bps/1024);
595 }
596 self.rate_limit = bps;
597 let mut kps = 1 + bps / 1024;
603 if kps > u32::MAX as u64 {
604 warn!("Throttling bandwidth limit");
605 kps = u32::MAX.into();
606 }
607 if let Some(bw_limit) = NonZeroU32::new(kps as u32) {
608 if let Some(burst) = NonZeroU32::new(10 * 1024) {
609 let bw_quota = Quota::per_second(bw_limit)
610 .allow_burst(burst);
611 self.bw_limiter = Some(RateLimiter::direct(bw_quota));
612 }
613 }
614 self
615 }
616
617 pub fn verbosity(mut self, level: u8) -> DashDownloader {
627 self.verbosity = level;
628 self
629 }
630
631 pub fn record_metainformation(mut self, record: bool) -> DashDownloader {
635 self.record_metainformation = record;
636 self
637 }
638
639 pub fn with_muxer_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
661 self.muxer_preference.insert(container.to_string(), ordering.to_string());
662 self
663 }
664
665 pub fn with_concat_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
688 self.concat_preference.insert(container.to_string(), ordering.to_string());
689 self
690 }
691
692 pub fn with_decryptor_preference(mut self, decryption_tool: &str) -> DashDownloader {
699 self.decryptor_preference = decryption_tool.to_string();
700 self
701 }
702
703 pub fn with_ffmpeg(mut self, ffmpeg_path: &str) -> DashDownloader {
718 self.ffmpeg_location = ffmpeg_path.to_string();
719 self
720 }
721
722 pub fn with_vlc(mut self, vlc_path: &str) -> DashDownloader {
737 self.vlc_location = vlc_path.to_string();
738 self
739 }
740
741 pub fn with_mkvmerge(mut self, path: &str) -> DashDownloader {
749 self.mkvmerge_location = path.to_string();
750 self
751 }
752
753 pub fn with_mp4box(mut self, path: &str) -> DashDownloader {
761 self.mp4box_location = path.to_string();
762 self
763 }
764
765 pub fn with_mp4decrypt(mut self, path: &str) -> DashDownloader {
773 self.mp4decrypt_location = path.to_string();
774 self
775 }
776
777 pub fn with_shaka_packager(mut self, path: &str) -> DashDownloader {
785 self.shaka_packager_location = path.to_string();
786 self
787 }
788
789 pub async fn download_to<P: Into<PathBuf>>(mut self, out: P) -> Result<PathBuf, DashMpdError> {
799 self.output_path = Some(out.into());
800 if self.http_client.is_none() {
801 let client = reqwest::Client::builder()
802 .timeout(Duration::new(30, 0))
803 .cookie_store(true)
804 .build()
805 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
806 self.http_client = Some(client);
807 }
808 fetch_mpd(&mut self).await
809 }
810
811 pub async fn download(mut self) -> Result<PathBuf, DashMpdError> {
818 let cwd = env::current_dir()
819 .map_err(|e| DashMpdError::Io(e, String::from("obtaining current directory")))?;
820 let filename = generate_filename_from_url(&self.mpd_url);
821 let outpath = cwd.join(filename);
822 self.output_path = Some(outpath);
823 if self.http_client.is_none() {
824 let client = reqwest::Client::builder()
825 .timeout(Duration::new(30, 0))
826 .cookie_store(true)
827 .build()
828 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
829 self.http_client = Some(client);
830 }
831 fetch_mpd(&mut self).await
832 }
833}
834
835
836fn mpd_is_dynamic(mpd: &MPD) -> bool {
837 if let Some(mpdtype) = mpd.mpdtype.as_ref() {
838 return mpdtype.eq("dynamic");
839 }
840 false
841}
842
843fn parse_range(range: &str) -> Result<(u64, u64), DashMpdError> {
846 let v: Vec<&str> = range.split_terminator('-').collect();
847 if v.len() != 2 {
848 return Err(DashMpdError::Parsing(format!("invalid range specifier: {range}")));
849 }
850 #[allow(clippy::indexing_slicing)]
851 let start: u64 = v[0].parse()
852 .map_err(|_| DashMpdError::Parsing(String::from("invalid start for range specifier")))?;
853 #[allow(clippy::indexing_slicing)]
854 let end: u64 = v[1].parse()
855 .map_err(|_| DashMpdError::Parsing(String::from("invalid end for range specifier")))?;
856 Ok((start, end))
857}
858
859#[derive(Debug)]
860struct MediaFragment {
861 period: u8,
862 url: Url,
863 start_byte: Option<u64>,
864 end_byte: Option<u64>,
865 is_init: bool,
866 timeout: Option<Duration>,
867}
868
869#[derive(Debug)]
870struct MediaFragmentBuilder {
871 period: u8,
872 url: Url,
873 start_byte: Option<u64>,
874 end_byte: Option<u64>,
875 is_init: bool,
876 timeout: Option<Duration>,
877}
878
879impl MediaFragmentBuilder {
880 pub fn new(period: u8, url: Url) -> MediaFragmentBuilder {
881 MediaFragmentBuilder {
882 period, url, start_byte: None, end_byte: None, is_init: false, timeout: None
883 }
884 }
885
886 pub fn with_range(mut self, start_byte: Option<u64>, end_byte: Option<u64>) -> MediaFragmentBuilder {
887 self.start_byte = start_byte;
888 self.end_byte = end_byte;
889 self
890 }
891
892 pub fn with_timeout(mut self, timeout: Duration) -> MediaFragmentBuilder {
893 self.timeout = Some(timeout);
894 self
895 }
896
897 pub fn set_init(mut self) -> MediaFragmentBuilder {
898 self.is_init = true;
899 self
900 }
901
902 pub fn build(self) -> MediaFragment {
903 MediaFragment {
904 period: self.period,
905 url: self.url,
906 start_byte: self.start_byte,
907 end_byte: self.end_byte,
908 is_init: self.is_init,
909 timeout: self.timeout
910 }
911 }
912}
913
914#[derive(Debug, Default)]
918struct PeriodOutputs {
919 fragments: Vec<MediaFragment>,
920 diagnostics: Vec<String>,
921 subtitle_formats: Vec<SubtitleType>,
922}
923
924#[derive(Debug, Default)]
925struct PeriodDownloads {
926 audio_fragments: Vec<MediaFragment>,
927 video_fragments: Vec<MediaFragment>,
928 subtitle_fragments: Vec<MediaFragment>,
929 subtitle_formats: Vec<SubtitleType>,
930 period_counter: u8,
931 id: Option<String>,
932}
933
934fn period_fragment_count(pd: &PeriodDownloads) -> usize {
935 pd.audio_fragments.len() +
936 pd.video_fragments.len() +
937 pd.subtitle_fragments.len()
938}
939
940
941
942async fn throttle_download_rate(downloader: &DashDownloader, size: u32) -> Result<(), DashMpdError> {
943 if downloader.rate_limit > 0 {
944 if let Some(cells) = NonZeroU32::new(size) {
945 if let Some(limiter) = downloader.bw_limiter.as_ref() {
946 #[allow(clippy::redundant_pattern_matching)]
947 if let Err(_) = limiter.until_n_ready(cells).await {
948 return Err(DashMpdError::Other(
949 "Bandwidth limit is too low".to_string()));
950 }
951 }
952 }
953 }
954 Ok(())
955}
956
957
958fn generate_filename_from_url(url: &str) -> PathBuf {
959 use sanitise_file_name::{sanitise_with_options, Options};
960
961 let mut path = url;
962 if let Some(p) = path.strip_prefix("http://") {
963 path = p;
964 } else if let Some(p) = path.strip_prefix("https://") {
965 path = p;
966 } else if let Some(p) = path.strip_prefix("file://") {
967 path = p;
968 }
969 if let Some(p) = path.strip_prefix("www.") {
970 path = p;
971 }
972 if let Some(p) = path.strip_prefix("ftp.") {
973 path = p;
974 }
975 if let Some(p) = path.strip_suffix(".mpd") {
976 path = p;
977 }
978 let mut sanitize_opts = Options::DEFAULT;
979 sanitize_opts.length_limit = 150;
980 PathBuf::from(sanitise_with_options(path, &sanitize_opts) + ".mp4")
985}
986
987fn output_path_for_period(base: &Path, period: u8) -> PathBuf {
1004 assert!(period > 0);
1005 if period == 1 {
1006 base.to_path_buf()
1007 } else {
1008 if let Some(stem) = base.file_stem() {
1009 if let Some(ext) = base.extension() {
1010 let fname = format!("{}-p{period}.{}", stem.to_string_lossy(), ext.to_string_lossy());
1011 return base.with_file_name(fname);
1012 }
1013 }
1014 let p = format!("dashmpd-p{period}");
1015 tmp_file_path(&p, base.extension().unwrap_or(OsStr::new("mp4")))
1016 .unwrap_or_else(|_| p.into())
1017 }
1018}
1019
1020fn is_absolute_url(s: &str) -> bool {
1021 s.starts_with("http://") ||
1022 s.starts_with("https://") ||
1023 s.starts_with("file://") ||
1024 s.starts_with("ftp://")
1025}
1026
1027fn merge_baseurls(current: &Url, new: &str) -> Result<Url, DashMpdError> {
1028 if is_absolute_url(new) {
1029 Url::parse(new)
1030 .map_err(|e| parse_error("parsing BaseURL", e))
1031 } else {
1032 let mut merged = current.join(new)
1045 .map_err(|e| parse_error("joining base with BaseURL", e))?;
1046 if merged.query().is_none() {
1047 merged.set_query(current.query());
1048 }
1049 Ok(merged)
1050 }
1051}
1052
1053fn content_type_audio_p(response: &reqwest::Response) -> bool {
1058 match response.headers().get("content-type") {
1059 Some(ct) => {
1060 let ctb = ct.as_bytes();
1061 ctb.starts_with(b"audio/") ||
1062 ctb.starts_with(b"video/") ||
1063 ctb.starts_with(b"application/octet-stream")
1064 },
1065 None => false,
1066 }
1067}
1068
1069fn content_type_video_p(response: &reqwest::Response) -> bool {
1071 match response.headers().get("content-type") {
1072 Some(ct) => {
1073 let ctb = ct.as_bytes();
1074 ctb.starts_with(b"video/") ||
1075 ctb.starts_with(b"application/octet-stream")
1076 },
1077 None => false,
1078 }
1079}
1080
1081
1082fn adaptation_lang_distance(a: &AdaptationSet, language_preference: &str) -> u8 {
1086 if let Some(lang) = &a.lang {
1087 if lang.eq(language_preference) {
1088 return 0;
1089 }
1090 if lang[0..2].eq(&language_preference[0..2]) {
1091 return 5;
1092 }
1093 100
1094 } else {
1095 100
1096 }
1097}
1098
1099fn adaptation_roles(a: &AdaptationSet) -> Vec<String> {
1102 let mut roles = Vec::new();
1103 for r in &a.Role {
1104 if let Some(rv) = &r.value {
1105 roles.push(String::from(rv));
1106 }
1107 }
1108 for cc in &a.ContentComponent {
1109 for r in &cc.Role {
1110 if let Some(rv) = &r.value {
1111 roles.push(String::from(rv));
1112 }
1113 }
1114 }
1115 roles
1116}
1117
1118fn adaptation_role_distance(a: &AdaptationSet, role_preference: &[String]) -> u8 {
1120 adaptation_roles(a).iter()
1121 .map(|r| role_preference.binary_search(r).unwrap_or(u8::MAX.into()))
1122 .map(|u| u8::try_from(u).unwrap_or(u8::MAX))
1123 .min()
1124 .unwrap_or(u8::MAX)
1125}
1126
1127
1128fn select_preferred_adaptations<'a>(
1136 adaptations: Vec<&'a AdaptationSet>,
1137 downloader: &DashDownloader) -> Vec<&'a AdaptationSet>
1138{
1139 let mut preferred: Vec<&'a AdaptationSet>;
1140 if let Some(ref lang) = downloader.language_preference {
1141 preferred = Vec::new();
1142 let distance: Vec<u8> = adaptations.iter()
1143 .map(|a| adaptation_lang_distance(a, lang))
1144 .collect();
1145 let min_distance = distance.iter().min().unwrap_or(&0);
1146 for (i, a) in adaptations.iter().enumerate() {
1147 if let Some(di) = distance.get(i) {
1148 if di == min_distance {
1149 preferred.push(a);
1150 }
1151 }
1152 }
1153 } else {
1154 preferred = adaptations;
1155 }
1156 let role_distance: Vec<u8> = preferred.iter()
1162 .map(|a| adaptation_role_distance(a, &downloader.role_preference))
1163 .collect();
1164 let role_distance_min = role_distance.iter().min().unwrap_or(&0);
1165 let mut best = Vec::new();
1166 for (i, a) in preferred.into_iter().enumerate() {
1167 if let Some(rdi) = role_distance.get(i) {
1168 if rdi == role_distance_min {
1169 best.push(a);
1170 }
1171 }
1172 }
1173 best
1174}
1175
1176
1177fn select_preferred_representation<'a>(
1183 representations: Vec<&'a Representation>,
1184 downloader: &DashDownloader) -> Option<&'a Representation>
1185{
1186 if representations.iter().all(|x| x.qualityRanking.is_some()) {
1187 match downloader.quality_preference {
1190 QualityPreference::Lowest =>
1191 representations.iter()
1192 .max_by_key(|r| r.qualityRanking.unwrap_or(u8::MAX))
1193 .copied(),
1194 QualityPreference::Highest =>
1195 representations.iter().min_by_key(|r| r.qualityRanking.unwrap_or(0))
1196 .copied(),
1197 QualityPreference::Intermediate => {
1198 let count = representations.len();
1199 match count {
1200 0 => None,
1201 1 => Some(representations[0]),
1202 _ => {
1203 let mut ranking: Vec<u8> = representations.iter()
1204 .map(|r| r.qualityRanking.unwrap_or(u8::MAX))
1205 .collect();
1206 ranking.sort_unstable();
1207 if let Some(want_ranking) = ranking.get(count / 2) {
1208 representations.iter()
1209 .find(|r| r.qualityRanking.unwrap_or(u8::MAX) == *want_ranking)
1210 .copied()
1211 } else {
1212 representations.first().copied()
1213 }
1214 },
1215 }
1216 },
1217 }
1218 } else {
1219 match downloader.quality_preference {
1221 QualityPreference::Lowest => representations.iter()
1222 .min_by_key(|r| r.bandwidth.unwrap_or(1_000_000_000))
1223 .copied(),
1224 QualityPreference::Highest => representations.iter()
1225 .max_by_key(|r| r.bandwidth.unwrap_or(0))
1226 .copied(),
1227 QualityPreference::Intermediate => {
1228 let count = representations.len();
1229 match count {
1230 0 => None,
1231 1 => Some(representations[0]),
1232 _ => {
1233 let mut ranking: Vec<u64> = representations.iter()
1234 .map(|r| r.bandwidth.unwrap_or(100_000_000))
1235 .collect();
1236 ranking.sort_unstable();
1237 if let Some(want_ranking) = ranking.get(count / 2) {
1238 representations.iter()
1239 .find(|r| r.bandwidth.unwrap_or(100_000_000) == *want_ranking)
1240 .copied()
1241 } else {
1242 representations.first().copied()
1243 }
1244 },
1245 }
1246 },
1247 }
1248 }
1249}
1250
1251
1252fn print_available_subtitles_representation(r: &Representation, a: &AdaptationSet) {
1254 let unspecified = "<unspecified>".to_string();
1255 let empty = "".to_string();
1256 let lang = r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unspecified));
1257 let codecs = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&empty));
1258 let typ = subtitle_type(&a);
1259 let stype = if !codecs.is_empty() {
1260 format!("{typ:?}/{codecs}")
1261 } else {
1262 format!("{typ:?}")
1263 };
1264 let role = a.Role.first()
1265 .map_or_else(|| String::from(""),
1266 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1267 let label = a.Label.first()
1268 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1269 info!(" subs {stype:>18} | {lang:>10} |{role}{label}");
1270}
1271
1272fn print_available_subtitles_adaptation(a: &AdaptationSet) {
1273 a.representations.iter()
1274 .for_each(|r| print_available_subtitles_representation(r, a));
1275}
1276
1277fn print_available_streams_representation(r: &Representation, a: &AdaptationSet, typ: &str) {
1279 let unspecified = "<unspecified>".to_string();
1281 let w = r.width.unwrap_or(a.width.unwrap_or(0));
1282 let h = r.height.unwrap_or(a.height.unwrap_or(0));
1283 let codec = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&unspecified));
1284 let bw = r.bandwidth.unwrap_or(a.maxBandwidth.unwrap_or(0));
1285 let fmt = if typ.eq("audio") {
1286 let unknown = String::from("?");
1287 format!("lang={}", r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unknown)))
1288 } else if w == 0 || h == 0 {
1289 String::from("")
1292 } else {
1293 format!("{w}x{h}")
1294 };
1295 let role = a.Role.first()
1296 .map_or_else(|| String::from(""),
1297 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1298 let label = a.Label.first()
1299 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1300 info!(" {typ} {codec:17} | {:5} Kbps | {fmt:>9}{role}{label}", bw / 1024);
1301}
1302
1303fn print_available_streams_adaptation(a: &AdaptationSet, typ: &str) {
1304 a.representations.iter()
1305 .for_each(|r| print_available_streams_representation(r, a, typ));
1306}
1307
1308fn print_available_streams_period(p: &Period) {
1309 p.adaptations.iter()
1310 .filter(is_audio_adaptation)
1311 .for_each(|a| print_available_streams_adaptation(a, "audio"));
1312 p.adaptations.iter()
1313 .filter(is_video_adaptation)
1314 .for_each(|a| print_available_streams_adaptation(a, "video"));
1315 p.adaptations.iter()
1316 .filter(is_subtitle_adaptation)
1317 .for_each(print_available_subtitles_adaptation);
1318}
1319
1320#[tracing::instrument(level="trace", skip_all)]
1321fn print_available_streams(mpd: &MPD) {
1322 let mut counter = 0;
1323 for p in &mpd.periods {
1324 let mut period_duration_secs: f64 = 0.0;
1325 if let Some(d) = mpd.mediaPresentationDuration {
1326 period_duration_secs = d.as_secs_f64();
1327 }
1328 if let Some(d) = &p.duration {
1329 period_duration_secs = d.as_secs_f64();
1330 }
1331 counter += 1;
1332 if let Some(id) = p.id.as_ref() {
1333 info!("Streams in period {id} (#{counter}), duration {period_duration_secs:.3}s:");
1334 } else {
1335 info!("Streams in period #{counter}, duration {period_duration_secs:.3}s:");
1336 }
1337 print_available_streams_period(p);
1338 }
1339}
1340
1341async fn extract_init_pssh(downloader: &DashDownloader, init_url: Url) -> Option<Vec<u8>> {
1342 use bstr::ByteSlice;
1343 use hex_literal::hex;
1344
1345 if let Some(client) = downloader.http_client.as_ref() {
1346 let mut req = client.get(init_url);
1347 if let Some(referer) = &downloader.referer {
1348 req = req.header("Referer", referer);
1349 }
1350 if let Some(username) = &downloader.auth_username {
1351 if let Some(password) = &downloader.auth_password {
1352 req = req.basic_auth(username, Some(password));
1353 }
1354 }
1355 if let Some(token) = &downloader.auth_bearer_token {
1356 req = req.bearer_auth(token);
1357 }
1358 if let Ok(mut resp) = req.send().await {
1359 let mut chunk_counter = 0;
1362 let mut segment_first_bytes = Vec::<u8>::new();
1363 while let Ok(Some(chunk)) = resp.chunk().await {
1364 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
1365 #[allow(clippy::redundant_pattern_matching)]
1366 if let Err(_) = throttle_download_rate(downloader, size).await {
1367 return None;
1368 }
1369 segment_first_bytes.append(&mut chunk.to_vec());
1370 chunk_counter += 1;
1371 if chunk_counter > 20 {
1372 break;
1373 }
1374 }
1375 let needle = b"pssh";
1376 for offset in segment_first_bytes.find_iter(needle) {
1377 #[allow(clippy::needless_range_loop)]
1378 for i in offset-4..offset+2 {
1379 if let Some(b) = segment_first_bytes.get(i) {
1380 if *b != 0 {
1381 continue;
1382 }
1383 }
1384 }
1385 #[allow(clippy::needless_range_loop)]
1386 for i in offset+4..offset+8 {
1387 if let Some(b) = segment_first_bytes.get(i) {
1388 if *b != 0 {
1389 continue;
1390 }
1391 }
1392 }
1393 if offset+24 > segment_first_bytes.len() {
1394 continue;
1395 }
1396 const WIDEVINE_SYSID: [u8; 16] = hex!("edef8ba979d64acea3c827dcd51d21ed");
1398 if let Some(sysid) = segment_first_bytes.get((offset+8)..(offset+24)) {
1399 if !sysid.eq(&WIDEVINE_SYSID) {
1400 continue;
1401 }
1402 }
1403 if let Some(length) = segment_first_bytes.get(offset-1) {
1404 let start = offset - 4;
1405 let end = start + *length as usize;
1406 if let Some(pssh) = &segment_first_bytes.get(start..end) {
1407 return Some(pssh.to_vec());
1408 }
1409 }
1410 }
1411 }
1412 None
1413 } else {
1414 None
1415 }
1416}
1417
1418
1419lazy_static! {
1428 static ref URL_TEMPLATE_IDS: Vec<(&'static str, String, Regex)> = {
1429 vec!["RepresentationID", "Number", "Time", "Bandwidth"].into_iter()
1430 .map(|k| (k, format!("${k}$"), Regex::new(&format!("\\${k}%0([\\d])d\\$")).unwrap()))
1431 .collect()
1432 };
1433}
1434
1435fn resolve_url_template(template: &str, params: &HashMap<&str, String>) -> String {
1436 let mut result = template.to_string();
1437 for (k, ident, rx) in URL_TEMPLATE_IDS.iter() {
1438 if result.contains(ident) {
1440 if let Some(value) = params.get(k as &str) {
1441 result = result.replace(ident, value);
1442 }
1443 }
1444 if let Some(cap) = rx.captures(&result) {
1446 if let Some(value) = params.get(k as &str) {
1447 if let Ok(width) = cap[1].parse::<usize>() {
1448 if let Some(m) = rx.find(&result) {
1449 let count = format!("{value:0>width$}");
1450 result = result[..m.start()].to_owned() + &count + &result[m.end()..];
1451 }
1452 }
1453 }
1454 }
1455 }
1456 result
1457}
1458
1459
1460fn reqwest_error_transient_p(e: &reqwest::Error) -> bool {
1461 if e.is_timeout() {
1462 return true;
1463 }
1464 if let Some(s) = e.status() {
1465 if s == reqwest::StatusCode::REQUEST_TIMEOUT ||
1466 s == reqwest::StatusCode::TOO_MANY_REQUESTS ||
1467 s == reqwest::StatusCode::SERVICE_UNAVAILABLE ||
1468 s == reqwest::StatusCode::GATEWAY_TIMEOUT {
1469 return true;
1470 }
1471 }
1472 false
1473}
1474
1475fn categorize_reqwest_error(e: reqwest::Error) -> backoff::Error<reqwest::Error> {
1476 if reqwest_error_transient_p(&e) {
1477 backoff::Error::retry_after(e, Duration::new(5, 0))
1478 } else {
1479 backoff::Error::permanent(e)
1480 }
1481}
1482
1483fn notify_transient<E: std::fmt::Debug>(err: E, dur: Duration) {
1484 warn!("Transient error after {dur:?}: {err:?}");
1485}
1486
1487fn network_error(why: &str, e: reqwest::Error) -> DashMpdError {
1488 if e.is_timeout() {
1489 DashMpdError::NetworkTimeout(format!("{why}: {e:?}"))
1490 } else if e.is_connect() {
1491 DashMpdError::NetworkConnect(format!("{why}: {e:?}"))
1492 } else {
1493 DashMpdError::Network(format!("{why}: {e:?}"))
1494 }
1495}
1496
1497fn parse_error(why: &str, e: impl std::error::Error) -> DashMpdError {
1498 DashMpdError::Parsing(format!("{why}: {e:#?}"))
1499}
1500
1501
1502async fn reqwest_bytes_with_retries(
1506 client: &reqwest::Client,
1507 req: reqwest::Request,
1508 retry_count: u32) -> Result<Bytes, reqwest::Error>
1509{
1510 let mut last_error = None;
1511 for _ in 0..retry_count {
1512 if let Some(rqw) = req.try_clone() {
1513 match client.execute(rqw).await {
1514 Ok(response) => {
1515 match response.error_for_status() {
1516 Ok(resp) => {
1517 match resp.bytes().await {
1518 Ok(bytes) => return Ok(bytes),
1519 Err(e) => {
1520 info!("Retrying after HTTP error {e:?}");
1521 last_error = Some(e);
1522 },
1523 }
1524 },
1525 Err(e) => {
1526 info!("Retrying after HTTP error {e:?}");
1527 last_error = Some(e);
1528 },
1529 }
1530 },
1531 Err(e) => {
1532 info!("Retrying after HTTP error {e:?}");
1533 last_error = Some(e);
1534 },
1535 }
1536 }
1537 }
1538 Err(last_error.unwrap())
1539}
1540
1541#[allow(unused_variables)]
1554fn maybe_record_metainformation(path: &Path, downloader: &DashDownloader, mpd: &MPD) {
1555 #[cfg(target_family = "unix")]
1556 if downloader.record_metainformation && (downloader.fetch_audio || downloader.fetch_video) {
1557 if let Ok(origin_url) = Url::parse(&downloader.mpd_url) {
1558 #[allow(clippy::collapsible_if)]
1560 if origin_url.username().is_empty() && origin_url.password().is_none() {
1561 #[cfg(target_family = "unix")]
1562 if xattr::set(path, "user.xdg.origin.url", downloader.mpd_url.as_bytes()).is_err() {
1563 info!("Failed to set user.xdg.origin.url xattr on output file");
1564 }
1565 }
1566 for pi in &mpd.ProgramInformation {
1567 if let Some(t) = &pi.Title {
1568 if let Some(tc) = &t.content {
1569 if xattr::set(path, "user.dublincore.title", tc.as_bytes()).is_err() {
1570 info!("Failed to set user.dublincore.title xattr on output file");
1571 }
1572 }
1573 }
1574 if let Some(source) = &pi.Source {
1575 if let Some(sc) = &source.content {
1576 if xattr::set(path, "user.dublincore.source", sc.as_bytes()).is_err() {
1577 info!("Failed to set user.dublincore.source xattr on output file");
1578 }
1579 }
1580 }
1581 if let Some(copyright) = &pi.Copyright {
1582 if let Some(cc) = ©right.content {
1583 if xattr::set(path, "user.dublincore.rights", cc.as_bytes()).is_err() {
1584 info!("Failed to set user.dublincore.rights xattr on output file");
1585 }
1586 }
1587 }
1588 }
1589 }
1590 }
1591}
1592
1593fn fetchable_xlink_href(href: &str) -> bool {
1597 (!href.is_empty()) && href.ne("urn:mpeg:dash:resolve-to-zero:2013")
1598}
1599
1600fn element_resolves_to_zero(xot: &mut Xot, element: xot::Node) -> bool {
1601 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
1602 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
1603 if let Some(href) = xot.get_attribute(element, xlink_href_name.into()) {
1604 return href.eq("urn:mpeg:dash:resolve-to-zero:2013");
1605 }
1606 false
1607}
1608
1609fn skip_xml_preamble(input: &str) -> &str {
1610 if input.starts_with("<?xml") {
1611 if let Some(end_pos) = input.find("?>") {
1612 return &input[end_pos + 2..]; }
1615 }
1616 input
1618}
1619
1620fn apply_xslt_stylesheets_xsltproc(
1624 downloader: &DashDownloader,
1625 xot: &mut Xot,
1626 doc: xot::Node) -> Result<String, DashMpdError> {
1627 let mut buf = Vec::new();
1628 xot.write(doc, &mut buf)
1629 .map_err(|e| parse_error("serializing rewritten manifest", e))?;
1630 for ss in &downloader.xslt_stylesheets {
1631 if downloader.verbosity > 0 {
1632 info!(" Applying XSLT stylesheet {} with xsltproc", ss.display());
1633 }
1634 let tmpmpd = tmp_file_path("dashxslt", OsStr::new("xslt"))?;
1635 fs::write(&tmpmpd, &buf)
1636 .map_err(|e| DashMpdError::Io(e, String::from("writing MPD")))?;
1637 let xsltproc = Command::new("xsltproc")
1638 .args([ss, &tmpmpd])
1639 .output()
1640 .map_err(|e| DashMpdError::Io(e, String::from("spawning xsltproc")))?;
1641 if !xsltproc.status.success() {
1642 let msg = format!("xsltproc returned {}", xsltproc.status);
1643 let out = partial_process_output(&xsltproc.stderr).to_string();
1644 return Err(DashMpdError::Io(std::io::Error::other(msg), out));
1645 }
1646 if env::var("DASHMPD_PERSIST_FILES").is_err() {
1647 if let Err(e) = fs::remove_file(&tmpmpd) {
1648 warn!("Error removing temporary MPD after XSLT processing: {e:?}");
1649 }
1650 }
1651 buf.clone_from(&xsltproc.stdout);
1652 }
1653 String::from_utf8(buf)
1654 .map_err(|e| parse_error("parsing UTF-8", e))
1655}
1656
1657async fn resolve_xlink_references(
1692 downloader: &DashDownloader,
1693 xot: &mut Xot,
1694 node: xot::Node) -> Result<(), DashMpdError>
1695{
1696 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
1697 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
1698 let xlinked = xot.descendants(node)
1699 .filter(|d| xot.get_attribute(*d, xlink_href_name.into()).is_some())
1700 .collect::<Vec<_>>();
1701 for xl in xlinked {
1702 if element_resolves_to_zero(xot, xl) {
1703 trace!("Removing node with resolve-to-zero xlink:href {xl:?}");
1704 if let Err(e) = xot.remove(xl) {
1705 return Err(parse_error("Failed to remove resolve-to-zero XML node", e));
1706 }
1707 } else if let Some(href) = xot.get_attribute(xl, xlink_href_name.into()) {
1708 if fetchable_xlink_href(href) {
1709 let xlink_url = if is_absolute_url(href) {
1710 Url::parse(href)
1711 .map_err(|e|
1712 if let Ok(ns) = xot.to_string(node) {
1713 parse_error(&format!("parsing XLink on {ns}"), e)
1714 } else {
1715 parse_error("parsing XLink", e)
1716 }
1717 )?
1718 } else {
1719 let mut merged = downloader.redirected_url.join(href)
1722 .map_err(|e|
1723 if let Ok(ns) = xot.to_string(node) {
1724 parse_error(&format!("parsing XLink on {ns}"), e)
1725 } else {
1726 parse_error("parsing XLink", e)
1727 }
1728 )?;
1729 merged.set_query(downloader.redirected_url.query());
1730 merged
1731 };
1732 let client = downloader.http_client.as_ref().unwrap();
1733 trace!("Fetching XLinked element {}", xlink_url.clone());
1734 let mut req = client.get(xlink_url.clone())
1735 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
1736 .header("Accept-Language", "en-US,en")
1737 .header("Sec-Fetch-Mode", "navigate");
1738 if let Some(referer) = &downloader.referer {
1739 req = req.header("Referer", referer);
1740 } else {
1741 req = req.header("Referer", downloader.redirected_url.to_string());
1742 }
1743 if let Some(username) = &downloader.auth_username {
1744 if let Some(password) = &downloader.auth_password {
1745 req = req.basic_auth(username, Some(password));
1746 }
1747 }
1748 if let Some(token) = &downloader.auth_bearer_token {
1749 req = req.bearer_auth(token);
1750 }
1751 let xml = req.send().await
1752 .map_err(|e|
1753 if let Ok(ns) = xot.to_string(node) {
1754 network_error(&format!("fetching XLink for {ns}"), e)
1755 } else {
1756 network_error("fetching XLink", e)
1757 }
1758 )?
1759 .error_for_status()
1760 .map_err(|e|
1761 if let Ok(ns) = xot.to_string(node) {
1762 network_error(&format!("fetching XLink for {ns}"), e)
1763 } else {
1764 network_error("fetching XLink", e)
1765 }
1766 )?
1767 .text().await
1768 .map_err(|e|
1769 if let Ok(ns) = xot.to_string(node) {
1770 network_error(&format!("resolving XLink for {ns}"), e)
1771 } else {
1772 network_error("resolving XLink", e)
1773 }
1774 )?;
1775 if downloader.verbosity > 2 {
1776 if let Ok(ns) = xot.to_string(node) {
1777 info!(" Resolved onLoad XLink {xlink_url} on {ns} -> {} octets", xml.len());
1778 } else {
1779 info!(" Resolved onLoad XLink {xlink_url} -> {} octets", xml.len());
1780 }
1781 }
1782 let wrapped_xml = r#"<?xml version="1.0" encoding="utf-8"?>"#.to_owned() +
1788 r#"<wrapper xmlns="urn:mpeg:dash:schema:mpd:2011" "# +
1789 r#"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" "# +
1790 r#"xmlns:cenc="urn:mpeg:cenc:2013" "# +
1791 r#"xmlns:mspr="urn:microsoft:playready" "# +
1792 r#"xmlns:xlink="http://www.w3.org/1999/xlink">"# +
1793 skip_xml_preamble(&xml) +
1794 r#"</wrapper>"#;
1795 let wrapper_doc = xot.parse(&wrapped_xml)
1796 .map_err(|e| parse_error("parsing xlinked content", e))?;
1797 let wrapper_doc_el = xot.document_element(wrapper_doc)
1798 .map_err(|e| parse_error("extracting XML document element", e))?;
1799 for needs_insertion in xot.children(wrapper_doc_el).collect::<Vec<_>>() {
1800 xot.insert_after(xl, needs_insertion)
1802 .map_err(|e| parse_error("inserting XLinked content", e))?;
1803 }
1804 xot.remove(xl)
1805 .map_err(|e| parse_error("removing XLink node", e))?;
1806 }
1807 }
1808 }
1809 Ok(())
1810}
1811
1812#[tracing::instrument(level="trace", skip_all)]
1813pub async fn parse_resolving_xlinks(
1814 downloader: &DashDownloader,
1815 xml: &[u8]) -> Result<MPD, DashMpdError>
1816{
1817 use xot::xmlname::NameStrInfo;
1818
1819 let mut xot = Xot::new();
1820 let doc = xot.parse_bytes(xml)
1821 .map_err(|e| parse_error("XML parsing", e))?;
1822 let doc_el = xot.document_element(doc)
1823 .map_err(|e| parse_error("extracting XML document element", e))?;
1824 let doc_name = match xot.node_name(doc_el) {
1825 Some(n) => n,
1826 None => return Err(DashMpdError::Parsing(String::from("missing root node name"))),
1827 };
1828 let root_name = xot.name_ref(doc_name, doc_el)
1829 .map_err(|e| parse_error("extracting root node name", e))?;
1830 let root_local_name = root_name.local_name();
1831 if !root_local_name.eq("MPD") {
1832 return Err(DashMpdError::Parsing(format!("root element is {root_local_name}, expecting <MPD>")));
1833 }
1834 for _ in 1..5 {
1837 resolve_xlink_references(downloader, &mut xot, doc).await?;
1838 }
1839 let rewritten = apply_xslt_stylesheets_xsltproc(downloader, &mut xot, doc)?;
1840 let mpd = parse(&rewritten)?;
1842 if downloader.conformity_checks {
1843 for emsg in check_conformity(&mpd) {
1844 warn!("DASH conformity error in manifest: {emsg}");
1845 }
1846 }
1847 Ok(mpd)
1848}
1849
1850async fn do_segmentbase_indexrange(
1851 downloader: &DashDownloader,
1852 period_counter: u8,
1853 base_url: Url,
1854 sb: &SegmentBase,
1855 dict: &HashMap<&str, String>
1856) -> Result<Vec<MediaFragment>, DashMpdError>
1857{
1858 let mut fragments = Vec::new();
1891 let mut start_byte: Option<u64> = None;
1892 let mut end_byte: Option<u64> = None;
1893 let mut indexable_segments = false;
1894 if downloader.use_index_range {
1895 if let Some(ir) = &sb.indexRange {
1896 let (s, e) = parse_range(ir)?;
1898 trace!("Fetching sidx for {}", base_url.clone());
1899 let mut req = downloader.http_client.as_ref()
1900 .unwrap()
1901 .get(base_url.clone())
1902 .header(RANGE, format!("bytes={s}-{e}"))
1903 .header("Referer", downloader.redirected_url.to_string())
1904 .header("Sec-Fetch-Mode", "navigate");
1905 if let Some(username) = &downloader.auth_username {
1906 if let Some(password) = &downloader.auth_password {
1907 req = req.basic_auth(username, Some(password));
1908 }
1909 }
1910 if let Some(token) = &downloader.auth_bearer_token {
1911 req = req.bearer_auth(token);
1912 }
1913 let mut resp = req.send().await
1914 .map_err(|e| network_error("fetching index data", e))?
1915 .error_for_status()
1916 .map_err(|e| network_error("fetching index data", e))?;
1917 let headers = std::mem::take(resp.headers_mut());
1918 if let Some(content_type) = headers.get(CONTENT_TYPE) {
1919 let idx = resp.bytes().await
1920 .map_err(|e| network_error("fetching index data", e))?;
1921 if idx.len() as u64 != e - s + 1 {
1922 warn!(" HTTP server does not support Range requests; can't use indexRange addressing");
1923 } else {
1924 #[allow(clippy::collapsible_else_if)]
1925 if content_type.eq("video/mp4") ||
1926 content_type.eq("audio/mp4") {
1927 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
1934 .with_range(Some(0), Some(e))
1935 .build();
1936 fragments.push(mf);
1937 let mut max_chunk_pos = 0;
1938 if let Ok(segment_chunks) = crate::sidx::from_isobmff_sidx(&idx, e+1) {
1939 trace!("Have {} segment chunks in sidx data", segment_chunks.len());
1940 for chunk in segment_chunks {
1941 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
1942 .with_range(Some(chunk.start), Some(chunk.end))
1943 .build();
1944 fragments.push(mf);
1945 if chunk.end > max_chunk_pos {
1946 max_chunk_pos = chunk.end;
1947 }
1948 }
1949 indexable_segments = true;
1950 }
1951 }
1952 }
1959 }
1960 }
1961 }
1962 if indexable_segments {
1963 if let Some(init) = &sb.Initialization {
1964 if let Some(range) = &init.range {
1965 let (s, e) = parse_range(range)?;
1966 start_byte = Some(s);
1967 end_byte = Some(e);
1968 }
1969 if let Some(su) = &init.sourceURL {
1970 let path = resolve_url_template(su, dict);
1971 let u = merge_baseurls(&base_url, &path)?;
1972 let mf = MediaFragmentBuilder::new(period_counter, u)
1973 .with_range(start_byte, end_byte)
1974 .set_init()
1975 .build();
1976 fragments.push(mf);
1977 } else {
1978 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
1980 .with_range(start_byte, end_byte)
1981 .set_init()
1982 .build();
1983 fragments.push(mf);
1984 }
1985 }
1986 } else {
1987 trace!("Falling back to retrieving full SegmentBase for {}", base_url.clone());
1992 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
1993 .with_timeout(Duration::new(10_000, 0))
1994 .build();
1995 fragments.push(mf);
1996 }
1997 Ok(fragments)
1998}
1999
2000
2001#[tracing::instrument(level="trace", skip_all)]
2002async fn do_period_audio(
2003 downloader: &DashDownloader,
2004 mpd: &MPD,
2005 period: &Period,
2006 period_counter: u8,
2007 base_url: Url
2008 ) -> Result<PeriodOutputs, DashMpdError>
2009{
2010 let mut fragments = Vec::new();
2011 let mut diagnostics = Vec::new();
2012 let mut opt_init: Option<String> = None;
2013 let mut opt_media: Option<String> = None;
2014 let mut opt_duration: Option<f64> = None;
2015 let mut timescale = 1;
2016 let mut start_number = 1;
2017 let mut period_duration_secs: f64 = 0.0;
2020 if let Some(d) = mpd.mediaPresentationDuration {
2021 period_duration_secs = d.as_secs_f64();
2022 }
2023 if let Some(d) = period.duration {
2024 period_duration_secs = d.as_secs_f64();
2025 }
2026 if let Some(s) = downloader.force_duration {
2027 period_duration_secs = s;
2028 }
2029 if let Some(st) = &period.SegmentTemplate {
2033 if let Some(i) = &st.initialization {
2034 opt_init = Some(i.to_string());
2035 }
2036 if let Some(m) = &st.media {
2037 opt_media = Some(m.to_string());
2038 }
2039 if let Some(d) = st.duration {
2040 opt_duration = Some(d);
2041 }
2042 if let Some(ts) = st.timescale {
2043 timescale = ts;
2044 }
2045 if let Some(s) = st.startNumber {
2046 start_number = s;
2047 }
2048 }
2049 let audio_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2052 .filter(is_audio_adaptation)
2053 .collect();
2054 let representations: Vec<&Representation> = select_preferred_adaptations(audio_adaptations, downloader)
2055 .iter()
2056 .flat_map(|a| a.representations.iter())
2057 .collect();
2058 if let Some(audio_repr) = select_preferred_representation(representations, downloader) {
2059 let audio_adaptation = period.adaptations.iter()
2063 .find(|a| a.representations.iter().any(|r| r.eq(audio_repr)))
2064 .unwrap();
2065 let mut base_url = base_url.clone();
2068 if let Some(bu) = &audio_adaptation.BaseURL.first() {
2069 base_url = merge_baseurls(&base_url, &bu.base)?;
2070 }
2071 if let Some(bu) = audio_repr.BaseURL.first() {
2072 base_url = merge_baseurls(&base_url, &bu.base)?;
2073 }
2074 if downloader.verbosity > 0 {
2075 let bw = if let Some(bw) = audio_repr.bandwidth {
2076 format!("bw={} Kbps ", bw / 1024)
2077 } else {
2078 String::from("")
2079 };
2080 let unknown = String::from("?");
2081 let lang = audio_repr.lang.as_ref()
2082 .unwrap_or(audio_adaptation.lang.as_ref()
2083 .unwrap_or(&unknown));
2084 let codec = audio_repr.codecs.as_ref()
2085 .unwrap_or(audio_adaptation.codecs.as_ref()
2086 .unwrap_or(&unknown));
2087 diagnostics.push(format!(" Audio stream selected: {bw}lang={lang} codec={codec}"));
2088 for cp in audio_repr.ContentProtection.iter()
2090 .chain(audio_adaptation.ContentProtection.iter())
2091 {
2092 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2093 if let Some(kid) = &cp.default_KID {
2094 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2095 }
2096 for pssh_element in cp.cenc_pssh.iter() {
2097 if let Some(pssh_b64) = &pssh_element.content {
2098 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
2099 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
2100 diagnostics.push(format!(" {pssh}"));
2101 }
2102 }
2103 }
2104 }
2105 }
2106 if let Some(st) = &audio_adaptation.SegmentTemplate {
2111 if let Some(i) = &st.initialization {
2112 opt_init = Some(i.to_string());
2113 }
2114 if let Some(m) = &st.media {
2115 opt_media = Some(m.to_string());
2116 }
2117 if let Some(d) = st.duration {
2118 opt_duration = Some(d);
2119 }
2120 if let Some(ts) = st.timescale {
2121 timescale = ts;
2122 }
2123 if let Some(s) = st.startNumber {
2124 start_number = s;
2125 }
2126 }
2127 let mut dict = HashMap::new();
2128 if let Some(rid) = &audio_repr.id {
2129 dict.insert("RepresentationID", rid.to_string());
2130 }
2131 if let Some(b) = &audio_repr.bandwidth {
2132 dict.insert("Bandwidth", b.to_string());
2133 }
2134 if let Some(sl) = &audio_adaptation.SegmentList {
2143 if downloader.verbosity > 1 {
2146 info!(" {}", "Using AdaptationSet>SegmentList addressing mode for audio representation".italic());
2147 }
2148 let mut start_byte: Option<u64> = None;
2149 let mut end_byte: Option<u64> = None;
2150 if let Some(init) = &sl.Initialization {
2151 if let Some(range) = &init.range {
2152 let (s, e) = parse_range(range)?;
2153 start_byte = Some(s);
2154 end_byte = Some(e);
2155 }
2156 if let Some(su) = &init.sourceURL {
2157 let path = resolve_url_template(su, &dict);
2158 let init_url = merge_baseurls(&base_url, &path)?;
2159 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2160 .with_range(start_byte, end_byte)
2161 .set_init()
2162 .build();
2163 fragments.push(mf);
2164 } else {
2165 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2166 .with_range(start_byte, end_byte)
2167 .set_init()
2168 .build();
2169 fragments.push(mf);
2170 }
2171 }
2172 for su in sl.segment_urls.iter() {
2173 start_byte = None;
2174 end_byte = None;
2175 if let Some(range) = &su.mediaRange {
2177 let (s, e) = parse_range(range)?;
2178 start_byte = Some(s);
2179 end_byte = Some(e);
2180 }
2181 if let Some(m) = &su.media {
2182 let u = merge_baseurls(&base_url, m)?;
2183 let mf = MediaFragmentBuilder::new(period_counter, u)
2184 .with_range(start_byte, end_byte)
2185 .build();
2186 fragments.push(mf);
2187 } else if let Some(bu) = audio_adaptation.BaseURL.first() {
2188 let u = merge_baseurls(&base_url, &bu.base)?;
2189 let mf = MediaFragmentBuilder::new(period_counter, u)
2190 .with_range(start_byte, end_byte)
2191 .build();
2192 fragments.push(mf);
2193 }
2194 }
2195 }
2196 if let Some(sl) = &audio_repr.SegmentList {
2197 if downloader.verbosity > 1 {
2199 info!(" {}", "Using Representation>SegmentList addressing mode for audio representation".italic());
2200 }
2201 let mut start_byte: Option<u64> = None;
2202 let mut end_byte: Option<u64> = None;
2203 if let Some(init) = &sl.Initialization {
2204 if let Some(range) = &init.range {
2205 let (s, e) = parse_range(range)?;
2206 start_byte = Some(s);
2207 end_byte = Some(e);
2208 }
2209 if let Some(su) = &init.sourceURL {
2210 let path = resolve_url_template(su, &dict);
2211 let init_url = merge_baseurls(&base_url, &path)?;
2212 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2213 .with_range(start_byte, end_byte)
2214 .set_init()
2215 .build();
2216 fragments.push(mf);
2217 } else {
2218 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2219 .with_range(start_byte, end_byte)
2220 .set_init()
2221 .build();
2222 fragments.push(mf);
2223 }
2224 }
2225 for su in sl.segment_urls.iter() {
2226 start_byte = None;
2227 end_byte = None;
2228 if let Some(range) = &su.mediaRange {
2230 let (s, e) = parse_range(range)?;
2231 start_byte = Some(s);
2232 end_byte = Some(e);
2233 }
2234 if let Some(m) = &su.media {
2235 let u = merge_baseurls(&base_url, m)?;
2236 let mf = MediaFragmentBuilder::new(period_counter, u)
2237 .with_range(start_byte, end_byte)
2238 .build();
2239 fragments.push(mf);
2240 } else if let Some(bu) = audio_repr.BaseURL.first() {
2241 let u = merge_baseurls(&base_url, &bu.base)?;
2242 let mf = MediaFragmentBuilder::new(period_counter, u)
2243 .with_range(start_byte, end_byte)
2244 .build();
2245 fragments.push(mf);
2246 }
2247 }
2248 } else if audio_repr.SegmentTemplate.is_some() ||
2249 audio_adaptation.SegmentTemplate.is_some()
2250 {
2251 let st;
2254 if let Some(it) = &audio_repr.SegmentTemplate {
2255 st = it;
2256 } else if let Some(it) = &audio_adaptation.SegmentTemplate {
2257 st = it;
2258 } else {
2259 panic!("unreachable");
2260 }
2261 if let Some(i) = &st.initialization {
2262 opt_init = Some(i.to_string());
2263 }
2264 if let Some(m) = &st.media {
2265 opt_media = Some(m.to_string());
2266 }
2267 if let Some(ts) = st.timescale {
2268 timescale = ts;
2269 }
2270 if let Some(sn) = st.startNumber {
2271 start_number = sn;
2272 }
2273 if let Some(stl) = &audio_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
2274 .or(audio_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
2275 {
2276 if downloader.verbosity > 1 {
2279 info!(" {}", "Using SegmentTemplate+SegmentTimeline addressing mode for audio representation".italic());
2280 }
2281 if let Some(init) = opt_init {
2282 let path = resolve_url_template(&init, &dict);
2283 let u = merge_baseurls(&base_url, &path)?;
2284 let mf = MediaFragmentBuilder::new(period_counter, u)
2285 .set_init()
2286 .build();
2287 fragments.push(mf);
2288 }
2289 if let Some(media) = opt_media {
2290 let audio_path = resolve_url_template(&media, &dict);
2291 let mut segment_time = 0;
2292 let mut segment_duration;
2293 let mut number = start_number;
2294 for s in &stl.segments {
2295 if let Some(t) = s.t {
2296 segment_time = t;
2297 }
2298 segment_duration = s.d;
2299 let dict = HashMap::from([("Time", segment_time.to_string()),
2301 ("Number", number.to_string())]);
2302 let path = resolve_url_template(&audio_path, &dict);
2303 let u = merge_baseurls(&base_url, &path)?;
2304 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2305 number += 1;
2306 if let Some(r) = s.r {
2307 let mut count = 0i64;
2308 let end_time = period_duration_secs * timescale as f64;
2310 loop {
2311 count += 1;
2312 if r >= 0 {
2318 if count > r {
2319 break;
2320 }
2321 if downloader.force_duration.is_some() && segment_time as f64 > end_time {
2322 break;
2323 }
2324 } else if segment_time as f64 > end_time {
2325 break;
2326 }
2327 segment_time += segment_duration;
2328 let dict = HashMap::from([("Time", segment_time.to_string()),
2329 ("Number", number.to_string())]);
2330 let path = resolve_url_template(&audio_path, &dict);
2331 let u = merge_baseurls(&base_url, &path)?;
2332 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2333 number += 1;
2334 }
2335 }
2336 segment_time += segment_duration;
2337 }
2338 } else {
2339 return Err(DashMpdError::UnhandledMediaStream(
2340 "SegmentTimeline without a media attribute".to_string()));
2341 }
2342 } else { if downloader.verbosity > 1 {
2347 info!(" {}", "Using SegmentTemplate addressing mode for audio representation".italic());
2348 }
2349 let mut total_number = 0i64;
2350 if let Some(init) = opt_init {
2351 let path = resolve_url_template(&init, &dict);
2352 let u = merge_baseurls(&base_url, &path)?;
2353 let mf = MediaFragmentBuilder::new(period_counter, u)
2354 .set_init()
2355 .build();
2356 fragments.push(mf);
2357 }
2358 if let Some(media) = opt_media {
2359 let audio_path = resolve_url_template(&media, &dict);
2360 let timescale = st.timescale.unwrap_or(timescale);
2361 let mut segment_duration: f64 = -1.0;
2362 if let Some(d) = opt_duration {
2363 segment_duration = d;
2365 }
2366 if let Some(std) = st.duration {
2367 segment_duration = std / timescale as f64;
2368 }
2369 if segment_duration < 0.0 {
2370 return Err(DashMpdError::UnhandledMediaStream(
2371 "Audio representation is missing SegmentTemplate@duration attribute".to_string()));
2372 }
2373 total_number += (period_duration_secs / segment_duration).round() as i64;
2374 let mut number = start_number;
2375 if mpd_is_dynamic(mpd) {
2378 if let Some(start_time) = mpd.availabilityStartTime {
2379 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
2380 number = (elapsed + number as f64 - 1f64).floor() as u64;
2381 } else {
2382 return Err(DashMpdError::UnhandledMediaStream(
2383 "dynamic manifest is missing @availabilityStartTime".to_string()));
2384 }
2385 }
2386 for _ in 1..=total_number {
2387 let dict = HashMap::from([("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 }
2393 }
2394 }
2395 } else if let Some(sb) = &audio_repr.SegmentBase {
2396 if downloader.verbosity > 1 {
2398 info!(" {}", "Using SegmentBase@indexRange addressing mode for audio representation".italic());
2399 }
2400 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
2401 fragments.extend(mf);
2402 } else if fragments.is_empty() {
2403 if let Some(bu) = audio_repr.BaseURL.first() {
2404 if downloader.verbosity > 1 {
2406 info!(" {}", "Using BaseURL addressing mode for audio representation".italic());
2407 }
2408 let u = merge_baseurls(&base_url, &bu.base)?;
2409 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2410 }
2411 }
2412 if fragments.is_empty() {
2413 return Err(DashMpdError::UnhandledMediaStream(
2414 "no usable addressing mode identified for audio representation".to_string()));
2415 }
2416 }
2417 Ok(PeriodOutputs { fragments, diagnostics, subtitle_formats: Vec::new() })
2418}
2419
2420
2421#[tracing::instrument(level="trace", skip_all)]
2422async fn do_period_video(
2423 downloader: &DashDownloader,
2424 mpd: &MPD,
2425 period: &Period,
2426 period_counter: u8,
2427 base_url: Url
2428 ) -> Result<PeriodOutputs, DashMpdError>
2429{
2430 let mut fragments = Vec::new();
2431 let mut diagnostics = Vec::new();
2432 let mut period_duration_secs: f64 = 0.0;
2433 let mut opt_init: Option<String> = None;
2434 let mut opt_media: Option<String> = None;
2435 let mut opt_duration: Option<f64> = None;
2436 let mut timescale = 1;
2437 let mut start_number = 1;
2438 if let Some(d) = mpd.mediaPresentationDuration {
2439 period_duration_secs = d.as_secs_f64();
2440 }
2441 if let Some(d) = period.duration {
2442 period_duration_secs = d.as_secs_f64();
2443 }
2444 if let Some(s) = downloader.force_duration {
2445 period_duration_secs = s;
2446 }
2447 if let Some(st) = &period.SegmentTemplate {
2451 if let Some(i) = &st.initialization {
2452 opt_init = Some(i.to_string());
2453 }
2454 if let Some(m) = &st.media {
2455 opt_media = Some(m.to_string());
2456 }
2457 if let Some(d) = st.duration {
2458 opt_duration = Some(d);
2459 }
2460 if let Some(ts) = st.timescale {
2461 timescale = ts;
2462 }
2463 if let Some(s) = st.startNumber {
2464 start_number = s;
2465 }
2466 }
2467 let video_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2474 .filter(is_video_adaptation)
2475 .collect();
2476 let representations: Vec<&Representation> = select_preferred_adaptations(video_adaptations, downloader)
2477 .iter()
2478 .flat_map(|a| a.representations.iter())
2479 .collect();
2480 let maybe_video_repr = if let Some(want) = downloader.video_width_preference {
2481 representations.iter()
2482 .min_by_key(|x| if let Some(w) = x.width { want.abs_diff(w) } else { u64::MAX })
2483 .copied()
2484 } else if let Some(want) = downloader.video_height_preference {
2485 representations.iter()
2486 .min_by_key(|x| if let Some(h) = x.height { want.abs_diff(h) } else { u64::MAX })
2487 .copied()
2488 } else {
2489 select_preferred_representation(representations, downloader)
2490 };
2491 if let Some(video_repr) = maybe_video_repr {
2492 let video_adaptation = period.adaptations.iter()
2496 .find(|a| a.representations.iter().any(|r| r.eq(video_repr)))
2497 .unwrap();
2498 let mut base_url = base_url.clone();
2501 if let Some(bu) = &video_adaptation.BaseURL.first() {
2502 base_url = merge_baseurls(&base_url, &bu.base)?;
2503 }
2504 if let Some(bu) = &video_repr.BaseURL.first() {
2505 base_url = merge_baseurls(&base_url, &bu.base)?;
2506 }
2507 if downloader.verbosity > 0 {
2508 let bw = if let Some(bw) = video_repr.bandwidth.or(video_adaptation.maxBandwidth) {
2509 format!("bw={} Kbps ", bw / 1024)
2510 } else {
2511 String::from("")
2512 };
2513 let unknown = String::from("?");
2514 let w = video_repr.width.unwrap_or(video_adaptation.width.unwrap_or(0));
2515 let h = video_repr.height.unwrap_or(video_adaptation.height.unwrap_or(0));
2516 let fmt = if w == 0 || h == 0 {
2517 String::from("")
2518 } else {
2519 format!("resolution={w}x{h} ")
2520 };
2521 let codec = video_repr.codecs.as_ref()
2522 .unwrap_or(video_adaptation.codecs.as_ref().unwrap_or(&unknown));
2523 diagnostics.push(format!(" Video stream selected: {bw}{fmt}codec={codec}"));
2524 for cp in video_repr.ContentProtection.iter()
2526 .chain(video_adaptation.ContentProtection.iter())
2527 {
2528 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2529 if let Some(kid) = &cp.default_KID {
2530 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2531 }
2532 for pssh_element in cp.cenc_pssh.iter() {
2533 if let Some(pssh_b64) = &pssh_element.content {
2534 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
2535 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
2536 diagnostics.push(format!(" {pssh}"));
2537 }
2538 }
2539 }
2540 }
2541 }
2542 let mut dict = HashMap::new();
2543 if let Some(rid) = &video_repr.id {
2544 dict.insert("RepresentationID", rid.to_string());
2545 }
2546 if let Some(b) = &video_repr.bandwidth {
2547 dict.insert("Bandwidth", b.to_string());
2548 }
2549 if let Some(st) = &video_adaptation.SegmentTemplate {
2554 if let Some(i) = &st.initialization {
2555 opt_init = Some(i.to_string());
2556 }
2557 if let Some(m) = &st.media {
2558 opt_media = Some(m.to_string());
2559 }
2560 if let Some(d) = st.duration {
2561 opt_duration = Some(d);
2562 }
2563 if let Some(ts) = st.timescale {
2564 timescale = ts;
2565 }
2566 if let Some(s) = st.startNumber {
2567 start_number = s;
2568 }
2569 }
2570 if let Some(sl) = &video_adaptation.SegmentList {
2574 if downloader.verbosity > 1 {
2576 info!(" {}", "Using AdaptationSet>SegmentList addressing mode for video representation".italic());
2577 }
2578 let mut start_byte: Option<u64> = None;
2579 let mut end_byte: Option<u64> = None;
2580 if let Some(init) = &sl.Initialization {
2581 if let Some(range) = &init.range {
2582 let (s, e) = parse_range(range)?;
2583 start_byte = Some(s);
2584 end_byte = Some(e);
2585 }
2586 if let Some(su) = &init.sourceURL {
2587 let path = resolve_url_template(su, &dict);
2588 let u = merge_baseurls(&base_url, &path)?;
2589 let mf = MediaFragmentBuilder::new(period_counter, u)
2590 .with_range(start_byte, end_byte)
2591 .set_init()
2592 .build();
2593 fragments.push(mf);
2594 }
2595 } else {
2596 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2597 .with_range(start_byte, end_byte)
2598 .set_init()
2599 .build();
2600 fragments.push(mf);
2601 }
2602 for su in sl.segment_urls.iter() {
2603 start_byte = None;
2604 end_byte = None;
2605 if let Some(range) = &su.mediaRange {
2607 let (s, e) = parse_range(range)?;
2608 start_byte = Some(s);
2609 end_byte = Some(e);
2610 }
2611 if let Some(m) = &su.media {
2612 let u = merge_baseurls(&base_url, m)?;
2613 let mf = MediaFragmentBuilder::new(period_counter, u)
2614 .with_range(start_byte, end_byte)
2615 .build();
2616 fragments.push(mf);
2617 } else if let Some(bu) = video_adaptation.BaseURL.first() {
2618 let u = merge_baseurls(&base_url, &bu.base)?;
2619 let mf = MediaFragmentBuilder::new(period_counter, u)
2620 .with_range(start_byte, end_byte)
2621 .build();
2622 fragments.push(mf);
2623 }
2624 }
2625 }
2626 if let Some(sl) = &video_repr.SegmentList {
2627 if downloader.verbosity > 1 {
2629 info!(" {}", "Using Representation>SegmentList addressing mode for video representation".italic());
2630 }
2631 let mut start_byte: Option<u64> = None;
2632 let mut end_byte: Option<u64> = None;
2633 if let Some(init) = &sl.Initialization {
2634 if let Some(range) = &init.range {
2635 let (s, e) = parse_range(range)?;
2636 start_byte = Some(s);
2637 end_byte = Some(e);
2638 }
2639 if let Some(su) = &init.sourceURL {
2640 let path = resolve_url_template(su, &dict);
2641 let u = merge_baseurls(&base_url, &path)?;
2642 let mf = MediaFragmentBuilder::new(period_counter, u)
2643 .with_range(start_byte, end_byte)
2644 .set_init()
2645 .build();
2646 fragments.push(mf);
2647 } else {
2648 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2649 .with_range(start_byte, end_byte)
2650 .set_init()
2651 .build();
2652 fragments.push(mf);
2653 }
2654 }
2655 for su in sl.segment_urls.iter() {
2656 start_byte = None;
2657 end_byte = None;
2658 if let Some(range) = &su.mediaRange {
2660 let (s, e) = parse_range(range)?;
2661 start_byte = Some(s);
2662 end_byte = Some(e);
2663 }
2664 if let Some(m) = &su.media {
2665 let u = merge_baseurls(&base_url, m)?;
2666 let mf = MediaFragmentBuilder::new(period_counter, u)
2667 .with_range(start_byte, end_byte)
2668 .build();
2669 fragments.push(mf);
2670 } else if let Some(bu) = video_repr.BaseURL.first() {
2671 let u = merge_baseurls(&base_url, &bu.base)?;
2672 let mf = MediaFragmentBuilder::new(period_counter, u)
2673 .with_range(start_byte, end_byte)
2674 .build();
2675 fragments.push(mf);
2676 }
2677 }
2678 } else if video_repr.SegmentTemplate.is_some() ||
2679 video_adaptation.SegmentTemplate.is_some() {
2680 let st;
2683 if let Some(it) = &video_repr.SegmentTemplate {
2684 st = it;
2685 } else if let Some(it) = &video_adaptation.SegmentTemplate {
2686 st = it;
2687 } else {
2688 panic!("impossible");
2689 }
2690 if let Some(i) = &st.initialization {
2691 opt_init = Some(i.to_string());
2692 }
2693 if let Some(m) = &st.media {
2694 opt_media = Some(m.to_string());
2695 }
2696 if let Some(ts) = st.timescale {
2697 timescale = ts;
2698 }
2699 if let Some(sn) = st.startNumber {
2700 start_number = sn;
2701 }
2702 if let Some(stl) = &video_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
2703 .or(video_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
2704 {
2705 if downloader.verbosity > 1 {
2707 info!(" {}", "Using SegmentTemplate+SegmentTimeline addressing mode for video representation".italic());
2708 }
2709 if let Some(init) = opt_init {
2710 let path = resolve_url_template(&init, &dict);
2711 let u = merge_baseurls(&base_url, &path)?;
2712 let mf = MediaFragmentBuilder::new(period_counter, u)
2713 .set_init()
2714 .build();
2715 fragments.push(mf);
2716 }
2717 if let Some(media) = opt_media {
2718 let video_path = resolve_url_template(&media, &dict);
2719 let mut segment_time = 0;
2720 let mut segment_duration;
2721 let mut number = start_number;
2722 for s in &stl.segments {
2723 if let Some(t) = s.t {
2724 segment_time = t;
2725 }
2726 segment_duration = s.d;
2727 let dict = HashMap::from([("Time", segment_time.to_string()),
2729 ("Number", number.to_string())]);
2730 let path = resolve_url_template(&video_path, &dict);
2731 let u = merge_baseurls(&base_url, &path)?;
2732 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2733 fragments.push(mf);
2734 number += 1;
2735 if let Some(r) = s.r {
2736 let mut count = 0i64;
2737 let end_time = period_duration_secs * timescale as f64;
2739 loop {
2740 count += 1;
2741 if r >= 0 {
2747 if count > r {
2748 break;
2749 }
2750 if downloader.force_duration.is_some() && segment_time as f64 > end_time {
2751 break;
2752 }
2753 } else if segment_time as f64 > end_time {
2754 break;
2755 }
2756 segment_time += segment_duration;
2757 let dict = HashMap::from([("Time", segment_time.to_string()),
2758 ("Number", number.to_string())]);
2759 let path = resolve_url_template(&video_path, &dict);
2760 let u = merge_baseurls(&base_url, &path)?;
2761 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2762 fragments.push(mf);
2763 number += 1;
2764 }
2765 }
2766 segment_time += segment_duration;
2767 }
2768 } else {
2769 return Err(DashMpdError::UnhandledMediaStream(
2770 "SegmentTimeline without a media attribute".to_string()));
2771 }
2772 } else { if downloader.verbosity > 1 {
2775 info!(" {}", "Using SegmentTemplate addressing mode for video representation".italic());
2776 }
2777 let mut total_number = 0i64;
2778 if let Some(init) = opt_init {
2779 let path = resolve_url_template(&init, &dict);
2780 let u = merge_baseurls(&base_url, &path)?;
2781 let mf = MediaFragmentBuilder::new(period_counter, u)
2782 .set_init()
2783 .build();
2784 fragments.push(mf);
2785 }
2786 if let Some(media) = opt_media {
2787 let video_path = resolve_url_template(&media, &dict);
2788 let timescale = st.timescale.unwrap_or(timescale);
2789 let mut segment_duration: f64 = -1.0;
2790 if let Some(d) = opt_duration {
2791 segment_duration = d;
2793 }
2794 if let Some(std) = st.duration {
2795 segment_duration = std / timescale as f64;
2796 }
2797 if segment_duration < 0.0 {
2798 return Err(DashMpdError::UnhandledMediaStream(
2799 "Video representation is missing SegmentTemplate@duration attribute".to_string()));
2800 }
2801 total_number += (period_duration_secs / segment_duration).round() as i64;
2802 let mut number = start_number;
2803 if mpd_is_dynamic(mpd) {
2813 if let Some(start_time) = mpd.availabilityStartTime {
2814 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
2815 number = (elapsed + number as f64 - 1f64).floor() as u64;
2816 } else {
2817 return Err(DashMpdError::UnhandledMediaStream(
2818 "dynamic manifest is missing @availabilityStartTime".to_string()));
2819 }
2820 }
2821 for _ in 1..=total_number {
2822 let dict = HashMap::from([("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 }
2829 }
2830 }
2831 } else if let Some(sb) = &video_repr.SegmentBase {
2832 if downloader.verbosity > 1 {
2834 info!(" {}", "Using SegmentBase@indexRange addressing mode for video representation".italic());
2835 }
2836 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
2837 fragments.extend(mf);
2838 } else if fragments.is_empty() {
2839 if let Some(bu) = video_repr.BaseURL.first() {
2840 if downloader.verbosity > 1 {
2842 info!(" {}", "Using BaseURL addressing mode for video representation".italic());
2843 }
2844 let u = merge_baseurls(&base_url, &bu.base)?;
2845 let mf = MediaFragmentBuilder::new(period_counter, u)
2846 .with_timeout(Duration::new(10000, 0))
2847 .build();
2848 fragments.push(mf);
2849 }
2850 }
2851 if fragments.is_empty() {
2852 return Err(DashMpdError::UnhandledMediaStream(
2853 "no usable addressing mode identified for video representation".to_string()));
2854 }
2855 }
2856 Ok(PeriodOutputs { fragments, diagnostics, subtitle_formats: Vec::new() })
2859}
2860
2861#[tracing::instrument(level="trace", skip_all)]
2862async fn do_period_subtitles(
2863 downloader: &DashDownloader,
2864 mpd: &MPD,
2865 period: &Period,
2866 period_counter: u8,
2867 base_url: Url
2868 ) -> Result<PeriodOutputs, DashMpdError>
2869{
2870 let client = downloader.http_client.as_ref().unwrap();
2871 let output_path = &downloader.output_path.as_ref().unwrap().clone();
2872 let period_output_path = output_path_for_period(output_path, period_counter);
2873 let mut fragments = Vec::new();
2874 let mut subtitle_formats = Vec::new();
2875 let mut period_duration_secs: f64 = 0.0;
2876 if let Some(d) = mpd.mediaPresentationDuration {
2877 period_duration_secs = d.as_secs_f64();
2878 }
2879 if let Some(d) = period.duration {
2880 period_duration_secs = d.as_secs_f64();
2881 }
2882 let maybe_subtitle_adaptation = if let Some(ref lang) = downloader.language_preference {
2883 period.adaptations.iter().filter(is_subtitle_adaptation)
2884 .min_by_key(|a| adaptation_lang_distance(a, lang))
2885 } else {
2886 period.adaptations.iter().find(is_subtitle_adaptation)
2888 };
2889 if downloader.fetch_subtitles {
2890 if let Some(subtitle_adaptation) = maybe_subtitle_adaptation {
2891 let subtitle_format = subtitle_type(&subtitle_adaptation);
2892 subtitle_formats.push(subtitle_format);
2893 if downloader.verbosity > 1 && downloader.fetch_subtitles {
2894 info!(" Retrieving subtitles in format {subtitle_format:?}");
2895 }
2896 let mut base_url = base_url.clone();
2899 if let Some(bu) = &subtitle_adaptation.BaseURL.first() {
2900 base_url = merge_baseurls(&base_url, &bu.base)?;
2901 }
2902 if let Some(rep) = subtitle_adaptation.representations.first() {
2905 if !rep.BaseURL.is_empty() {
2906 for st_bu in rep.BaseURL.iter() {
2907 let st_url = merge_baseurls(&base_url, &st_bu.base)?;
2908 let mut req = client.get(st_url.clone());
2909 if let Some(referer) = &downloader.referer {
2910 req = req.header("Referer", referer);
2911 } else {
2912 req = req.header("Referer", base_url.to_string());
2913 }
2914 let rqw = req.build()
2915 .map_err(|e| network_error("building request", e))?;
2916 let subs = reqwest_bytes_with_retries(client, rqw, 5).await
2917 .map_err(|e| network_error("fetching subtitles", e))?;
2918 let mut subs_path = period_output_path.clone();
2919 let subtitle_format = subtitle_type(&subtitle_adaptation);
2920 match subtitle_format {
2921 SubtitleType::Vtt => subs_path.set_extension("vtt"),
2922 SubtitleType::Srt => subs_path.set_extension("srt"),
2923 SubtitleType::Ttml => subs_path.set_extension("ttml"),
2924 SubtitleType::Sami => subs_path.set_extension("sami"),
2925 SubtitleType::Wvtt => subs_path.set_extension("wvtt"),
2926 SubtitleType::Stpp => subs_path.set_extension("stpp"),
2927 _ => subs_path.set_extension("sub"),
2928 };
2929 subtitle_formats.push(subtitle_format);
2930 let mut subs_file = File::create(subs_path.clone())
2931 .map_err(|e| DashMpdError::Io(e, String::from("creating subtitle file")))?;
2932 if downloader.verbosity > 2 {
2933 info!(" Subtitle {st_url} -> {} octets", subs.len());
2934 }
2935 match subs_file.write_all(&subs) {
2936 Ok(()) => {
2937 if downloader.verbosity > 0 {
2938 info!(" Downloaded subtitles ({subtitle_format:?}) to {}",
2939 subs_path.display());
2940 }
2941 },
2942 Err(e) => {
2943 error!("Unable to write subtitle file: {e:?}");
2944 return Err(DashMpdError::Io(e, String::from("writing subtitle data")));
2945 },
2946 }
2947 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
2948 subtitle_formats.contains(&SubtitleType::Ttxt)
2949 {
2950 if downloader.verbosity > 0 {
2951 info!(" Converting subtitles to SRT format with MP4Box ");
2952 }
2953 let out = subs_path.with_extension("srt");
2954 let out_str = out.to_string_lossy();
2961 let subs_str = subs_path.to_string_lossy();
2962 let args = vec![
2963 "-srt", "1",
2964 "-out", &out_str,
2965 &subs_str];
2966 if downloader.verbosity > 0 {
2967 info!(" Running MPBox {}", args.join(" "));
2968 }
2969 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
2970 .args(args)
2971 .output()
2972 {
2973 let msg = partial_process_output(&mp4box.stdout);
2974 if !msg.is_empty() {
2975 info!("MP4Box stdout: {msg}");
2976 }
2977 let msg = partial_process_output(&mp4box.stderr);
2978 if !msg.is_empty() {
2979 info!("MP4Box stderr: {msg}");
2980 }
2981 if mp4box.status.success() {
2982 info!(" Converted subtitles to SRT");
2983 } else {
2984 warn!("Error running MP4Box to convert subtitles");
2985 }
2986 }
2987 }
2988 }
2989 } else if rep.SegmentTemplate.is_some() || subtitle_adaptation.SegmentTemplate.is_some() {
2990 let mut opt_init: Option<String> = None;
2991 let mut opt_media: Option<String> = None;
2992 let mut opt_duration: Option<f64> = None;
2993 let mut timescale = 1;
2994 let mut start_number = 1;
2995 if let Some(st) = &rep.SegmentTemplate {
3000 if let Some(i) = &st.initialization {
3001 opt_init = Some(i.to_string());
3002 }
3003 if let Some(m) = &st.media {
3004 opt_media = Some(m.to_string());
3005 }
3006 if let Some(d) = st.duration {
3007 opt_duration = Some(d);
3008 }
3009 if let Some(ts) = st.timescale {
3010 timescale = ts;
3011 }
3012 if let Some(s) = st.startNumber {
3013 start_number = s;
3014 }
3015 }
3016 let rid = match &rep.id {
3017 Some(id) => id,
3018 None => return Err(
3019 DashMpdError::UnhandledMediaStream(
3020 "Missing @id on Representation node".to_string())),
3021 };
3022 let mut dict = HashMap::from([("RepresentationID", rid.to_string())]);
3023 if let Some(b) = &rep.bandwidth {
3024 dict.insert("Bandwidth", b.to_string());
3025 }
3026 if let Some(sl) = &rep.SegmentList {
3030 if downloader.verbosity > 1 {
3033 info!(" {}", "Using AdaptationSet>SegmentList addressing mode for subtitle representation".italic());
3034 }
3035 let mut start_byte: Option<u64> = None;
3036 let mut end_byte: Option<u64> = None;
3037 if let Some(init) = &sl.Initialization {
3038 if let Some(range) = &init.range {
3039 let (s, e) = parse_range(range)?;
3040 start_byte = Some(s);
3041 end_byte = Some(e);
3042 }
3043 if let Some(su) = &init.sourceURL {
3044 let path = resolve_url_template(su, &dict);
3045 let u = merge_baseurls(&base_url, &path)?;
3046 let mf = MediaFragmentBuilder::new(period_counter, u)
3047 .with_range(start_byte, end_byte)
3048 .set_init()
3049 .build();
3050 fragments.push(mf);
3051 } else {
3052 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3053 .with_range(start_byte, end_byte)
3054 .set_init()
3055 .build();
3056 fragments.push(mf);
3057 }
3058 }
3059 for su in sl.segment_urls.iter() {
3060 start_byte = None;
3061 end_byte = None;
3062 if let Some(range) = &su.mediaRange {
3064 let (s, e) = parse_range(range)?;
3065 start_byte = Some(s);
3066 end_byte = Some(e);
3067 }
3068 if let Some(m) = &su.media {
3069 let u = merge_baseurls(&base_url, m)?;
3070 let mf = MediaFragmentBuilder::new(period_counter, u)
3071 .with_range(start_byte, end_byte)
3072 .build();
3073 fragments.push(mf);
3074 } else if let Some(bu) = subtitle_adaptation.BaseURL.first() {
3075 let u = merge_baseurls(&base_url, &bu.base)?;
3076 let mf = MediaFragmentBuilder::new(period_counter, u)
3077 .with_range(start_byte, end_byte)
3078 .build();
3079 fragments.push(mf);
3080 }
3081 }
3082 }
3083 if let Some(sl) = &rep.SegmentList {
3084 if downloader.verbosity > 1 {
3086 info!(" {}", "Using Representation>SegmentList addressing mode for subtitle representation".italic());
3087 }
3088 let mut start_byte: Option<u64> = None;
3089 let mut end_byte: Option<u64> = None;
3090 if let Some(init) = &sl.Initialization {
3091 if let Some(range) = &init.range {
3092 let (s, e) = parse_range(range)?;
3093 start_byte = Some(s);
3094 end_byte = Some(e);
3095 }
3096 if let Some(su) = &init.sourceURL {
3097 let path = resolve_url_template(su, &dict);
3098 let u = merge_baseurls(&base_url, &path)?;
3099 let mf = MediaFragmentBuilder::new(period_counter, u)
3100 .with_range(start_byte, end_byte)
3101 .set_init()
3102 .build();
3103 fragments.push(mf);
3104 } else {
3105 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3106 .with_range(start_byte, end_byte)
3107 .set_init()
3108 .build();
3109 fragments.push(mf);
3110 }
3111 }
3112 for su in sl.segment_urls.iter() {
3113 start_byte = None;
3114 end_byte = None;
3115 if let Some(range) = &su.mediaRange {
3117 let (s, e) = parse_range(range)?;
3118 start_byte = Some(s);
3119 end_byte = Some(e);
3120 }
3121 if let Some(m) = &su.media {
3122 let u = merge_baseurls(&base_url, m)?;
3123 let mf = MediaFragmentBuilder::new(period_counter, u)
3124 .with_range(start_byte, end_byte)
3125 .build();
3126 fragments.push(mf);
3127 } else if let Some(bu) = &rep.BaseURL.first() {
3128 let u = merge_baseurls(&base_url, &bu.base)?;
3129 let mf = MediaFragmentBuilder::new(period_counter, u)
3130 .with_range(start_byte, end_byte)
3131 .build();
3132 fragments.push(mf);
3133 };
3134 }
3135 } else if rep.SegmentTemplate.is_some() ||
3136 subtitle_adaptation.SegmentTemplate.is_some()
3137 {
3138 let st;
3141 if let Some(it) = &rep.SegmentTemplate {
3142 st = it;
3143 } else if let Some(it) = &subtitle_adaptation.SegmentTemplate {
3144 st = it;
3145 } else {
3146 panic!("unreachable");
3147 }
3148 if let Some(i) = &st.initialization {
3149 opt_init = Some(i.to_string());
3150 }
3151 if let Some(m) = &st.media {
3152 opt_media = Some(m.to_string());
3153 }
3154 if let Some(ts) = st.timescale {
3155 timescale = ts;
3156 }
3157 if let Some(sn) = st.startNumber {
3158 start_number = sn;
3159 }
3160 if let Some(stl) = &rep.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
3161 .or(subtitle_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
3162 {
3163 if downloader.verbosity > 1 {
3166 info!(" {}", "Using SegmentTemplate+SegmentTimeline addressing mode for subtitle representation".italic());
3167 }
3168 if let Some(init) = opt_init {
3169 let path = resolve_url_template(&init, &dict);
3170 let u = merge_baseurls(&base_url, &path)?;
3171 let mf = MediaFragmentBuilder::new(period_counter, u)
3172 .set_init()
3173 .build();
3174 fragments.push(mf);
3175 }
3176 if let Some(media) = opt_media {
3177 let sub_path = resolve_url_template(&media, &dict);
3178 let mut segment_time = 0;
3179 let mut segment_duration;
3180 let mut number = start_number;
3181 for s in &stl.segments {
3182 if let Some(t) = s.t {
3183 segment_time = t;
3184 }
3185 segment_duration = s.d;
3186 let dict = HashMap::from([("Time", segment_time.to_string()),
3188 ("Number", number.to_string())]);
3189 let path = resolve_url_template(&sub_path, &dict);
3190 let u = merge_baseurls(&base_url, &path)?;
3191 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3192 fragments.push(mf);
3193 number += 1;
3194 if let Some(r) = s.r {
3195 let mut count = 0i64;
3196 let end_time = period_duration_secs * timescale as f64;
3198 loop {
3199 count += 1;
3200 if r >= 0 {
3206 if count > r {
3207 break;
3208 }
3209 if downloader.force_duration.is_some() &&
3210 segment_time as f64 > end_time
3211 {
3212 break;
3213 }
3214 } else if segment_time as f64 > end_time {
3215 break;
3216 }
3217 segment_time += segment_duration;
3218 let dict = HashMap::from([("Time", segment_time.to_string()),
3219 ("Number", number.to_string())]);
3220 let path = resolve_url_template(&sub_path, &dict);
3221 let u = merge_baseurls(&base_url, &path)?;
3222 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3223 fragments.push(mf);
3224 number += 1;
3225 }
3226 }
3227 segment_time += segment_duration;
3228 }
3229 } else {
3230 return Err(DashMpdError::UnhandledMediaStream(
3231 "SegmentTimeline without a media attribute".to_string()));
3232 }
3233 } else { if downloader.verbosity > 0 {
3238 info!(" {}", "Using SegmentTemplate addressing mode for stpp subtitles".italic());
3239 }
3240 if let Some(i) = &st.initialization {
3241 opt_init = Some(i.to_string());
3242 }
3243 if let Some(m) = &st.media {
3244 opt_media = Some(m.to_string());
3245 }
3246 if let Some(d) = st.duration {
3247 opt_duration = Some(d);
3248 }
3249 if let Some(ts) = st.timescale {
3250 timescale = ts;
3251 }
3252 if let Some(s) = st.startNumber {
3253 start_number = s;
3254 }
3255 let rid = match &rep.id {
3256 Some(id) => id,
3257 None => return Err(
3258 DashMpdError::UnhandledMediaStream(
3259 "Missing @id on Representation node".to_string())),
3260 };
3261 let mut dict = HashMap::from([("RepresentationID", rid.to_string())]);
3262 if let Some(b) = &rep.bandwidth {
3263 dict.insert("Bandwidth", b.to_string());
3264 }
3265 let mut total_number = 0i64;
3266 if let Some(init) = opt_init {
3267 let path = resolve_url_template(&init, &dict);
3268 let u = merge_baseurls(&base_url, &path)?;
3269 let mf = MediaFragmentBuilder::new(period_counter, u)
3270 .set_init()
3271 .build();
3272 fragments.push(mf);
3273 }
3274 if let Some(media) = opt_media {
3275 let sub_path = resolve_url_template(&media, &dict);
3276 let mut segment_duration: f64 = -1.0;
3277 if let Some(d) = opt_duration {
3278 segment_duration = d;
3280 }
3281 if let Some(std) = st.duration {
3282 segment_duration = std / timescale as f64;
3283 }
3284 if segment_duration < 0.0 {
3285 return Err(DashMpdError::UnhandledMediaStream(
3286 "Subtitle representation is missing SegmentTemplate@duration".to_string()));
3287 }
3288 total_number += (period_duration_secs / segment_duration).ceil() as i64;
3289 let mut number = start_number;
3290 for _ in 1..=total_number {
3291 let dict = HashMap::from([("Number", number.to_string())]);
3292 let path = resolve_url_template(&sub_path, &dict);
3293 let u = merge_baseurls(&base_url, &path)?;
3294 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3295 fragments.push(mf);
3296 number += 1;
3297 }
3298 }
3299 }
3300 } else if let Some(sb) = &rep.SegmentBase {
3301 info!(" Using SegmentBase@indexRange for subs");
3303 if downloader.verbosity > 1 {
3304 info!(" {}", "Using SegmentBase@indexRange addressing mode for subtitle representation".italic());
3305 }
3306 let mut start_byte: Option<u64> = None;
3307 let mut end_byte: Option<u64> = None;
3308 if let Some(init) = &sb.Initialization {
3309 if let Some(range) = &init.range {
3310 let (s, e) = parse_range(range)?;
3311 start_byte = Some(s);
3312 end_byte = Some(e);
3313 }
3314 if let Some(su) = &init.sourceURL {
3315 let path = resolve_url_template(su, &dict);
3316 let u = merge_baseurls(&base_url, &path)?;
3317 let mf = MediaFragmentBuilder::new(period_counter, u)
3318 .with_range(start_byte, end_byte)
3319 .set_init()
3320 .build();
3321 fragments.push(mf);
3322 }
3323 }
3324 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3325 .set_init()
3326 .build();
3327 fragments.push(mf);
3328 }
3331 }
3332 }
3333 }
3334 }
3335 Ok(PeriodOutputs { fragments, diagnostics: Vec::new(), subtitle_formats })
3336}
3337
3338
3339struct DownloadState {
3342 period_counter: u8,
3343 segment_count: usize,
3344 segment_counter: usize,
3345 download_errors: u32
3346}
3347
3348#[tracing::instrument(level="trace", skip_all)]
3355async fn fetch_fragment(
3356 downloader: &mut DashDownloader,
3357 frag: &MediaFragment,
3358 fragment_type: &str,
3359 progress_percent: u32) -> Result<std::fs::File, DashMpdError>
3360{
3361 let send_request = || async {
3362 trace!("send_request {}", frag.url.clone());
3363 let mut req = downloader.http_client.as_ref().unwrap()
3366 .get(frag.url.clone())
3367 .header("Accept", format!("{fragment_type}/*;q=0.9,*/*;q=0.5"))
3368 .header("Sec-Fetch-Mode", "navigate");
3369 if let Some(sb) = &frag.start_byte {
3370 if let Some(eb) = &frag.end_byte {
3371 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
3372 }
3373 }
3374 if let Some(ts) = &frag.timeout {
3375 req = req.timeout(*ts);
3376 }
3377 if let Some(referer) = &downloader.referer {
3378 req = req.header("Referer", referer);
3379 } else {
3380 req = req.header("Referer", downloader.redirected_url.to_string());
3381 }
3382 if let Some(username) = &downloader.auth_username {
3383 if let Some(password) = &downloader.auth_password {
3384 req = req.basic_auth(username, Some(password));
3385 }
3386 }
3387 if let Some(token) = &downloader.auth_bearer_token {
3388 req = req.bearer_auth(token);
3389 }
3390 req.send().await
3391 .map_err(categorize_reqwest_error)?
3392 .error_for_status()
3393 .map_err(categorize_reqwest_error)
3394 };
3395 match retry_notify(ExponentialBackoff::default(), send_request, notify_transient).await {
3396 Ok(response) => {
3397 match response.error_for_status() {
3398 Ok(mut resp) => {
3399 let mut tmp_out = tempfile::tempfile()
3400 .map_err(|e| DashMpdError::Io(e, String::from("creating tmpfile for fragment")))?;
3401 let content_type_checker = if fragment_type.eq("audio") {
3402 content_type_audio_p
3403 } else if fragment_type.eq("video") {
3404 content_type_video_p
3405 } else {
3406 panic!("fragment_type not audio or video");
3407 };
3408 if !downloader.content_type_checks || content_type_checker(&resp) {
3409 let mut fragment_out: Option<File> = None;
3410 if let Some(ref fragment_path) = downloader.fragment_path {
3411 if let Some(path) = frag.url.path_segments()
3412 .unwrap_or_else(|| "".split(' '))
3413 .next_back()
3414 {
3415 let vf_file = fragment_path.clone().join(fragment_type).join(path);
3416 if let Ok(f) = File::create(vf_file) {
3417 fragment_out = Some(f)
3418 }
3419 }
3420 }
3421 let mut segment_size = 0;
3422 while let Some(chunk) = resp.chunk().await
3428 .map_err(|e| network_error(&format!("fetching DASH {fragment_type} segment"), e))?
3429 {
3430 segment_size += chunk.len();
3431 downloader.bw_estimator_bytes += chunk.len();
3432 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
3433 throttle_download_rate(downloader, size).await?;
3434 if let Err(e) = tmp_out.write_all(&chunk) {
3435 return Err(DashMpdError::Io(e, format!("writing DASH {fragment_type} data")));
3436 }
3437 if let Some(ref mut fout) = fragment_out {
3438 fout.write_all(&chunk)
3439 .map_err(|e| DashMpdError::Io(e, format!("writing {fragment_type} fragment")))?;
3440 }
3441 let elapsed = downloader.bw_estimator_started.elapsed().as_secs_f64();
3442 if (elapsed > 1.5) || (downloader.bw_estimator_bytes > 100_000) {
3443 let bw = downloader.bw_estimator_bytes as f64 / (1e6 * elapsed);
3444 let msg = if bw > 0.5 {
3445 format!("Fetching {fragment_type} segments ({bw:.1} MB/s)")
3446 } else {
3447 let kbs = (bw * 1000.0).round() as u64;
3448 format!("Fetching {fragment_type} segments ({kbs:3} kB/s)")
3449 };
3450 for observer in &downloader.progress_observers {
3451 observer.update(progress_percent, &msg);
3452 }
3453 downloader.bw_estimator_started = Instant::now();
3454 downloader.bw_estimator_bytes = 0;
3455 }
3456 }
3457 if downloader.verbosity > 2 {
3458 if let Some(sb) = &frag.start_byte {
3459 if let Some(eb) = &frag.end_byte {
3460 info!(" {fragment_type} segment {} range {sb}-{eb} -> {} octets",
3461 frag.url, segment_size);
3462 }
3463 } else {
3464 info!(" {fragment_type} segment {} -> {segment_size} octets", &frag.url);
3465 }
3466 }
3467 } else {
3468 warn!("{} {} with non-{fragment_type} content-type", "Ignoring segment".red(), frag.url);
3469 };
3470 tmp_out.sync_all()
3471 .map_err(|e| DashMpdError::Io(e, format!("syncing {fragment_type} fragment")))?;
3472 Ok(tmp_out)
3473 },
3474 Err(e) => Err(network_error("HTTP error", e)),
3475 }
3476 },
3477 Err(e) => Err(network_error(&format!("{e:?}"), e)),
3478 }
3479}
3480
3481
3482#[tracing::instrument(level="trace", skip_all)]
3484async fn fetch_period_audio(
3485 downloader: &mut DashDownloader,
3486 tmppath: PathBuf,
3487 audio_fragments: &[MediaFragment],
3488 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3489{
3490 let start_download = Instant::now();
3491 let mut have_audio = false;
3492 {
3493 let tmpfile_audio = File::create(tmppath.clone())
3497 .map_err(|e| DashMpdError::Io(e, String::from("creating audio tmpfile")))?;
3498 let mut tmpfile_audio = BufWriter::new(tmpfile_audio);
3499 if let Some(ref fragment_path) = downloader.fragment_path {
3501 let audio_fragment_dir = fragment_path.join("audio");
3502 if !audio_fragment_dir.exists() {
3503 fs::create_dir_all(audio_fragment_dir)
3504 .map_err(|e| DashMpdError::Io(e, String::from("creating audio fragment dir")))?;
3505 }
3506 }
3507 for frag in audio_fragments.iter().filter(|f| f.period == ds.period_counter) {
3511 ds.segment_counter += 1;
3512 let progress_percent = (100.0 * ds.segment_counter as f32 / (2.0 + ds.segment_count as f32)).ceil() as u32;
3513 let url = &frag.url;
3514 if url.scheme() == "data" {
3518 let us = &url.to_string();
3519 let du = DataUrl::process(us)
3520 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
3521 if du.mime_type().type_ != "audio" {
3522 return Err(DashMpdError::UnhandledMediaStream(
3523 String::from("expecting audio content in data URL")));
3524 }
3525 let (body, _fragment) = du.decode_to_vec()
3526 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
3527 if downloader.verbosity > 2 {
3528 info!(" Audio segment data URL -> {} octets", body.len());
3529 }
3530 if let Err(e) = tmpfile_audio.write_all(&body) {
3531 error!("Unable to write DASH audio data: {e:?}");
3532 return Err(DashMpdError::Io(e, String::from("writing DASH audio data")));
3533 }
3534 have_audio = true;
3535 } else {
3536 'done: for _ in 0..downloader.fragment_retry_count {
3538 match fetch_fragment(downloader, frag, "audio", progress_percent).await {
3539 Ok(mut frag_file) => {
3540 frag_file.rewind()
3541 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
3542 let mut buf = Vec::new();
3543 frag_file.read_to_end(&mut buf)
3544 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
3545 if let Err(e) = tmpfile_audio.write_all(&buf) {
3546 error!("Unable to write DASH audio data: {e:?}");
3547 return Err(DashMpdError::Io(e, String::from("writing DASH audio data")));
3548 }
3549 have_audio = true;
3550 break 'done;
3551 },
3552 Err(e) => {
3553 if downloader.verbosity > 0 {
3554 error!("Error fetching audio segment {url}: {e:?}");
3555 }
3556 ds.download_errors += 1;
3557 if ds.download_errors > downloader.max_error_count {
3558 error!("max_error_count network errors encountered");
3559 return Err(DashMpdError::Network(
3560 String::from("more than max_error_count network errors")));
3561 }
3562 },
3563 }
3564 info!(" Retrying audio segment {url}");
3565 if downloader.sleep_between_requests > 0 {
3566 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
3567 }
3568 }
3569 }
3570 }
3571 tmpfile_audio.flush().map_err(|e| {
3572 error!("Couldn't flush DASH audio file: {e}");
3573 DashMpdError::Io(e, String::from("flushing DASH audio file"))
3574 })?;
3575 } if !downloader.decryption_keys.is_empty() {
3577 if downloader.verbosity > 0 {
3578 let metadata = fs::metadata(tmppath.clone())
3579 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted audio metadata")))?;
3580 info!(" Attempting to decrypt audio stream ({} kB) with {}",
3581 metadata.len() / 1024,
3582 downloader.decryptor_preference);
3583 }
3584 let out_ext = downloader.output_path.as_ref().unwrap()
3585 .extension()
3586 .unwrap_or(OsStr::new("mp4"));
3587 let decrypted = tmp_file_path("dashmpd-decrypted-audio", out_ext)?;
3588 if downloader.decryptor_preference.eq("mp4decrypt") {
3589 let mut args = Vec::new();
3590 for (k, v) in downloader.decryption_keys.iter() {
3591 args.push("--key".to_string());
3592 args.push(format!("{k}:{v}"));
3593 }
3594 args.push(String::from(tmppath.to_string_lossy()));
3595 args.push(String::from(decrypted.to_string_lossy()));
3596 if downloader.verbosity > 1 {
3597 info!(" Running mp4decrypt {}", args.join(" "));
3598 }
3599 let out = Command::new(downloader.mp4decrypt_location.clone())
3600 .args(args)
3601 .output()
3602 .map_err(|e| DashMpdError::Io(e, String::from("spawning mp4decrypt")))?;
3603 let mut no_output = true;
3604 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3605 if downloader.verbosity > 0 {
3606 info!(" Decrypted audio stream of size {} kB.", metadata.len() / 1024);
3607 }
3608 no_output = false;
3609 }
3610 if !out.status.success() || no_output {
3611 warn!(" mp4decrypt subprocess failed");
3612 let msg = partial_process_output(&out.stdout);
3613 if !msg.is_empty() {
3614 warn!(" mp4decrypt stdout: {msg}");
3615 }
3616 let msg = partial_process_output(&out.stderr);
3617 if !msg.is_empty() {
3618 warn!(" mp4decrypt stderr: {msg}");
3619 }
3620 }
3621 if no_output {
3622 error!("{}", "Failed to decrypt audio stream with mp4decrypt".red());
3623 warn!(" Undecrypted audio left in {}", tmppath.display());
3624 return Err(DashMpdError::Decrypting(String::from("audio stream")));
3625 }
3626 } else if downloader.decryptor_preference.eq("shaka") {
3627 let mut args = Vec::new();
3628 let mut keys = Vec::new();
3629 if downloader.verbosity < 1 {
3630 args.push("--quiet".to_string());
3631 }
3632 args.push(format!("in={},stream=audio,output={}", tmppath.display(), decrypted.display()));
3633 let mut drm_label = 0;
3634 #[allow(clippy::explicit_counter_loop)]
3635 for (k, v) in downloader.decryption_keys.iter() {
3636 keys.push(format!("label=lbl{drm_label}:key_id={k}:key={v}"));
3637 drm_label += 1;
3638 }
3639 args.push("--enable_raw_key_decryption".to_string());
3640 args.push("--keys".to_string());
3641 args.push(keys.join(","));
3642 if downloader.verbosity > 1 {
3643 info!(" Running shaka-packager {}", args.join(" "));
3644 }
3645 let out = Command::new(downloader.shaka_packager_location.clone())
3646 .args(args)
3647 .output()
3648 .map_err(|e| DashMpdError::Io(e, String::from("spawning shaka-packager")))?;
3649 let mut no_output = false;
3650 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3651 if downloader.verbosity > 0 {
3652 info!(" Decrypted audio stream of size {} kB.", metadata.len() / 1024);
3653 }
3654 if metadata.len() == 0 {
3655 no_output = true;
3656 }
3657 } else {
3658 no_output = true;
3659 }
3660 if !out.status.success() || no_output {
3661 warn!(" shaka-packager subprocess failed");
3662 let msg = partial_process_output(&out.stdout);
3663 if !msg.is_empty() {
3664 warn!(" shaka-packager stdout: {msg}");
3665 }
3666 let msg = partial_process_output(&out.stderr);
3667 if !msg.is_empty() {
3668 warn!(" shaka-packager stderr: {msg}");
3669 }
3670 }
3671 if no_output {
3672 error!(" {}", "Failed to decrypt audio stream with shaka-packager".red());
3673 warn!(" Undecrypted audio stream left in {}", tmppath.display());
3674 return Err(DashMpdError::Decrypting(String::from("audio stream")));
3675 }
3676 } else if downloader.decryptor_preference.eq("mp4box") {
3679 let mut args = Vec::new();
3680 let drmfile = tmp_file_path("mp4boxcrypt", OsStr::new("xml"))?;
3681 let mut drmfile_contents = String::from("<GPACDRM>\n <CrypTrack>\n");
3682 for (k, v) in downloader.decryption_keys.iter() {
3683 drmfile_contents += &format!(" <key KID=\"0x{k}\" value=\"0x{v}\"/>\n");
3684 }
3685 drmfile_contents += " </CrypTrack>\n</GPACDRM>\n";
3686 fs::write(&drmfile, drmfile_contents)
3687 .map_err(|e| DashMpdError::Io(e, String::from("writing to MP4Box decrypt file")))?;
3688 args.push("-decrypt".to_string());
3689 args.push(drmfile.display().to_string());
3690 args.push(String::from(tmppath.to_string_lossy()));
3691 args.push("-out".to_string());
3692 args.push(String::from(decrypted.to_string_lossy()));
3693 if downloader.verbosity > 1 {
3694 info!(" Running decryption application MP4Box {}", args.join(" "));
3695 }
3696 let out = Command::new(downloader.mp4box_location.clone())
3697 .args(args)
3698 .output()
3699 .map_err(|e| DashMpdError::Io(e, String::from("spawning MP4Box")))?;
3700 let mut no_output = false;
3701 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3702 if downloader.verbosity > 0 {
3703 info!(" Decrypted audio stream of size {} kB.", metadata.len() / 1024);
3704 }
3705 if metadata.len() == 0 {
3706 no_output = true;
3707 }
3708 } else {
3709 no_output = true;
3710 }
3711 if !out.status.success() || no_output {
3712 warn!(" MP4Box decryption subprocess failed");
3713 let msg = partial_process_output(&out.stdout);
3714 if !msg.is_empty() {
3715 warn!(" MP4Box stdout: {msg}");
3716 }
3717 let msg = partial_process_output(&out.stderr);
3718 if !msg.is_empty() {
3719 warn!(" MP4Box stderr: {msg}");
3720 }
3721 }
3722 if no_output {
3723 error!(" {}", "Failed to decrypt audio stream with MP4Box".red());
3724 warn!(" Undecrypted audio stream left in {}", tmppath.display());
3725 return Err(DashMpdError::Decrypting(String::from("audio stream")));
3726 }
3727 } else {
3728 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
3729 }
3730 fs::rename(decrypted, tmppath.clone())
3731 .map_err(|e| DashMpdError::Io(e, String::from("renaming decrypted audio")))?;
3732 }
3733 if let Ok(metadata) = fs::metadata(tmppath.clone()) {
3734 if downloader.verbosity > 1 {
3735 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
3736 let elapsed = start_download.elapsed();
3737 info!(" Wrote {mbytes:.1}MB to DASH audio file ({:.1} MB/s)",
3738 mbytes / elapsed.as_secs_f64());
3739 }
3740 }
3741 Ok(have_audio)
3742}
3743
3744
3745#[tracing::instrument(level="trace", skip_all)]
3747async fn fetch_period_video(
3748 downloader: &mut DashDownloader,
3749 tmppath: PathBuf,
3750 video_fragments: &[MediaFragment],
3751 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3752{
3753 let start_download = Instant::now();
3754 let mut have_video = false;
3755 {
3756 let tmpfile_video = File::create(tmppath.clone())
3759 .map_err(|e| DashMpdError::Io(e, String::from("creating video tmpfile")))?;
3760 let mut tmpfile_video = BufWriter::new(tmpfile_video);
3761 if let Some(ref fragment_path) = downloader.fragment_path {
3763 let video_fragment_dir = fragment_path.join("video");
3764 if !video_fragment_dir.exists() {
3765 fs::create_dir_all(video_fragment_dir)
3766 .map_err(|e| DashMpdError::Io(e, String::from("creating video fragment dir")))?;
3767 }
3768 }
3769 for frag in video_fragments.iter().filter(|f| f.period == ds.period_counter) {
3770 ds.segment_counter += 1;
3771 let progress_percent = (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32;
3772 if frag.url.scheme() == "data" {
3773 let us = &frag.url.to_string();
3774 let du = DataUrl::process(us)
3775 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
3776 if du.mime_type().type_ != "video" {
3777 return Err(DashMpdError::UnhandledMediaStream(
3778 String::from("expecting video content in data URL")));
3779 }
3780 let (body, _fragment) = du.decode_to_vec()
3781 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
3782 if downloader.verbosity > 2 {
3783 info!(" Video segment data URL -> {} octets", body.len());
3784 }
3785 if let Err(e) = tmpfile_video.write_all(&body) {
3786 error!("Unable to write DASH video data: {e:?}");
3787 return Err(DashMpdError::Io(e, String::from("writing DASH video data")));
3788 }
3789 have_video = true;
3790 } else {
3791 'done: for _ in 0..downloader.fragment_retry_count {
3792 match fetch_fragment(downloader, frag, "video", progress_percent).await {
3793 Ok(mut frag_file) => {
3794 frag_file.rewind()
3795 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
3796 let mut buf = Vec::new();
3797 frag_file.read_to_end(&mut buf)
3798 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
3799 if let Err(e) = tmpfile_video.write_all(&buf) {
3800 error!("Unable to write DASH video data: {e:?}");
3801 return Err(DashMpdError::Io(e, String::from("writing DASH video data")));
3802 }
3803 have_video = true;
3804 break 'done;
3805 },
3806 Err(e) => {
3807 if downloader.verbosity > 0 {
3808 error!(" Error fetching video segment {}: {e:?}", frag.url);
3809 }
3810 ds.download_errors += 1;
3811 if ds.download_errors > downloader.max_error_count {
3812 return Err(DashMpdError::Network(
3813 String::from("more than max_error_count network errors")));
3814 }
3815 },
3816 }
3817 info!(" Retrying video segment {}", frag.url);
3818 if downloader.sleep_between_requests > 0 {
3819 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
3820 }
3821 }
3822 }
3823 }
3824 tmpfile_video.flush().map_err(|e| {
3825 error!(" Couldn't flush video file: {e}");
3826 DashMpdError::Io(e, String::from("flushing video file"))
3827 })?;
3828 } if !downloader.decryption_keys.is_empty() {
3830 if downloader.verbosity > 0 {
3831 let metadata = fs::metadata(tmppath.clone())
3832 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted video metadata")))?;
3833 info!(" Attempting to decrypt video stream ({} kB) with {}",
3834 metadata.len() / 1024,
3835 downloader.decryptor_preference);
3836 }
3837 let out_ext = downloader.output_path.as_ref().unwrap()
3838 .extension()
3839 .unwrap_or(OsStr::new("mp4"));
3840 let decrypted = tmp_file_path("dashmpd-decrypted-video", out_ext)?;
3841 if downloader.decryptor_preference.eq("mp4decrypt") {
3842 let mut args = Vec::new();
3843 for (k, v) in downloader.decryption_keys.iter() {
3844 args.push("--key".to_string());
3845 args.push(format!("{k}:{v}"));
3846 }
3847 args.push(tmppath.to_string_lossy().to_string());
3848 args.push(decrypted.to_string_lossy().to_string());
3849 if downloader.verbosity > 1 {
3850 info!(" Running mp4decrypt {}", args.join(" "));
3851 }
3852 let out = Command::new(downloader.mp4decrypt_location.clone())
3853 .args(args)
3854 .output()
3855 .map_err(|e| DashMpdError::Io(e, String::from("spawning mp4decrypt")))?;
3856 let mut no_output = false;
3857 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3858 if downloader.verbosity > 0 {
3859 info!(" Decrypted video stream of size {} kB.", metadata.len() / 1024);
3860 }
3861 if metadata.len() == 0 {
3862 no_output = true;
3863 }
3864 } else {
3865 no_output = true;
3866 }
3867 if !out.status.success() || no_output {
3868 error!(" mp4decrypt subprocess failed");
3869 let msg = partial_process_output(&out.stdout);
3870 if !msg.is_empty() {
3871 warn!(" mp4decrypt stdout: {msg}");
3872 }
3873 let msg = partial_process_output(&out.stderr);
3874 if !msg.is_empty() {
3875 warn!(" mp4decrypt stderr: {msg}");
3876 }
3877 }
3878 if no_output {
3879 error!(" {}", "Failed to decrypt video stream with mp4decrypt".red());
3880 warn!(" Undecrypted video stream left in {}", tmppath.display());
3881 return Err(DashMpdError::Decrypting(String::from("video stream")));
3882 }
3883 } else if downloader.decryptor_preference.eq("shaka") {
3884 let mut args = Vec::new();
3885 let mut keys = Vec::new();
3886 if downloader.verbosity < 1 {
3887 args.push("--quiet".to_string());
3888 }
3889 args.push(format!("in={},stream=video,output={}", tmppath.display(), decrypted.display()));
3890 let mut drm_label = 0;
3891 #[allow(clippy::explicit_counter_loop)]
3892 for (k, v) in downloader.decryption_keys.iter() {
3893 keys.push(format!("label=lbl{drm_label}:key_id={k}:key={v}"));
3894 drm_label += 1;
3895 }
3896 args.push("--enable_raw_key_decryption".to_string());
3897 args.push("--keys".to_string());
3898 args.push(keys.join(","));
3899 if downloader.verbosity > 1 {
3900 info!(" Running shaka-packager {}", args.join(" "));
3901 }
3902 let out = Command::new(downloader.shaka_packager_location.clone())
3903 .args(args)
3904 .output()
3905 .map_err(|e| DashMpdError::Io(e, String::from("spawning shaka-packager")))?;
3906 let mut no_output = true;
3907 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3908 if downloader.verbosity > 0 {
3909 info!(" Decrypted video stream of size {} kB.", metadata.len() / 1024);
3910 }
3911 no_output = false;
3912 }
3913 if !out.status.success() || no_output {
3914 warn!(" shaka-packager subprocess failed");
3915 let msg = partial_process_output(&out.stdout);
3916 if !msg.is_empty() {
3917 warn!(" shaka-packager stdout: {msg}");
3918 }
3919 let msg = partial_process_output(&out.stderr);
3920 if !msg.is_empty() {
3921 warn!(" shaka-packager stderr: {msg}");
3922 }
3923 }
3924 if no_output {
3925 error!(" {}", "Failed to decrypt video stream with shaka-packager".red());
3926 warn!(" Undecrypted video left in {}", tmppath.display());
3927 return Err(DashMpdError::Decrypting(String::from("video stream")));
3928 }
3929 } else if downloader.decryptor_preference.eq("mp4box") {
3930 let mut args = Vec::new();
3931 let drmfile = tmp_file_path("mp4boxcrypt", OsStr::new("xml"))?;
3932 let mut drmfile_contents = String::from("<GPACDRM>\n <CrypTrack>\n");
3933 for (k, v) in downloader.decryption_keys.iter() {
3934 drmfile_contents += &format!(" <key KID=\"0x{k}\" value=\"0x{v}\"/>\n");
3935 }
3936 drmfile_contents += " </CrypTrack>\n</GPACDRM>\n";
3937 fs::write(&drmfile, drmfile_contents)
3938 .map_err(|e| DashMpdError::Io(e, String::from("writing to MP4Box decrypt file")))?;
3939 args.push("-decrypt".to_string());
3940 args.push(drmfile.display().to_string());
3941 args.push(String::from(tmppath.to_string_lossy()));
3942 args.push("-out".to_string());
3943 args.push(String::from(decrypted.to_string_lossy()));
3944 if downloader.verbosity > 1 {
3945 info!(" Running decryption application MP4Box {}", args.join(" "));
3946 }
3947 let out = Command::new(downloader.mp4box_location.clone())
3948 .args(args)
3949 .output()
3950 .map_err(|e| DashMpdError::Io(e, String::from("spawning MP4Box")))?;
3951 let mut no_output = false;
3952 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3953 if downloader.verbosity > 0 {
3954 info!(" Decrypted video stream of size {} kB.", metadata.len() / 1024);
3955 }
3956 if metadata.len() == 0 {
3957 no_output = true;
3958 }
3959 } else {
3960 no_output = true;
3961 }
3962 if !out.status.success() || no_output {
3963 warn!(" MP4Box decryption subprocess failed");
3964 let msg = partial_process_output(&out.stdout);
3965 if !msg.is_empty() {
3966 warn!(" MP4Box stdout: {msg}");
3967 }
3968 let msg = partial_process_output(&out.stderr);
3969 if !msg.is_empty() {
3970 warn!(" MP4Box stderr: {msg}");
3971 }
3972 }
3973 if no_output {
3974 error!(" {}", "Failed to decrypt video stream with MP4Box".red());
3975 warn!(" Undecrypted video stream left in {}", tmppath.display());
3976 return Err(DashMpdError::Decrypting(String::from("video stream")));
3977 }
3978 } else {
3979 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
3980 }
3981 fs::rename(decrypted, tmppath.clone())
3982 .map_err(|e| DashMpdError::Io(e, String::from("renaming decrypted video")))?;
3983 }
3984 if let Ok(metadata) = fs::metadata(tmppath.clone()) {
3985 if downloader.verbosity > 1 {
3986 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
3987 let elapsed = start_download.elapsed();
3988 info!(" Wrote {mbytes:.1}MB to DASH video file ({:.1} MB/s)",
3989 mbytes / elapsed.as_secs_f64());
3990 }
3991 }
3992 Ok(have_video)
3993}
3994
3995
3996#[tracing::instrument(level="trace", skip_all)]
3998async fn fetch_period_subtitles(
3999 downloader: &DashDownloader,
4000 tmppath: PathBuf,
4001 subtitle_fragments: &[MediaFragment],
4002 subtitle_formats: &[SubtitleType],
4003 ds: &mut DownloadState) -> Result<bool, DashMpdError>
4004{
4005 let client = downloader.http_client.clone().unwrap();
4006 let start_download = Instant::now();
4007 let mut have_subtitles = false;
4008 {
4009 let tmpfile_subs = File::create(tmppath.clone())
4010 .map_err(|e| DashMpdError::Io(e, String::from("creating subs tmpfile")))?;
4011 let mut tmpfile_subs = BufWriter::new(tmpfile_subs);
4012 for frag in subtitle_fragments {
4013 ds.segment_counter += 1;
4015 let progress_percent = (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32;
4016 for observer in &downloader.progress_observers {
4017 observer.update(progress_percent, "Fetching subtitle segments");
4018 }
4019 if frag.url.scheme() == "data" {
4020 let us = &frag.url.to_string();
4021 let du = DataUrl::process(us)
4022 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
4023 if du.mime_type().type_ != "video" {
4024 return Err(DashMpdError::UnhandledMediaStream(
4025 String::from("expecting video content in data URL")));
4026 }
4027 let (body, _fragment) = du.decode_to_vec()
4028 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
4029 if downloader.verbosity > 2 {
4030 info!(" Subtitle segment data URL -> {} octets", body.len());
4031 }
4032 if let Err(e) = tmpfile_subs.write_all(&body) {
4033 error!("Unable to write DASH subtitle data: {e:?}");
4034 return Err(DashMpdError::Io(e, String::from("writing DASH subtitle data")));
4035 }
4036 have_subtitles = true;
4037 } else {
4038 let fetch = || async {
4039 let mut req = client.get(frag.url.clone())
4040 .header("Sec-Fetch-Mode", "navigate");
4041 if let Some(sb) = &frag.start_byte {
4042 if let Some(eb) = &frag.end_byte {
4043 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
4044 }
4045 }
4046 if let Some(referer) = &downloader.referer {
4047 req = req.header("Referer", referer);
4048 } else {
4049 req = req.header("Referer", downloader.redirected_url.to_string());
4050 }
4051 if let Some(username) = &downloader.auth_username {
4052 if let Some(password) = &downloader.auth_password {
4053 req = req.basic_auth(username, Some(password));
4054 }
4055 }
4056 if let Some(token) = &downloader.auth_bearer_token {
4057 req = req.bearer_auth(token);
4058 }
4059 req.send().await
4060 .map_err(categorize_reqwest_error)?
4061 .error_for_status()
4062 .map_err(categorize_reqwest_error)
4063 };
4064 let mut failure = None;
4065 match retry_notify(ExponentialBackoff::default(), fetch, notify_transient).await {
4066 Ok(response) => {
4067 if response.status().is_success() {
4068 let dash_bytes = response.bytes().await
4069 .map_err(|e| network_error("fetching DASH subtitle segment", e))?;
4070 if downloader.verbosity > 2 {
4071 if let Some(sb) = &frag.start_byte {
4072 if let Some(eb) = &frag.end_byte {
4073 info!(" Subtitle segment {} range {sb}-{eb} -> {} octets",
4074 &frag.url, dash_bytes.len());
4075 }
4076 } else {
4077 info!(" Subtitle segment {} -> {} octets", &frag.url, dash_bytes.len());
4078 }
4079 }
4080 let size = min((dash_bytes.len()/1024 + 1) as u32, u32::MAX);
4081 throttle_download_rate(downloader, size).await?;
4082 if let Err(e) = tmpfile_subs.write_all(&dash_bytes) {
4083 return Err(DashMpdError::Io(e, String::from("writing DASH subtitle data")));
4084 }
4085 have_subtitles = true;
4086 } else {
4087 failure = Some(format!("HTTP error {}", response.status().as_str()));
4088 }
4089 },
4090 Err(e) => failure = Some(format!("{e}")),
4091 }
4092 if let Some(f) = failure {
4093 if downloader.verbosity > 0 {
4094 error!("{} fetching subtitle segment {}", f.red(), &frag.url);
4095 }
4096 ds.download_errors += 1;
4097 if ds.download_errors > downloader.max_error_count {
4098 return Err(DashMpdError::Network(
4099 String::from("more than max_error_count network errors")));
4100 }
4101 }
4102 }
4103 if downloader.sleep_between_requests > 0 {
4104 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
4105 }
4106 }
4107 tmpfile_subs.flush().map_err(|e| {
4108 error!("Couldn't flush subs file: {e}");
4109 DashMpdError::Io(e, String::from("flushing subtitle file"))
4110 })?;
4111 } if have_subtitles {
4113 if let Ok(metadata) = fs::metadata(tmppath.clone()) {
4114 if downloader.verbosity > 1 {
4115 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4116 let elapsed = start_download.elapsed();
4117 info!(" Wrote {mbytes:.1}MB to DASH subtitle file ({:.1} MB/s)",
4118 mbytes / elapsed.as_secs_f64());
4119 }
4120 }
4121 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
4124 subtitle_formats.contains(&SubtitleType::Ttxt)
4125 {
4126 if downloader.verbosity > 0 {
4128 if let Some(fmt) = subtitle_formats.first() {
4129 info!(" Downloaded media contains subtitles in {fmt:?} format");
4130 }
4131 info!(" {}", "Running MP4Box to extract subtitles".italic());
4132 }
4133 let out = downloader.output_path.as_ref().unwrap()
4134 .with_extension("srt");
4135 let out_str = out.to_string_lossy();
4136 let tmp_str = tmppath.to_string_lossy();
4137 let args = vec![
4138 "-srt", "1",
4139 "-out", &out_str,
4140 &tmp_str];
4141 if downloader.verbosity > 0 {
4142 info!(" Running MP4Box {}", args.join(" "));
4143 }
4144 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4145 .args(args)
4146 .output()
4147 {
4148 let msg = partial_process_output(&mp4box.stdout);
4149 if !msg.is_empty() {
4150 info!(" MP4Box stdout: {msg}");
4151 }
4152 let msg = partial_process_output(&mp4box.stderr);
4153 if !msg.is_empty() {
4154 info!(" MP4Box stderr: {msg}");
4155 }
4156 if mp4box.status.success() {
4157 info!(" Extracted subtitles as SRT");
4158 } else {
4159 warn!(" Error running MP4Box to extract subtitles");
4160 }
4161 } else {
4162 warn!(" Failed to spawn MP4Box to extract subtitles");
4163 }
4164 }
4165 if subtitle_formats.contains(&SubtitleType::Stpp) {
4166 if downloader.verbosity > 0 {
4167 info!(" Converting STPP subtitles to TTML format with ffmpeg");
4168 }
4169 let out = downloader.output_path.as_ref().unwrap()
4170 .with_extension("ttml");
4171 let tmppath_arg = &tmppath.to_string_lossy();
4172 let out_arg = &out.to_string_lossy();
4173 let ffmpeg_args = vec![
4174 "-hide_banner",
4175 "-nostats",
4176 "-loglevel", "error",
4177 "-y", "-nostdin",
4179 "-i", tmppath_arg,
4180 "-f", "data",
4181 "-map", "0",
4182 "-c", "copy",
4183 out_arg];
4184 if downloader.verbosity > 0 {
4185 info!(" Running ffmpeg {}", ffmpeg_args.join(" "));
4186 }
4187 if let Ok(ffmpeg) = Command::new(downloader.ffmpeg_location.clone())
4188 .args(ffmpeg_args)
4189 .output()
4190 {
4191 let msg = partial_process_output(&ffmpeg.stdout);
4192 if !msg.is_empty() {
4193 info!(" ffmpeg stdout: {msg}");
4194 }
4195 let msg = partial_process_output(&ffmpeg.stderr);
4196 if !msg.is_empty() {
4197 info!(" ffmpeg stderr: {msg}");
4198 }
4199 if ffmpeg.status.success() {
4200 info!(" Converted STPP subtitles to TTML format");
4201 } else {
4202 warn!(" Error running ffmpeg to convert subtitles");
4203 }
4204 }
4205 }
4209
4210 }
4211 Ok(have_subtitles)
4212}
4213
4214
4215async fn fetch_mpd_http(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4217 let client = &downloader.http_client.clone().unwrap();
4218 let send_request = || async {
4219 let mut req = client.get(&downloader.mpd_url)
4220 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4221 .header("Accept-Language", "en-US,en")
4222 .header("Upgrade-Insecure-Requests", "1")
4223 .header("Sec-Fetch-Mode", "navigate");
4224 if let Some(referer) = &downloader.referer {
4225 req = req.header("Referer", referer);
4226 }
4227 if let Some(username) = &downloader.auth_username {
4228 if let Some(password) = &downloader.auth_password {
4229 req = req.basic_auth(username, Some(password));
4230 }
4231 }
4232 if let Some(token) = &downloader.auth_bearer_token {
4233 req = req.bearer_auth(token);
4234 }
4235 req.send().await
4236 .map_err(categorize_reqwest_error)?
4237 .error_for_status()
4238 .map_err(categorize_reqwest_error)
4239 };
4240 for observer in &downloader.progress_observers {
4241 observer.update(1, "Fetching DASH manifest");
4242 }
4243 if downloader.verbosity > 0 {
4244 if !downloader.fetch_audio && !downloader.fetch_video && !downloader.fetch_subtitles {
4245 info!("Only simulating media downloads");
4246 }
4247 info!("Fetching the DASH manifest");
4248 }
4249 let response = retry_notify(ExponentialBackoff::default(), send_request, notify_transient)
4250 .await
4251 .map_err(|e| network_error("requesting DASH manifest", e))?;
4252 if !response.status().is_success() {
4253 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4254 return Err(DashMpdError::Network(msg));
4255 }
4256 downloader.redirected_url = response.url().clone();
4257 response.bytes().await
4258 .map_err(|e| network_error("fetching DASH manifest", e))
4259}
4260
4261async fn fetch_mpd_file(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4264 if ! &downloader.mpd_url.starts_with("file://") {
4265 return Err(DashMpdError::Other(String::from("expecting file:// URL scheme")));
4266 }
4267 let url = Url::parse(&downloader.mpd_url)
4268 .map_err(|_| DashMpdError::Other(String::from("parsing MPD URL")))?;
4269 let path = url.to_file_path()
4270 .map_err(|_| DashMpdError::Other(String::from("extracting path from file:// URL")))?;
4271 let octets = fs::read(path)
4272 .map_err(|_| DashMpdError::Other(String::from("reading from file:// URL")))?;
4273 Ok(Bytes::from(octets))
4274}
4275
4276
4277#[tracing::instrument(level="trace", skip_all)]
4278async fn fetch_mpd(downloader: &mut DashDownloader) -> Result<PathBuf, DashMpdError> {
4279 let xml = if downloader.mpd_url.starts_with("file://") {
4280 fetch_mpd_file(downloader).await?
4281 } else {
4282 fetch_mpd_http(downloader).await?
4283 };
4284 let mut mpd: MPD = parse_resolving_xlinks(downloader, &xml).await
4285 .map_err(|e| parse_error("parsing DASH XML", e))?;
4286 let client = &downloader.http_client.clone().unwrap();
4289 if let Some(new_location) = &mpd.locations.first() {
4290 let new_url = &new_location.url;
4291 if downloader.verbosity > 0 {
4292 info!("Redirecting to new manifest <Location> {new_url}");
4293 }
4294 let send_request = || async {
4295 let mut req = client.get(new_url)
4296 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4297 .header("Accept-Language", "en-US,en")
4298 .header("Sec-Fetch-Mode", "navigate");
4299 if let Some(referer) = &downloader.referer {
4300 req = req.header("Referer", referer);
4301 } else {
4302 req = req.header("Referer", downloader.redirected_url.to_string());
4303 }
4304 if let Some(username) = &downloader.auth_username {
4305 if let Some(password) = &downloader.auth_password {
4306 req = req.basic_auth(username, Some(password));
4307 }
4308 }
4309 if let Some(token) = &downloader.auth_bearer_token {
4310 req = req.bearer_auth(token);
4311 }
4312 req.send().await
4313 .map_err(categorize_reqwest_error)?
4314 .error_for_status()
4315 .map_err(categorize_reqwest_error)
4316 };
4317 let response = retry_notify(ExponentialBackoff::default(), send_request, notify_transient)
4318 .await
4319 .map_err(|e| network_error("requesting relocated DASH manifest", e))?;
4320 if !response.status().is_success() {
4321 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4322 return Err(DashMpdError::Network(msg));
4323 }
4324 downloader.redirected_url = response.url().clone();
4325 let xml = response.bytes().await
4326 .map_err(|e| network_error("fetching relocated DASH manifest", e))?;
4327 mpd = parse_resolving_xlinks(downloader, &xml).await
4328 .map_err(|e| parse_error("parsing relocated DASH XML", e))?;
4329 }
4330 if mpd_is_dynamic(&mpd) {
4331 if downloader.allow_live_streams {
4334 if downloader.verbosity > 0 {
4335 warn!("Attempting to download from live stream (this may not work).");
4336 }
4337 } else {
4338 return Err(DashMpdError::UnhandledMediaStream("Don't know how to download dynamic MPD".to_string()));
4339 }
4340 }
4341 let mut toplevel_base_url = downloader.redirected_url.clone();
4342 if let Some(bu) = &mpd.base_url.first() {
4344 toplevel_base_url = merge_baseurls(&downloader.redirected_url, &bu.base)?;
4345 }
4346 if let Some(base) = &downloader.base_url {
4349 toplevel_base_url = merge_baseurls(&downloader.redirected_url, base)?;
4350 }
4351 if downloader.verbosity > 0 {
4352 let pcount = mpd.periods.len();
4353 info!("DASH manifest has {pcount} period{}", if pcount > 1 { "s" } else { "" });
4354 print_available_streams(&mpd);
4355 }
4356 let mut pds: Vec<PeriodDownloads> = Vec::new();
4364 let mut period_counter = 0;
4365 for mpd_period in &mpd.periods {
4366 let period = mpd_period.clone();
4367 period_counter += 1;
4368 if let Some(min) = downloader.minimum_period_duration {
4369 if let Some(duration) = period.duration {
4370 if duration < min {
4371 if let Some(id) = period.id.as_ref() {
4372 info!("Skipping period {id} (#{period_counter}): duration is less than requested minimum");
4373 } else {
4374 info!("Skipping period #{period_counter}: duration is less than requested minimum");
4375 }
4376 continue;
4377 }
4378 }
4379 }
4380 let mut pd = PeriodDownloads { period_counter, ..Default::default() };
4381 if let Some(id) = period.id.as_ref() {
4382 pd.id = Some(id.clone());
4383 }
4384 if downloader.verbosity > 0 {
4385 if let Some(id) = period.id.as_ref() {
4386 info!("Preparing download for period {id} (#{period_counter})");
4387 } else {
4388 info!("Preparing download for period #{period_counter}");
4389 }
4390 }
4391 let mut base_url = toplevel_base_url.clone();
4392 if let Some(bu) = period.BaseURL.first() {
4394 base_url = merge_baseurls(&base_url, &bu.base)?;
4395 }
4396 let mut audio_outputs = PeriodOutputs::default();
4397 if downloader.fetch_audio {
4398 audio_outputs = do_period_audio(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4399 for f in audio_outputs.fragments {
4400 pd.audio_fragments.push(f);
4401 }
4402 }
4403 let mut video_outputs = PeriodOutputs::default();
4404 if downloader.fetch_video {
4405 video_outputs = do_period_video(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4406 for f in video_outputs.fragments {
4407 pd.video_fragments.push(f);
4408 }
4409 }
4410 match do_period_subtitles(downloader, &mpd, &period, period_counter, base_url.clone()).await {
4411 Ok(subtitle_outputs) => {
4412 for f in subtitle_outputs.fragments {
4413 pd.subtitle_fragments.push(f);
4414 }
4415 for f in subtitle_outputs.subtitle_formats {
4416 pd.subtitle_formats.push(f);
4417 }
4418 },
4419 Err(e) => warn!(" Ignoring error triggered while processing subtitles: {e}"),
4420 }
4421 if downloader.verbosity > 0 {
4423 use base64::prelude::{Engine as _, BASE64_STANDARD};
4424
4425 audio_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4426 for f in pd.audio_fragments.iter().filter(|f| f.is_init) {
4427 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4428 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4429 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4430 info!(" {}", pssh.to_string());
4431 }
4432 }
4433 }
4434 video_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4435 for f in pd.video_fragments.iter().filter(|f| f.is_init) {
4436 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4437 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4438 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4439 info!(" {}", pssh.to_string());
4440 }
4441 }
4442 }
4443 }
4444 pds.push(pd);
4445 } let output_path = &downloader.output_path.as_ref().unwrap().clone();
4450 let mut period_output_paths: Vec<PathBuf> = Vec::new();
4451 let mut ds = DownloadState {
4452 period_counter: 0,
4453 segment_count: pds.iter().map(period_fragment_count).sum(),
4455 segment_counter: 0,
4456 download_errors: 0
4457 };
4458 for pd in pds {
4459 let mut have_audio = false;
4460 let mut have_video = false;
4461 let mut have_subtitles = false;
4462 ds.period_counter = pd.period_counter;
4463 let period_output_path = output_path_for_period(output_path, pd.period_counter);
4464 #[allow(clippy::collapsible_if)]
4465 if downloader.verbosity > 0 {
4466 if downloader.fetch_audio || downloader.fetch_video || downloader.fetch_subtitles {
4467 let idnum = if let Some(id) = pd.id {
4468 format!("id={} (#{})", id, pd.period_counter)
4469 } else {
4470 format!("#{}", pd.period_counter)
4471 };
4472 info!("Period {idnum}: fetching {} audio, {} video and {} subtitle segments",
4473 pd.audio_fragments.len(),
4474 pd.video_fragments.len(),
4475 pd.subtitle_fragments.len());
4476 }
4477 }
4478 let output_ext = downloader.output_path.as_ref().unwrap()
4479 .extension()
4480 .unwrap_or(OsStr::new("mp4"));
4481 let tmppath_audio = if let Some(ref path) = downloader.keep_audio {
4482 path.clone()
4483 } else {
4484 tmp_file_path("dashmpd-audio", output_ext)?
4485 };
4486 let tmppath_video = if let Some(ref path) = downloader.keep_video {
4487 path.clone()
4488 } else {
4489 tmp_file_path("dashmpd-video", output_ext)?
4490 };
4491 let tmppath_subs = tmp_file_path("dashmpd-subs", OsStr::new("sub"))?;
4492 if downloader.fetch_audio && !pd.audio_fragments.is_empty() {
4493 have_audio = fetch_period_audio(downloader,
4494 tmppath_audio.clone(), &pd.audio_fragments,
4495 &mut ds).await?;
4496 }
4497 if downloader.fetch_video && !pd.video_fragments.is_empty() {
4498 have_video = fetch_period_video(downloader,
4499 tmppath_video.clone(), &pd.video_fragments,
4500 &mut ds).await?;
4501 }
4502 if downloader.fetch_subtitles && !pd.subtitle_fragments.is_empty() {
4506 have_subtitles = fetch_period_subtitles(downloader,
4507 tmppath_subs.clone(),
4508 &pd.subtitle_fragments,
4509 &pd.subtitle_formats,
4510 &mut ds).await?;
4511 }
4512
4513 if have_audio && have_video {
4516 for observer in &downloader.progress_observers {
4517 observer.update(99, "Muxing audio and video");
4518 }
4519 if downloader.verbosity > 1 {
4520 info!(" {}", "Muxing audio and video streams".italic());
4521 }
4522 let audio_tracks = vec![
4523 AudioTrack {
4524 language: String::from("unk"),
4525 path: tmppath_audio.clone()
4526 }];
4527 mux_audio_video(downloader, &period_output_path, &audio_tracks, &tmppath_video)?;
4528 if pd.subtitle_formats.contains(&SubtitleType::Stpp) {
4529 let container = match &period_output_path.extension() {
4530 Some(ext) => ext.to_str().unwrap_or("mp4"),
4531 None => "mp4",
4532 };
4533 if container.eq("mp4") {
4534 if downloader.verbosity > 1 {
4535 if let Some(fmt) = &pd.subtitle_formats.first() {
4536 info!(" Downloaded media contains subtitles in {fmt:?} format");
4537 }
4538 info!(" Running MP4Box to merge subtitles with output MP4 container");
4539 }
4540 let tmp_str = tmppath_subs.to_string_lossy();
4543 let period_output_str = period_output_path.to_string_lossy();
4544 let args = vec!["-add", &tmp_str, &period_output_str];
4545 if downloader.verbosity > 0 {
4546 info!(" Running MP4Box {}", args.join(" "));
4547 }
4548 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4549 .args(args)
4550 .output()
4551 {
4552 let msg = partial_process_output(&mp4box.stdout);
4553 if !msg.is_empty() {
4554 info!(" MP4Box stdout: {msg}");
4555 }
4556 let msg = partial_process_output(&mp4box.stderr);
4557 if !msg.is_empty() {
4558 info!(" MP4Box stderr: {msg}");
4559 }
4560 if mp4box.status.success() {
4561 info!(" Merged subtitles with MP4 container");
4562 } else {
4563 warn!(" Error running MP4Box to merge subtitles");
4564 }
4565 } else {
4566 warn!(" Failed to spawn MP4Box to merge subtitles");
4567 }
4568 } else if container.eq("mkv") || container.eq("webm") {
4569 let srt = period_output_path.with_extension("srt");
4581 if srt.exists() {
4582 if downloader.verbosity > 0 {
4583 info!(" Running mkvmerge to merge subtitles with output Matroska container");
4584 }
4585 let tmppath = temporary_outpath(".mkv")?;
4586 let pop_arg = &period_output_path.to_string_lossy();
4587 let srt_arg = &srt.to_string_lossy();
4588 let mkvmerge_args = vec!["-o", &tmppath, pop_arg, srt_arg];
4589 if downloader.verbosity > 0 {
4590 info!(" Running mkvmerge {}", mkvmerge_args.join(" "));
4591 }
4592 if let Ok(mkvmerge) = Command::new(downloader.mkvmerge_location.clone())
4593 .args(mkvmerge_args)
4594 .output()
4595 {
4596 let msg = partial_process_output(&mkvmerge.stdout);
4597 if !msg.is_empty() {
4598 info!(" mkvmerge stdout: {msg}");
4599 }
4600 let msg = partial_process_output(&mkvmerge.stderr);
4601 if !msg.is_empty() {
4602 info!(" mkvmerge stderr: {msg}");
4603 }
4604 if mkvmerge.status.success() {
4605 info!(" Merged subtitles with Matroska container");
4606 {
4609 let tmpfile = File::open(tmppath.clone())
4610 .map_err(|e| DashMpdError::Io(
4611 e, String::from("opening mkvmerge output")))?;
4612 let mut merged = BufReader::new(tmpfile);
4613 let outfile = File::create(period_output_path.clone())
4615 .map_err(|e| DashMpdError::Io(
4616 e, String::from("creating output file")))?;
4617 let mut sink = BufWriter::new(outfile);
4618 io::copy(&mut merged, &mut sink)
4619 .map_err(|e| DashMpdError::Io(
4620 e, String::from("copying mkvmerge output to output file")))?;
4621 }
4622 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4623 if let Err(e) = fs::remove_file(tmppath) {
4624 warn!(" Error deleting temporary mkvmerge output: {e}");
4625 }
4626 }
4627 } else {
4628 warn!(" Error running mkvmerge to merge subtitles");
4629 }
4630 }
4631 }
4632 }
4633 }
4634 } else if have_audio {
4635 copy_audio_to_container(downloader, &period_output_path, &tmppath_audio)?;
4636 } else if have_video {
4637 copy_video_to_container(downloader, &period_output_path, &tmppath_video)?;
4638 } else if downloader.fetch_video && downloader.fetch_audio {
4639 return Err(DashMpdError::UnhandledMediaStream("no audio or video streams found".to_string()));
4640 } else if downloader.fetch_video {
4641 return Err(DashMpdError::UnhandledMediaStream("no video streams found".to_string()));
4642 } else if downloader.fetch_audio {
4643 return Err(DashMpdError::UnhandledMediaStream("no audio streams found".to_string()));
4644 }
4645 #[allow(clippy::collapsible_if)]
4646 if downloader.keep_audio.is_none() && downloader.fetch_audio {
4647 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4648 if tmppath_audio.exists() && fs::remove_file(tmppath_audio).is_err() {
4649 info!(" Failed to delete temporary file for audio stream");
4650 }
4651 }
4652 }
4653 #[allow(clippy::collapsible_if)]
4654 if downloader.keep_video.is_none() && downloader.fetch_video {
4655 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4656 if tmppath_video.exists() && fs::remove_file(tmppath_video).is_err() {
4657 info!(" Failed to delete temporary file for video stream");
4658 }
4659 }
4660 }
4661 #[allow(clippy::collapsible_if)]
4662 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4663 if downloader.fetch_subtitles && tmppath_subs.exists() && fs::remove_file(tmppath_subs).is_err() {
4664 info!(" Failed to delete temporary file for subtitles");
4665 }
4666 }
4667 if downloader.verbosity > 1 && (downloader.fetch_audio || downloader.fetch_video || have_subtitles) {
4668 if let Ok(metadata) = fs::metadata(period_output_path.clone()) {
4669 info!(" Wrote {:.1}MB to media file", metadata.len() as f64 / (1024.0 * 1024.0));
4670 }
4671 }
4672 if have_audio || have_video {
4673 period_output_paths.push(period_output_path);
4674 }
4675 } #[allow(clippy::comparison_chain)]
4677 if period_output_paths.len() == 1 {
4678 maybe_record_metainformation(output_path, downloader, &mpd);
4680 } else if period_output_paths.len() > 1 {
4681 #[allow(unused_mut)]
4686 let mut concatenated = false;
4687 #[cfg(not(feature = "libav"))]
4688 if downloader.concatenate_periods && video_containers_concatable(downloader, &period_output_paths) {
4689 info!("Preparing to concatenate multiple Periods into one output file");
4690 concat_output_files(downloader, &period_output_paths)?;
4691 for p in &period_output_paths[1..] {
4692 if fs::remove_file(p).is_err() {
4693 warn!(" Failed to delete temporary file {}", p.display());
4694 }
4695 }
4696 concatenated = true;
4697 if let Some(pop) = period_output_paths.first() {
4698 maybe_record_metainformation(pop, downloader, &mpd);
4699 }
4700 }
4701 if !concatenated {
4702 info!("Media content has been saved in a separate file for each period:");
4703 period_counter = 0;
4705 for p in period_output_paths {
4706 period_counter += 1;
4707 info!(" Period #{period_counter}: {}", p.display());
4708 maybe_record_metainformation(&p, downloader, &mpd);
4709 }
4710 }
4711 }
4712 let have_content_protection = mpd.periods.iter().any(
4713 |p| p.adaptations.iter().any(
4714 |a| (!a.ContentProtection.is_empty()) ||
4715 a.representations.iter().any(
4716 |r| !r.ContentProtection.is_empty())));
4717 if have_content_protection && downloader.decryption_keys.is_empty() {
4718 warn!("Manifest seems to use ContentProtection (DRM), but you didn't provide decryption keys.");
4719 }
4720 for observer in &downloader.progress_observers {
4721 observer.update(100, "Done");
4722 }
4723 Ok(PathBuf::from(output_path))
4724}
4725
4726
4727#[cfg(test)]
4728mod tests {
4729 #[test]
4730 fn test_resolve_url_template() {
4731 use std::collections::HashMap;
4732 use super::resolve_url_template;
4733
4734 assert_eq!(resolve_url_template("AA$Time$BB", &HashMap::from([("Time", "ZZZ".to_string())])),
4735 "AAZZZBB");
4736 assert_eq!(resolve_url_template("AA$Number%06d$BB", &HashMap::from([("Number", "42".to_string())])),
4737 "AA000042BB");
4738 let dict = HashMap::from([("RepresentationID", "640x480".to_string()),
4739 ("Number", "42".to_string()),
4740 ("Time", "ZZZ".to_string())]);
4741 assert_eq!(resolve_url_template("AA/$RepresentationID$/segment-$Number%05d$.mp4", &dict),
4742 "AA/640x480/segment-00042.mp4");
4743 }
4744}