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 std::sync::Arc;
13use std::borrow::Cow;
14use std::collections::HashMap;
15use std::cmp::min;
16use std::ffi::OsStr;
17use std::num::NonZeroU32;
18use tracing::{trace, info, warn, error};
19use colored::*;
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;
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 .keep(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 {
346 self.language_preference = Some(lang);
347 self
348 }
349
350 pub fn prefer_roles(mut self, role_preference: Vec<String>) -> DashDownloader {
360 if role_preference.len() < u8::MAX as usize {
361 self.role_preference = role_preference;
362 } else {
363 warn!("Ignoring role_preference ordering due to excessive length");
364 }
365 self
366 }
367
368 pub fn prefer_video_width(mut self, width: u64) -> DashDownloader {
371 self.video_width_preference = Some(width);
372 self
373 }
374
375 pub fn prefer_video_height(mut self, height: u64) -> DashDownloader {
378 self.video_height_preference = Some(height);
379 self
380 }
381
382 pub fn video_only(mut self) -> DashDownloader {
384 self.fetch_audio = false;
385 self.fetch_video = true;
386 self
387 }
388
389 pub fn audio_only(mut self) -> DashDownloader {
391 self.fetch_audio = true;
392 self.fetch_video = false;
393 self
394 }
395
396 pub fn keep_video_as<P: Into<PathBuf>>(mut self, video_path: P) -> DashDownloader {
399 self.keep_video = Some(video_path.into());
400 self
401 }
402
403 pub fn keep_audio_as<P: Into<PathBuf>>(mut self, audio_path: P) -> DashDownloader {
406 self.keep_audio = Some(audio_path.into());
407 self
408 }
409
410 pub fn save_fragments_to<P: Into<PathBuf>>(mut self, fragment_path: P) -> DashDownloader {
413 self.fragment_path = Some(fragment_path.into());
414 self
415 }
416
417 pub fn add_decryption_key(mut self, id: String, key: String) -> DashDownloader {
429 self.decryption_keys.insert(id, key);
430 self
431 }
432
433 pub fn with_xslt_stylesheet<P: Into<PathBuf>>(mut self, stylesheet: P) -> DashDownloader {
445 self.xslt_stylesheets.push(stylesheet.into());
446 self
447 }
448
449 pub fn minimum_period_duration(mut self, value: Duration) -> DashDownloader {
452 self.minimum_period_duration = Some(value);
453 self
454 }
455
456 pub fn fetch_audio(mut self, value: bool) -> DashDownloader {
460 self.fetch_audio = value;
461 self
462 }
463
464 pub fn fetch_video(mut self, value: bool) -> DashDownloader {
468 self.fetch_video = value;
469 self
470 }
471
472 pub fn fetch_subtitles(mut self, value: bool) -> DashDownloader {
480 self.fetch_subtitles = value;
481 self
482 }
483
484 pub fn concatenate_periods(mut self, value: bool) -> DashDownloader {
488 self.concatenate_periods = value;
489 self
490 }
491
492 pub fn without_content_type_checks(mut self) -> DashDownloader {
495 self.content_type_checks = false;
496 self
497 }
498
499 pub fn content_type_checks(mut self, value: bool) -> DashDownloader {
502 self.content_type_checks = value;
503 self
504 }
505
506 pub fn conformity_checks(mut self, value: bool) -> DashDownloader {
509 self.conformity_checks = value;
510 self
511 }
512
513 pub fn use_index_range(mut self, value: bool) -> DashDownloader {
528 self.use_index_range = value;
529 self
530 }
531
532 pub fn fragment_retry_count(mut self, count: u32) -> DashDownloader {
536 self.fragment_retry_count = count;
537 self
538 }
539
540 pub fn max_error_count(mut self, count: u32) -> DashDownloader {
547 self.max_error_count = count;
548 self
549 }
550
551 pub fn sleep_between_requests(mut self, seconds: u8) -> DashDownloader {
553 self.sleep_between_requests = seconds;
554 self
555 }
556
557 pub fn allow_live_streams(mut self, value: bool) -> DashDownloader {
569 self.allow_live_streams = value;
570 self
571 }
572
573 pub fn force_duration(mut self, seconds: f64) -> DashDownloader {
579 self.force_duration = Some(seconds);
580 self
581 }
582
583 pub fn with_rate_limit(mut self, bps: u64) -> DashDownloader {
589 if bps < 10 * 1024 {
590 warn!("Limiting bandwidth below 10kB/s is unlikely to be stable");
591 }
592 if self.verbosity > 1 {
593 info!("Limiting bandwidth to {} kB/s", bps/1024);
594 }
595 self.rate_limit = bps;
596 let mut kps = 1 + bps / 1024;
602 if kps > u32::MAX as u64 {
603 warn!("Throttling bandwidth limit");
604 kps = u32::MAX.into();
605 }
606 if let Some(bw_limit) = NonZeroU32::new(kps as u32) {
607 if let Some(burst) = NonZeroU32::new(10 * 1024) {
608 let bw_quota = Quota::per_second(bw_limit)
609 .allow_burst(burst);
610 self.bw_limiter = Some(RateLimiter::direct(bw_quota));
611 }
612 }
613 self
614 }
615
616 pub fn verbosity(mut self, level: u8) -> DashDownloader {
626 self.verbosity = level;
627 self
628 }
629
630 pub fn record_metainformation(mut self, record: bool) -> DashDownloader {
634 self.record_metainformation = record;
635 self
636 }
637
638 pub fn with_muxer_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
660 self.muxer_preference.insert(container.to_string(), ordering.to_string());
661 self
662 }
663
664 pub fn with_concat_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
687 self.concat_preference.insert(container.to_string(), ordering.to_string());
688 self
689 }
690
691 pub fn with_decryptor_preference(mut self, decryption_tool: &str) -> DashDownloader {
698 self.decryptor_preference = decryption_tool.to_string();
699 self
700 }
701
702 pub fn with_ffmpeg(mut self, ffmpeg_path: &str) -> DashDownloader {
717 self.ffmpeg_location = ffmpeg_path.to_string();
718 self
719 }
720
721 pub fn with_vlc(mut self, vlc_path: &str) -> DashDownloader {
736 self.vlc_location = vlc_path.to_string();
737 self
738 }
739
740 pub fn with_mkvmerge(mut self, path: &str) -> DashDownloader {
748 self.mkvmerge_location = path.to_string();
749 self
750 }
751
752 pub fn with_mp4box(mut self, path: &str) -> DashDownloader {
760 self.mp4box_location = path.to_string();
761 self
762 }
763
764 pub fn with_mp4decrypt(mut self, path: &str) -> DashDownloader {
772 self.mp4decrypt_location = path.to_string();
773 self
774 }
775
776 pub fn with_shaka_packager(mut self, path: &str) -> DashDownloader {
784 self.shaka_packager_location = path.to_string();
785 self
786 }
787
788 pub async fn download_to<P: Into<PathBuf>>(mut self, out: P) -> Result<PathBuf, DashMpdError> {
798 self.output_path = Some(out.into());
799 if self.http_client.is_none() {
800 let client = reqwest::Client::builder()
801 .timeout(Duration::new(30, 0))
802 .cookie_store(true)
803 .build()
804 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
805 self.http_client = Some(client);
806 }
807 fetch_mpd(&mut self).await
808 }
809
810 pub async fn download(mut self) -> Result<PathBuf, DashMpdError> {
817 let cwd = env::current_dir()
818 .map_err(|e| DashMpdError::Io(e, String::from("obtaining current directory")))?;
819 let filename = generate_filename_from_url(&self.mpd_url);
820 let outpath = cwd.join(filename);
821 self.output_path = Some(outpath);
822 if self.http_client.is_none() {
823 let client = reqwest::Client::builder()
824 .timeout(Duration::new(30, 0))
825 .cookie_store(true)
826 .build()
827 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
828 self.http_client = Some(client);
829 }
830 fetch_mpd(&mut self).await
831 }
832}
833
834
835fn parse_range(range: &str) -> Result<(u64, u64), DashMpdError> {
838 let v: Vec<&str> = range.split_terminator('-').collect();
839 if v.len() != 2 {
840 return Err(DashMpdError::Parsing(format!("invalid range specifier: {range}")));
841 }
842 #[allow(clippy::indexing_slicing)]
843 let start: u64 = v[0].parse()
844 .map_err(|_| DashMpdError::Parsing(String::from("invalid start for range specifier")))?;
845 #[allow(clippy::indexing_slicing)]
846 let end: u64 = v[1].parse()
847 .map_err(|_| DashMpdError::Parsing(String::from("invalid end for range specifier")))?;
848 Ok((start, end))
849}
850
851#[derive(Debug)]
852struct MediaFragment {
853 period: u8,
854 url: Url,
855 start_byte: Option<u64>,
856 end_byte: Option<u64>,
857 is_init: bool,
858 timeout: Option<Duration>,
859}
860
861#[derive(Debug)]
862struct MediaFragmentBuilder {
863 period: u8,
864 url: Url,
865 start_byte: Option<u64>,
866 end_byte: Option<u64>,
867 is_init: bool,
868 timeout: Option<Duration>,
869}
870
871impl MediaFragmentBuilder {
872 pub fn new(period: u8, url: Url) -> MediaFragmentBuilder {
873 MediaFragmentBuilder {
874 period, url, start_byte: None, end_byte: None, is_init: false, timeout: None
875 }
876 }
877
878 pub fn with_range(mut self, start_byte: Option<u64>, end_byte: Option<u64>) -> MediaFragmentBuilder {
879 self.start_byte = start_byte;
880 self.end_byte = end_byte;
881 self
882 }
883
884 pub fn with_timeout(mut self, timeout: Duration) -> MediaFragmentBuilder {
885 self.timeout = Some(timeout);
886 self
887 }
888
889 pub fn set_init(mut self) -> MediaFragmentBuilder {
890 self.is_init = true;
891 self
892 }
893
894 pub fn build(self) -> MediaFragment {
895 MediaFragment {
896 period: self.period,
897 url: self.url,
898 start_byte: self.start_byte,
899 end_byte: self.end_byte,
900 is_init: self.is_init,
901 timeout: self.timeout
902 }
903 }
904}
905
906#[derive(Debug, Default)]
910struct PeriodOutputs {
911 fragments: Vec<MediaFragment>,
912 diagnostics: Vec<String>,
913 subtitle_formats: Vec<SubtitleType>,
914}
915
916#[derive(Debug, Default)]
917struct PeriodDownloads {
918 audio_fragments: Vec<MediaFragment>,
919 video_fragments: Vec<MediaFragment>,
920 subtitle_fragments: Vec<MediaFragment>,
921 subtitle_formats: Vec<SubtitleType>,
922 period_counter: u8,
923 id: Option<String>,
924}
925
926fn period_fragment_count(pd: &PeriodDownloads) -> usize {
927 pd.audio_fragments.len() +
928 pd.video_fragments.len() +
929 pd.subtitle_fragments.len()
930}
931
932
933
934async fn throttle_download_rate(downloader: &DashDownloader, size: u32) -> Result<(), DashMpdError> {
935 if downloader.rate_limit > 0 {
936 if let Some(cells) = NonZeroU32::new(size) {
937 if let Some(limiter) = downloader.bw_limiter.as_ref() {
938 #[allow(clippy::redundant_pattern_matching)]
939 if let Err(_) = limiter.until_n_ready(cells).await {
940 return Err(DashMpdError::Other(
941 "Bandwidth limit is too low".to_string()));
942 }
943 }
944 }
945 }
946 Ok(())
947}
948
949
950fn generate_filename_from_url(url: &str) -> PathBuf {
951 use sanitise_file_name::{sanitise_with_options, Options};
952
953 let mut path = url;
954 if let Some(p) = path.strip_prefix("http://") {
955 path = p;
956 } else if let Some(p) = path.strip_prefix("https://") {
957 path = p;
958 } else if let Some(p) = path.strip_prefix("file://") {
959 path = p;
960 }
961 if let Some(p) = path.strip_prefix("www.") {
962 path = p;
963 }
964 if let Some(p) = path.strip_prefix("ftp.") {
965 path = p;
966 }
967 if let Some(p) = path.strip_suffix(".mpd") {
968 path = p;
969 }
970 let mut sanitize_opts = Options::DEFAULT;
971 sanitize_opts.length_limit = 150;
972 PathBuf::from(sanitise_with_options(path, &sanitize_opts) + ".mp4")
977}
978
979fn output_path_for_period(base: &Path, period: u8) -> PathBuf {
996 assert!(period > 0);
997 if period == 1 {
998 base.to_path_buf()
999 } else {
1000 if let Some(stem) = base.file_stem() {
1001 if let Some(ext) = base.extension() {
1002 let fname = format!("{}-p{period}.{}", stem.to_string_lossy(), ext.to_string_lossy());
1003 return base.with_file_name(fname);
1004 }
1005 }
1006 let p = format!("dashmpd-p{period}");
1007 tmp_file_path(&p, base.extension().unwrap_or(OsStr::new("mp4")))
1008 .unwrap_or_else(|_| p.into())
1009 }
1010}
1011
1012fn is_absolute_url(s: &str) -> bool {
1013 s.starts_with("http://") ||
1014 s.starts_with("https://") ||
1015 s.starts_with("file://") ||
1016 s.starts_with("ftp://")
1017}
1018
1019fn merge_baseurls(current: &Url, new: &str) -> Result<Url, DashMpdError> {
1020 if is_absolute_url(new) {
1021 Url::parse(new)
1022 .map_err(|e| parse_error("parsing BaseURL", e))
1023 } else {
1024 let mut merged = current.join(new)
1037 .map_err(|e| parse_error("joining base with BaseURL", e))?;
1038 if merged.query().is_none() {
1039 merged.set_query(current.query());
1040 }
1041 Ok(merged)
1042 }
1043}
1044
1045fn content_type_audio_p(response: &reqwest::Response) -> bool {
1050 match response.headers().get("content-type") {
1051 Some(ct) => {
1052 let ctb = ct.as_bytes();
1053 ctb.starts_with(b"audio/") ||
1054 ctb.starts_with(b"video/") ||
1055 ctb.starts_with(b"application/octet-stream")
1056 },
1057 None => false,
1058 }
1059}
1060
1061fn content_type_video_p(response: &reqwest::Response) -> bool {
1063 match response.headers().get("content-type") {
1064 Some(ct) => {
1065 let ctb = ct.as_bytes();
1066 ctb.starts_with(b"video/") ||
1067 ctb.starts_with(b"application/octet-stream")
1068 },
1069 None => false,
1070 }
1071}
1072
1073
1074fn adaptation_lang_distance(a: &AdaptationSet, language_preference: &str) -> u8 {
1078 if let Some(lang) = &a.lang {
1079 if lang.eq(language_preference) {
1080 return 0;
1081 }
1082 if lang[0..2].eq(&language_preference[0..2]) {
1083 return 5;
1084 }
1085 100
1086 } else {
1087 100
1088 }
1089}
1090
1091fn adaptation_roles(a: &AdaptationSet) -> Vec<String> {
1094 let mut roles = Vec::new();
1095 for r in &a.Role {
1096 if let Some(rv) = &r.value {
1097 roles.push(String::from(rv));
1098 }
1099 }
1100 for cc in &a.ContentComponent {
1101 for r in &cc.Role {
1102 if let Some(rv) = &r.value {
1103 roles.push(String::from(rv));
1104 }
1105 }
1106 }
1107 roles
1108}
1109
1110fn adaptation_role_distance(a: &AdaptationSet, role_preference: &[String]) -> u8 {
1112 adaptation_roles(a).iter()
1113 .map(|r| role_preference.binary_search(r).unwrap_or(u8::MAX.into()))
1114 .map(|u| u8::try_from(u).unwrap_or(u8::MAX))
1115 .min()
1116 .unwrap_or(u8::MAX)
1117}
1118
1119
1120fn select_preferred_adaptations<'a>(
1128 adaptations: Vec<&'a AdaptationSet>,
1129 downloader: &DashDownloader) -> Vec<&'a AdaptationSet>
1130{
1131 let mut preferred: Vec<&'a AdaptationSet>;
1132 if let Some(ref lang) = downloader.language_preference {
1133 preferred = Vec::new();
1134 let distance: Vec<u8> = adaptations.iter()
1135 .map(|a| adaptation_lang_distance(a, lang))
1136 .collect();
1137 let min_distance = distance.iter().min().unwrap_or(&0);
1138 for (i, a) in adaptations.iter().enumerate() {
1139 if let Some(di) = distance.get(i) {
1140 if di == min_distance {
1141 preferred.push(a);
1142 }
1143 }
1144 }
1145 } else {
1146 preferred = adaptations;
1147 }
1148 let role_distance: Vec<u8> = preferred.iter()
1154 .map(|a| adaptation_role_distance(a, &downloader.role_preference))
1155 .collect();
1156 let role_distance_min = role_distance.iter().min().unwrap_or(&0);
1157 let mut best = Vec::new();
1158 for (i, a) in preferred.into_iter().enumerate() {
1159 if let Some(rdi) = role_distance.get(i) {
1160 if rdi == role_distance_min {
1161 best.push(a);
1162 }
1163 }
1164 }
1165 best
1166}
1167
1168
1169fn select_preferred_representation<'a>(
1175 representations: Vec<&'a Representation>,
1176 downloader: &DashDownloader) -> Option<&'a Representation>
1177{
1178 if representations.iter().all(|x| x.qualityRanking.is_some()) {
1179 match downloader.quality_preference {
1182 QualityPreference::Lowest =>
1183 representations.iter()
1184 .max_by_key(|r| r.qualityRanking.unwrap_or(u8::MAX))
1185 .copied(),
1186 QualityPreference::Highest =>
1187 representations.iter().min_by_key(|r| r.qualityRanking.unwrap_or(0))
1188 .copied(),
1189 QualityPreference::Intermediate => {
1190 let count = representations.len();
1191 match count {
1192 0 => None,
1193 1 => Some(representations[0]),
1194 _ => {
1195 let mut ranking: Vec<u8> = representations.iter()
1196 .map(|r| r.qualityRanking.unwrap_or(u8::MAX))
1197 .collect();
1198 ranking.sort_unstable();
1199 if let Some(want_ranking) = ranking.get(count / 2) {
1200 representations.iter()
1201 .find(|r| r.qualityRanking.unwrap_or(u8::MAX) == *want_ranking)
1202 .copied()
1203 } else {
1204 representations.first().copied()
1205 }
1206 },
1207 }
1208 },
1209 }
1210 } else {
1211 match downloader.quality_preference {
1213 QualityPreference::Lowest => representations.iter()
1214 .min_by_key(|r| r.bandwidth.unwrap_or(1_000_000_000))
1215 .copied(),
1216 QualityPreference::Highest => representations.iter()
1217 .max_by_key(|r| r.bandwidth.unwrap_or(0))
1218 .copied(),
1219 QualityPreference::Intermediate => {
1220 let count = representations.len();
1221 match count {
1222 0 => None,
1223 1 => Some(representations[0]),
1224 _ => {
1225 let mut ranking: Vec<u64> = representations.iter()
1226 .map(|r| r.bandwidth.unwrap_or(100_000_000))
1227 .collect();
1228 ranking.sort_unstable();
1229 if let Some(want_ranking) = ranking.get(count / 2) {
1230 representations.iter()
1231 .find(|r| r.bandwidth.unwrap_or(100_000_000) == *want_ranking)
1232 .copied()
1233 } else {
1234 representations.first().copied()
1235 }
1236 },
1237 }
1238 },
1239 }
1240 }
1241}
1242
1243
1244fn print_available_subtitles_representation(r: &Representation, a: &AdaptationSet) {
1246 let unspecified = "<unspecified>".to_string();
1247 let empty = "".to_string();
1248 let lang = r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unspecified));
1249 let codecs = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&empty));
1250 let typ = subtitle_type(&a);
1251 let stype = if !codecs.is_empty() {
1252 format!("{typ:?}/{codecs}")
1253 } else {
1254 format!("{typ:?}")
1255 };
1256 let role = a.Role.first()
1257 .map_or_else(|| String::from(""),
1258 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1259 let label = a.Label.first()
1260 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1261 info!(" subs {stype:>18} | {lang:>10} |{role}{label}");
1262}
1263
1264fn print_available_subtitles_adaptation(a: &AdaptationSet) {
1265 a.representations.iter()
1266 .for_each(|r| print_available_subtitles_representation(r, a));
1267}
1268
1269fn print_available_streams_representation(r: &Representation, a: &AdaptationSet, typ: &str) {
1271 let unspecified = "<unspecified>".to_string();
1273 let w = r.width.unwrap_or(a.width.unwrap_or(0));
1274 let h = r.height.unwrap_or(a.height.unwrap_or(0));
1275 let codec = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&unspecified));
1276 let bw = r.bandwidth.unwrap_or(a.maxBandwidth.unwrap_or(0));
1277 let fmt = if typ.eq("audio") {
1278 let unknown = String::from("?");
1279 format!("lang={}", r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unknown)))
1280 } else if w == 0 || h == 0 {
1281 String::from("")
1284 } else {
1285 format!("{w}x{h}")
1286 };
1287 let role = a.Role.first()
1288 .map_or_else(|| String::from(""),
1289 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1290 let label = a.Label.first()
1291 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1292 info!(" {typ} {codec:17} | {:5} Kbps | {fmt:>9}{role}{label}", bw / 1024);
1293}
1294
1295fn print_available_streams_adaptation(a: &AdaptationSet, typ: &str) {
1296 a.representations.iter()
1297 .for_each(|r| print_available_streams_representation(r, a, typ));
1298}
1299
1300fn print_available_streams_period(p: &Period) {
1301 p.adaptations.iter()
1302 .filter(is_audio_adaptation)
1303 .for_each(|a| print_available_streams_adaptation(a, "audio"));
1304 p.adaptations.iter()
1305 .filter(is_video_adaptation)
1306 .for_each(|a| print_available_streams_adaptation(a, "video"));
1307 p.adaptations.iter()
1308 .filter(is_subtitle_adaptation)
1309 .for_each(print_available_subtitles_adaptation);
1310}
1311
1312#[tracing::instrument(level="trace", skip_all)]
1313fn print_available_streams(mpd: &MPD) {
1314 let mut counter = 0;
1315 for p in &mpd.periods {
1316 let mut period_duration_secs: f64 = 0.0;
1317 if let Some(d) = mpd.mediaPresentationDuration {
1318 period_duration_secs = d.as_secs_f64();
1319 }
1320 if let Some(d) = &p.duration {
1321 period_duration_secs = d.as_secs_f64();
1322 }
1323 counter += 1;
1324 if let Some(id) = p.id.as_ref() {
1325 info!("Streams in period {id} (#{counter}), duration {period_duration_secs:.3}s:");
1326 } else {
1327 info!("Streams in period #{counter}, duration {period_duration_secs:.3}s:");
1328 }
1329 print_available_streams_period(p);
1330 }
1331}
1332
1333async fn extract_init_pssh(downloader: &DashDownloader, init_url: Url) -> Option<Vec<u8>> {
1334 use bstr::ByteSlice;
1335 use hex_literal::hex;
1336
1337 if let Some(client) = downloader.http_client.as_ref() {
1338 let mut req = client.get(init_url);
1339 if let Some(referer) = &downloader.referer {
1340 req = req.header("Referer", referer);
1341 }
1342 if let Some(username) = &downloader.auth_username {
1343 if let Some(password) = &downloader.auth_password {
1344 req = req.basic_auth(username, Some(password));
1345 }
1346 }
1347 if let Some(token) = &downloader.auth_bearer_token {
1348 req = req.bearer_auth(token);
1349 }
1350 if let Ok(mut resp) = req.send().await {
1351 let mut chunk_counter = 0;
1354 let mut segment_first_bytes = Vec::<u8>::new();
1355 while let Ok(Some(chunk)) = resp.chunk().await {
1356 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
1357 #[allow(clippy::redundant_pattern_matching)]
1358 if let Err(_) = throttle_download_rate(downloader, size).await {
1359 return None;
1360 }
1361 segment_first_bytes.append(&mut chunk.to_vec());
1362 chunk_counter += 1;
1363 if chunk_counter > 20 {
1364 break;
1365 }
1366 }
1367 let needle = b"pssh";
1368 for offset in segment_first_bytes.find_iter(needle) {
1369 #[allow(clippy::needless_range_loop)]
1370 for i in offset-4..offset+2 {
1371 if let Some(b) = segment_first_bytes.get(i) {
1372 if *b != 0 {
1373 continue;
1374 }
1375 }
1376 }
1377 #[allow(clippy::needless_range_loop)]
1378 for i in offset+4..offset+8 {
1379 if let Some(b) = segment_first_bytes.get(i) {
1380 if *b != 0 {
1381 continue;
1382 }
1383 }
1384 }
1385 if offset+24 > segment_first_bytes.len() {
1386 continue;
1387 }
1388 const WIDEVINE_SYSID: [u8; 16] = hex!("edef8ba979d64acea3c827dcd51d21ed");
1390 if let Some(sysid) = segment_first_bytes.get((offset+8)..(offset+24)) {
1391 if !sysid.eq(&WIDEVINE_SYSID) {
1392 continue;
1393 }
1394 }
1395 if let Some(length) = segment_first_bytes.get(offset-1) {
1396 let start = offset - 4;
1397 let end = start + *length as usize;
1398 if let Some(pssh) = &segment_first_bytes.get(start..end) {
1399 return Some(pssh.to_vec());
1400 }
1401 }
1402 }
1403 }
1404 None
1405 } else {
1406 None
1407 }
1408}
1409
1410
1411lazy_static! {
1420 static ref URL_TEMPLATE_IDS: Vec<(&'static str, String, Regex)> = {
1421 vec!["RepresentationID", "Number", "Time", "Bandwidth"].into_iter()
1422 .map(|k| (k, format!("${k}$"), Regex::new(&format!("\\${k}%0([\\d])d\\$")).unwrap()))
1423 .collect()
1424 };
1425}
1426
1427fn resolve_url_template(template: &str, params: &HashMap<&str, String>) -> String {
1428 let mut result = template.to_string();
1429 for (k, ident, rx) in URL_TEMPLATE_IDS.iter() {
1430 if result.contains(ident) {
1432 if let Some(value) = params.get(k as &str) {
1433 result = result.replace(ident, value);
1434 }
1435 }
1436 if let Some(cap) = rx.captures(&result) {
1438 if let Some(value) = params.get(k as &str) {
1439 if let Ok(width) = cap[1].parse::<usize>() {
1440 if let Some(m) = rx.find(&result) {
1441 let count = format!("{value:0>width$}");
1442 result = result[..m.start()].to_owned() + &count + &result[m.end()..];
1443 }
1444 }
1445 }
1446 }
1447 }
1448 result
1449}
1450
1451
1452fn reqwest_error_transient_p(e: &reqwest::Error) -> bool {
1453 if e.is_timeout() {
1454 return true;
1455 }
1456 if let Some(s) = e.status() {
1457 if s == reqwest::StatusCode::REQUEST_TIMEOUT ||
1458 s == reqwest::StatusCode::TOO_MANY_REQUESTS ||
1459 s == reqwest::StatusCode::SERVICE_UNAVAILABLE ||
1460 s == reqwest::StatusCode::GATEWAY_TIMEOUT {
1461 return true;
1462 }
1463 }
1464 false
1465}
1466
1467fn categorize_reqwest_error(e: reqwest::Error) -> backoff::Error<reqwest::Error> {
1468 if reqwest_error_transient_p(&e) {
1469 backoff::Error::retry_after(e, Duration::new(5, 0))
1470 } else {
1471 backoff::Error::permanent(e)
1472 }
1473}
1474
1475fn notify_transient<E: std::fmt::Debug>(err: E, dur: Duration) {
1476 warn!("Transient error after {dur:?}: {err:?}");
1477}
1478
1479fn network_error(why: &str, e: reqwest::Error) -> DashMpdError {
1480 if e.is_timeout() {
1481 DashMpdError::NetworkTimeout(format!("{why}: {e:?}"))
1482 } else if e.is_connect() {
1483 DashMpdError::NetworkConnect(format!("{why}: {e:?}"))
1484 } else {
1485 DashMpdError::Network(format!("{why}: {e:?}"))
1486 }
1487}
1488
1489fn parse_error(why: &str, e: impl std::error::Error) -> DashMpdError {
1490 DashMpdError::Parsing(format!("{why}: {e:#?}"))
1491}
1492
1493
1494async fn reqwest_bytes_with_retries(
1498 client: &reqwest::Client,
1499 req: reqwest::Request,
1500 retry_count: u32) -> Result<Bytes, reqwest::Error>
1501{
1502 let mut last_error = None;
1503 for _ in 0..retry_count {
1504 if let Some(rqw) = req.try_clone() {
1505 match client.execute(rqw).await {
1506 Ok(response) => {
1507 match response.error_for_status() {
1508 Ok(resp) => {
1509 match resp.bytes().await {
1510 Ok(bytes) => return Ok(bytes),
1511 Err(e) => {
1512 info!("Retrying after HTTP error {e:?}");
1513 last_error = Some(e);
1514 },
1515 }
1516 },
1517 Err(e) => {
1518 info!("Retrying after HTTP error {e:?}");
1519 last_error = Some(e);
1520 },
1521 }
1522 },
1523 Err(e) => {
1524 info!("Retrying after HTTP error {e:?}");
1525 last_error = Some(e);
1526 },
1527 }
1528 }
1529 }
1530 Err(last_error.unwrap())
1531}
1532
1533#[allow(unused_variables)]
1546fn maybe_record_metainformation(path: &Path, downloader: &DashDownloader, mpd: &MPD) {
1547 #[cfg(target_family = "unix")]
1548 if downloader.record_metainformation && (downloader.fetch_audio || downloader.fetch_video) {
1549 if let Ok(origin_url) = Url::parse(&downloader.mpd_url) {
1550 #[allow(clippy::collapsible_if)]
1552 if origin_url.username().is_empty() && origin_url.password().is_none() {
1553 #[cfg(target_family = "unix")]
1554 if xattr::set(path, "user.xdg.origin.url", downloader.mpd_url.as_bytes()).is_err() {
1555 info!("Failed to set user.xdg.origin.url xattr on output file");
1556 }
1557 }
1558 for pi in &mpd.ProgramInformation {
1559 if let Some(t) = &pi.Title {
1560 if let Some(tc) = &t.content {
1561 if xattr::set(path, "user.dublincore.title", tc.as_bytes()).is_err() {
1562 info!("Failed to set user.dublincore.title xattr on output file");
1563 }
1564 }
1565 }
1566 if let Some(source) = &pi.Source {
1567 if let Some(sc) = &source.content {
1568 if xattr::set(path, "user.dublincore.source", sc.as_bytes()).is_err() {
1569 info!("Failed to set user.dublincore.source xattr on output file");
1570 }
1571 }
1572 }
1573 if let Some(copyright) = &pi.Copyright {
1574 if let Some(cc) = ©right.content {
1575 if xattr::set(path, "user.dublincore.rights", cc.as_bytes()).is_err() {
1576 info!("Failed to set user.dublincore.rights xattr on output file");
1577 }
1578 }
1579 }
1580 }
1581 }
1582 }
1583}
1584
1585fn fetchable_xlink_href(href: &str) -> bool {
1589 (!href.is_empty()) && href.ne("urn:mpeg:dash:resolve-to-zero:2013")
1590}
1591
1592fn element_resolves_to_zero(xot: &mut Xot, element: xot::Node) -> bool {
1593 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
1594 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
1595 if let Some(href) = xot.get_attribute(element, xlink_href_name.into()) {
1596 return href.eq("urn:mpeg:dash:resolve-to-zero:2013");
1597 }
1598 false
1599}
1600
1601fn skip_xml_preamble(input: &str) -> &str {
1602 if input.starts_with("<?xml") {
1603 if let Some(end_pos) = input.find("?>") {
1604 return &input[end_pos + 2..]; }
1607 }
1608 input
1610}
1611
1612fn apply_xslt_stylesheets_xsltproc(
1616 downloader: &DashDownloader,
1617 xot: &mut Xot,
1618 doc: xot::Node) -> Result<String, DashMpdError> {
1619 let mut buf = Vec::new();
1620 xot.write(doc, &mut buf)
1621 .map_err(|e| parse_error("serializing rewritten manifest", e))?;
1622 for ss in &downloader.xslt_stylesheets {
1623 if downloader.verbosity > 0 {
1624 info!(" Applying XSLT stylesheet {} with xsltproc", ss.display());
1625 }
1626 let tmpmpd = tmp_file_path("dashxslt", OsStr::new("xslt"))?;
1627 fs::write(&tmpmpd, &buf)
1628 .map_err(|e| DashMpdError::Io(e, String::from("writing MPD")))?;
1629 let xsltproc = Command::new("xsltproc")
1630 .args([ss, &tmpmpd])
1631 .output()
1632 .map_err(|e| DashMpdError::Io(e, String::from("spawning xsltproc")))?;
1633 if !xsltproc.status.success() {
1634 let msg = format!("xsltproc returned {}", xsltproc.status);
1635 let out = partial_process_output(&xsltproc.stderr).to_string();
1636 return Err(DashMpdError::Io(std::io::Error::other(msg), out));
1637 }
1638 if env::var("DASHMPD_PERSIST_FILES").is_err() {
1639 if let Err(e) = fs::remove_file(&tmpmpd) {
1640 warn!("Error removing temporary MPD after XSLT processing: {e:?}");
1641 }
1642 }
1643 buf.clone_from(&xsltproc.stdout);
1644 }
1645 String::from_utf8(buf)
1646 .map_err(|e| parse_error("parsing UTF-8", e))
1647}
1648
1649async fn resolve_xlink_references(
1684 downloader: &DashDownloader,
1685 xot: &mut Xot,
1686 node: xot::Node) -> Result<(), DashMpdError>
1687{
1688 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
1689 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
1690 let xlinked = xot.descendants(node)
1691 .filter(|d| xot.get_attribute(*d, xlink_href_name.into()).is_some())
1692 .collect::<Vec<_>>();
1693 for xl in xlinked {
1694 if element_resolves_to_zero(xot, xl) {
1695 trace!("Removing node with resolve-to-zero xlink:href {xl:?}");
1696 if let Err(e) = xot.remove(xl) {
1697 return Err(parse_error("Failed to remove resolve-to-zero XML node", e));
1698 }
1699 } else if let Some(href) = xot.get_attribute(xl, xlink_href_name.into()) {
1700 if fetchable_xlink_href(href) {
1701 let xlink_url = if is_absolute_url(href) {
1702 Url::parse(href)
1703 .map_err(|e|
1704 if let Ok(ns) = xot.to_string(node) {
1705 parse_error(&format!("parsing XLink on {ns}"), e)
1706 } else {
1707 parse_error("parsing XLink", e)
1708 }
1709 )?
1710 } else {
1711 let mut merged = downloader.redirected_url.join(href)
1714 .map_err(|e|
1715 if let Ok(ns) = xot.to_string(node) {
1716 parse_error(&format!("parsing XLink on {ns}"), e)
1717 } else {
1718 parse_error("parsing XLink", e)
1719 }
1720 )?;
1721 merged.set_query(downloader.redirected_url.query());
1722 merged
1723 };
1724 let client = downloader.http_client.as_ref().unwrap();
1725 trace!("Fetching XLinked element {}", xlink_url.clone());
1726 let mut req = client.get(xlink_url.clone())
1727 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
1728 .header("Accept-Language", "en-US,en")
1729 .header("Sec-Fetch-Mode", "navigate");
1730 if let Some(referer) = &downloader.referer {
1731 req = req.header("Referer", referer);
1732 } else {
1733 req = req.header("Referer", downloader.redirected_url.to_string());
1734 }
1735 if let Some(username) = &downloader.auth_username {
1736 if let Some(password) = &downloader.auth_password {
1737 req = req.basic_auth(username, Some(password));
1738 }
1739 }
1740 if let Some(token) = &downloader.auth_bearer_token {
1741 req = req.bearer_auth(token);
1742 }
1743 let xml = req.send().await
1744 .map_err(|e|
1745 if let Ok(ns) = xot.to_string(node) {
1746 network_error(&format!("fetching XLink for {ns}"), e)
1747 } else {
1748 network_error("fetching XLink", e)
1749 }
1750 )?
1751 .error_for_status()
1752 .map_err(|e|
1753 if let Ok(ns) = xot.to_string(node) {
1754 network_error(&format!("fetching XLink for {ns}"), e)
1755 } else {
1756 network_error("fetching XLink", e)
1757 }
1758 )?
1759 .text().await
1760 .map_err(|e|
1761 if let Ok(ns) = xot.to_string(node) {
1762 network_error(&format!("resolving XLink for {ns}"), e)
1763 } else {
1764 network_error("resolving XLink", e)
1765 }
1766 )?;
1767 if downloader.verbosity > 2 {
1768 if let Ok(ns) = xot.to_string(node) {
1769 info!(" Resolved onLoad XLink {xlink_url} on {ns} -> {} octets", xml.len());
1770 } else {
1771 info!(" Resolved onLoad XLink {xlink_url} -> {} octets", xml.len());
1772 }
1773 }
1774 let wrapped_xml = r#"<?xml version="1.0" encoding="utf-8"?>"#.to_owned() +
1780 r#"<wrapper xmlns="urn:mpeg:dash:schema:mpd:2011" "# +
1781 r#"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" "# +
1782 r#"xmlns:cenc="urn:mpeg:cenc:2013" "# +
1783 r#"xmlns:mspr="urn:microsoft:playready" "# +
1784 r#"xmlns:xlink="http://www.w3.org/1999/xlink">"# +
1785 skip_xml_preamble(&xml) +
1786 r#"</wrapper>"#;
1787 let wrapper_doc = xot.parse(&wrapped_xml)
1788 .map_err(|e| parse_error("parsing xlinked content", e))?;
1789 let wrapper_doc_el = xot.document_element(wrapper_doc)
1790 .map_err(|e| parse_error("extracting XML document element", e))?;
1791 for needs_insertion in xot.children(wrapper_doc_el).collect::<Vec<_>>() {
1792 xot.insert_after(xl, needs_insertion)
1794 .map_err(|e| parse_error("inserting XLinked content", e))?;
1795 }
1796 xot.remove(xl)
1797 .map_err(|e| parse_error("removing XLink node", e))?;
1798 }
1799 }
1800 }
1801 Ok(())
1802}
1803
1804#[tracing::instrument(level="trace", skip_all)]
1805pub async fn parse_resolving_xlinks(
1806 downloader: &DashDownloader,
1807 xml: &[u8]) -> Result<MPD, DashMpdError>
1808{
1809 use xot::xmlname::NameStrInfo;
1810
1811 let mut xot = Xot::new();
1812 let doc = xot.parse_bytes(xml)
1813 .map_err(|e| parse_error("XML parsing", e))?;
1814 let doc_el = xot.document_element(doc)
1815 .map_err(|e| parse_error("extracting XML document element", e))?;
1816 let doc_name = match xot.node_name(doc_el) {
1817 Some(n) => n,
1818 None => return Err(DashMpdError::Parsing(String::from("missing root node name"))),
1819 };
1820 let root_name = xot.name_ref(doc_name, doc_el)
1821 .map_err(|e| parse_error("extracting root node name", e))?;
1822 let root_local_name = root_name.local_name();
1823 if !root_local_name.eq("MPD") {
1824 return Err(DashMpdError::Parsing(format!("root element is {}, expecting <MPD>", root_local_name)));
1825 }
1826 for _ in 1..5 {
1829 resolve_xlink_references(downloader, &mut xot, doc).await?;
1830 }
1831 let rewritten = apply_xslt_stylesheets_xsltproc(downloader, &mut xot, doc)?;
1832 let mpd = parse(&rewritten)?;
1834 if downloader.conformity_checks {
1835 for emsg in check_conformity(&mpd) {
1836 warn!("DASH conformity error in manifest: {emsg}");
1837 }
1838 }
1839 Ok(mpd)
1840}
1841
1842async fn do_segmentbase_indexrange(
1843 downloader: &DashDownloader,
1844 period_counter: u8,
1845 base_url: Url,
1846 sb: &SegmentBase,
1847 dict: &HashMap<&str, String>
1848) -> Result<Vec<MediaFragment>, DashMpdError>
1849{
1850 let mut fragments = Vec::new();
1883 let mut start_byte: Option<u64> = None;
1884 let mut end_byte: Option<u64> = None;
1885 let mut indexable_segments = false;
1886 if downloader.use_index_range {
1887 if let Some(ir) = &sb.indexRange {
1888 let (s, e) = parse_range(ir)?;
1890 trace!("Fetching sidx for {}", base_url.clone());
1891 let mut req = downloader.http_client.as_ref()
1892 .unwrap()
1893 .get(base_url.clone())
1894 .header(RANGE, format!("bytes={s}-{e}"))
1895 .header("Referer", downloader.redirected_url.to_string())
1896 .header("Sec-Fetch-Mode", "navigate");
1897 if let Some(username) = &downloader.auth_username {
1898 if let Some(password) = &downloader.auth_password {
1899 req = req.basic_auth(username, Some(password));
1900 }
1901 }
1902 if let Some(token) = &downloader.auth_bearer_token {
1903 req = req.bearer_auth(token);
1904 }
1905 let mut resp = req.send().await
1906 .map_err(|e| network_error("fetching index data", e))?
1907 .error_for_status()
1908 .map_err(|e| network_error("fetching index data", e))?;
1909 let headers = std::mem::take(resp.headers_mut());
1910 if let Some(content_type) = headers.get(CONTENT_TYPE) {
1911 let idx = resp.bytes().await
1912 .map_err(|e| network_error("fetching index data", e))?;
1913 if idx.len() as u64 != e - s + 1 {
1914 warn!(" HTTP server does not support Range requests; can't use indexRange addressing");
1915 } else {
1916 #[allow(clippy::collapsible_else_if)]
1917 if content_type.eq("video/mp4") ||
1918 content_type.eq("audio/mp4") {
1919 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
1926 .with_range(Some(0), Some(e))
1927 .build();
1928 fragments.push(mf);
1929 let mut max_chunk_pos = 0;
1930 if let Ok(segment_chunks) = crate::sidx::from_isobmff_sidx(&idx, e+1) {
1931 trace!("Have {} segment chunks in sidx data", segment_chunks.len());
1932 for chunk in segment_chunks {
1933 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
1934 .with_range(Some(chunk.start), Some(chunk.end))
1935 .build();
1936 fragments.push(mf);
1937 if chunk.end > max_chunk_pos {
1938 max_chunk_pos = chunk.end;
1939 }
1940 }
1941 indexable_segments = true;
1942 }
1943 }
1944 }
1951 }
1952 }
1953 }
1954 if indexable_segments {
1955 if let Some(init) = &sb.Initialization {
1956 if let Some(range) = &init.range {
1957 let (s, e) = parse_range(range)?;
1958 start_byte = Some(s);
1959 end_byte = Some(e);
1960 }
1961 if let Some(su) = &init.sourceURL {
1962 let path = resolve_url_template(su, dict);
1963 let u = merge_baseurls(&base_url, &path)?;
1964 let mf = MediaFragmentBuilder::new(period_counter, u)
1965 .with_range(start_byte, end_byte)
1966 .set_init()
1967 .build();
1968 fragments.push(mf);
1969 } else {
1970 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
1972 .with_range(start_byte, end_byte)
1973 .set_init()
1974 .build();
1975 fragments.push(mf);
1976 }
1977 }
1978 } else {
1979 trace!("Falling back to retrieving full SegmentBase for {}", base_url.clone());
1984 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
1985 .with_timeout(Duration::new(10_000, 0))
1986 .build();
1987 fragments.push(mf);
1988 }
1989 Ok(fragments)
1990}
1991
1992
1993#[tracing::instrument(level="trace", skip_all)]
1994async fn do_period_audio(
1995 downloader: &DashDownloader,
1996 mpd: &MPD,
1997 period: &Period,
1998 period_counter: u8,
1999 base_url: Url
2000 ) -> Result<PeriodOutputs, DashMpdError>
2001{
2002 let mut fragments = Vec::new();
2003 let mut diagnostics = Vec::new();
2004 let mut opt_init: Option<String> = None;
2005 let mut opt_media: Option<String> = None;
2006 let mut opt_duration: Option<f64> = None;
2007 let mut timescale = 1;
2008 let mut start_number = 1;
2009 let mut period_duration_secs: f64 = 0.0;
2012 if let Some(d) = mpd.mediaPresentationDuration {
2013 period_duration_secs = d.as_secs_f64();
2014 }
2015 if let Some(d) = period.duration {
2016 period_duration_secs = d.as_secs_f64();
2017 }
2018 if let Some(s) = downloader.force_duration {
2019 period_duration_secs = s;
2020 }
2021 if let Some(st) = &period.SegmentTemplate {
2025 if let Some(i) = &st.initialization {
2026 opt_init = Some(i.to_string());
2027 }
2028 if let Some(m) = &st.media {
2029 opt_media = Some(m.to_string());
2030 }
2031 if let Some(d) = st.duration {
2032 opt_duration = Some(d);
2033 }
2034 if let Some(ts) = st.timescale {
2035 timescale = ts;
2036 }
2037 if let Some(s) = st.startNumber {
2038 start_number = s;
2039 }
2040 }
2041 let audio_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2044 .filter(is_audio_adaptation)
2045 .collect();
2046 let representations: Vec<&Representation> = select_preferred_adaptations(audio_adaptations, downloader)
2047 .iter()
2048 .flat_map(|a| a.representations.iter())
2049 .collect();
2050 if let Some(audio_repr) = select_preferred_representation(representations, downloader) {
2051 let audio_adaptation = period.adaptations.iter()
2055 .find(|a| a.representations.iter().any(|r| r.eq(audio_repr)))
2056 .unwrap();
2057 let mut base_url = base_url.clone();
2060 if let Some(bu) = &audio_adaptation.BaseURL.first() {
2061 base_url = merge_baseurls(&base_url, &bu.base)?;
2062 }
2063 if let Some(bu) = audio_repr.BaseURL.first() {
2064 base_url = merge_baseurls(&base_url, &bu.base)?;
2065 }
2066 if downloader.verbosity > 0 {
2067 let bw = if let Some(bw) = audio_repr.bandwidth {
2068 format!("bw={} Kbps ", bw / 1024)
2069 } else {
2070 String::from("")
2071 };
2072 let unknown = String::from("?");
2073 let lang = audio_repr.lang.as_ref()
2074 .unwrap_or(audio_adaptation.lang.as_ref()
2075 .unwrap_or(&unknown));
2076 let codec = audio_repr.codecs.as_ref()
2077 .unwrap_or(audio_adaptation.codecs.as_ref()
2078 .unwrap_or(&unknown));
2079 diagnostics.push(format!(" Audio stream selected: {bw}lang={lang} codec={codec}"));
2080 for cp in audio_repr.ContentProtection.iter()
2082 .chain(audio_adaptation.ContentProtection.iter())
2083 {
2084 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2085 if let Some(kid) = &cp.default_KID {
2086 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2087 }
2088 for pssh_element in cp.cenc_pssh.iter() {
2089 if let Some(pssh_b64) = &pssh_element.content {
2090 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
2091 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
2092 diagnostics.push(format!(" {pssh}"));
2093 }
2094 }
2095 }
2096 }
2097 }
2098 if let Some(st) = &audio_adaptation.SegmentTemplate {
2103 if let Some(i) = &st.initialization {
2104 opt_init = Some(i.to_string());
2105 }
2106 if let Some(m) = &st.media {
2107 opt_media = Some(m.to_string());
2108 }
2109 if let Some(d) = st.duration {
2110 opt_duration = Some(d);
2111 }
2112 if let Some(ts) = st.timescale {
2113 timescale = ts;
2114 }
2115 if let Some(s) = st.startNumber {
2116 start_number = s;
2117 }
2118 }
2119 let mut dict = HashMap::new();
2120 if let Some(rid) = &audio_repr.id {
2121 dict.insert("RepresentationID", rid.to_string());
2122 }
2123 if let Some(b) = &audio_repr.bandwidth {
2124 dict.insert("Bandwidth", b.to_string());
2125 }
2126 if let Some(sl) = &audio_adaptation.SegmentList {
2135 if downloader.verbosity > 1 {
2138 info!(" {}", "Using AdaptationSet>SegmentList addressing mode for audio representation".italic());
2139 }
2140 let mut start_byte: Option<u64> = None;
2141 let mut end_byte: Option<u64> = None;
2142 if let Some(init) = &sl.Initialization {
2143 if let Some(range) = &init.range {
2144 let (s, e) = parse_range(range)?;
2145 start_byte = Some(s);
2146 end_byte = Some(e);
2147 }
2148 if let Some(su) = &init.sourceURL {
2149 let path = resolve_url_template(su, &dict);
2150 let init_url = merge_baseurls(&base_url, &path)?;
2151 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2152 .with_range(start_byte, end_byte)
2153 .set_init()
2154 .build();
2155 fragments.push(mf);
2156 } else {
2157 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2158 .with_range(start_byte, end_byte)
2159 .set_init()
2160 .build();
2161 fragments.push(mf);
2162 }
2163 }
2164 for su in sl.segment_urls.iter() {
2165 start_byte = None;
2166 end_byte = None;
2167 if let Some(range) = &su.mediaRange {
2169 let (s, e) = parse_range(range)?;
2170 start_byte = Some(s);
2171 end_byte = Some(e);
2172 }
2173 if let Some(m) = &su.media {
2174 let u = merge_baseurls(&base_url, m)?;
2175 let mf = MediaFragmentBuilder::new(period_counter, u)
2176 .with_range(start_byte, end_byte)
2177 .build();
2178 fragments.push(mf);
2179 } else if let Some(bu) = audio_adaptation.BaseURL.first() {
2180 let u = merge_baseurls(&base_url, &bu.base)?;
2181 let mf = MediaFragmentBuilder::new(period_counter, u)
2182 .with_range(start_byte, end_byte)
2183 .build();
2184 fragments.push(mf);
2185 }
2186 }
2187 }
2188 if let Some(sl) = &audio_repr.SegmentList {
2189 if downloader.verbosity > 1 {
2191 info!(" {}", "Using Representation>SegmentList addressing mode for audio representation".italic());
2192 }
2193 let mut start_byte: Option<u64> = None;
2194 let mut end_byte: Option<u64> = None;
2195 if let Some(init) = &sl.Initialization {
2196 if let Some(range) = &init.range {
2197 let (s, e) = parse_range(range)?;
2198 start_byte = Some(s);
2199 end_byte = Some(e);
2200 }
2201 if let Some(su) = &init.sourceURL {
2202 let path = resolve_url_template(su, &dict);
2203 let init_url = merge_baseurls(&base_url, &path)?;
2204 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2205 .with_range(start_byte, end_byte)
2206 .set_init()
2207 .build();
2208 fragments.push(mf);
2209 } else {
2210 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2211 .with_range(start_byte, end_byte)
2212 .set_init()
2213 .build();
2214 fragments.push(mf);
2215 }
2216 }
2217 for su in sl.segment_urls.iter() {
2218 start_byte = None;
2219 end_byte = None;
2220 if let Some(range) = &su.mediaRange {
2222 let (s, e) = parse_range(range)?;
2223 start_byte = Some(s);
2224 end_byte = Some(e);
2225 }
2226 if let Some(m) = &su.media {
2227 let u = merge_baseurls(&base_url, m)?;
2228 let mf = MediaFragmentBuilder::new(period_counter, u)
2229 .with_range(start_byte, end_byte)
2230 .build();
2231 fragments.push(mf);
2232 } else if let Some(bu) = audio_repr.BaseURL.first() {
2233 let u = merge_baseurls(&base_url, &bu.base)?;
2234 let mf = MediaFragmentBuilder::new(period_counter, u)
2235 .with_range(start_byte, end_byte)
2236 .build();
2237 fragments.push(mf);
2238 }
2239 }
2240 } else if audio_repr.SegmentTemplate.is_some() ||
2241 audio_adaptation.SegmentTemplate.is_some()
2242 {
2243 let st;
2246 if let Some(it) = &audio_repr.SegmentTemplate {
2247 st = it;
2248 } else if let Some(it) = &audio_adaptation.SegmentTemplate {
2249 st = it;
2250 } else {
2251 panic!("unreachable");
2252 }
2253 if let Some(i) = &st.initialization {
2254 opt_init = Some(i.to_string());
2255 }
2256 if let Some(m) = &st.media {
2257 opt_media = Some(m.to_string());
2258 }
2259 if let Some(ts) = st.timescale {
2260 timescale = ts;
2261 }
2262 if let Some(sn) = st.startNumber {
2263 start_number = sn;
2264 }
2265 if let Some(stl) = &audio_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
2266 .or(audio_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
2267 {
2268 if downloader.verbosity > 1 {
2271 info!(" {}", "Using SegmentTemplate+SegmentTimeline addressing mode for audio representation".italic());
2272 }
2273 if let Some(init) = opt_init {
2274 let path = resolve_url_template(&init, &dict);
2275 let u = merge_baseurls(&base_url, &path)?;
2276 let mf = MediaFragmentBuilder::new(period_counter, u)
2277 .set_init()
2278 .build();
2279 fragments.push(mf);
2280 }
2281 if let Some(media) = opt_media {
2282 let audio_path = resolve_url_template(&media, &dict);
2283 let mut segment_time = 0;
2284 let mut segment_duration;
2285 let mut number = start_number;
2286 for s in &stl.segments {
2287 if let Some(t) = s.t {
2288 segment_time = t;
2289 }
2290 segment_duration = s.d;
2291 let dict = HashMap::from([("Time", segment_time.to_string()),
2293 ("Number", number.to_string())]);
2294 let path = resolve_url_template(&audio_path, &dict);
2295 let u = merge_baseurls(&base_url, &path)?;
2296 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2297 number += 1;
2298 if let Some(r) = s.r {
2299 let mut count = 0i64;
2300 let end_time = period_duration_secs * timescale as f64;
2302 loop {
2303 count += 1;
2304 if r >= 0 {
2310 if count > r {
2311 break;
2312 }
2313 if downloader.force_duration.is_some() && segment_time as f64 > end_time {
2314 break;
2315 }
2316 } else if segment_time as f64 > end_time {
2317 break;
2318 }
2319 segment_time += segment_duration;
2320 let dict = HashMap::from([("Time", segment_time.to_string()),
2321 ("Number", number.to_string())]);
2322 let path = resolve_url_template(&audio_path, &dict);
2323 let u = merge_baseurls(&base_url, &path)?;
2324 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2325 number += 1;
2326 }
2327 }
2328 segment_time += segment_duration;
2329 }
2330 } else {
2331 return Err(DashMpdError::UnhandledMediaStream(
2332 "SegmentTimeline without a media attribute".to_string()));
2333 }
2334 } else { if downloader.verbosity > 1 {
2339 info!(" {}", "Using SegmentTemplate addressing mode for audio representation".italic());
2340 }
2341 let mut total_number = 0i64;
2342 if let Some(init) = opt_init {
2343 let path = resolve_url_template(&init, &dict);
2344 let u = merge_baseurls(&base_url, &path)?;
2345 let mf = MediaFragmentBuilder::new(period_counter, u)
2346 .set_init()
2347 .build();
2348 fragments.push(mf);
2349 }
2350 if let Some(media) = opt_media {
2351 let audio_path = resolve_url_template(&media, &dict);
2352 let timescale = st.timescale.unwrap_or(timescale);
2353 let mut segment_duration: f64 = -1.0;
2354 if let Some(d) = opt_duration {
2355 segment_duration = d;
2357 }
2358 if let Some(std) = st.duration {
2359 segment_duration = std / timescale as f64;
2360 }
2361 if segment_duration < 0.0 {
2362 return Err(DashMpdError::UnhandledMediaStream(
2363 "Audio representation is missing SegmentTemplate@duration attribute".to_string()));
2364 }
2365 total_number += (period_duration_secs / segment_duration).round() as i64;
2366 let mut number = start_number;
2367 for _ in 1..=total_number {
2368 let dict = HashMap::from([("Number", number.to_string())]);
2369 let path = resolve_url_template(&audio_path, &dict);
2370 let u = merge_baseurls(&base_url, &path)?;
2371 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2372 number += 1;
2373 }
2374 }
2375 }
2376 } else if let Some(sb) = &audio_repr.SegmentBase {
2377 if downloader.verbosity > 1 {
2379 info!(" {}", "Using SegmentBase@indexRange addressing mode for audio representation".italic());
2380 }
2381 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
2382 fragments.extend(mf);
2383 } else if fragments.is_empty() {
2384 if let Some(bu) = audio_repr.BaseURL.first() {
2385 if downloader.verbosity > 1 {
2387 info!(" {}", "Using BaseURL addressing mode for audio representation".italic());
2388 }
2389 let u = merge_baseurls(&base_url, &bu.base)?;
2390 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2391 }
2392 }
2393 if fragments.is_empty() {
2394 return Err(DashMpdError::UnhandledMediaStream(
2395 "no usable addressing mode identified for audio representation".to_string()));
2396 }
2397 }
2398 Ok(PeriodOutputs { fragments, diagnostics, subtitle_formats: Vec::new() })
2399}
2400
2401
2402#[tracing::instrument(level="trace", skip_all)]
2403async fn do_period_video(
2404 downloader: &DashDownloader,
2405 mpd: &MPD,
2406 period: &Period,
2407 period_counter: u8,
2408 base_url: Url
2409 ) -> Result<PeriodOutputs, DashMpdError>
2410{
2411 let mut fragments = Vec::new();
2412 let mut diagnostics = Vec::new();
2413 let mut period_duration_secs: f64 = 0.0;
2414 let mut opt_init: Option<String> = None;
2415 let mut opt_media: Option<String> = None;
2416 let mut opt_duration: Option<f64> = None;
2417 let mut timescale = 1;
2418 let mut start_number = 1;
2419 if let Some(d) = mpd.mediaPresentationDuration {
2420 period_duration_secs = d.as_secs_f64();
2421 }
2422 if let Some(d) = period.duration {
2423 period_duration_secs = d.as_secs_f64();
2424 }
2425 if let Some(s) = downloader.force_duration {
2426 period_duration_secs = s;
2427 }
2428 if let Some(st) = &period.SegmentTemplate {
2432 if let Some(i) = &st.initialization {
2433 opt_init = Some(i.to_string());
2434 }
2435 if let Some(m) = &st.media {
2436 opt_media = Some(m.to_string());
2437 }
2438 if let Some(d) = st.duration {
2439 opt_duration = Some(d);
2440 }
2441 if let Some(ts) = st.timescale {
2442 timescale = ts;
2443 }
2444 if let Some(s) = st.startNumber {
2445 start_number = s;
2446 }
2447 }
2448 let video_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2455 .filter(is_video_adaptation)
2456 .collect();
2457 let representations: Vec<&Representation> = select_preferred_adaptations(video_adaptations, downloader)
2458 .iter()
2459 .flat_map(|a| a.representations.iter())
2460 .collect();
2461 let maybe_video_repr = if let Some(want) = downloader.video_width_preference {
2462 representations.iter()
2463 .min_by_key(|x| if let Some(w) = x.width { want.abs_diff(w) } else { u64::MAX })
2464 .copied()
2465 } else if let Some(want) = downloader.video_height_preference {
2466 representations.iter()
2467 .min_by_key(|x| if let Some(h) = x.height { want.abs_diff(h) } else { u64::MAX })
2468 .copied()
2469 } else {
2470 select_preferred_representation(representations, downloader)
2471 };
2472 if let Some(video_repr) = maybe_video_repr {
2473 let video_adaptation = period.adaptations.iter()
2477 .find(|a| a.representations.iter().any(|r| r.eq(video_repr)))
2478 .unwrap();
2479 let mut base_url = base_url.clone();
2482 if let Some(bu) = &video_adaptation.BaseURL.first() {
2483 base_url = merge_baseurls(&base_url, &bu.base)?;
2484 }
2485 if let Some(bu) = &video_repr.BaseURL.first() {
2486 base_url = merge_baseurls(&base_url, &bu.base)?;
2487 }
2488 if downloader.verbosity > 0 {
2489 let bw = if let Some(bw) = video_repr.bandwidth.or(video_adaptation.maxBandwidth) {
2490 format!("bw={} Kbps ", bw / 1024)
2491 } else {
2492 String::from("")
2493 };
2494 let unknown = String::from("?");
2495 let w = video_repr.width.unwrap_or(video_adaptation.width.unwrap_or(0));
2496 let h = video_repr.height.unwrap_or(video_adaptation.height.unwrap_or(0));
2497 let fmt = if w == 0 || h == 0 {
2498 String::from("")
2499 } else {
2500 format!("resolution={w}x{h} ")
2501 };
2502 let codec = video_repr.codecs.as_ref()
2503 .unwrap_or(video_adaptation.codecs.as_ref().unwrap_or(&unknown));
2504 diagnostics.push(format!(" Video stream selected: {bw}{fmt}codec={codec}"));
2505 for cp in video_repr.ContentProtection.iter()
2507 .chain(video_adaptation.ContentProtection.iter())
2508 {
2509 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2510 if let Some(kid) = &cp.default_KID {
2511 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2512 }
2513 for pssh_element in cp.cenc_pssh.iter() {
2514 if let Some(pssh_b64) = &pssh_element.content {
2515 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
2516 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
2517 diagnostics.push(format!(" {pssh}"));
2518 }
2519 }
2520 }
2521 }
2522 }
2523 let mut dict = HashMap::new();
2524 if let Some(rid) = &video_repr.id {
2525 dict.insert("RepresentationID", rid.to_string());
2526 }
2527 if let Some(b) = &video_repr.bandwidth {
2528 dict.insert("Bandwidth", b.to_string());
2529 }
2530 if let Some(st) = &video_adaptation.SegmentTemplate {
2535 if let Some(i) = &st.initialization {
2536 opt_init = Some(i.to_string());
2537 }
2538 if let Some(m) = &st.media {
2539 opt_media = Some(m.to_string());
2540 }
2541 if let Some(d) = st.duration {
2542 opt_duration = Some(d);
2543 }
2544 if let Some(ts) = st.timescale {
2545 timescale = ts;
2546 }
2547 if let Some(s) = st.startNumber {
2548 start_number = s;
2549 }
2550 }
2551 if let Some(sl) = &video_adaptation.SegmentList {
2555 if downloader.verbosity > 1 {
2557 info!(" {}", "Using AdaptationSet>SegmentList addressing mode for video representation".italic());
2558 }
2559 let mut start_byte: Option<u64> = None;
2560 let mut end_byte: Option<u64> = None;
2561 if let Some(init) = &sl.Initialization {
2562 if let Some(range) = &init.range {
2563 let (s, e) = parse_range(range)?;
2564 start_byte = Some(s);
2565 end_byte = Some(e);
2566 }
2567 if let Some(su) = &init.sourceURL {
2568 let path = resolve_url_template(su, &dict);
2569 let u = merge_baseurls(&base_url, &path)?;
2570 let mf = MediaFragmentBuilder::new(period_counter, u)
2571 .with_range(start_byte, end_byte)
2572 .set_init()
2573 .build();
2574 fragments.push(mf);
2575 }
2576 } else {
2577 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2578 .with_range(start_byte, end_byte)
2579 .set_init()
2580 .build();
2581 fragments.push(mf);
2582 }
2583 for su in sl.segment_urls.iter() {
2584 start_byte = None;
2585 end_byte = None;
2586 if let Some(range) = &su.mediaRange {
2588 let (s, e) = parse_range(range)?;
2589 start_byte = Some(s);
2590 end_byte = Some(e);
2591 }
2592 if let Some(m) = &su.media {
2593 let u = merge_baseurls(&base_url, m)?;
2594 let mf = MediaFragmentBuilder::new(period_counter, u)
2595 .with_range(start_byte, end_byte)
2596 .build();
2597 fragments.push(mf);
2598 } else if let Some(bu) = video_adaptation.BaseURL.first() {
2599 let u = merge_baseurls(&base_url, &bu.base)?;
2600 let mf = MediaFragmentBuilder::new(period_counter, u)
2601 .with_range(start_byte, end_byte)
2602 .build();
2603 fragments.push(mf);
2604 }
2605 }
2606 }
2607 if let Some(sl) = &video_repr.SegmentList {
2608 if downloader.verbosity > 1 {
2610 info!(" {}", "Using Representation>SegmentList addressing mode for video representation".italic());
2611 }
2612 let mut start_byte: Option<u64> = None;
2613 let mut end_byte: Option<u64> = None;
2614 if let Some(init) = &sl.Initialization {
2615 if let Some(range) = &init.range {
2616 let (s, e) = parse_range(range)?;
2617 start_byte = Some(s);
2618 end_byte = Some(e);
2619 }
2620 if let Some(su) = &init.sourceURL {
2621 let path = resolve_url_template(su, &dict);
2622 let u = merge_baseurls(&base_url, &path)?;
2623 let mf = MediaFragmentBuilder::new(period_counter, u)
2624 .with_range(start_byte, end_byte)
2625 .set_init()
2626 .build();
2627 fragments.push(mf);
2628 } else {
2629 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2630 .with_range(start_byte, end_byte)
2631 .set_init()
2632 .build();
2633 fragments.push(mf);
2634 }
2635 }
2636 for su in sl.segment_urls.iter() {
2637 start_byte = None;
2638 end_byte = None;
2639 if let Some(range) = &su.mediaRange {
2641 let (s, e) = parse_range(range)?;
2642 start_byte = Some(s);
2643 end_byte = Some(e);
2644 }
2645 if let Some(m) = &su.media {
2646 let u = merge_baseurls(&base_url, m)?;
2647 let mf = MediaFragmentBuilder::new(period_counter, u)
2648 .with_range(start_byte, end_byte)
2649 .build();
2650 fragments.push(mf);
2651 } else if let Some(bu) = video_repr.BaseURL.first() {
2652 let u = merge_baseurls(&base_url, &bu.base)?;
2653 let mf = MediaFragmentBuilder::new(period_counter, u)
2654 .with_range(start_byte, end_byte)
2655 .build();
2656 fragments.push(mf);
2657 }
2658 }
2659 } else if video_repr.SegmentTemplate.is_some() ||
2660 video_adaptation.SegmentTemplate.is_some() {
2661 let st;
2664 if let Some(it) = &video_repr.SegmentTemplate {
2665 st = it;
2666 } else if let Some(it) = &video_adaptation.SegmentTemplate {
2667 st = it;
2668 } else {
2669 panic!("impossible");
2670 }
2671 if let Some(i) = &st.initialization {
2672 opt_init = Some(i.to_string());
2673 }
2674 if let Some(m) = &st.media {
2675 opt_media = Some(m.to_string());
2676 }
2677 if let Some(ts) = st.timescale {
2678 timescale = ts;
2679 }
2680 if let Some(sn) = st.startNumber {
2681 start_number = sn;
2682 }
2683 if let Some(stl) = &video_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
2684 .or(video_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
2685 {
2686 if downloader.verbosity > 1 {
2688 info!(" {}", "Using SegmentTemplate+SegmentTimeline addressing mode for video representation".italic());
2689 }
2690 if let Some(init) = opt_init {
2691 let path = resolve_url_template(&init, &dict);
2692 let u = merge_baseurls(&base_url, &path)?;
2693 let mf = MediaFragmentBuilder::new(period_counter, u)
2694 .set_init()
2695 .build();
2696 fragments.push(mf);
2697 }
2698 if let Some(media) = opt_media {
2699 let video_path = resolve_url_template(&media, &dict);
2700 let mut segment_time = 0;
2701 let mut segment_duration;
2702 let mut number = start_number;
2703 for s in &stl.segments {
2707 if let Some(t) = s.t {
2708 segment_time = t;
2709 }
2710 segment_duration = s.d;
2711 let dict = HashMap::from([("Time", segment_time.to_string()),
2713 ("Number", number.to_string())]);
2714 let path = resolve_url_template(&video_path, &dict);
2715 let u = merge_baseurls(&base_url, &path)?;
2716 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2717 fragments.push(mf);
2718 number += 1;
2719 if let Some(r) = s.r {
2720 let mut count = 0i64;
2721 let end_time = period_duration_secs * timescale as f64;
2723 loop {
2724 count += 1;
2725 if r >= 0 {
2731 if count > r {
2732 break;
2733 }
2734 if downloader.force_duration.is_some() && segment_time as f64 > end_time {
2735 break;
2736 }
2737 } else if segment_time as f64 > end_time {
2738 break;
2739 }
2740 segment_time += segment_duration;
2741 let dict = HashMap::from([("Time", segment_time.to_string()),
2742 ("Number", number.to_string())]);
2743 let path = resolve_url_template(&video_path, &dict);
2744 let u = merge_baseurls(&base_url, &path)?;
2745 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2746 fragments.push(mf);
2747 number += 1;
2748 }
2749 }
2750 segment_time += segment_duration;
2751 }
2752 } else {
2753 return Err(DashMpdError::UnhandledMediaStream(
2754 "SegmentTimeline without a media attribute".to_string()));
2755 }
2756 } else { if downloader.verbosity > 1 {
2759 info!(" {}", "Using SegmentTemplate addressing mode for video representation".italic());
2760 }
2761 let mut total_number = 0i64;
2762 if let Some(init) = opt_init {
2763 let path = resolve_url_template(&init, &dict);
2764 let u = merge_baseurls(&base_url, &path)?;
2765 let mf = MediaFragmentBuilder::new(period_counter, u)
2766 .set_init()
2767 .build();
2768 fragments.push(mf);
2769 }
2770 if let Some(media) = opt_media {
2771 let video_path = resolve_url_template(&media, &dict);
2772 let timescale = st.timescale.unwrap_or(timescale);
2773 let mut segment_duration: f64 = -1.0;
2774 if let Some(d) = opt_duration {
2775 segment_duration = d;
2777 }
2778 if let Some(std) = st.duration {
2779 segment_duration = std / timescale as f64;
2780 }
2781 if segment_duration < 0.0 {
2782 return Err(DashMpdError::UnhandledMediaStream(
2783 "Video representation is missing SegmentTemplate@duration attribute".to_string()));
2784 }
2785 total_number += (period_duration_secs / segment_duration).round() as i64;
2786 let mut number = start_number;
2787 for _ in 1..=total_number {
2788 let dict = HashMap::from([("Number", number.to_string())]);
2789 let path = resolve_url_template(&video_path, &dict);
2790 let u = merge_baseurls(&base_url, &path)?;
2791 let mf = MediaFragmentBuilder::new(period_counter, u).build();
2792 fragments.push(mf);
2793 number += 1;
2794 }
2795 }
2796 }
2797 } else if let Some(sb) = &video_repr.SegmentBase {
2798 if downloader.verbosity > 1 {
2800 info!(" {}", "Using SegmentBase@indexRange addressing mode for video representation".italic());
2801 }
2802 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
2803 fragments.extend(mf);
2804 } else if fragments.is_empty() {
2805 if let Some(bu) = video_repr.BaseURL.first() {
2806 if downloader.verbosity > 1 {
2808 info!(" {}", "Using BaseURL addressing mode for video representation".italic());
2809 }
2810 let u = merge_baseurls(&base_url, &bu.base)?;
2811 let mf = MediaFragmentBuilder::new(period_counter, u)
2812 .with_timeout(Duration::new(10000, 0))
2813 .build();
2814 fragments.push(mf);
2815 }
2816 }
2817 if fragments.is_empty() {
2818 return Err(DashMpdError::UnhandledMediaStream(
2819 "no usable addressing mode identified for video representation".to_string()));
2820 }
2821 }
2822 Ok(PeriodOutputs { fragments, diagnostics, subtitle_formats: Vec::new() })
2825}
2826
2827#[tracing::instrument(level="trace", skip_all)]
2828async fn do_period_subtitles(
2829 downloader: &DashDownloader,
2830 mpd: &MPD,
2831 period: &Period,
2832 period_counter: u8,
2833 base_url: Url
2834 ) -> Result<PeriodOutputs, DashMpdError>
2835{
2836 let client = downloader.http_client.as_ref().unwrap();
2837 let output_path = &downloader.output_path.as_ref().unwrap().clone();
2838 let period_output_path = output_path_for_period(output_path, period_counter);
2839 let mut fragments = Vec::new();
2840 let mut subtitle_formats = Vec::new();
2841 let mut period_duration_secs: f64 = 0.0;
2842 if let Some(d) = mpd.mediaPresentationDuration {
2843 period_duration_secs = d.as_secs_f64();
2844 }
2845 if let Some(d) = period.duration {
2846 period_duration_secs = d.as_secs_f64();
2847 }
2848 let maybe_subtitle_adaptation = if let Some(ref lang) = downloader.language_preference {
2849 period.adaptations.iter().filter(is_subtitle_adaptation)
2850 .min_by_key(|a| adaptation_lang_distance(a, lang))
2851 } else {
2852 period.adaptations.iter().find(is_subtitle_adaptation)
2854 };
2855 if downloader.fetch_subtitles {
2856 if let Some(subtitle_adaptation) = maybe_subtitle_adaptation {
2857 let subtitle_format = subtitle_type(&subtitle_adaptation);
2858 subtitle_formats.push(subtitle_format);
2859 if downloader.verbosity > 1 && downloader.fetch_subtitles {
2860 info!(" Retrieving subtitles in format {subtitle_format:?}");
2861 }
2862 let mut base_url = base_url.clone();
2865 if let Some(bu) = &subtitle_adaptation.BaseURL.first() {
2866 base_url = merge_baseurls(&base_url, &bu.base)?;
2867 }
2868 if let Some(rep) = subtitle_adaptation.representations.first() {
2871 if !rep.BaseURL.is_empty() {
2872 for st_bu in rep.BaseURL.iter() {
2873 let st_url = merge_baseurls(&base_url, &st_bu.base)?;
2874 let mut req = client.get(st_url.clone());
2875 if let Some(referer) = &downloader.referer {
2876 req = req.header("Referer", referer);
2877 } else {
2878 req = req.header("Referer", base_url.to_string());
2879 }
2880 let rqw = req.build()
2881 .map_err(|e| network_error("building request", e))?;
2882 let subs = reqwest_bytes_with_retries(client, rqw, 5).await
2883 .map_err(|e| network_error("fetching subtitles", e))?;
2884 let mut subs_path = period_output_path.clone();
2885 let subtitle_format = subtitle_type(&subtitle_adaptation);
2886 match subtitle_format {
2887 SubtitleType::Vtt => subs_path.set_extension("vtt"),
2888 SubtitleType::Srt => subs_path.set_extension("srt"),
2889 SubtitleType::Ttml => subs_path.set_extension("ttml"),
2890 SubtitleType::Sami => subs_path.set_extension("sami"),
2891 SubtitleType::Wvtt => subs_path.set_extension("wvtt"),
2892 SubtitleType::Stpp => subs_path.set_extension("stpp"),
2893 _ => subs_path.set_extension("sub"),
2894 };
2895 subtitle_formats.push(subtitle_format);
2896 let mut subs_file = File::create(subs_path.clone())
2897 .map_err(|e| DashMpdError::Io(e, String::from("creating subtitle file")))?;
2898 if downloader.verbosity > 2 {
2899 info!(" Subtitle {st_url} -> {} octets", subs.len());
2900 }
2901 match subs_file.write_all(&subs) {
2902 Ok(()) => {
2903 if downloader.verbosity > 0 {
2904 info!(" Downloaded subtitles ({subtitle_format:?}) to {}",
2905 subs_path.display());
2906 }
2907 },
2908 Err(e) => {
2909 error!("Unable to write subtitle file: {e:?}");
2910 return Err(DashMpdError::Io(e, String::from("writing subtitle data")));
2911 },
2912 }
2913 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
2914 subtitle_formats.contains(&SubtitleType::Ttxt)
2915 {
2916 if downloader.verbosity > 0 {
2917 info!(" Converting subtitles to SRT format with MP4Box ");
2918 }
2919 let out = subs_path.with_extension("srt");
2920 let out_str = out.to_string_lossy();
2927 let subs_str = subs_path.to_string_lossy();
2928 let args = vec![
2929 "-srt", "1",
2930 "-out", &out_str,
2931 &subs_str];
2932 if downloader.verbosity > 0 {
2933 info!(" Running MPBox {}", args.join(" "));
2934 }
2935 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
2936 .args(args)
2937 .output()
2938 {
2939 let msg = partial_process_output(&mp4box.stdout);
2940 if !msg.is_empty() {
2941 info!("MP4Box stdout: {msg}");
2942 }
2943 let msg = partial_process_output(&mp4box.stderr);
2944 if !msg.is_empty() {
2945 info!("MP4Box stderr: {msg}");
2946 }
2947 if mp4box.status.success() {
2948 info!(" Converted subtitles to SRT");
2949 } else {
2950 warn!("Error running MP4Box to convert subtitles");
2951 }
2952 }
2953 }
2954 }
2955 } else if rep.SegmentTemplate.is_some() || subtitle_adaptation.SegmentTemplate.is_some() {
2956 let mut opt_init: Option<String> = None;
2957 let mut opt_media: Option<String> = None;
2958 let mut opt_duration: Option<f64> = None;
2959 let mut timescale = 1;
2960 let mut start_number = 1;
2961 if let Some(st) = &rep.SegmentTemplate {
2966 if let Some(i) = &st.initialization {
2967 opt_init = Some(i.to_string());
2968 }
2969 if let Some(m) = &st.media {
2970 opt_media = Some(m.to_string());
2971 }
2972 if let Some(d) = st.duration {
2973 opt_duration = Some(d);
2974 }
2975 if let Some(ts) = st.timescale {
2976 timescale = ts;
2977 }
2978 if let Some(s) = st.startNumber {
2979 start_number = s;
2980 }
2981 }
2982 let rid = match &rep.id {
2983 Some(id) => id,
2984 None => return Err(
2985 DashMpdError::UnhandledMediaStream(
2986 "Missing @id on Representation node".to_string())),
2987 };
2988 let mut dict = HashMap::from([("RepresentationID", rid.to_string())]);
2989 if let Some(b) = &rep.bandwidth {
2990 dict.insert("Bandwidth", b.to_string());
2991 }
2992 if let Some(sl) = &rep.SegmentList {
2996 if downloader.verbosity > 1 {
2999 info!(" {}", "Using AdaptationSet>SegmentList addressing mode for subtitle representation".italic());
3000 }
3001 let mut start_byte: Option<u64> = None;
3002 let mut end_byte: Option<u64> = None;
3003 if let Some(init) = &sl.Initialization {
3004 if let Some(range) = &init.range {
3005 let (s, e) = parse_range(range)?;
3006 start_byte = Some(s);
3007 end_byte = Some(e);
3008 }
3009 if let Some(su) = &init.sourceURL {
3010 let path = resolve_url_template(su, &dict);
3011 let u = merge_baseurls(&base_url, &path)?;
3012 let mf = MediaFragmentBuilder::new(period_counter, u)
3013 .with_range(start_byte, end_byte)
3014 .set_init()
3015 .build();
3016 fragments.push(mf);
3017 } else {
3018 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3019 .with_range(start_byte, end_byte)
3020 .set_init()
3021 .build();
3022 fragments.push(mf);
3023 }
3024 }
3025 for su in sl.segment_urls.iter() {
3026 start_byte = None;
3027 end_byte = None;
3028 if let Some(range) = &su.mediaRange {
3030 let (s, e) = parse_range(range)?;
3031 start_byte = Some(s);
3032 end_byte = Some(e);
3033 }
3034 if let Some(m) = &su.media {
3035 let u = merge_baseurls(&base_url, m)?;
3036 let mf = MediaFragmentBuilder::new(period_counter, u)
3037 .with_range(start_byte, end_byte)
3038 .build();
3039 fragments.push(mf);
3040 } else if let Some(bu) = subtitle_adaptation.BaseURL.first() {
3041 let u = merge_baseurls(&base_url, &bu.base)?;
3042 let mf = MediaFragmentBuilder::new(period_counter, u)
3043 .with_range(start_byte, end_byte)
3044 .build();
3045 fragments.push(mf);
3046 }
3047 }
3048 }
3049 if let Some(sl) = &rep.SegmentList {
3050 if downloader.verbosity > 1 {
3052 info!(" {}", "Using Representation>SegmentList addressing mode for subtitle representation".italic());
3053 }
3054 let mut start_byte: Option<u64> = None;
3055 let mut end_byte: Option<u64> = None;
3056 if let Some(init) = &sl.Initialization {
3057 if let Some(range) = &init.range {
3058 let (s, e) = parse_range(range)?;
3059 start_byte = Some(s);
3060 end_byte = Some(e);
3061 }
3062 if let Some(su) = &init.sourceURL {
3063 let path = resolve_url_template(su, &dict);
3064 let u = merge_baseurls(&base_url, &path)?;
3065 let mf = MediaFragmentBuilder::new(period_counter, u)
3066 .with_range(start_byte, end_byte)
3067 .set_init()
3068 .build();
3069 fragments.push(mf);
3070 } else {
3071 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3072 .with_range(start_byte, end_byte)
3073 .set_init()
3074 .build();
3075 fragments.push(mf);
3076 }
3077 }
3078 for su in sl.segment_urls.iter() {
3079 start_byte = None;
3080 end_byte = None;
3081 if let Some(range) = &su.mediaRange {
3083 let (s, e) = parse_range(range)?;
3084 start_byte = Some(s);
3085 end_byte = Some(e);
3086 }
3087 if let Some(m) = &su.media {
3088 let u = merge_baseurls(&base_url, m)?;
3089 let mf = MediaFragmentBuilder::new(period_counter, u)
3090 .with_range(start_byte, end_byte)
3091 .build();
3092 fragments.push(mf);
3093 } else if let Some(bu) = &rep.BaseURL.first() {
3094 let u = merge_baseurls(&base_url, &bu.base)?;
3095 let mf = MediaFragmentBuilder::new(period_counter, u)
3096 .with_range(start_byte, end_byte)
3097 .build();
3098 fragments.push(mf);
3099 };
3100 }
3101 } else if rep.SegmentTemplate.is_some() ||
3102 subtitle_adaptation.SegmentTemplate.is_some()
3103 {
3104 let st;
3107 if let Some(it) = &rep.SegmentTemplate {
3108 st = it;
3109 } else if let Some(it) = &subtitle_adaptation.SegmentTemplate {
3110 st = it;
3111 } else {
3112 panic!("unreachable");
3113 }
3114 if let Some(i) = &st.initialization {
3115 opt_init = Some(i.to_string());
3116 }
3117 if let Some(m) = &st.media {
3118 opt_media = Some(m.to_string());
3119 }
3120 if let Some(ts) = st.timescale {
3121 timescale = ts;
3122 }
3123 if let Some(sn) = st.startNumber {
3124 start_number = sn;
3125 }
3126 if let Some(stl) = &rep.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
3127 .or(subtitle_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
3128 {
3129 if downloader.verbosity > 1 {
3132 info!(" {}", "Using SegmentTemplate+SegmentTimeline addressing mode for subtitle representation".italic());
3133 }
3134 if let Some(init) = opt_init {
3135 let path = resolve_url_template(&init, &dict);
3136 let u = merge_baseurls(&base_url, &path)?;
3137 let mf = MediaFragmentBuilder::new(period_counter, u)
3138 .set_init()
3139 .build();
3140 fragments.push(mf);
3141 }
3142 if let Some(media) = opt_media {
3143 let sub_path = resolve_url_template(&media, &dict);
3144 let mut segment_time = 0;
3145 let mut segment_duration;
3146 let mut number = start_number;
3147 for s in &stl.segments {
3148 if let Some(t) = s.t {
3149 segment_time = t;
3150 }
3151 segment_duration = s.d;
3152 let dict = HashMap::from([("Time", segment_time.to_string()),
3154 ("Number", number.to_string())]);
3155 let path = resolve_url_template(&sub_path, &dict);
3156 let u = merge_baseurls(&base_url, &path)?;
3157 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3158 fragments.push(mf);
3159 number += 1;
3160 if let Some(r) = s.r {
3161 let mut count = 0i64;
3162 let end_time = period_duration_secs * timescale as f64;
3164 loop {
3165 count += 1;
3166 if r >= 0 {
3172 if count > r {
3173 break;
3174 }
3175 if downloader.force_duration.is_some() &&
3176 segment_time as f64 > end_time
3177 {
3178 break;
3179 }
3180 } else if segment_time as f64 > end_time {
3181 break;
3182 }
3183 segment_time += segment_duration;
3184 let dict = HashMap::from([("Time", segment_time.to_string()),
3185 ("Number", number.to_string())]);
3186 let path = resolve_url_template(&sub_path, &dict);
3187 let u = merge_baseurls(&base_url, &path)?;
3188 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3189 fragments.push(mf);
3190 number += 1;
3191 }
3192 }
3193 segment_time += segment_duration;
3194 }
3195 } else {
3196 return Err(DashMpdError::UnhandledMediaStream(
3197 "SegmentTimeline without a media attribute".to_string()));
3198 }
3199 } else { if downloader.verbosity > 0 {
3204 info!(" {}", "Using SegmentTemplate addressing mode for stpp subtitles".italic());
3205 }
3206 if let Some(i) = &st.initialization {
3207 opt_init = Some(i.to_string());
3208 }
3209 if let Some(m) = &st.media {
3210 opt_media = Some(m.to_string());
3211 }
3212 if let Some(d) = st.duration {
3213 opt_duration = Some(d);
3214 }
3215 if let Some(ts) = st.timescale {
3216 timescale = ts;
3217 }
3218 if let Some(s) = st.startNumber {
3219 start_number = s;
3220 }
3221 let rid = match &rep.id {
3222 Some(id) => id,
3223 None => return Err(
3224 DashMpdError::UnhandledMediaStream(
3225 "Missing @id on Representation node".to_string())),
3226 };
3227 let mut dict = HashMap::from([("RepresentationID", rid.to_string())]);
3228 if let Some(b) = &rep.bandwidth {
3229 dict.insert("Bandwidth", b.to_string());
3230 }
3231 let mut total_number = 0i64;
3232 if let Some(init) = opt_init {
3233 let path = resolve_url_template(&init, &dict);
3234 let u = merge_baseurls(&base_url, &path)?;
3235 let mf = MediaFragmentBuilder::new(period_counter, u)
3236 .set_init()
3237 .build();
3238 fragments.push(mf);
3239 }
3240 if let Some(media) = opt_media {
3241 let sub_path = resolve_url_template(&media, &dict);
3242 let mut segment_duration: f64 = -1.0;
3243 if let Some(d) = opt_duration {
3244 segment_duration = d;
3246 }
3247 if let Some(std) = st.duration {
3248 segment_duration = std / timescale as f64;
3249 }
3250 if segment_duration < 0.0 {
3251 return Err(DashMpdError::UnhandledMediaStream(
3252 "Subtitle representation is missing SegmentTemplate@duration".to_string()));
3253 }
3254 total_number += (period_duration_secs / segment_duration).ceil() as i64;
3255 let mut number = start_number;
3256 for _ in 1..=total_number {
3257 let dict = HashMap::from([("Number", number.to_string())]);
3258 let path = resolve_url_template(&sub_path, &dict);
3259 let u = merge_baseurls(&base_url, &path)?;
3260 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3261 fragments.push(mf);
3262 number += 1;
3263 }
3264 }
3265 }
3266 } else if let Some(sb) = &rep.SegmentBase {
3267 info!(" Using SegmentBase@indexRange for subs");
3269 if downloader.verbosity > 1 {
3270 info!(" {}", "Using SegmentBase@indexRange addressing mode for subtitle representation".italic());
3271 }
3272 let mut start_byte: Option<u64> = None;
3273 let mut end_byte: Option<u64> = None;
3274 if let Some(init) = &sb.Initialization {
3275 if let Some(range) = &init.range {
3276 let (s, e) = parse_range(range)?;
3277 start_byte = Some(s);
3278 end_byte = Some(e);
3279 }
3280 if let Some(su) = &init.sourceURL {
3281 let path = resolve_url_template(su, &dict);
3282 let u = merge_baseurls(&base_url, &path)?;
3283 let mf = MediaFragmentBuilder::new(period_counter, u)
3284 .with_range(start_byte, end_byte)
3285 .set_init()
3286 .build();
3287 fragments.push(mf);
3288 }
3289 }
3290 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3291 .set_init()
3292 .build();
3293 fragments.push(mf);
3294 }
3297 }
3298 }
3299 }
3300 }
3301 Ok(PeriodOutputs { fragments, diagnostics: Vec::new(), subtitle_formats })
3302}
3303
3304
3305struct DownloadState {
3308 period_counter: u8,
3309 segment_count: usize,
3310 segment_counter: usize,
3311 download_errors: u32
3312}
3313
3314#[tracing::instrument(level="trace", skip_all)]
3321async fn fetch_fragment(
3322 downloader: &mut DashDownloader,
3323 frag: &MediaFragment,
3324 fragment_type: &str,
3325 progress_percent: u32) -> Result<std::fs::File, DashMpdError>
3326{
3327 let send_request = || async {
3328 trace!("send_request {}", frag.url.clone());
3329 let mut req = downloader.http_client.as_ref().unwrap()
3332 .get(frag.url.clone())
3333 .header("Accept", format!("{}/*;q=0.9,*/*;q=0.5", fragment_type))
3334 .header("Sec-Fetch-Mode", "navigate");
3335 if let Some(sb) = &frag.start_byte {
3336 if let Some(eb) = &frag.end_byte {
3337 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
3338 }
3339 }
3340 if let Some(ts) = &frag.timeout {
3341 req = req.timeout(*ts);
3342 }
3343 if let Some(referer) = &downloader.referer {
3344 req = req.header("Referer", referer);
3345 } else {
3346 req = req.header("Referer", downloader.redirected_url.to_string());
3347 }
3348 if let Some(username) = &downloader.auth_username {
3349 if let Some(password) = &downloader.auth_password {
3350 req = req.basic_auth(username, Some(password));
3351 }
3352 }
3353 if let Some(token) = &downloader.auth_bearer_token {
3354 req = req.bearer_auth(token);
3355 }
3356 req.send().await
3357 .map_err(categorize_reqwest_error)?
3358 .error_for_status()
3359 .map_err(categorize_reqwest_error)
3360 };
3361 match retry_notify(ExponentialBackoff::default(), send_request, notify_transient).await {
3362 Ok(response) => {
3363 match response.error_for_status() {
3364 Ok(mut resp) => {
3365 let mut tmp_out = tempfile::tempfile()
3366 .map_err(|e| DashMpdError::Io(e, String::from("creating tmpfile for fragment")))?;
3367 let content_type_checker = if fragment_type.eq("audio") {
3368 content_type_audio_p
3369 } else if fragment_type.eq("video") {
3370 content_type_video_p
3371 } else {
3372 panic!("fragment_type not audio or video");
3373 };
3374 if !downloader.content_type_checks || content_type_checker(&resp) {
3375 let mut fragment_out: Option<File> = None;
3376 if let Some(ref fragment_path) = downloader.fragment_path {
3377 if let Some(path) = frag.url.path_segments()
3378 .unwrap_or_else(|| "".split(' '))
3379 .next_back()
3380 {
3381 let vf_file = fragment_path.clone().join(fragment_type).join(path);
3382 if let Ok(f) = File::create(vf_file) {
3383 fragment_out = Some(f)
3384 }
3385 }
3386 }
3387 let mut segment_size = 0;
3388 while let Some(chunk) = resp.chunk().await
3394 .map_err(|e| network_error(&format!("fetching DASH {fragment_type} segment"), e))?
3395 {
3396 segment_size += chunk.len();
3397 downloader.bw_estimator_bytes += chunk.len();
3398 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
3399 throttle_download_rate(downloader, size).await?;
3400 if let Err(e) = tmp_out.write_all(&chunk) {
3401 return Err(DashMpdError::Io(e, format!("writing DASH {fragment_type} data")));
3402 }
3403 if let Some(ref mut fout) = fragment_out {
3404 fout.write_all(&chunk)
3405 .map_err(|e| DashMpdError::Io(e, format!("writing {fragment_type} fragment")))?;
3406 }
3407 let elapsed = downloader.bw_estimator_started.elapsed().as_secs_f64();
3408 if (elapsed > 1.5) || (downloader.bw_estimator_bytes > 100_000) {
3409 let bw = downloader.bw_estimator_bytes as f64 / (1e6 * elapsed);
3410 let msg = if bw > 0.5 {
3411 format!("Fetching {fragment_type} segments ({bw:.1} MB/s)")
3412 } else {
3413 let kbs = (bw * 1000.0).round() as u64;
3414 format!("Fetching {fragment_type} segments ({kbs:3} kB/s)")
3415 };
3416 for observer in &downloader.progress_observers {
3417 observer.update(progress_percent, &msg);
3418 }
3419 downloader.bw_estimator_started = Instant::now();
3420 downloader.bw_estimator_bytes = 0;
3421 }
3422 }
3423 if downloader.verbosity > 2 {
3424 if let Some(sb) = &frag.start_byte {
3425 if let Some(eb) = &frag.end_byte {
3426 info!(" {fragment_type} segment {} range {sb}-{eb} -> {} octets",
3427 frag.url, segment_size);
3428 }
3429 } else {
3430 info!(" {fragment_type} segment {} -> {segment_size} octets", &frag.url);
3431 }
3432 }
3433 } else {
3434 warn!("{} {} with non-{fragment_type} content-type", "Ignoring segment".red(), frag.url);
3435 };
3436 tmp_out.sync_all()
3437 .map_err(|e| DashMpdError::Io(e, format!("syncing {fragment_type} fragment")))?;
3438 Ok(tmp_out)
3439 },
3440 Err(e) => Err(network_error("HTTP error", e)),
3441 }
3442 },
3443 Err(e) => Err(network_error(&format!("{e:?}"), e)),
3444 }
3445}
3446
3447
3448#[tracing::instrument(level="trace", skip_all)]
3450async fn fetch_period_audio(
3451 downloader: &mut DashDownloader,
3452 tmppath: PathBuf,
3453 audio_fragments: &[MediaFragment],
3454 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3455{
3456 let start_download = Instant::now();
3457 let mut have_audio = false;
3458 {
3459 let tmpfile_audio = File::create(tmppath.clone())
3463 .map_err(|e| DashMpdError::Io(e, String::from("creating audio tmpfile")))?;
3464 let mut tmpfile_audio = BufWriter::new(tmpfile_audio);
3465 if let Some(ref fragment_path) = downloader.fragment_path {
3467 let audio_fragment_dir = fragment_path.join("audio");
3468 if !audio_fragment_dir.exists() {
3469 fs::create_dir_all(audio_fragment_dir)
3470 .map_err(|e| DashMpdError::Io(e, String::from("creating audio fragment dir")))?;
3471 }
3472 }
3473 for frag in audio_fragments.iter().filter(|f| f.period == ds.period_counter) {
3477 ds.segment_counter += 1;
3478 let progress_percent = (100.0 * ds.segment_counter as f32 / (2.0 + ds.segment_count as f32)).ceil() as u32;
3479 let url = &frag.url;
3480 if url.scheme() == "data" {
3484 let us = &url.to_string();
3485 let du = DataUrl::process(us)
3486 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
3487 if du.mime_type().type_ != "audio" {
3488 return Err(DashMpdError::UnhandledMediaStream(
3489 String::from("expecting audio content in data URL")));
3490 }
3491 let (body, _fragment) = du.decode_to_vec()
3492 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
3493 if downloader.verbosity > 2 {
3494 info!(" Audio segment data URL -> {} octets", body.len());
3495 }
3496 if let Err(e) = tmpfile_audio.write_all(&body) {
3497 error!("Unable to write DASH audio data: {e:?}");
3498 return Err(DashMpdError::Io(e, String::from("writing DASH audio data")));
3499 }
3500 have_audio = true;
3501 } else {
3502 'done: for _ in 0..downloader.fragment_retry_count {
3504 match fetch_fragment(downloader, frag, "audio", progress_percent).await {
3505 Ok(mut frag_file) => {
3506 frag_file.rewind()
3507 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
3508 let mut buf = Vec::new();
3509 frag_file.read_to_end(&mut buf)
3510 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
3511 if let Err(e) = tmpfile_audio.write_all(&buf) {
3512 error!("Unable to write DASH audio data: {e:?}");
3513 return Err(DashMpdError::Io(e, String::from("writing DASH audio data")));
3514 }
3515 have_audio = true;
3516 break 'done;
3517 },
3518 Err(e) => {
3519 if downloader.verbosity > 0 {
3520 error!("Error fetching audio segment {url}: {e:?}");
3521 }
3522 ds.download_errors += 1;
3523 if ds.download_errors > downloader.max_error_count {
3524 error!("max_error_count network errors encountered");
3525 return Err(DashMpdError::Network(
3526 String::from("more than max_error_count network errors")));
3527 }
3528 },
3529 }
3530 info!(" Retrying audio segment {url}");
3531 if downloader.sleep_between_requests > 0 {
3532 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
3533 }
3534 }
3535 }
3536 }
3537 tmpfile_audio.flush().map_err(|e| {
3538 error!("Couldn't flush DASH audio file: {e}");
3539 DashMpdError::Io(e, String::from("flushing DASH audio file"))
3540 })?;
3541 } if !downloader.decryption_keys.is_empty() {
3543 if downloader.verbosity > 0 {
3544 let metadata = fs::metadata(tmppath.clone())
3545 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted audio metadata")))?;
3546 info!(" Attempting to decrypt audio stream ({} kB) with {}",
3547 metadata.len() / 1024,
3548 downloader.decryptor_preference);
3549 }
3550 let out_ext = downloader.output_path.as_ref().unwrap()
3551 .extension()
3552 .unwrap_or(OsStr::new("mp4"));
3553 let decrypted = tmp_file_path("dashmpd-decrypted-audio", out_ext)?;
3554 if downloader.decryptor_preference.eq("mp4decrypt") {
3555 let mut args = Vec::new();
3556 for (k, v) in downloader.decryption_keys.iter() {
3557 args.push("--key".to_string());
3558 args.push(format!("{k}:{v}"));
3559 }
3560 args.push(String::from(tmppath.to_string_lossy()));
3561 args.push(String::from(decrypted.to_string_lossy()));
3562 if downloader.verbosity > 1 {
3563 info!(" Running mp4decrypt {}", args.join(" "));
3564 }
3565 let out = Command::new(downloader.mp4decrypt_location.clone())
3566 .args(args)
3567 .output()
3568 .map_err(|e| DashMpdError::Io(e, String::from("spawning mp4decrypt")))?;
3569 let mut no_output = true;
3570 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3571 if downloader.verbosity > 0 {
3572 info!(" Decrypted audio stream of size {} kB.", metadata.len() / 1024);
3573 }
3574 no_output = false;
3575 }
3576 if !out.status.success() || no_output {
3577 warn!(" mp4decrypt subprocess failed");
3578 let msg = partial_process_output(&out.stdout);
3579 if !msg.is_empty() {
3580 warn!(" mp4decrypt stdout: {msg}");
3581 }
3582 let msg = partial_process_output(&out.stderr);
3583 if !msg.is_empty() {
3584 warn!(" mp4decrypt stderr: {msg}");
3585 }
3586 }
3587 if no_output {
3588 error!("{}", "Failed to decrypt audio stream with mp4decrypt".red());
3589 warn!(" Undecrypted audio left in {}", tmppath.display());
3590 return Err(DashMpdError::Decrypting(String::from("audio stream")));
3591 }
3592 } else if downloader.decryptor_preference.eq("shaka") {
3593 let mut args = Vec::new();
3594 let mut keys = Vec::new();
3595 if downloader.verbosity < 1 {
3596 args.push("--quiet".to_string());
3597 }
3598 args.push(format!("in={},stream=audio,output={}", tmppath.display(), decrypted.display()));
3599 let mut drm_label = 0;
3600 #[allow(clippy::explicit_counter_loop)]
3601 for (k, v) in downloader.decryption_keys.iter() {
3602 keys.push(format!("label=lbl{drm_label}:key_id={k}:key={v}"));
3603 drm_label += 1;
3604 }
3605 args.push("--enable_raw_key_decryption".to_string());
3606 args.push("--keys".to_string());
3607 args.push(keys.join(","));
3608 if downloader.verbosity > 1 {
3609 info!(" Running shaka-packager {}", args.join(" "));
3610 }
3611 let out = Command::new(downloader.shaka_packager_location.clone())
3612 .args(args)
3613 .output()
3614 .map_err(|e| DashMpdError::Io(e, String::from("spawning shaka-packager")))?;
3615 let mut no_output = false;
3616 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3617 if downloader.verbosity > 0 {
3618 info!(" Decrypted audio stream of size {} kB.", metadata.len() / 1024);
3619 }
3620 if metadata.len() == 0 {
3621 no_output = true;
3622 }
3623 } else {
3624 no_output = true;
3625 }
3626 if !out.status.success() || no_output {
3627 warn!(" shaka-packager subprocess failed");
3628 let msg = partial_process_output(&out.stdout);
3629 if !msg.is_empty() {
3630 warn!(" shaka-packager stdout: {msg}");
3631 }
3632 let msg = partial_process_output(&out.stderr);
3633 if msg.is_empty() {
3634 warn!(" shaka-packager stderr: {msg}");
3635 }
3636 }
3637 if no_output {
3638 error!(" {}", "Failed to decrypt audio stream with shaka-packager".red());
3639 warn!(" Undecrypted audio stream left in {}", tmppath.display());
3640 return Err(DashMpdError::Decrypting(String::from("audio stream")));
3641 }
3642 } else {
3643 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
3644 }
3645 fs::rename(decrypted, tmppath.clone())
3646 .map_err(|e| DashMpdError::Io(e, String::from("renaming decrypted audio")))?;
3647 }
3648 if let Ok(metadata) = fs::metadata(tmppath.clone()) {
3649 if downloader.verbosity > 1 {
3650 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
3651 let elapsed = start_download.elapsed();
3652 info!(" Wrote {mbytes:.1}MB to DASH audio file ({:.1} MB/s)",
3653 mbytes / elapsed.as_secs_f64());
3654 }
3655 }
3656 Ok(have_audio)
3657}
3658
3659
3660#[tracing::instrument(level="trace", skip_all)]
3662async fn fetch_period_video(
3663 downloader: &mut DashDownloader,
3664 tmppath: PathBuf,
3665 video_fragments: &[MediaFragment],
3666 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3667{
3668 let start_download = Instant::now();
3669 let mut have_video = false;
3670 {
3671 let tmpfile_video = File::create(tmppath.clone())
3674 .map_err(|e| DashMpdError::Io(e, String::from("creating video tmpfile")))?;
3675 let mut tmpfile_video = BufWriter::new(tmpfile_video);
3676 if let Some(ref fragment_path) = downloader.fragment_path {
3678 let video_fragment_dir = fragment_path.join("video");
3679 if !video_fragment_dir.exists() {
3680 fs::create_dir_all(video_fragment_dir)
3681 .map_err(|e| DashMpdError::Io(e, String::from("creating video fragment dir")))?;
3682 }
3683 }
3684 for frag in video_fragments.iter().filter(|f| f.period == ds.period_counter) {
3685 ds.segment_counter += 1;
3686 let progress_percent = (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32;
3687 if frag.url.scheme() == "data" {
3688 let us = &frag.url.to_string();
3689 let du = DataUrl::process(us)
3690 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
3691 if du.mime_type().type_ != "video" {
3692 return Err(DashMpdError::UnhandledMediaStream(
3693 String::from("expecting video content in data URL")));
3694 }
3695 let (body, _fragment) = du.decode_to_vec()
3696 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
3697 if downloader.verbosity > 2 {
3698 info!(" Video segment data URL -> {} octets", body.len());
3699 }
3700 if let Err(e) = tmpfile_video.write_all(&body) {
3701 error!("Unable to write DASH video data: {e:?}");
3702 return Err(DashMpdError::Io(e, String::from("writing DASH video data")));
3703 }
3704 have_video = true;
3705 } else {
3706 'done: for _ in 0..downloader.fragment_retry_count {
3707 match fetch_fragment(downloader, frag, "video", progress_percent).await {
3708 Ok(mut frag_file) => {
3709 frag_file.rewind()
3710 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
3711 let mut buf = Vec::new();
3712 frag_file.read_to_end(&mut buf)
3713 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
3714 if let Err(e) = tmpfile_video.write_all(&buf) {
3715 error!("Unable to write DASH video data: {e:?}");
3716 return Err(DashMpdError::Io(e, String::from("writing DASH video data")));
3717 }
3718 have_video = true;
3719 break 'done;
3720 },
3721 Err(e) => {
3722 if downloader.verbosity > 0 {
3723 error!(" Error fetching video segment {}: {e:?}", frag.url);
3724 }
3725 ds.download_errors += 1;
3726 if ds.download_errors > downloader.max_error_count {
3727 return Err(DashMpdError::Network(
3728 String::from("more than max_error_count network errors")));
3729 }
3730 },
3731 }
3732 info!(" Retrying video segment {}", frag.url);
3733 if downloader.sleep_between_requests > 0 {
3734 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
3735 }
3736 }
3737 }
3738 }
3739 tmpfile_video.flush().map_err(|e| {
3740 error!(" Couldn't flush video file: {e}");
3741 DashMpdError::Io(e, String::from("flushing video file"))
3742 })?;
3743 } if !downloader.decryption_keys.is_empty() {
3745 if downloader.verbosity > 0 {
3746 let metadata = fs::metadata(tmppath.clone())
3747 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted video metadata")))?;
3748 info!(" Attempting to decrypt video stream ({} kB) with {}",
3749 metadata.len() / 1024,
3750 downloader.decryptor_preference);
3751 }
3752 let out_ext = downloader.output_path.as_ref().unwrap()
3753 .extension()
3754 .unwrap_or(OsStr::new("mp4"));
3755 let decrypted = tmp_file_path("dashmpd-decrypted-video", out_ext)?;
3756 if downloader.decryptor_preference.eq("mp4decrypt") {
3757 let mut args = Vec::new();
3758 for (k, v) in downloader.decryption_keys.iter() {
3759 args.push("--key".to_string());
3760 args.push(format!("{k}:{v}"));
3761 }
3762 args.push(tmppath.to_string_lossy().to_string());
3763 args.push(decrypted.to_string_lossy().to_string());
3764 if downloader.verbosity > 1 {
3765 info!(" Running mp4decrypt {}", args.join(" "));
3766 }
3767 let out = Command::new(downloader.mp4decrypt_location.clone())
3768 .args(args)
3769 .output()
3770 .map_err(|e| DashMpdError::Io(e, String::from("spawning mp4decrypt")))?;
3771 let mut no_output = false;
3772 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3773 if downloader.verbosity > 0 {
3774 info!(" Decrypted video stream of size {} kB.", metadata.len() / 1024);
3775 }
3776 if metadata.len() == 0 {
3777 no_output = true;
3778 }
3779 } else {
3780 no_output = true;
3781 }
3782 if !out.status.success() || no_output {
3783 error!(" mp4decrypt subprocess failed");
3784 let msg = partial_process_output(&out.stdout);
3785 if !msg.is_empty() {
3786 warn!(" mp4decrypt stdout: {msg}");
3787 }
3788 let msg = partial_process_output(&out.stderr);
3789 if !msg.is_empty() {
3790 warn!(" mp4decrypt stderr: {msg}");
3791 }
3792 }
3793 if no_output {
3794 error!(" {}", "Failed to decrypt video stream with mp4decrypt".red());
3795 warn!(" Undecrypted video stream left in {}", tmppath.display());
3796 return Err(DashMpdError::Decrypting(String::from("video stream")));
3797 }
3798 } else if downloader.decryptor_preference.eq("shaka") {
3799 let mut args = Vec::new();
3800 let mut keys = Vec::new();
3801 if downloader.verbosity < 1 {
3802 args.push("--quiet".to_string());
3803 }
3804 args.push(format!("in={},stream=video,output={}", tmppath.display(), decrypted.display()));
3805 let mut drm_label = 0;
3806 #[allow(clippy::explicit_counter_loop)]
3807 for (k, v) in downloader.decryption_keys.iter() {
3808 keys.push(format!("label=lbl{drm_label}:key_id={k}:key={v}"));
3809 drm_label += 1;
3810 }
3811 args.push("--enable_raw_key_decryption".to_string());
3812 args.push("--keys".to_string());
3813 args.push(keys.join(","));
3814 if downloader.verbosity > 1 {
3815 info!(" Running shaka-packager {}", args.join(" "));
3816 }
3817 let out = Command::new(downloader.shaka_packager_location.clone())
3818 .args(args)
3819 .output()
3820 .map_err(|e| DashMpdError::Io(e, String::from("spawning shaka-packager")))?;
3821 let mut no_output = true;
3822 if let Ok(metadata) = fs::metadata(decrypted.clone()) {
3823 if downloader.verbosity > 0 {
3824 info!(" Decrypted video stream of size {} kB.", metadata.len() / 1024);
3825 }
3826 no_output = false;
3827 }
3828 if !out.status.success() || no_output {
3829 warn!(" shaka-packager subprocess failed");
3830 let msg = partial_process_output(&out.stdout);
3831 if !msg.is_empty() {
3832 warn!(" shaka-packager stdout: {msg}");
3833 }
3834 let msg = partial_process_output(&out.stderr);
3835 if !msg.is_empty() {
3836 warn!(" shaka-packager stderr: {msg}");
3837 }
3838 }
3839 if no_output {
3840 error!(" {}", "Failed to decrypt video stream with shaka-packager".red());
3841 warn!(" Undecrypted video left in {}", tmppath.display());
3842 return Err(DashMpdError::Decrypting(String::from("video stream")));
3843 }
3844 } else {
3845 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
3846 }
3847 fs::rename(decrypted, tmppath.clone())
3848 .map_err(|e| DashMpdError::Io(e, String::from("renaming decrypted video")))?;
3849 }
3850 if let Ok(metadata) = fs::metadata(tmppath.clone()) {
3851 if downloader.verbosity > 1 {
3852 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
3853 let elapsed = start_download.elapsed();
3854 info!(" Wrote {mbytes:.1}MB to DASH video file ({:.1} MB/s)",
3855 mbytes / elapsed.as_secs_f64());
3856 }
3857 }
3858 Ok(have_video)
3859}
3860
3861
3862#[tracing::instrument(level="trace", skip_all)]
3864async fn fetch_period_subtitles(
3865 downloader: &DashDownloader,
3866 tmppath: PathBuf,
3867 subtitle_fragments: &[MediaFragment],
3868 subtitle_formats: &[SubtitleType],
3869 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3870{
3871 let client = downloader.http_client.clone().unwrap();
3872 let start_download = Instant::now();
3873 let mut have_subtitles = false;
3874 {
3875 let tmpfile_subs = File::create(tmppath.clone())
3876 .map_err(|e| DashMpdError::Io(e, String::from("creating subs tmpfile")))?;
3877 let mut tmpfile_subs = BufWriter::new(tmpfile_subs);
3878 for frag in subtitle_fragments {
3879 ds.segment_counter += 1;
3881 let progress_percent = (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32;
3882 for observer in &downloader.progress_observers {
3883 observer.update(progress_percent, "Fetching subtitle segments");
3884 }
3885 if frag.url.scheme() == "data" {
3886 let us = &frag.url.to_string();
3887 let du = DataUrl::process(us)
3888 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
3889 if du.mime_type().type_ != "video" {
3890 return Err(DashMpdError::UnhandledMediaStream(
3891 String::from("expecting video content in data URL")));
3892 }
3893 let (body, _fragment) = du.decode_to_vec()
3894 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
3895 if downloader.verbosity > 2 {
3896 info!(" Subtitle segment data URL -> {} octets", body.len());
3897 }
3898 if let Err(e) = tmpfile_subs.write_all(&body) {
3899 error!("Unable to write DASH subtitle data: {e:?}");
3900 return Err(DashMpdError::Io(e, String::from("writing DASH subtitle data")));
3901 }
3902 have_subtitles = true;
3903 } else {
3904 let fetch = || async {
3905 let mut req = client.get(frag.url.clone())
3906 .header("Sec-Fetch-Mode", "navigate");
3907 if let Some(sb) = &frag.start_byte {
3908 if let Some(eb) = &frag.end_byte {
3909 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
3910 }
3911 }
3912 if let Some(referer) = &downloader.referer {
3913 req = req.header("Referer", referer);
3914 } else {
3915 req = req.header("Referer", downloader.redirected_url.to_string());
3916 }
3917 if let Some(username) = &downloader.auth_username {
3918 if let Some(password) = &downloader.auth_password {
3919 req = req.basic_auth(username, Some(password));
3920 }
3921 }
3922 if let Some(token) = &downloader.auth_bearer_token {
3923 req = req.bearer_auth(token);
3924 }
3925 req.send().await
3926 .map_err(categorize_reqwest_error)?
3927 .error_for_status()
3928 .map_err(categorize_reqwest_error)
3929 };
3930 let mut failure = None;
3931 match retry_notify(ExponentialBackoff::default(), fetch, notify_transient).await {
3932 Ok(response) => {
3933 if response.status().is_success() {
3934 let dash_bytes = response.bytes().await
3935 .map_err(|e| network_error("fetching DASH subtitle segment", e))?;
3936 if downloader.verbosity > 2 {
3937 if let Some(sb) = &frag.start_byte {
3938 if let Some(eb) = &frag.end_byte {
3939 info!(" Subtitle segment {} range {sb}-{eb} -> {} octets",
3940 &frag.url, dash_bytes.len());
3941 }
3942 } else {
3943 info!(" Subtitle segment {} -> {} octets", &frag.url, dash_bytes.len());
3944 }
3945 }
3946 let size = min((dash_bytes.len()/1024 + 1) as u32, u32::MAX);
3947 throttle_download_rate(downloader, size).await?;
3948 if let Err(e) = tmpfile_subs.write_all(&dash_bytes) {
3949 return Err(DashMpdError::Io(e, String::from("writing DASH subtitle data")));
3950 }
3951 have_subtitles = true;
3952 } else {
3953 failure = Some(format!("HTTP error {}", response.status().as_str()));
3954 }
3955 },
3956 Err(e) => failure = Some(format!("{e}")),
3957 }
3958 if let Some(f) = failure {
3959 if downloader.verbosity > 0 {
3960 error!("{} fetching subtitle segment {}", f.red(), &frag.url);
3961 }
3962 ds.download_errors += 1;
3963 if ds.download_errors > downloader.max_error_count {
3964 return Err(DashMpdError::Network(
3965 String::from("more than max_error_count network errors")));
3966 }
3967 }
3968 }
3969 if downloader.sleep_between_requests > 0 {
3970 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
3971 }
3972 }
3973 tmpfile_subs.flush().map_err(|e| {
3974 error!("Couldn't flush subs file: {e}");
3975 DashMpdError::Io(e, String::from("flushing subtitle file"))
3976 })?;
3977 } if have_subtitles {
3979 if let Ok(metadata) = fs::metadata(tmppath.clone()) {
3980 if downloader.verbosity > 1 {
3981 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
3982 let elapsed = start_download.elapsed();
3983 info!(" Wrote {mbytes:.1}MB to DASH subtitle file ({:.1} MB/s)",
3984 mbytes / elapsed.as_secs_f64());
3985 }
3986 }
3987 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
3990 subtitle_formats.contains(&SubtitleType::Ttxt)
3991 {
3992 if downloader.verbosity > 0 {
3994 if let Some(fmt) = subtitle_formats.first() {
3995 info!(" Downloaded media contains subtitles in {fmt:?} format");
3996 }
3997 info!(" {}", "Running MP4Box to extract subtitles".italic());
3998 }
3999 let out = downloader.output_path.as_ref().unwrap()
4000 .with_extension("srt");
4001 let out_str = out.to_string_lossy();
4002 let tmp_str = tmppath.to_string_lossy();
4003 let args = vec![
4004 "-srt", "1",
4005 "-out", &out_str,
4006 &tmp_str];
4007 if downloader.verbosity > 0 {
4008 info!(" Running MP4Box {}", args.join(" "));
4009 }
4010 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4011 .args(args)
4012 .output()
4013 {
4014 let msg = partial_process_output(&mp4box.stdout);
4015 if !msg.is_empty() {
4016 info!(" MP4Box stdout: {msg}");
4017 }
4018 let msg = partial_process_output(&mp4box.stderr);
4019 if !msg.is_empty() {
4020 info!(" MP4Box stderr: {msg}");
4021 }
4022 if mp4box.status.success() {
4023 info!(" Extracted subtitles as SRT");
4024 } else {
4025 warn!(" Error running MP4Box to extract subtitles");
4026 }
4027 } else {
4028 warn!(" Failed to spawn MP4Box to extract subtitles");
4029 }
4030 }
4031 if subtitle_formats.contains(&SubtitleType::Stpp) {
4032 if downloader.verbosity > 0 {
4033 info!(" Converting STPP subtitles to TTML format with ffmpeg");
4034 }
4035 let out = downloader.output_path.as_ref().unwrap()
4036 .with_extension("ttml");
4037 let tmppath_arg = &tmppath.to_string_lossy();
4038 let out_arg = &out.to_string_lossy();
4039 let ffmpeg_args = vec![
4040 "-hide_banner",
4041 "-nostats",
4042 "-loglevel", "error",
4043 "-y", "-nostdin",
4045 "-i", tmppath_arg,
4046 "-f", "data",
4047 "-map", "0",
4048 "-c", "copy",
4049 out_arg];
4050 if downloader.verbosity > 0 {
4051 info!(" Running ffmpeg {}", ffmpeg_args.join(" "));
4052 }
4053 if let Ok(ffmpeg) = Command::new(downloader.ffmpeg_location.clone())
4054 .args(ffmpeg_args)
4055 .output()
4056 {
4057 let msg = partial_process_output(&ffmpeg.stdout);
4058 if !msg.is_empty() {
4059 info!(" ffmpeg stdout: {msg}");
4060 }
4061 let msg = partial_process_output(&ffmpeg.stderr);
4062 if !msg.is_empty() {
4063 info!(" ffmpeg stderr: {msg}");
4064 }
4065 if ffmpeg.status.success() {
4066 info!(" Converted STPP subtitles to TTML format");
4067 } else {
4068 warn!(" Error running ffmpeg to convert subtitles");
4069 }
4070 }
4071 }
4075
4076 }
4077 Ok(have_subtitles)
4078}
4079
4080
4081async fn fetch_mpd_http(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4083 let client = &downloader.http_client.clone().unwrap();
4084 let send_request = || async {
4085 let mut req = client.get(&downloader.mpd_url)
4086 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4087 .header("Accept-Language", "en-US,en")
4088 .header("Upgrade-Insecure-Requests", "1")
4089 .header("Sec-Fetch-Mode", "navigate");
4090 if let Some(referer) = &downloader.referer {
4091 req = req.header("Referer", referer);
4092 }
4093 if let Some(username) = &downloader.auth_username {
4094 if let Some(password) = &downloader.auth_password {
4095 req = req.basic_auth(username, Some(password));
4096 }
4097 }
4098 if let Some(token) = &downloader.auth_bearer_token {
4099 req = req.bearer_auth(token);
4100 }
4101 req.send().await
4102 .map_err(categorize_reqwest_error)?
4103 .error_for_status()
4104 .map_err(categorize_reqwest_error)
4105 };
4106 for observer in &downloader.progress_observers {
4107 observer.update(1, "Fetching DASH manifest");
4108 }
4109 if downloader.verbosity > 0 {
4110 if !downloader.fetch_audio && !downloader.fetch_video && !downloader.fetch_subtitles {
4111 info!("Only simulating media downloads");
4112 }
4113 info!("Fetching the DASH manifest");
4114 }
4115 let response = retry_notify(ExponentialBackoff::default(), send_request, notify_transient)
4116 .await
4117 .map_err(|e| network_error("requesting DASH manifest", e))?;
4118 if !response.status().is_success() {
4119 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4120 return Err(DashMpdError::Network(msg));
4121 }
4122 downloader.redirected_url = response.url().clone();
4123 response.bytes().await
4124 .map_err(|e| network_error("fetching DASH manifest", e))
4125}
4126
4127async fn fetch_mpd_file(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4130 if ! &downloader.mpd_url.starts_with("file://") {
4131 return Err(DashMpdError::Other(String::from("expecting file:// URL scheme")));
4132 }
4133 let url = Url::parse(&downloader.mpd_url)
4134 .map_err(|_| DashMpdError::Other(String::from("parsing MPD URL")))?;
4135 let path = url.to_file_path()
4136 .map_err(|_| DashMpdError::Other(String::from("extracting path from file:// URL")))?;
4137 let octets = fs::read(path)
4138 .map_err(|_| DashMpdError::Other(String::from("reading from file:// URL")))?;
4139 Ok(Bytes::from(octets))
4140}
4141
4142
4143#[tracing::instrument(level="trace", skip_all)]
4144async fn fetch_mpd(downloader: &mut DashDownloader) -> Result<PathBuf, DashMpdError> {
4145 let xml = if downloader.mpd_url.starts_with("file://") {
4146 fetch_mpd_file(downloader).await?
4147 } else {
4148 fetch_mpd_http(downloader).await?
4149 };
4150 let mut mpd: MPD = parse_resolving_xlinks(downloader, &xml).await
4151 .map_err(|e| parse_error("parsing DASH XML", e))?;
4152 let client = &downloader.http_client.clone().unwrap();
4155 if let Some(new_location) = &mpd.locations.first() {
4156 let new_url = &new_location.url;
4157 if downloader.verbosity > 0 {
4158 info!("Redirecting to new manifest <Location> {new_url}");
4159 }
4160 let send_request = || async {
4161 let mut req = client.get(new_url)
4162 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4163 .header("Accept-Language", "en-US,en")
4164 .header("Sec-Fetch-Mode", "navigate");
4165 if let Some(referer) = &downloader.referer {
4166 req = req.header("Referer", referer);
4167 } else {
4168 req = req.header("Referer", downloader.redirected_url.to_string());
4169 }
4170 if let Some(username) = &downloader.auth_username {
4171 if let Some(password) = &downloader.auth_password {
4172 req = req.basic_auth(username, Some(password));
4173 }
4174 }
4175 if let Some(token) = &downloader.auth_bearer_token {
4176 req = req.bearer_auth(token);
4177 }
4178 req.send().await
4179 .map_err(categorize_reqwest_error)?
4180 .error_for_status()
4181 .map_err(categorize_reqwest_error)
4182 };
4183 let response = retry_notify(ExponentialBackoff::default(), send_request, notify_transient)
4184 .await
4185 .map_err(|e| network_error("requesting relocated DASH manifest", e))?;
4186 if !response.status().is_success() {
4187 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4188 return Err(DashMpdError::Network(msg));
4189 }
4190 downloader.redirected_url = response.url().clone();
4191 let xml = response.bytes().await
4192 .map_err(|e| network_error("fetching relocated DASH manifest", e))?;
4193 mpd = parse_resolving_xlinks(downloader, &xml).await
4194 .map_err(|e| parse_error("parsing relocated DASH XML", e))?;
4195 }
4196 if let Some(mpdtype) = mpd.mpdtype.as_ref() {
4197 if mpdtype.eq("dynamic") {
4198 if downloader.allow_live_streams {
4201 if downloader.verbosity > 0 {
4202 warn!("Attempting to download from live stream (this may not work).");
4203 }
4204 } else {
4205 return Err(DashMpdError::UnhandledMediaStream("Don't know how to download dynamic MPD".to_string()));
4206 }
4207 }
4208 }
4209 let mut toplevel_base_url = downloader.redirected_url.clone();
4210 if let Some(bu) = &mpd.base_url.first() {
4212 toplevel_base_url = merge_baseurls(&downloader.redirected_url, &bu.base)?;
4213 }
4214 if let Some(base) = &downloader.base_url {
4217 toplevel_base_url = merge_baseurls(&downloader.redirected_url, base)?;
4218 }
4219 if downloader.verbosity > 0 {
4220 let pcount = mpd.periods.len();
4221 info!("DASH manifest has {pcount} period{}", if pcount > 1 { "s" } else { "" });
4222 print_available_streams(&mpd);
4223 }
4224 let mut pds: Vec<PeriodDownloads> = Vec::new();
4232 let mut period_counter = 0;
4233 for mpd_period in &mpd.periods {
4234 let period = mpd_period.clone();
4235 period_counter += 1;
4236 if let Some(min) = downloader.minimum_period_duration {
4237 if let Some(duration) = period.duration {
4238 if duration < min {
4239 if let Some(id) = period.id.as_ref() {
4240 info!("Skipping period {id} (#{period_counter}): duration is less than requested minimum");
4241 } else {
4242 info!("Skipping period #{period_counter}: duration is less than requested minimum");
4243 }
4244 continue;
4245 }
4246 }
4247 }
4248 let mut pd = PeriodDownloads { period_counter, ..Default::default() };
4249 if let Some(id) = period.id.as_ref() {
4250 pd.id = Some(id.clone());
4251 }
4252 if downloader.verbosity > 0 {
4253 if let Some(id) = period.id.as_ref() {
4254 info!("Preparing download for period {id} (#{period_counter})");
4255 } else {
4256 info!("Preparing download for period #{period_counter}");
4257 }
4258 }
4259 let mut base_url = toplevel_base_url.clone();
4260 if let Some(bu) = period.BaseURL.first() {
4262 base_url = merge_baseurls(&base_url, &bu.base)?;
4263 }
4264 let mut audio_outputs = PeriodOutputs::default();
4265 if downloader.fetch_audio {
4266 audio_outputs = do_period_audio(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4267 for f in audio_outputs.fragments {
4268 pd.audio_fragments.push(f);
4269 }
4270 }
4271 let mut video_outputs = PeriodOutputs::default();
4272 if downloader.fetch_video {
4273 video_outputs = do_period_video(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4274 for f in video_outputs.fragments {
4275 pd.video_fragments.push(f);
4276 }
4277 }
4278 match do_period_subtitles(downloader, &mpd, &period, period_counter, base_url.clone()).await {
4279 Ok(subtitle_outputs) => {
4280 for f in subtitle_outputs.fragments {
4281 pd.subtitle_fragments.push(f);
4282 }
4283 for f in subtitle_outputs.subtitle_formats {
4284 pd.subtitle_formats.push(f);
4285 }
4286 },
4287 Err(e) => warn!(" Ignoring error triggered while processing subtitles: {e}"),
4288 }
4289 if downloader.verbosity > 0 {
4291 use base64::prelude::{Engine as _, BASE64_STANDARD};
4292
4293 audio_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4294 for f in pd.audio_fragments.iter().filter(|f| f.is_init) {
4295 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4296 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4297 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4298 info!(" {}", pssh.to_string());
4299 }
4300 }
4301 }
4302 video_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4303 for f in pd.video_fragments.iter().filter(|f| f.is_init) {
4304 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4305 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4306 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4307 info!(" {}", pssh.to_string());
4308 }
4309 }
4310 }
4311 }
4312 pds.push(pd);
4313 } let output_path = &downloader.output_path.as_ref().unwrap().clone();
4318 let mut period_output_paths: Vec<PathBuf> = Vec::new();
4319 let mut ds = DownloadState {
4320 period_counter: 0,
4321 segment_count: pds.iter().map(period_fragment_count).sum(),
4323 segment_counter: 0,
4324 download_errors: 0
4325 };
4326 for pd in pds {
4327 let mut have_audio = false;
4328 let mut have_video = false;
4329 let mut have_subtitles = false;
4330 ds.period_counter = pd.period_counter;
4331 let period_output_path = output_path_for_period(output_path, pd.period_counter);
4332 #[allow(clippy::collapsible_if)]
4333 if downloader.verbosity > 0 {
4334 if downloader.fetch_audio || downloader.fetch_video || downloader.fetch_subtitles {
4335 let idnum = if let Some(id) = pd.id {
4336 format!("id={} (#{})", id, pd.period_counter)
4337 } else {
4338 format!("#{}", pd.period_counter)
4339 };
4340 info!("Period {idnum}: fetching {} audio, {} video and {} subtitle segments",
4341 pd.audio_fragments.len(),
4342 pd.video_fragments.len(),
4343 pd.subtitle_fragments.len());
4344 }
4345 }
4346 let output_ext = downloader.output_path.as_ref().unwrap()
4347 .extension()
4348 .unwrap_or(OsStr::new("mp4"));
4349 let tmppath_audio = if let Some(ref path) = downloader.keep_audio {
4350 path.clone()
4351 } else {
4352 tmp_file_path("dashmpd-audio", output_ext)?
4353 };
4354 let tmppath_video = if let Some(ref path) = downloader.keep_video {
4355 path.clone()
4356 } else {
4357 tmp_file_path("dashmpd-video", output_ext)?
4358 };
4359 let tmppath_subs = tmp_file_path("dashmpd-subs", OsStr::new("sub"))?;
4360 if downloader.fetch_audio && !pd.audio_fragments.is_empty() {
4361 have_audio = fetch_period_audio(downloader,
4362 tmppath_audio.clone(), &pd.audio_fragments,
4363 &mut ds).await?;
4364 }
4365 if downloader.fetch_video && !pd.video_fragments.is_empty() {
4366 have_video = fetch_period_video(downloader,
4367 tmppath_video.clone(), &pd.video_fragments,
4368 &mut ds).await?;
4369 }
4370 if downloader.fetch_subtitles && !pd.subtitle_fragments.is_empty() {
4374 have_subtitles = fetch_period_subtitles(downloader,
4375 tmppath_subs.clone(),
4376 &pd.subtitle_fragments,
4377 &pd.subtitle_formats,
4378 &mut ds).await?;
4379 }
4380
4381 if have_audio && have_video {
4384 for observer in &downloader.progress_observers {
4385 observer.update(99, "Muxing audio and video");
4386 }
4387 if downloader.verbosity > 1 {
4388 info!(" {}", "Muxing audio and video streams".italic());
4389 }
4390 mux_audio_video(downloader, &period_output_path, &tmppath_audio, &tmppath_video)?;
4391 if pd.subtitle_formats.contains(&SubtitleType::Stpp) {
4392 let container = match &period_output_path.extension() {
4393 Some(ext) => ext.to_str().unwrap_or("mp4"),
4394 None => "mp4",
4395 };
4396 if container.eq("mp4") {
4397 if downloader.verbosity > 1 {
4398 if let Some(fmt) = &pd.subtitle_formats.first() {
4399 info!(" Downloaded media contains subtitles in {fmt:?} format");
4400 }
4401 info!(" Running MP4Box to merge subtitles with output MP4 container");
4402 }
4403 let tmp_str = tmppath_subs.to_string_lossy();
4406 let period_output_str = period_output_path.to_string_lossy();
4407 let args = vec!["-add", &tmp_str, &period_output_str];
4408 if downloader.verbosity > 0 {
4409 info!(" Running MP4Box {}", args.join(" "));
4410 }
4411 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4412 .args(args)
4413 .output()
4414 {
4415 let msg = partial_process_output(&mp4box.stdout);
4416 if !msg.is_empty() {
4417 info!(" MP4Box stdout: {msg}");
4418 }
4419 let msg = partial_process_output(&mp4box.stderr);
4420 if !msg.is_empty() {
4421 info!(" MP4Box stderr: {msg}");
4422 }
4423 if mp4box.status.success() {
4424 info!(" Merged subtitles with MP4 container");
4425 } else {
4426 warn!(" Error running MP4Box to merge subtitles");
4427 }
4428 } else {
4429 warn!(" Failed to spawn MP4Box to merge subtitles");
4430 }
4431 } else if container.eq("mkv") || container.eq("webm") {
4432 let srt = period_output_path.with_extension("srt");
4444 if srt.exists() {
4445 if downloader.verbosity > 0 {
4446 info!(" Running mkvmerge to merge subtitles with output Matroska container");
4447 }
4448 let tmppath = temporary_outpath(".mkv")?;
4449 let pop_arg = &period_output_path.to_string_lossy();
4450 let srt_arg = &srt.to_string_lossy();
4451 let mkvmerge_args = vec!["-o", &tmppath, pop_arg, srt_arg];
4452 if downloader.verbosity > 0 {
4453 info!(" Running mkvmerge {}", mkvmerge_args.join(" "));
4454 }
4455 if let Ok(mkvmerge) = Command::new(downloader.mkvmerge_location.clone())
4456 .args(mkvmerge_args)
4457 .output()
4458 {
4459 let msg = partial_process_output(&mkvmerge.stdout);
4460 if !msg.is_empty() {
4461 info!(" mkvmerge stdout: {msg}");
4462 }
4463 let msg = partial_process_output(&mkvmerge.stderr);
4464 if !msg.is_empty() {
4465 info!(" mkvmerge stderr: {msg}");
4466 }
4467 if mkvmerge.status.success() {
4468 info!(" Merged subtitles with Matroska container");
4469 {
4472 let tmpfile = File::open(tmppath.clone())
4473 .map_err(|e| DashMpdError::Io(
4474 e, String::from("opening mkvmerge output")))?;
4475 let mut merged = BufReader::new(tmpfile);
4476 let outfile = File::create(period_output_path.clone())
4478 .map_err(|e| DashMpdError::Io(
4479 e, String::from("creating output file")))?;
4480 let mut sink = BufWriter::new(outfile);
4481 io::copy(&mut merged, &mut sink)
4482 .map_err(|e| DashMpdError::Io(
4483 e, String::from("copying mkvmerge output to output file")))?;
4484 }
4485 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4486 if let Err(e) = fs::remove_file(tmppath) {
4487 warn!(" Error deleting temporary mkvmerge output: {e}");
4488 }
4489 }
4490 } else {
4491 warn!(" Error running mkvmerge to merge subtitles");
4492 }
4493 }
4494 }
4495 }
4496 }
4497 } else if have_audio {
4498 copy_audio_to_container(downloader, &period_output_path, &tmppath_audio)?;
4499 } else if have_video {
4500 copy_video_to_container(downloader, &period_output_path, &tmppath_video)?;
4501 } else if downloader.fetch_video && downloader.fetch_audio {
4502 return Err(DashMpdError::UnhandledMediaStream("no audio or video streams found".to_string()));
4503 } else if downloader.fetch_video {
4504 return Err(DashMpdError::UnhandledMediaStream("no video streams found".to_string()));
4505 } else if downloader.fetch_audio {
4506 return Err(DashMpdError::UnhandledMediaStream("no audio streams found".to_string()));
4507 }
4508 #[allow(clippy::collapsible_if)]
4509 if downloader.keep_audio.is_none() && downloader.fetch_audio {
4510 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4511 if tmppath_audio.exists() && fs::remove_file(tmppath_audio).is_err() {
4512 info!(" Failed to delete temporary file for audio stream");
4513 }
4514 }
4515 }
4516 #[allow(clippy::collapsible_if)]
4517 if downloader.keep_video.is_none() && downloader.fetch_video {
4518 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4519 if tmppath_video.exists() && fs::remove_file(tmppath_video).is_err() {
4520 info!(" Failed to delete temporary file for video stream");
4521 }
4522 }
4523 }
4524 #[allow(clippy::collapsible_if)]
4525 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4526 if downloader.fetch_subtitles && tmppath_subs.exists() && fs::remove_file(tmppath_subs).is_err() {
4527 info!(" Failed to delete temporary file for subtitles");
4528 }
4529 }
4530 if downloader.verbosity > 1 && (downloader.fetch_audio || downloader.fetch_video || have_subtitles) {
4531 if let Ok(metadata) = fs::metadata(period_output_path.clone()) {
4532 info!(" Wrote {:.1}MB to media file", metadata.len() as f64 / (1024.0 * 1024.0));
4533 }
4534 }
4535 if have_audio || have_video {
4536 period_output_paths.push(period_output_path);
4537 }
4538 } #[allow(clippy::comparison_chain)]
4540 if period_output_paths.len() == 1 {
4541 maybe_record_metainformation(output_path, downloader, &mpd);
4543 } else if period_output_paths.len() > 1 {
4544 #[allow(unused_mut)]
4549 let mut concatenated = false;
4550 #[cfg(not(feature = "libav"))]
4551 if downloader.concatenate_periods && video_containers_concatable(downloader, &period_output_paths) {
4552 info!("Preparing to concatenate multiple Periods into one output file");
4553 concat_output_files(downloader, &period_output_paths)?;
4554 for p in &period_output_paths[1..] {
4555 if fs::remove_file(p).is_err() {
4556 warn!(" Failed to delete temporary file {}", p.display());
4557 }
4558 }
4559 concatenated = true;
4560 if let Some(pop) = period_output_paths.first() {
4561 maybe_record_metainformation(pop, downloader, &mpd);
4562 }
4563 }
4564 if !concatenated {
4565 info!("Media content has been saved in a separate file for each period:");
4566 period_counter = 0;
4568 for p in period_output_paths {
4569 period_counter += 1;
4570 info!(" Period #{period_counter}: {}", p.display());
4571 maybe_record_metainformation(&p, downloader, &mpd);
4572 }
4573 }
4574 }
4575 let have_content_protection = mpd.periods.iter().any(
4576 |p| p.adaptations.iter().any(
4577 |a| (!a.ContentProtection.is_empty()) ||
4578 a.representations.iter().any(
4579 |r| !r.ContentProtection.is_empty())));
4580 if have_content_protection && downloader.decryption_keys.is_empty() {
4581 warn!("Manifest seems to use ContentProtection (DRM), but you didn't provide decryption keys.");
4582 }
4583 for observer in &downloader.progress_observers {
4584 observer.update(100, "Done");
4585 }
4586 Ok(PathBuf::from(output_path))
4587}
4588
4589
4590#[cfg(test)]
4591mod tests {
4592 #[test]
4593 fn test_resolve_url_template() {
4594 use std::collections::HashMap;
4595 use super::resolve_url_template;
4596
4597 assert_eq!(resolve_url_template("AA$Time$BB", &HashMap::from([("Time", "ZZZ".to_string())])),
4598 "AAZZZBB");
4599 assert_eq!(resolve_url_template("AA$Number%06d$BB", &HashMap::from([("Number", "42".to_string())])),
4600 "AA000042BB");
4601 let dict = HashMap::from([("RepresentationID", "640x480".to_string()),
4602 ("Number", "42".to_string()),
4603 ("Time", "ZZZ".to_string())]);
4604 assert_eq!(resolve_url_template("AA/$RepresentationID$/segment-$Number%05d$.mp4", &dict),
4605 "AA/640x480/segment-00042.mp4");
4606 }
4607}