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