1use std::env;
4use tokio::io;
5use tokio::fs;
6use tokio::fs::File;
7use tokio::io::{BufReader, BufWriter, AsyncWriteExt, AsyncSeekExt, AsyncReadExt};
8use std::path::{Path, PathBuf};
9use std::process::Command;
10use std::time::Duration;
11use tokio::time::Instant;
12use chrono::Utc;
13use std::sync::Arc;
14use std::borrow::Cow;
15use std::collections::HashMap;
16use std::cmp::min;
17use std::ffi::OsStr;
18use std::num::NonZeroU32;
19use futures_util::TryFutureExt;
20use tracing::{trace, info, warn, error};
21use regex::Regex;
22use url::Url;
23use bytes::Bytes;
24use data_url::DataUrl;
25use reqwest::header::{RANGE, CONTENT_TYPE};
26use backon::{ExponentialBuilder, Retryable};
27use governor::{Quota, RateLimiter};
28use lazy_static::lazy_static;
29use xot::{xmlname, Xot};
30use edit_distance::edit_distance;
31use crate::{MPD, Period, Representation, AdaptationSet, SegmentBase, DashMpdError};
32use crate::{parse, mux_audio_video, copy_video_to_container, copy_audio_to_container};
33use crate::{is_audio_adaptation, is_video_adaptation, is_subtitle_adaptation};
34use crate::{subtitle_type, content_protection_type, SubtitleType};
35use crate::check_conformity;
36#[cfg(not(feature = "libav"))]
37use crate::ffmpeg::concat_output_files;
38use crate::media::{temporary_outpath, AudioTrack};
39use crate::decryption::{
40 decrypt_mp4decrypt,
41 decrypt_shaka,
42 decrypt_shaka_container,
43 decrypt_mp4box,
44 decrypt_mp4box_container
45};
46#[allow(unused_imports)]
47use crate::media::video_containers_concatable;
48
49#[cfg(all(feature = "sandbox", target_os = "linux"))]
50use crate::sandbox::{restrict_thread};
51
52
53pub type HttpClient = reqwest::Client;
55type DirectRateLimiter = RateLimiter<governor::state::direct::NotKeyed,
56 governor::state::InMemoryState,
57 governor::clock::DefaultClock,
58 governor::middleware::NoOpMiddleware>;
59
60
61pub fn partial_process_output(output: &[u8]) -> Cow<'_, str> {
64 let len = min(output.len(), 4096);
65 #[allow(clippy::indexing_slicing)]
66 String::from_utf8_lossy(&output[0..len])
67}
68
69
70pub fn tmp_file_path(prefix: &str, extension: &OsStr) -> Result<PathBuf, DashMpdError> {
73 if let Some(ext) = extension.to_str() {
74 let fmt = format!(".{}", extension.to_string_lossy());
76 let suffix = if ext.starts_with('.') {
77 extension
78 } else {
79 OsStr::new(&fmt)
80 };
81 let file = tempfile::Builder::new()
82 .prefix(prefix)
83 .suffix(suffix)
84 .rand_bytes(7)
85 .disable_cleanup(env::var("DASHMPD_PERSIST_FILES").is_ok())
86 .tempfile()
87 .map_err(|e| DashMpdError::Io(e, String::from("creating temporary file")))?;
88 Ok(file.path().to_path_buf())
89 } else {
90 Err(DashMpdError::Other(String::from("converting filename extension")))
91 }
92}
93
94
95#[cfg(unix)]
99async fn ensure_permissions_readable(path: &Path) -> Result<(), DashMpdError> {
100 use std::fs::Permissions;
101 use std::os::unix::fs::PermissionsExt;
102
103 let perms = Permissions::from_mode(0o644);
104 fs::set_permissions(path, perms)
105 .map_err(|e| DashMpdError::Io(e, String::from("setting file permissions"))).await?;
106 Ok(())
107}
108
109#[cfg(not(unix))]
110async fn ensure_permissions_readable(path: &Path) -> Result<(), DashMpdError> {
111 let mut perms = fs::metadata(path).await
112 .map_err(|e| DashMpdError::Io(e, String::from("reading file permissions")))?
113 .permissions();
114 perms.set_readonly(false);
115 fs::set_permissions(path, perms)
116 .map_err(|e| DashMpdError::Io(e, String::from("setting file permissions"))).await?;
117 Ok(())
118}
119
120
121pub trait ProgressObserver: Send + Sync {
124 fn update(&self, percent: u32, bandwidth: u64, message: &str);
125}
126
127
128#[derive(PartialEq, Eq, Clone, Copy, Default)]
131pub enum QualityPreference { #[default] Lowest, Intermediate, Highest }
132
133
134pub struct DashDownloader {
154 pub mpd_url: String,
155 pub redirected_url: Url,
156 base_url: Option<String>,
157 referer: Option<String>,
158 auth_username: Option<String>,
159 auth_password: Option<String>,
160 auth_bearer_token: Option<String>,
161 pub output_path: Option<PathBuf>,
162 http_client: Option<HttpClient>,
163 quality_preference: QualityPreference,
164 language_preference_audio: Option<String>,
165 language_preference_subtitles: Option<String>,
166 role_preference: Vec<String>,
167 video_width_preference: Option<u64>,
168 video_height_preference: Option<u64>,
169 video_codec_preference: Vec<String>,
170 video_id_wanted: Option<String>,
171 fetch_video: bool,
172 fetch_audio: bool,
173 fetch_subtitles: bool,
174 keep_video: Option<PathBuf>,
175 keep_audio: Option<PathBuf>,
177 concatenate_periods: bool,
178 fragment_path: Option<PathBuf>,
179 pub decryption_keys: HashMap<String, String>,
180 xslt_stylesheets: Vec<PathBuf>,
181 minimum_period_duration: Option<Duration>,
182 content_type_checks: bool,
183 conformity_checks: bool,
184 use_index_range: bool,
185 fragment_retry_count: u32,
186 max_error_count: u32,
187 progress_observers: Vec<Arc<dyn ProgressObserver>>,
188 sleep_between_requests: u8,
189 allow_live_streams: bool,
190 force_duration: Option<f64>,
191 rate_limit: u64,
192 bw_limiter: Option<DirectRateLimiter>,
193 bw_estimator_started: Instant,
194 bw_estimator_bytes: usize,
195 pub sandbox: bool,
196 pub verbosity: u8,
197 record_metainformation: bool,
198 pub muxer_preference: HashMap<String, String>,
199 pub concat_preference: HashMap<String, String>,
200 pub decryptor_preference: String,
201 pub ffmpeg_location: String,
202 pub vlc_location: String,
203 pub mkvmerge_location: String,
204 pub mp4box_location: String,
205 pub mp4decrypt_location: String,
206 pub shaka_packager_location: String,
207}
208
209
210#[cfg(not(doctest))]
213impl DashDownloader {
232 pub fn new(mpd_url: &str) -> DashDownloader {
238 DashDownloader {
239 mpd_url: String::from(mpd_url),
240 redirected_url: Url::parse(mpd_url).unwrap(),
241 base_url: None,
242 referer: None,
243 auth_username: None,
244 auth_password: None,
245 auth_bearer_token: None,
246 output_path: None,
247 http_client: None,
248 quality_preference: QualityPreference::Lowest,
249 language_preference_audio: None,
250 language_preference_subtitles: None,
251 role_preference: vec!["main".to_string(), "alternate".to_string()],
252 video_width_preference: None,
253 video_height_preference: None,
254 video_codec_preference: Vec::new(),
255 video_id_wanted: None,
256 fetch_video: true,
257 fetch_audio: true,
258 fetch_subtitles: false,
259 keep_video: None,
260 keep_audio: None,
261 concatenate_periods: true,
262 fragment_path: None,
263 decryption_keys: HashMap::new(),
264 xslt_stylesheets: Vec::new(),
265 minimum_period_duration: None,
266 content_type_checks: true,
267 conformity_checks: true,
268 use_index_range: true,
269 fragment_retry_count: 10,
270 max_error_count: 30,
271 progress_observers: Vec::new(),
272 sleep_between_requests: 0,
273 allow_live_streams: false,
274 force_duration: None,
275 rate_limit: 0,
276 bw_limiter: None,
277 bw_estimator_started: Instant::now(),
278 bw_estimator_bytes: 0,
279 sandbox: false,
280 verbosity: 0,
281 record_metainformation: true,
282 muxer_preference: HashMap::new(),
283 concat_preference: HashMap::new(),
284 decryptor_preference: String::from("mp4decrypt"),
285 ffmpeg_location: String::from("ffmpeg"),
286 vlc_location: if cfg!(target_os = "windows") {
287 String::from("c:/Program Files/VideoLAN/VLC/vlc.exe")
290 } else {
291 String::from("vlc")
292 },
293 mkvmerge_location: String::from("mkvmerge"),
294 mp4box_location: if cfg!(target_os = "windows") {
295 String::from("MP4Box.exe")
296 } else if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
297 String::from("MP4Box")
298 } else {
299 String::from("mp4box")
300 },
301 mp4decrypt_location: String::from("mp4decrypt"),
302 shaka_packager_location: String::from("shaka-packager"),
303 }
304 }
305
306 #[must_use]
309 pub fn with_base_url(mut self, base_url: String) -> DashDownloader {
310 self.base_url = Some(base_url);
311 self
312 }
313
314
315 #[must_use]
337 pub fn with_http_client(mut self, client: HttpClient) -> DashDownloader {
338 self.http_client = Some(client);
339 self
340 }
341
342 #[must_use]
346 pub fn with_referer(mut self, referer: String) -> DashDownloader {
347 self.referer = Some(referer);
348 self
349 }
350
351 #[must_use]
354 pub fn with_authentication(mut self, username: &str, password: &str) -> DashDownloader {
355 self.auth_username = Some(username.to_string());
356 self.auth_password = Some(password.to_string());
357 self
358 }
359
360 #[must_use]
363 pub fn with_auth_bearer(mut self, token: &str) -> DashDownloader {
364 self.auth_bearer_token = Some(token.to_string());
365 self
366 }
367
368 #[must_use]
371 pub fn add_progress_observer(mut self, observer: Arc<dyn ProgressObserver>) -> DashDownloader {
372 self.progress_observers.push(observer);
373 self
374 }
375
376 #[must_use]
379 pub fn best_quality(mut self) -> DashDownloader {
380 self.quality_preference = QualityPreference::Highest;
381 self
382 }
383
384 #[must_use]
387 pub fn intermediate_quality(mut self) -> DashDownloader {
388 self.quality_preference = QualityPreference::Intermediate;
389 self
390 }
391
392 #[must_use]
395 pub fn worst_quality(mut self) -> DashDownloader {
396 self.quality_preference = QualityPreference::Lowest;
397 self
398 }
399
400 #[must_use]
407 pub fn prefer_language(mut self, lang: String) -> DashDownloader {
408 self.language_preference_audio = Some(lang.clone());
409 self.language_preference_subtitles = Some(lang);
410 self
411 }
412
413 #[must_use]
418 pub fn prefer_audio_language(mut self, lang: String) -> DashDownloader {
419 self.language_preference_audio = Some(lang);
420 self
421 }
422
423 #[must_use]
428 pub fn prefer_subtitle_language(mut self, lang: String) -> DashDownloader {
429 self.language_preference_subtitles = Some(lang);
430 self
431 }
432
433
434 #[must_use]
444 pub fn prefer_roles(mut self, role_preference: Vec<String>) -> DashDownloader {
445 if role_preference.len() < u8::MAX as usize {
446 self.role_preference = role_preference;
447 } else {
448 warn!("Ignoring role_preference ordering due to excessive length");
449 }
450 self
451 }
452
453 #[must_use]
456 pub fn prefer_video_width(mut self, width: u64) -> DashDownloader {
457 self.video_width_preference = Some(width);
458 self
459 }
460
461 #[must_use]
464 pub fn prefer_video_height(mut self, height: u64) -> DashDownloader {
465 self.video_height_preference = Some(height);
466 self
467 }
468
469 #[must_use]
474 pub fn prefer_video_codecs(mut self, codec_preference: Vec<String>) -> DashDownloader {
475 if codec_preference.len() < u8::MAX as usize {
476 self.video_codec_preference = codec_preference;
477 } else {
478 warn!("Ignoring video codec_preference due to excessive length");
479 }
480 self
481 }
482
483 #[must_use]
489 pub fn want_video_id_substring(mut self, substring: String) -> DashDownloader {
490 self.video_id_wanted = Some(substring);
491 self
492 }
493
494 #[must_use]
496 pub fn video_only(mut self) -> DashDownloader {
497 self.fetch_audio = false;
498 self.fetch_video = true;
499 self
500 }
501
502 #[must_use]
504 pub fn audio_only(mut self) -> DashDownloader {
505 self.fetch_audio = true;
506 self.fetch_video = false;
507 self
508 }
509
510 #[must_use]
513 pub fn keep_video_as<P: Into<PathBuf>>(mut self, video_path: P) -> DashDownloader {
514 self.keep_video = Some(video_path.into());
515 self
516 }
517
518 #[must_use]
521 pub fn keep_audio_as<P: Into<PathBuf>>(mut self, audio_path: P) -> DashDownloader {
522 self.keep_audio = Some(audio_path.into());
523 self
524 }
525
526 #[must_use]
529 pub fn save_fragments_to<P: Into<PathBuf>>(mut self, fragment_path: P) -> DashDownloader {
530 self.fragment_path = Some(fragment_path.into());
531 self
532 }
533
534 #[must_use]
546 pub fn add_decryption_key(mut self, id: String, key: String) -> DashDownloader {
547 self.decryption_keys.insert(id, key);
548 self
549 }
550
551 #[must_use]
563 pub fn with_xslt_stylesheet<P: Into<PathBuf>>(mut self, stylesheet: P) -> DashDownloader {
564 self.xslt_stylesheets.push(stylesheet.into());
565 self
566 }
567
568 #[must_use]
571 pub fn minimum_period_duration(mut self, value: Duration) -> DashDownloader {
572 self.minimum_period_duration = Some(value);
573 self
574 }
575
576 #[must_use]
580 pub fn fetch_audio(mut self, value: bool) -> DashDownloader {
581 self.fetch_audio = value;
582 self
583 }
584
585 #[must_use]
589 pub fn fetch_video(mut self, value: bool) -> DashDownloader {
590 self.fetch_video = value;
591 self
592 }
593
594 #[must_use]
602 pub fn fetch_subtitles(mut self, value: bool) -> DashDownloader {
603 self.fetch_subtitles = value;
604 self
605 }
606
607 #[must_use]
611 pub fn concatenate_periods(mut self, value: bool) -> DashDownloader {
612 self.concatenate_periods = value;
613 self
614 }
615
616 #[must_use]
619 pub fn without_content_type_checks(mut self) -> DashDownloader {
620 self.content_type_checks = false;
621 self
622 }
623
624 #[must_use]
627 pub fn content_type_checks(mut self, value: bool) -> DashDownloader {
628 self.content_type_checks = value;
629 self
630 }
631
632 #[must_use]
635 pub fn conformity_checks(mut self, value: bool) -> DashDownloader {
636 self.conformity_checks = value;
637 self
638 }
639
640 #[must_use]
655 pub fn use_index_range(mut self, value: bool) -> DashDownloader {
656 self.use_index_range = value;
657 self
658 }
659
660 #[must_use]
664 pub fn fragment_retry_count(mut self, count: u32) -> DashDownloader {
665 self.fragment_retry_count = count;
666 self
667 }
668
669 #[must_use]
676 pub fn max_error_count(mut self, count: u32) -> DashDownloader {
677 self.max_error_count = count;
678 self
679 }
680
681 #[must_use]
683 pub fn sleep_between_requests(mut self, seconds: u8) -> DashDownloader {
684 self.sleep_between_requests = seconds;
685 self
686 }
687
688 #[must_use]
700 pub fn allow_live_streams(mut self, value: bool) -> DashDownloader {
701 self.allow_live_streams = value;
702 self
703 }
704
705 #[must_use]
711 pub fn force_duration(mut self, seconds: f64) -> DashDownloader {
712 if seconds < 0.0 {
713 warn!("Ignoring negative value for force_duration()");
714 } else {
715 self.force_duration = Some(seconds);
716 if self.verbosity > 1 {
717 info!("Setting forced duration to {seconds:.1} seconds");
718 }
719 }
720 self
721 }
722
723 #[must_use]
729 pub fn with_rate_limit(mut self, bps: u64) -> DashDownloader {
730 if bps < 10 * 1024 {
731 warn!("Limiting bandwidth below 10kB/s is unlikely to be stable");
732 }
733 if self.verbosity > 1 {
734 info!("Limiting bandwidth to {} kB/s", bps/1024);
735 }
736 self.rate_limit = bps;
737 let mut kps = 1 + bps / 1024;
743 if kps > u64::from(u32::MAX) {
744 warn!("Throttling bandwidth limit");
745 kps = u32::MAX.into();
746 }
747 if let Some(bw_limit) = NonZeroU32::new(kps as u32) {
748 if let Some(burst) = NonZeroU32::new(10 * 1024) {
749 let bw_quota = Quota::per_second(bw_limit)
750 .allow_burst(burst);
751 self.bw_limiter = Some(RateLimiter::direct(bw_quota));
752 }
753 }
754 self
755 }
756
757 #[must_use]
767 pub fn verbosity(mut self, level: u8) -> DashDownloader {
768 self.verbosity = level;
769 self
770 }
771
772 #[must_use]
782 pub fn sandbox(mut self, enable: bool) -> DashDownloader {
783 #[cfg(not(all(feature = "sandbox", target_os = "linux")))]
784 if enable {
785 warn!("Sandboxing only available on Linux with crate feature sandbox enabled");
786 }
787 if self.verbosity > 1 && enable {
788 info!("Enabling sandboxing support");
789 }
790 self.sandbox = enable;
791 self
792 }
793
794 #[must_use]
798 pub fn record_metainformation(mut self, record: bool) -> DashDownloader {
799 self.record_metainformation = record;
800 self
801 }
802
803 #[must_use]
825 pub fn with_muxer_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
826 self.muxer_preference.insert(container.to_string(), ordering.to_string());
827 self
828 }
829
830 #[must_use]
853 pub fn with_concat_preference(mut self, container: &str, ordering: &str) -> DashDownloader {
854 self.concat_preference.insert(container.to_string(), ordering.to_string());
855 self
856 }
857
858 #[must_use]
867 pub fn with_decryptor_preference(mut self, decryption_tool: &str) -> DashDownloader {
868 self.decryptor_preference = decryption_tool.to_string();
869 self
870 }
871
872 #[must_use]
887 pub fn with_ffmpeg(mut self, ffmpeg_path: &str) -> DashDownloader {
888 self.ffmpeg_location = ffmpeg_path.to_string();
889 self
890 }
891
892 #[must_use]
907 pub fn with_vlc(mut self, vlc_path: &str) -> DashDownloader {
908 self.vlc_location = vlc_path.to_string();
909 self
910 }
911
912 #[must_use]
920 pub fn with_mkvmerge(mut self, path: &str) -> DashDownloader {
921 self.mkvmerge_location = path.to_string();
922 self
923 }
924
925 #[must_use]
933 pub fn with_mp4box(mut self, path: &str) -> DashDownloader {
934 self.mp4box_location = path.to_string();
935 self
936 }
937
938 #[must_use]
946 pub fn with_mp4decrypt(mut self, path: &str) -> DashDownloader {
947 self.mp4decrypt_location = path.to_string();
948 self
949 }
950
951 #[must_use]
959 pub fn with_shaka_packager(mut self, path: &str) -> DashDownloader {
960 self.shaka_packager_location = path.to_string();
961 self
962 }
963
964 pub async fn download_to<P: Into<PathBuf>>(mut self, out: P) -> Result<PathBuf, DashMpdError> {
974 self.output_path = Some(out.into());
975 if self.http_client.is_none() {
976 let client = reqwest::Client::builder()
977 .timeout(Duration::new(30, 0))
978 .cookie_store(true)
979 .build()
980 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
981 self.http_client = Some(client);
982 }
983 fetch_mpd(&mut self).await
984 }
985
986 pub async fn download(mut self) -> Result<PathBuf, DashMpdError> {
993 let cwd = env::current_dir()
994 .map_err(|e| DashMpdError::Io(e, String::from("obtaining current directory")))?;
995 let filename = generate_filename_from_url(&self.mpd_url);
996 let outpath = cwd.join(filename);
997 self.output_path = Some(outpath);
998 if self.http_client.is_none() {
999 let client = reqwest::Client::builder()
1000 .timeout(Duration::new(30, 0))
1001 .cookie_store(true)
1002 .build()
1003 .map_err(|_| DashMpdError::Network(String::from("building HTTP client")))?;
1004 self.http_client = Some(client);
1005 }
1006 fetch_mpd(&mut self).await
1007 }
1008}
1009
1010
1011fn mpd_is_dynamic(mpd: &MPD) -> bool {
1012 if let Some(mpdtype) = mpd.mpdtype.as_ref() {
1013 return mpdtype.eq("dynamic");
1014 }
1015 false
1016}
1017
1018fn parse_range(range: &str) -> Result<(u64, u64), DashMpdError> {
1021 let v: Vec<&str> = range.split_terminator('-').collect();
1022 if v.len() != 2 {
1023 return Err(DashMpdError::Parsing(format!("invalid range specifier: {range}")));
1024 }
1025 #[allow(clippy::indexing_slicing)]
1026 let start: u64 = v[0].parse()
1027 .map_err(|_| DashMpdError::Parsing(String::from("invalid start for range specifier")))?;
1028 #[allow(clippy::indexing_slicing)]
1029 let end: u64 = v[1].parse()
1030 .map_err(|_| DashMpdError::Parsing(String::from("invalid end for range specifier")))?;
1031 Ok((start, end))
1032}
1033
1034#[derive(Debug)]
1035struct MediaFragment {
1036 period: u8,
1037 url: Url,
1038 start_byte: Option<u64>,
1039 end_byte: Option<u64>,
1040 is_init: bool,
1041 timeout: Option<Duration>,
1042}
1043
1044#[derive(Debug)]
1045struct MediaFragmentBuilder {
1046 period: u8,
1047 url: Url,
1048 start_byte: Option<u64>,
1049 end_byte: Option<u64>,
1050 is_init: bool,
1051 timeout: Option<Duration>,
1052}
1053
1054impl MediaFragmentBuilder {
1055 pub fn new(period: u8, url: Url) -> MediaFragmentBuilder {
1056 MediaFragmentBuilder {
1057 period, url, start_byte: None, end_byte: None, is_init: false, timeout: None
1058 }
1059 }
1060
1061 pub fn with_range(mut self, start_byte: Option<u64>, end_byte: Option<u64>) -> MediaFragmentBuilder {
1062 self.start_byte = start_byte;
1063 self.end_byte = end_byte;
1064 self
1065 }
1066
1067 pub fn with_timeout(mut self, timeout: Duration) -> MediaFragmentBuilder {
1068 self.timeout = Some(timeout);
1069 self
1070 }
1071
1072 pub fn set_init(mut self) -> MediaFragmentBuilder {
1073 self.is_init = true;
1074 self
1075 }
1076
1077 pub fn build(self) -> MediaFragment {
1078 MediaFragment {
1079 period: self.period,
1080 url: self.url,
1081 start_byte: self.start_byte,
1082 end_byte: self.end_byte,
1083 is_init: self.is_init,
1084 timeout: self.timeout
1085 }
1086 }
1087}
1088
1089#[derive(Debug, Default)]
1093struct PeriodOutputs {
1094 fragments: Vec<MediaFragment>,
1095 diagnostics: Vec<String>,
1096 subtitle_formats: Vec<SubtitleType>,
1097 selected_audio_language: String,
1098}
1099
1100#[derive(Debug, Default)]
1101struct PeriodDownloads {
1102 audio_fragments: Vec<MediaFragment>,
1103 video_fragments: Vec<MediaFragment>,
1104 subtitle_fragments: Vec<MediaFragment>,
1105 subtitle_formats: Vec<SubtitleType>,
1106 period_counter: u8,
1107 id: Option<String>,
1108 selected_audio_language: String,
1109}
1110
1111fn period_fragment_count(pd: &PeriodDownloads) -> usize {
1112 pd.audio_fragments.len() +
1113 pd.video_fragments.len() +
1114 pd.subtitle_fragments.len()
1115}
1116
1117
1118
1119async fn throttle_download_rate(downloader: &DashDownloader, size: u32) -> Result<(), DashMpdError> {
1120 if downloader.rate_limit > 0 {
1121 if let Some(cells) = NonZeroU32::new(size) {
1122 if let Some(limiter) = downloader.bw_limiter.as_ref() {
1123 #[allow(clippy::redundant_pattern_matching)]
1124 if let Err(_) = limiter.until_n_ready(cells).await {
1125 return Err(DashMpdError::Other(
1126 "Bandwidth limit is too low".to_string()));
1127 }
1128 }
1129 }
1130 }
1131 Ok(())
1132}
1133
1134
1135fn generate_filename_from_url(url: &str) -> PathBuf {
1136 use sanitise_file_name::{sanitise_with_options, Options};
1137
1138 let mut path = url;
1139 if let Some(p) = path.strip_prefix("http://") {
1140 path = p;
1141 } else if let Some(p) = path.strip_prefix("https://") {
1142 path = p;
1143 } else if let Some(p) = path.strip_prefix("file://") {
1144 path = p;
1145 }
1146 if let Some(p) = path.strip_prefix("www.") {
1147 path = p;
1148 }
1149 if let Some(p) = path.strip_prefix("ftp.") {
1150 path = p;
1151 }
1152 if let Some(p) = path.strip_suffix(".mpd") {
1153 path = p;
1154 }
1155 let mut sanitize_opts = Options::DEFAULT;
1156 sanitize_opts.length_limit = 150;
1157 PathBuf::from(sanitise_with_options(path, &sanitize_opts) + ".mp4")
1162}
1163
1164fn output_path_for_period(base: &Path, period: u8) -> PathBuf {
1181 assert!(period > 0);
1182 if period == 1 {
1183 base.to_path_buf()
1184 } else {
1185 if let Some(stem) = base.file_stem() {
1186 if let Some(ext) = base.extension() {
1187 let fname = format!("{}-p{period}.{}", stem.to_string_lossy(), ext.to_string_lossy());
1188 return base.with_file_name(fname);
1189 }
1190 }
1191 let p = format!("dashmpd-p{period}");
1192 tmp_file_path(&p, base.extension().unwrap_or(OsStr::new("mp4")))
1193 .unwrap_or_else(|_| p.into())
1194 }
1195}
1196
1197fn is_absolute_url(s: &str) -> bool {
1198 s.starts_with("http://") ||
1199 s.starts_with("https://") ||
1200 s.starts_with("file://") ||
1201 s.starts_with("ftp://")
1202}
1203
1204fn merge_baseurls(current: &Url, new: &str) -> Result<Url, DashMpdError> {
1205 if is_absolute_url(new) {
1206 Url::parse(new)
1207 .map_err(|e| parse_error("parsing BaseURL", e))
1208 } else {
1209 let mut merged = current.join(new)
1222 .map_err(|e| parse_error("joining base with BaseURL", e))?;
1223 if merged.query().is_none() {
1224 merged.set_query(current.query());
1225 }
1226 Ok(merged)
1227 }
1228}
1229
1230fn content_type_audio_p(response: &reqwest::Response) -> bool {
1235 match response.headers().get("content-type") {
1236 Some(ct) => {
1237 let ctb = ct.as_bytes();
1238 ctb.starts_with(b"audio/") ||
1239 ctb.starts_with(b"video/") ||
1240 ctb.starts_with(b"application/octet-stream")
1241 },
1242 None => false,
1243 }
1244}
1245
1246fn content_type_video_p(response: &reqwest::Response) -> bool {
1248 match response.headers().get("content-type") {
1249 Some(ct) => {
1250 let ctb = ct.as_bytes();
1251 ctb.starts_with(b"video/") ||
1252 ctb.starts_with(b"application/octet-stream")
1253 },
1254 None => false,
1255 }
1256}
1257
1258
1259fn adaptation_lang_distance(a: &AdaptationSet, language_preference: &str) -> u8 {
1263 if let Some(lang) = &a.lang {
1264 if lang.eq(language_preference) {
1265 return 0;
1266 }
1267 edit_distance(lang, language_preference)
1269 .try_into()
1270 .unwrap_or(u8::MAX)
1271 } else {
1272 100
1273 }
1274}
1275
1276fn adaptation_roles(a: &AdaptationSet) -> Vec<String> {
1279 let mut roles = Vec::new();
1280 for r in &a.Role {
1281 if let Some(rv) = &r.value {
1282 roles.push(String::from(rv));
1283 }
1284 }
1285 for cc in &a.ContentComponent {
1286 for r in &cc.Role {
1287 if let Some(rv) = &r.value {
1288 roles.push(String::from(rv));
1289 }
1290 }
1291 }
1292 roles
1293}
1294
1295fn adaptation_role_distance(a: &AdaptationSet, role_preference: &[String]) -> u8 {
1297 adaptation_roles(a).iter()
1298 .map(|r| role_preference.binary_search(r).unwrap_or(u8::MAX.into()))
1299 .map(|u| u8::try_from(u).unwrap_or(u8::MAX))
1300 .min()
1301 .unwrap_or(u8::MAX)
1302}
1303
1304
1305fn select_preferred_adaptations<'a>(
1313 adaptations: Vec<&'a AdaptationSet>,
1314 downloader: &DashDownloader) -> Vec<&'a AdaptationSet>
1315{
1316 let mut preferred: Vec<&'a AdaptationSet>;
1317 if let Some(ref lang) = downloader.language_preference_audio {
1319 preferred = Vec::new();
1320 let distance: Vec<u8> = adaptations.iter()
1321 .map(|a| adaptation_lang_distance(a, lang))
1322 .collect();
1323 let min_distance = distance.iter().min().unwrap_or(&0);
1324 for (i, a) in adaptations.iter().enumerate() {
1325 if let Some(di) = distance.get(i) {
1326 if di == min_distance {
1327 preferred.push(a);
1328 }
1329 }
1330 }
1331 } else {
1332 preferred = adaptations;
1333 }
1334 let role_distance: Vec<u8> = preferred.iter()
1340 .map(|a| adaptation_role_distance(a, &downloader.role_preference))
1341 .collect();
1342 let role_distance_min = role_distance.iter().min().unwrap_or(&0);
1343 let mut best = Vec::new();
1344 for (i, a) in preferred.into_iter().enumerate() {
1345 if let Some(rdi) = role_distance.get(i) {
1346 if rdi == role_distance_min {
1347 best.push(a);
1348 }
1349 }
1350 }
1351 best
1352}
1353
1354
1355fn representation_filter_video_id<'a>(
1358 representations: Vec<&'a Representation>,
1359 downloader: &DashDownloader) -> Vec<&'a Representation>
1360{
1361 if let Some(wantid) = &downloader.video_id_wanted {
1362 representations.iter()
1363 .filter(|r| r.id.as_ref().is_some_and(|i| i.contains(wantid)))
1364 .copied()
1365 .collect()
1366 } else {
1367 representations
1368 }
1369}
1370
1371fn representation_filter_video_width<'a>(
1376 representations: Vec<&'a Representation>,
1377 downloader: &DashDownloader) -> Vec<&'a Representation>
1378{
1379 if let Some(want) = downloader.video_width_preference {
1380 let best = representations.iter()
1381 .min_by_key(|x| if let Some(w) = x.width { want.abs_diff(w) } else { u64::MAX });
1382 match best {
1383 Some(b) => representations.iter()
1384 .filter(|r| r.width == b.width)
1385 .copied()
1386 .collect::<Vec<&Representation>>(),
1387 None => representations,
1388 }
1389 } else {
1390 representations
1391 }
1392}
1393
1394fn representation_filter_video_height<'a>(
1399 representations: Vec<&'a Representation>,
1400 downloader: &DashDownloader) -> Vec<&'a Representation>
1401{
1402 if let Some(want) = downloader.video_height_preference {
1403 let best = representations.iter()
1404 .min_by_key(|x| if let Some(h) = x.height { want.abs_diff(h) } else { u64::MAX });
1405 match best {
1406 Some(b) => representations.iter()
1407 .filter(|r| r.height == b.height)
1408 .copied()
1409 .collect::<Vec<&Representation>>(),
1410 None => representations,
1411 }
1412 } else {
1413 representations
1414 }
1415}
1416
1417fn representation_filter_video_codec<'a>(
1424 representations: Vec<&'a Representation>,
1425 downloader: &DashDownloader) -> Vec<&'a Representation>
1426{
1427 if downloader.video_codec_preference.is_empty() {
1428 representations
1429 } else {
1430 let best = representations.iter()
1431 .min_by_key(|r|
1432 if let Some(codec) = &r.codecs {
1433 downloader.video_codec_preference.iter()
1434 .position(|prefc| codec.starts_with(prefc))
1435 .unwrap_or(usize::MAX)
1436 } else {
1437 usize::MAX
1438 });
1439 match best {
1440 Some(b) => if let Some(bcodec) = &b.codecs {
1441 let bcodec_start = match bcodec.find('.') {
1446 Some(idx) => &bcodec[..idx],
1447 None => bcodec,
1448 };
1449 representations.iter()
1450 .filter(|r| r.codecs.as_ref()
1451 .is_some_and(|rc| rc.starts_with(bcodec_start)))
1452 .copied()
1453 .collect()
1454 } else {
1455 representations
1456 },
1457 None => representations,
1458 }
1459 }
1460}
1461
1462fn representation_filter_video_quality<'a>(
1468 representations: Vec<&'a Representation>,
1469 downloader: &DashDownloader) -> Vec<&'a Representation>
1470{
1471 if representations.iter().all(|x| x.qualityRanking.is_some()) {
1472 match downloader.quality_preference {
1475 QualityPreference::Lowest => {
1476 let best = representations.iter()
1477 .max_by_key(|r| r.qualityRanking.unwrap_or(u8::MAX));
1478 match best {
1479 Some(b) => representations.iter()
1480 .filter(|r| r.qualityRanking.unwrap_or(u8::MAX) ==
1481 b.qualityRanking.unwrap_or(u8::MAX))
1482 .copied()
1483 .collect(),
1484 None => representations,
1485 }
1486 },
1487 QualityPreference::Highest => {
1488 let best = representations.iter()
1489 .min_by_key(|r| r.qualityRanking.unwrap_or(0));
1490 match best {
1491 Some(b) => representations.iter()
1492 .filter(|r| r.qualityRanking.unwrap_or(0) ==
1493 b.qualityRanking.unwrap_or(0))
1494 .copied()
1495 .collect(),
1496 None => representations,
1497 }
1498 },
1499 QualityPreference::Intermediate => {
1500 let count = representations.len();
1501 match count {
1502 0 => representations,
1503 1 => representations,
1504 _ => {
1505 let mut ranking: Vec<u8> = representations.iter()
1506 .map(|r| r.qualityRanking.unwrap_or(u8::MAX))
1507 .collect();
1508 ranking.sort_unstable();
1509 if let Some(want_ranking) = ranking.get(count / 2) {
1510 representations.iter()
1511 .filter(|r| r.qualityRanking.unwrap_or(u8::MAX) == *want_ranking)
1512 .copied()
1513 .collect()
1514 } else {
1515 representations
1516 }
1517 },
1518 }
1519 },
1520 }
1521 } else {
1522 let bw_large = 1_000_000_000;
1524 match downloader.quality_preference {
1525 QualityPreference::Lowest => {
1526 let best = representations.iter()
1527 .min_by_key(|r| r.bandwidth.unwrap_or(bw_large));
1528 match best {
1529 Some(b) => representations.iter()
1530 .filter(|r| r.bandwidth.unwrap_or(bw_large) ==
1531 b.bandwidth.unwrap_or(bw_large))
1532 .copied()
1533 .collect(),
1534 None => representations,
1535 }
1536 },
1537 QualityPreference::Highest => {
1538 for r in &representations {
1539 println!("££ possible rep with bandwidth = {}", r.bandwidth.unwrap_or(0));
1540 }
1541 let best = representations.iter()
1542 .max_by_key(|r| r.bandwidth.unwrap_or(0));
1543 println!("££ Got highest bandwidth rep {:?}", best);
1544 match best {
1545 Some(b) => representations.iter()
1546 .filter(|r| r.bandwidth.unwrap_or(0) ==
1547 b.bandwidth.unwrap_or(0))
1548 .copied()
1549 .collect(),
1550 None => representations,
1551 }
1552 }
1553 QualityPreference::Intermediate => {
1554 let count = representations.len();
1555 match count {
1556 0 => representations,
1557 1 => representations,
1558 _ => {
1559 let mut ranking: Vec<u64> = representations.iter()
1560 .map(|r| r.bandwidth.unwrap_or(bw_large))
1561 .collect();
1562 ranking.sort_unstable();
1563 if let Some(want_ranking) = ranking.get(count / 2) {
1564 representations.iter()
1565 .filter(|r| r.bandwidth.unwrap_or(bw_large) == *want_ranking)
1566 .copied()
1567 .collect()
1568 } else {
1569 representations
1570 }
1571 },
1572 }
1573 },
1574 }
1575 }
1576}
1577
1578
1579fn select_preferred_representation<'a>(
1585 representations: &[&'a Representation],
1586 downloader: &DashDownloader) -> Option<&'a Representation>
1587{
1588 if representations.iter().all(|x| x.qualityRanking.is_some()) {
1589 match downloader.quality_preference {
1592 QualityPreference::Lowest =>
1593 representations.iter()
1594 .max_by_key(|r| r.qualityRanking.unwrap_or(u8::MAX))
1595 .copied(),
1596 QualityPreference::Highest =>
1597 representations.iter().min_by_key(|r| r.qualityRanking.unwrap_or(0))
1598 .copied(),
1599 QualityPreference::Intermediate => {
1600 let count = representations.len();
1601 match count {
1602 0 => None,
1603 1 => Some(representations[0]),
1604 _ => {
1605 let mut ranking: Vec<u8> = representations.iter()
1606 .map(|r| r.qualityRanking.unwrap_or(u8::MAX))
1607 .collect();
1608 ranking.sort_unstable();
1609 if let Some(want_ranking) = ranking.get(count / 2) {
1610 representations.iter()
1611 .find(|r| r.qualityRanking.unwrap_or(u8::MAX) == *want_ranking)
1612 .copied()
1613 } else {
1614 representations.first().copied()
1615 }
1616 },
1617 }
1618 },
1619 }
1620 } else {
1621 match downloader.quality_preference {
1623 QualityPreference::Lowest => representations.iter()
1624 .min_by_key(|r| r.bandwidth.unwrap_or(1_000_000_000))
1625 .copied(),
1626 QualityPreference::Highest => representations.iter()
1627 .max_by_key(|r| r.bandwidth.unwrap_or(0))
1628 .copied(),
1629 QualityPreference::Intermediate => {
1630 let count = representations.len();
1631 match count {
1632 0 => None,
1633 1 => Some(representations[0]),
1634 _ => {
1635 let mut ranking: Vec<u64> = representations.iter()
1636 .map(|r| r.bandwidth.unwrap_or(100_000_000))
1637 .collect();
1638 ranking.sort_unstable();
1639 if let Some(want_ranking) = ranking.get(count / 2) {
1640 representations.iter()
1641 .find(|r| r.bandwidth.unwrap_or(100_000_000) == *want_ranking)
1642 .copied()
1643 } else {
1644 representations.first().copied()
1645 }
1646 },
1647 }
1648 },
1649 }
1650 }
1651}
1652
1653
1654fn print_available_subtitles_representation(r: &Representation, a: &AdaptationSet) {
1656 let unspecified = "<unspecified>".to_string();
1657 let empty = "".to_string();
1658 let lang = r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unspecified));
1659 let codecs = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&empty));
1660 let typ = subtitle_type(&a);
1661 let stype = if !codecs.is_empty() {
1662 format!("{typ:?}/{codecs}")
1663 } else {
1664 format!("{typ:?}")
1665 };
1666 let role = a.Role.first()
1667 .map_or_else(|| String::from(""),
1668 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1669 let label = a.Label.first()
1670 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1671 info!(" subs {stype:>18} | {lang:>10} |{role}{label}");
1672}
1673
1674fn print_available_subtitles_adaptation(a: &AdaptationSet) {
1675 a.representations.iter()
1676 .for_each(|r| print_available_subtitles_representation(r, a));
1677}
1678
1679fn print_available_streams_representation(r: &Representation, a: &AdaptationSet, typ: &str) {
1681 let unspecified = "<unspecified>".to_string();
1683 let w = r.width.unwrap_or(a.width.unwrap_or(0));
1684 let h = r.height.unwrap_or(a.height.unwrap_or(0));
1685 let codec = r.codecs.as_ref().unwrap_or(a.codecs.as_ref().unwrap_or(&unspecified));
1686 let bw = r.bandwidth.unwrap_or(a.maxBandwidth.unwrap_or(0));
1687 let fmt = if typ.eq("audio") {
1688 let unknown = String::from("?");
1689 format!("lang={}", r.lang.as_ref().unwrap_or(a.lang.as_ref().unwrap_or(&unknown)))
1690 } else if w == 0 || h == 0 {
1691 String::from("")
1694 } else {
1695 format!("{w}x{h}")
1696 };
1697 let role = a.Role.first()
1698 .map_or_else(|| String::from(""),
1699 |r| r.value.as_ref().map_or_else(|| String::from(""), |v| format!(" role={v}")));
1700 let label = a.Label.first()
1701 .map_or_else(|| String::from(""), |l| format!(" label={}", l.clone().content));
1702 let maybe_id = if let Some(rid) = &r.id {
1703 format!(" (id={rid})")
1704 } else {
1705 String::from("")
1706 };
1707 info!(" {typ} {codec:17} | {:5} Kbps | {fmt:>9}{role}{label}{maybe_id}", bw / 1024);
1708}
1709
1710fn print_available_streams_adaptation(a: &AdaptationSet, typ: &str) {
1711 a.representations.iter()
1712 .for_each(|r| print_available_streams_representation(r, a, typ));
1713}
1714
1715fn print_available_streams_period(p: &Period) {
1716 p.adaptations.iter()
1717 .filter(is_audio_adaptation)
1718 .for_each(|a| print_available_streams_adaptation(a, "audio"));
1719 p.adaptations.iter()
1720 .filter(is_video_adaptation)
1721 .for_each(|a| print_available_streams_adaptation(a, "video"));
1722 p.adaptations.iter()
1723 .filter(is_subtitle_adaptation)
1724 .for_each(print_available_subtitles_adaptation);
1725}
1726
1727#[tracing::instrument(level="trace", skip_all)]
1728fn print_available_streams(mpd: &MPD) {
1729 use humantime::format_duration;
1730
1731 let mut counter = 0;
1732 for p in &mpd.periods {
1733 let mut period_duration_secs: f64 = -1.0;
1734 if let Some(d) = mpd.mediaPresentationDuration {
1735 period_duration_secs = d.as_secs_f64();
1736 }
1737 if let Some(d) = &p.duration {
1738 period_duration_secs = d.as_secs_f64();
1739 }
1740 counter += 1;
1741 let duration = if period_duration_secs > 0.0 {
1742 format_duration(Duration::from_secs_f64(period_duration_secs)).to_string()
1743 } else {
1744 String::from("unknown")
1745 };
1746 if let Some(id) = p.id.as_ref() {
1747 info!("Streams in period {id} (#{counter}), duration {duration}:");
1748 } else {
1749 info!("Streams in period #{counter}, duration {duration}:");
1750 }
1751 print_available_streams_period(p);
1752 }
1753}
1754
1755async fn extract_init_pssh(downloader: &DashDownloader, init_url: Url) -> Option<Vec<u8>> {
1756 use bstr::ByteSlice;
1757 use hex_literal::hex;
1758
1759 if let Some(client) = downloader.http_client.as_ref() {
1760 let mut req = client.get(init_url);
1761 if let Some(referer) = &downloader.referer {
1762 req = req.header("Referer", referer);
1763 }
1764 if let Some(username) = &downloader.auth_username {
1765 if let Some(password) = &downloader.auth_password {
1766 req = req.basic_auth(username, Some(password));
1767 }
1768 }
1769 if let Some(token) = &downloader.auth_bearer_token {
1770 req = req.bearer_auth(token);
1771 }
1772 if let Ok(mut resp) = req.send().await {
1773 let mut chunk_counter = 0;
1776 let mut segment_first_bytes = Vec::<u8>::new();
1777 while let Ok(Some(chunk)) = resp.chunk().await {
1778 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
1779 #[allow(clippy::redundant_pattern_matching)]
1780 if let Err(_) = throttle_download_rate(downloader, size).await {
1781 return None;
1782 }
1783 segment_first_bytes.append(&mut chunk.to_vec());
1784 chunk_counter += 1;
1785 if chunk_counter > 20 {
1786 break;
1787 }
1788 }
1789 let needle = b"pssh";
1790 for offset in segment_first_bytes.find_iter(needle) {
1791 #[allow(clippy::needless_range_loop)]
1792 for i in offset-4..offset+2 {
1793 if let Some(b) = segment_first_bytes.get(i) {
1794 if *b != 0 {
1795 continue;
1796 }
1797 }
1798 }
1799 #[allow(clippy::needless_range_loop)]
1800 for i in offset+4..offset+8 {
1801 if let Some(b) = segment_first_bytes.get(i) {
1802 if *b != 0 {
1803 continue;
1804 }
1805 }
1806 }
1807 if offset+24 > segment_first_bytes.len() {
1808 continue;
1809 }
1810 const WIDEVINE_SYSID: [u8; 16] = hex!("edef8ba979d64acea3c827dcd51d21ed");
1812 if let Some(sysid) = segment_first_bytes.get((offset+8)..(offset+24)) {
1813 if !sysid.eq(&WIDEVINE_SYSID) {
1814 continue;
1815 }
1816 }
1817 if let Some(length) = segment_first_bytes.get(offset-1) {
1818 let start = offset - 4;
1819 let end = start + *length as usize;
1820 if let Some(pssh) = &segment_first_bytes.get(start..end) {
1821 return Some(pssh.to_vec());
1822 }
1823 }
1824 }
1825 }
1826 None
1827 } else {
1828 None
1829 }
1830}
1831
1832
1833lazy_static! {
1842 static ref URL_TEMPLATE_IDS: Vec<(&'static str, String, Regex)> = {
1843 vec!["RepresentationID", "Number", "Time", "Bandwidth"].into_iter()
1844 .map(|k| (k, format!("${k}$"), Regex::new(&format!("\\${k}%0([\\d])d\\$")).unwrap()))
1845 .collect()
1846 };
1847}
1848
1849fn resolve_url_template(template: &str, params: &HashMap<&str, String>) -> String {
1850 let mut result = template.to_string();
1851 for (k, ident, rx) in URL_TEMPLATE_IDS.iter() {
1852 if result.contains(ident) {
1854 if let Some(value) = params.get(k as &str) {
1855 result = result.replace(ident, value);
1856 }
1857 }
1858 if let Some(cap) = rx.captures(&result) {
1860 if let Some(value) = params.get(k as &str) {
1861 if let Ok(width) = cap[1].parse::<usize>() {
1862 if let Some(m) = rx.find(&result) {
1863 let count = format!("{value:0>width$}");
1864 result = result[..m.start()].to_owned() + &count + &result[m.end()..];
1865 }
1866 }
1867 }
1868 }
1869 }
1870 result
1871}
1872
1873
1874fn reqwest_error_transient_p(e: &reqwest::Error) -> bool {
1875 if e.is_timeout() {
1876 return true;
1877 }
1878 if let Some(s) = e.status() {
1879 if s == reqwest::StatusCode::REQUEST_TIMEOUT ||
1880 s == reqwest::StatusCode::TOO_MANY_REQUESTS ||
1881 s == reqwest::StatusCode::SERVICE_UNAVAILABLE ||
1882 s == reqwest::StatusCode::GATEWAY_TIMEOUT {
1883 return true;
1884 }
1885 }
1886 false
1887}
1888
1889fn notify_transient<E: std::fmt::Debug>(err: &E, dur: Duration) {
1890 warn!("Transient error after {dur:?}: {err:?}");
1891}
1892
1893fn network_error(why: &str, e: &reqwest::Error) -> DashMpdError {
1894 if e.is_timeout() {
1895 DashMpdError::NetworkTimeout(format!("{why}: {e:?}"))
1896 } else if e.is_connect() {
1897 DashMpdError::NetworkConnect(format!("{why}: {e:?}"))
1898 } else {
1899 DashMpdError::Network(format!("{why}: {e:?}"))
1900 }
1901}
1902
1903fn parse_error(why: &str, e: impl std::error::Error) -> DashMpdError {
1904 DashMpdError::Parsing(format!("{why}: {e:#?}"))
1905}
1906
1907
1908async fn reqwest_bytes_with_retries(
1912 client: &reqwest::Client,
1913 req: reqwest::Request,
1914 retry_count: u32) -> Result<Bytes, reqwest::Error>
1915{
1916 let mut last_error = None;
1917 for _ in 0..retry_count {
1918 if let Some(rqw) = req.try_clone() {
1919 match client.execute(rqw).await {
1920 Ok(response) => {
1921 match response.error_for_status() {
1922 Ok(resp) => {
1923 match resp.bytes().await {
1924 Ok(bytes) => return Ok(bytes),
1925 Err(e) => {
1926 info!("Retrying after HTTP error {e:?}");
1927 last_error = Some(e);
1928 },
1929 }
1930 },
1931 Err(e) => {
1932 info!("Retrying after HTTP error {e:?}");
1933 last_error = Some(e);
1934 },
1935 }
1936 },
1937 Err(e) => {
1938 info!("Retrying after HTTP error {e:?}");
1939 last_error = Some(e);
1940 },
1941 }
1942 }
1943 }
1944 Err(last_error.unwrap())
1945}
1946
1947#[allow(unused_variables)]
1960fn maybe_record_metainformation(path: &Path, downloader: &DashDownloader, mpd: &MPD) {
1961 #[cfg(target_family = "unix")]
1962 if downloader.record_metainformation && (downloader.fetch_audio || downloader.fetch_video) {
1963 if let Ok(origin_url) = Url::parse(&downloader.mpd_url) {
1964 #[allow(clippy::collapsible_if)]
1966 if origin_url.username().is_empty() && origin_url.password().is_none() {
1967 #[cfg(target_family = "unix")]
1968 if xattr::set(path, "user.xdg.origin.url", downloader.mpd_url.as_bytes()).is_err() {
1969 info!("Failed to set user.xdg.origin.url xattr on output file");
1970 }
1971 }
1972 for pi in &mpd.ProgramInformation {
1973 if let Some(t) = &pi.Title {
1974 if let Some(tc) = &t.content {
1975 if xattr::set(path, "user.dublincore.title", tc.as_bytes()).is_err() {
1976 info!("Failed to set user.dublincore.title xattr on output file");
1977 }
1978 }
1979 }
1980 if let Some(source) = &pi.Source {
1981 if let Some(sc) = &source.content {
1982 if xattr::set(path, "user.dublincore.source", sc.as_bytes()).is_err() {
1983 info!("Failed to set user.dublincore.source xattr on output file");
1984 }
1985 }
1986 }
1987 if let Some(copyright) = &pi.Copyright {
1988 if let Some(cc) = ©right.content {
1989 if xattr::set(path, "user.dublincore.rights", cc.as_bytes()).is_err() {
1990 info!("Failed to set user.dublincore.rights xattr on output file");
1991 }
1992 }
1993 }
1994 }
1995 }
1996 }
1997}
1998
1999fn fetchable_xlink_href(href: &str) -> bool {
2003 (!href.is_empty()) && href.ne("urn:mpeg:dash:resolve-to-zero:2013")
2004}
2005
2006fn element_resolves_to_zero(xot: &mut Xot, element: xot::Node) -> bool {
2007 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
2008 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
2009 if let Some(href) = xot.get_attribute(element, xlink_href_name.into()) {
2010 return href.eq("urn:mpeg:dash:resolve-to-zero:2013");
2011 }
2012 false
2013}
2014
2015fn skip_xml_preamble(input: &str) -> &str {
2016 if input.starts_with("<?xml") {
2017 if let Some(end_pos) = input.find("?>") {
2018 return &input[end_pos + 2..]; }
2021 }
2022 input
2024}
2025
2026async fn apply_xslt_stylesheets(
2027 downloader: &DashDownloader,
2028 xot: &mut Xot,
2029 doc: xot::Node) -> Result<String, DashMpdError> {
2030 #[cfg(feature = "xee-xslt")]
2031 return apply_xslt_stylesheets_xee(downloader, xot, doc).await;
2032 #[cfg(not(feature = "xee-xslt"))]
2033 return apply_xslt_stylesheets_xsltproc(downloader, xot, doc).await;
2034}
2035
2036async fn apply_xslt_stylesheets_xsltproc(
2040 downloader: &DashDownloader,
2041 xot: &mut Xot,
2042 doc: xot::Node) -> Result<String, DashMpdError> {
2043 let mut buf = Vec::new();
2044 xot.write(doc, &mut buf)
2045 .map_err(|e| parse_error("serializing rewritten manifest", e))?;
2046 for ss in &downloader.xslt_stylesheets {
2047 if downloader.verbosity > 0 {
2048 info!("Applying XSLT stylesheet {} with xsltproc", ss.display());
2049 }
2050 let tmpmpd = tmp_file_path("dashxslt", OsStr::new("xslt"))?;
2051 fs::write(&tmpmpd, &buf).await
2052 .map_err(|e| DashMpdError::Io(e, String::from("writing MPD")))?;
2053 let xsltproc = Command::new("xsltproc")
2054 .args([ss, &tmpmpd])
2055 .output()
2056 .map_err(|e| DashMpdError::Io(e, String::from("spawning xsltproc")))?;
2057 if !xsltproc.status.success() {
2058 let msg = format!("xsltproc returned {}", xsltproc.status);
2059 let out = partial_process_output(&xsltproc.stderr).to_string();
2060 return Err(DashMpdError::Io(std::io::Error::other(msg), out));
2061 }
2062 if env::var("DASHMPD_PERSIST_FILES").is_err() {
2063 if let Err(e) = fs::remove_file(&tmpmpd).await {
2064 warn!("Error removing temporary MPD after XSLT processing: {e:?}");
2065 }
2066 }
2067 buf.clone_from(&xsltproc.stdout);
2068 if downloader.verbosity > 2 {
2069 println!("Rewritten XSLT: {}", String::from_utf8_lossy(&buf));
2070 }
2071 }
2072 String::from_utf8(buf)
2073 .map_err(|e| parse_error("parsing UTF-8", e))
2074}
2075
2076#[cfg(feature = "xee-xslt")]
2080async fn apply_xslt_stylesheets_xee(
2081 downloader: &DashDownloader,
2082 xot: &mut Xot,
2083 doc: xot::Node) -> Result<String, DashMpdError>
2084{
2085 use xee_xslt_compiler::evaluate;
2086 use std::fmt::Write;
2087
2088 let mut xml = xot.to_string(doc)
2089 .map_err(|e| parse_error("serializing rewritten manifest", e))?;
2090 for ss in &downloader.xslt_stylesheets {
2091 if downloader.verbosity > 0 {
2092 info!(" Applying XSLT stylesheet {} with xee", ss.display());
2093 }
2094 let xslt = fs::read_to_string(ss).await
2095 .map_err(|_| DashMpdError::Other(String::from("reading XSLT stylesheet")))?;
2096 let seq = evaluate(xot, &xml, &xslt)
2097 .map_err(|e| DashMpdError::Other(format!("applying XSLT: {e:?}")))?;
2098 let mut f = String::new();
2099 for item in seq.iter() {
2100 match item.to_node() {
2101 Ok(n) => f.write_str(&xot.to_string(n).expect("writing to string"))
2102 .expect("writing to string"),
2103 Err(e) => error!("xee non-node item {item:?}: {e:?}"),
2104 }
2105 }
2106 xml = f;
2107 }
2108 Ok(xml)
2109}
2110
2111async fn resolve_xlink_references(
2115 downloader: &DashDownloader,
2116 xot: &mut Xot,
2117 node: xot::Node) -> Result<(), DashMpdError>
2118{
2119 let xlink_ns = xmlname::CreateNamespace::new(xot, "xlink", "http://www.w3.org/1999/xlink");
2120 let xlink_href_name = xmlname::CreateName::namespaced(xot, "href", &xlink_ns);
2121 let xlinked = xot.descendants(node)
2122 .filter(|d| xot.get_attribute(*d, xlink_href_name.into()).is_some())
2123 .collect::<Vec<_>>();
2124 for xl in xlinked {
2125 if element_resolves_to_zero(xot, xl) {
2126 trace!("Removing node with resolve-to-zero xlink:href {xl:?}");
2127 if let Err(e) = xot.remove(xl) {
2128 return Err(parse_error("Failed to remove resolve-to-zero XML node", e));
2129 }
2130 } else if let Some(href) = xot.get_attribute(xl, xlink_href_name.into()) {
2131 if fetchable_xlink_href(href) {
2132 let xlink_url = if is_absolute_url(href) {
2133 Url::parse(href)
2134 .map_err(|e|
2135 if let Ok(ns) = xot.to_string(node) {
2136 parse_error(&format!("parsing XLink on {ns}"), e)
2137 } else {
2138 parse_error("parsing XLink", e)
2139 }
2140 )?
2141 } else {
2142 let mut merged = downloader.redirected_url.join(href)
2145 .map_err(|e|
2146 if let Ok(ns) = xot.to_string(node) {
2147 parse_error(&format!("parsing XLink on {ns}"), e)
2148 } else {
2149 parse_error("parsing XLink", e)
2150 }
2151 )?;
2152 merged.set_query(downloader.redirected_url.query());
2153 merged
2154 };
2155 let client = downloader.http_client.as_ref().unwrap();
2156 trace!("Fetching XLinked element {}", xlink_url.clone());
2157 let mut req = client.get(xlink_url.clone())
2158 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
2159 .header("Accept-Language", "en-US,en")
2160 .header("Sec-Fetch-Mode", "navigate");
2161 if let Some(referer) = &downloader.referer {
2162 req = req.header("Referer", referer);
2163 } else {
2164 req = req.header("Referer", downloader.redirected_url.to_string());
2165 }
2166 if let Some(username) = &downloader.auth_username {
2167 if let Some(password) = &downloader.auth_password {
2168 req = req.basic_auth(username, Some(password));
2169 }
2170 }
2171 if let Some(token) = &downloader.auth_bearer_token {
2172 req = req.bearer_auth(token);
2173 }
2174 let xml = req.send().await
2175 .map_err(|e|
2176 if let Ok(ns) = xot.to_string(node) {
2177 network_error(&format!("fetching XLink for {ns}"), &e)
2178 } else {
2179 network_error("fetching XLink", &e)
2180 }
2181 )?
2182 .error_for_status()
2183 .map_err(|e|
2184 if let Ok(ns) = xot.to_string(node) {
2185 network_error(&format!("fetching XLink for {ns}"), &e)
2186 } else {
2187 network_error("fetching XLink", &e)
2188 }
2189 )?
2190 .text().await
2191 .map_err(|e|
2192 if let Ok(ns) = xot.to_string(node) {
2193 network_error(&format!("resolving XLink for {ns}"), &e)
2194 } else {
2195 network_error("resolving XLink", &e)
2196 }
2197 )?;
2198 if downloader.verbosity > 2 {
2199 if let Ok(ns) = xot.to_string(node) {
2200 info!(" Resolved onLoad XLink {xlink_url} on {ns} -> {} octets", xml.len());
2201 } else {
2202 info!(" Resolved onLoad XLink {xlink_url} -> {} octets", xml.len());
2203 }
2204 }
2205 let wrapped_xml = r#"<?xml version="1.0" encoding="utf-8"?>"#.to_owned() +
2211 r#"<wrapper xmlns="urn:mpeg:dash:schema:mpd:2011" "# +
2212 r#"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" "# +
2213 r#"xmlns:cenc="urn:mpeg:cenc:2013" "# +
2214 r#"xmlns:mspr="urn:microsoft:playready" "# +
2215 r#"xmlns:xlink="http://www.w3.org/1999/xlink">"# +
2216 skip_xml_preamble(&xml) +
2217 r"</wrapper>";
2218 let wrapper_doc = xot.parse(&wrapped_xml)
2219 .map_err(|e| parse_error("parsing xlinked content", e))?;
2220 let wrapper_doc_el = xot.document_element(wrapper_doc)
2221 .map_err(|e| parse_error("extracting XML document element", e))?;
2222 for needs_insertion in xot.children(wrapper_doc_el).collect::<Vec<_>>() {
2223 xot.insert_after(xl, needs_insertion)
2225 .map_err(|e| parse_error("inserting XLinked content", e))?;
2226 }
2227 xot.remove(xl)
2228 .map_err(|e| parse_error("removing XLink node", e))?;
2229 }
2230 }
2231 }
2232 Ok(())
2233}
2234
2235#[tracing::instrument(level="trace", skip_all)]
2236pub async fn parse_resolving_xlinks(
2237 downloader: &DashDownloader,
2238 xml: &[u8]) -> Result<MPD, DashMpdError>
2239{
2240 use xot::xmlname::NameStrInfo;
2241
2242 let mut xot = Xot::new();
2243 let doc = xot.parse_bytes(xml)
2244 .map_err(|e| parse_error("XML parsing", e))?;
2245 let doc_el = xot.document_element(doc)
2246 .map_err(|e| parse_error("extracting XML document element", e))?;
2247 let doc_name = match xot.node_name(doc_el) {
2248 Some(n) => n,
2249 None => return Err(DashMpdError::Parsing(String::from("missing root node name"))),
2250 };
2251 let root_name = xot.name_ref(doc_name, doc_el)
2252 .map_err(|e| parse_error("extracting root node name", e))?;
2253 let root_local_name = root_name.local_name();
2254 if !root_local_name.eq("MPD") {
2255 return Err(DashMpdError::Parsing(format!("root element is {root_local_name}, expecting <MPD>")));
2256 }
2257 for _ in 1..5 {
2260 resolve_xlink_references(downloader, &mut xot, doc).await?;
2261 }
2262 let rewritten = apply_xslt_stylesheets(downloader, &mut xot, doc).await?;
2263 let mpd = parse(&rewritten)?;
2265 if downloader.conformity_checks {
2266 for emsg in check_conformity(&mpd) {
2267 warn!("DASH conformity error in manifest: {emsg}");
2268 }
2269 }
2270 Ok(mpd)
2271}
2272
2273async fn do_segmentbase_indexrange(
2274 downloader: &DashDownloader,
2275 period_counter: u8,
2276 base_url: Url,
2277 sb: &SegmentBase,
2278 dict: &HashMap<&str, String>
2279) -> Result<Vec<MediaFragment>, DashMpdError>
2280{
2281 let mut fragments = Vec::new();
2314 let mut start_byte: Option<u64> = None;
2315 let mut end_byte: Option<u64> = None;
2316 let mut indexable_segments = false;
2317 if downloader.use_index_range {
2318 if let Some(ir) = &sb.indexRange {
2319 let (s, e) = parse_range(ir)?;
2321 trace!("Fetching sidx for {}", base_url.clone());
2322 let mut req = downloader.http_client.as_ref()
2323 .unwrap()
2324 .get(base_url.clone())
2325 .header(RANGE, format!("bytes={s}-{e}"))
2326 .header("Referer", downloader.redirected_url.to_string())
2327 .header("Sec-Fetch-Mode", "navigate");
2328 if let Some(username) = &downloader.auth_username {
2329 if let Some(password) = &downloader.auth_password {
2330 req = req.basic_auth(username, Some(password));
2331 }
2332 }
2333 if let Some(token) = &downloader.auth_bearer_token {
2334 req = req.bearer_auth(token);
2335 }
2336 let mut resp = req.send().await
2337 .map_err(|e| network_error("fetching index data", &e))?
2338 .error_for_status()
2339 .map_err(|e| network_error("fetching index data", &e))?;
2340 let headers = std::mem::take(resp.headers_mut());
2341 if let Some(content_type) = headers.get(CONTENT_TYPE) {
2342 let idx = resp.bytes().await
2343 .map_err(|e| network_error("fetching index data", &e))?;
2344 if idx.len() as u64 != e - s + 1 {
2345 warn!(" HTTP server does not support Range requests; can't use indexRange addressing");
2346 } else {
2347 #[allow(clippy::collapsible_else_if)]
2348 if content_type.eq("video/mp4") ||
2349 content_type.eq("audio/mp4") {
2350 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2357 .with_range(Some(0), Some(e))
2358 .build();
2359 fragments.push(mf);
2360 let mut max_chunk_pos = 0;
2361 if let Ok(segment_chunks) = crate::sidx::from_isobmff_sidx(&idx, e+1) {
2362 trace!("Have {} segment chunks in sidx data", segment_chunks.len());
2363 for chunk in segment_chunks {
2364 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2365 .with_range(Some(chunk.start), Some(chunk.end))
2366 .build();
2367 fragments.push(mf);
2368 if chunk.end > max_chunk_pos {
2369 max_chunk_pos = chunk.end;
2370 }
2371 }
2372 indexable_segments = true;
2373 }
2374 }
2375 }
2382 }
2383 }
2384 }
2385 if indexable_segments {
2386 if let Some(init) = &sb.Initialization {
2387 if let Some(range) = &init.range {
2388 let (s, e) = parse_range(range)?;
2389 start_byte = Some(s);
2390 end_byte = Some(e);
2391 }
2392 if let Some(su) = &init.sourceURL {
2393 let path = resolve_url_template(su, dict);
2394 let u = merge_baseurls(&base_url, &path)?;
2395 let mf = MediaFragmentBuilder::new(period_counter, u)
2396 .with_range(start_byte, end_byte)
2397 .set_init()
2398 .build();
2399 fragments.push(mf);
2400 } else {
2401 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2403 .with_range(start_byte, end_byte)
2404 .set_init()
2405 .build();
2406 fragments.push(mf);
2407 }
2408 }
2409 } else {
2410 trace!("Falling back to retrieving full SegmentBase for {}", base_url.clone());
2415 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2416 .with_timeout(Duration::new(10_000, 0))
2417 .build();
2418 fragments.push(mf);
2419 }
2420 Ok(fragments)
2421}
2422
2423
2424#[tracing::instrument(level="trace", skip_all)]
2425async fn do_period_audio(
2426 downloader: &DashDownloader,
2427 mpd: &MPD,
2428 period: &Period,
2429 period_counter: u8,
2430 base_url: Url
2431) -> Result<PeriodOutputs, DashMpdError>
2432{
2433 let mut fragments = Vec::new();
2434 let mut diagnostics = Vec::new();
2435 let mut opt_init: Option<String> = None;
2436 let mut opt_media: Option<String> = None;
2437 let mut opt_duration: Option<f64> = None;
2438 let mut timescale = 1;
2439 let mut start_number = 1;
2440 let mut period_duration_secs: f64 = -1.0;
2443 if let Some(d) = mpd.mediaPresentationDuration {
2444 period_duration_secs = d.as_secs_f64();
2445 }
2446 if let Some(d) = period.duration {
2447 period_duration_secs = d.as_secs_f64();
2448 }
2449 if let Some(s) = downloader.force_duration {
2450 period_duration_secs = s;
2451 }
2452 if let Some(st) = &period.SegmentTemplate {
2456 if let Some(i) = &st.initialization {
2457 opt_init = Some(i.clone());
2458 }
2459 if let Some(m) = &st.media {
2460 opt_media = Some(m.clone());
2461 }
2462 if let Some(d) = st.duration {
2463 opt_duration = Some(d);
2464 }
2465 if let Some(ts) = st.timescale {
2466 timescale = ts;
2467 }
2468 if let Some(s) = st.startNumber {
2469 start_number = s;
2470 }
2471 }
2472 let mut selected_audio_language = "unk";
2473 let audio_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2476 .filter(is_audio_adaptation)
2477 .collect();
2478 let representations: Vec<&Representation> = select_preferred_adaptations(audio_adaptations, downloader)
2479 .iter()
2480 .flat_map(|a| a.representations.iter())
2481 .collect();
2482 if let Some(audio_repr) = select_preferred_representation(&representations, downloader) {
2483 let audio_adaptation = period.adaptations.iter()
2487 .find(|a| a.representations.iter().any(|r| r.eq(audio_repr)))
2488 .unwrap();
2489 if let Some(lang) = audio_repr.lang.as_ref().or(audio_adaptation.lang.as_ref()) {
2490 selected_audio_language = lang;
2491 }
2492 let mut base_url = base_url.clone();
2495 if let Some(bu) = &audio_adaptation.BaseURL.first() {
2496 base_url = merge_baseurls(&base_url, &bu.base)?;
2497 }
2498 if let Some(bu) = audio_repr.BaseURL.first() {
2499 base_url = merge_baseurls(&base_url, &bu.base)?;
2500 }
2501 if downloader.verbosity > 0 {
2502 let bw = if let Some(bw) = audio_repr.bandwidth {
2503 format!("bw={} Kbps ", bw / 1024)
2504 } else {
2505 String::from("")
2506 };
2507 let unknown = String::from("?");
2508 let lang = audio_repr.lang.as_ref()
2509 .unwrap_or(audio_adaptation.lang.as_ref()
2510 .unwrap_or(&unknown));
2511 let codec = audio_repr.codecs.as_ref()
2512 .unwrap_or(audio_adaptation.codecs.as_ref()
2513 .unwrap_or(&unknown));
2514 let maybe_id = if let Some(rid) = &audio_repr.id {
2515 format!(" (id={rid})")
2516 } else {
2517 String::from("")
2518 };
2519 diagnostics.push(format!(" Audio stream selected: {bw}lang={lang} codec={codec}{maybe_id}"));
2520 for cp in audio_repr.ContentProtection.iter()
2522 .chain(audio_adaptation.ContentProtection.iter())
2523 {
2524 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2525 if let Some(kid) = &cp.default_KID {
2526 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2527 }
2528 for pssh_element in &cp.cenc_pssh {
2529 if let Some(pssh_b64) = &pssh_element.content {
2530 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
2531 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
2532 diagnostics.push(format!(" {pssh}"));
2533 }
2534 }
2535 }
2536 }
2537 }
2538 if let Some(st) = &audio_adaptation.SegmentTemplate {
2543 if let Some(i) = &st.initialization {
2544 opt_init = Some(i.clone());
2545 }
2546 if let Some(m) = &st.media {
2547 opt_media = Some(m.clone());
2548 }
2549 if let Some(d) = st.duration {
2550 opt_duration = Some(d);
2551 }
2552 if let Some(ts) = st.timescale {
2553 timescale = ts;
2554 }
2555 if let Some(s) = st.startNumber {
2556 start_number = s;
2557 }
2558 }
2559 let mut dict = HashMap::new();
2560 if let Some(rid) = &audio_repr.id {
2561 dict.insert("RepresentationID", rid.clone());
2562 }
2563 if let Some(b) = &audio_repr.bandwidth {
2564 dict.insert("Bandwidth", b.to_string());
2565 }
2566 if let Some(sl) = &audio_adaptation.SegmentList {
2575 if downloader.verbosity > 1 {
2578 info!(" Using AdaptationSet>SegmentList addressing mode for audio representation");
2579 }
2580 let mut start_byte: Option<u64> = None;
2581 let mut end_byte: Option<u64> = None;
2582 if let Some(init) = &sl.Initialization {
2583 if let Some(range) = &init.range {
2584 let (s, e) = parse_range(range)?;
2585 start_byte = Some(s);
2586 end_byte = Some(e);
2587 }
2588 if let Some(su) = &init.sourceURL {
2589 let path = resolve_url_template(su, &dict);
2590 let init_url = merge_baseurls(&base_url, &path)?;
2591 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2592 .with_range(start_byte, end_byte)
2593 .set_init()
2594 .build();
2595 fragments.push(mf);
2596 } else {
2597 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2598 .with_range(start_byte, end_byte)
2599 .set_init()
2600 .build();
2601 fragments.push(mf);
2602 }
2603 }
2604 for su in &sl.segment_urls {
2605 start_byte = None;
2606 end_byte = None;
2607 if let Some(range) = &su.mediaRange {
2609 let (s, e) = parse_range(range)?;
2610 start_byte = Some(s);
2611 end_byte = Some(e);
2612 }
2613 if let Some(m) = &su.media {
2614 let u = merge_baseurls(&base_url, m)?;
2615 let mf = MediaFragmentBuilder::new(period_counter, u)
2616 .with_range(start_byte, end_byte)
2617 .build();
2618 fragments.push(mf);
2619 } else if let Some(bu) = audio_adaptation.BaseURL.first() {
2620 let u = merge_baseurls(&base_url, &bu.base)?;
2621 let mf = MediaFragmentBuilder::new(period_counter, u)
2622 .with_range(start_byte, end_byte)
2623 .build();
2624 fragments.push(mf);
2625 }
2626 }
2627 }
2628 if let Some(sl) = &audio_repr.SegmentList {
2629 if downloader.verbosity > 1 {
2631 info!(" Using Representation>SegmentList addressing mode for audio representation");
2632 }
2633 let mut start_byte: Option<u64> = None;
2634 let mut end_byte: Option<u64> = None;
2635 if let Some(init) = &sl.Initialization {
2636 if let Some(range) = &init.range {
2637 let (s, e) = parse_range(range)?;
2638 start_byte = Some(s);
2639 end_byte = Some(e);
2640 }
2641 if let Some(su) = &init.sourceURL {
2642 let path = resolve_url_template(su, &dict);
2643 let init_url = merge_baseurls(&base_url, &path)?;
2644 let mf = MediaFragmentBuilder::new(period_counter, init_url)
2645 .with_range(start_byte, end_byte)
2646 .set_init()
2647 .build();
2648 fragments.push(mf);
2649 } else {
2650 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
2651 .with_range(start_byte, end_byte)
2652 .set_init()
2653 .build();
2654 fragments.push(mf);
2655 }
2656 }
2657 for su in &sl.segment_urls {
2658 start_byte = None;
2659 end_byte = None;
2660 if let Some(range) = &su.mediaRange {
2662 let (s, e) = parse_range(range)?;
2663 start_byte = Some(s);
2664 end_byte = Some(e);
2665 }
2666 if let Some(m) = &su.media {
2667 let u = merge_baseurls(&base_url, m)?;
2668 let mf = MediaFragmentBuilder::new(period_counter, u)
2669 .with_range(start_byte, end_byte)
2670 .build();
2671 fragments.push(mf);
2672 } else if let Some(bu) = audio_repr.BaseURL.first() {
2673 let u = merge_baseurls(&base_url, &bu.base)?;
2674 let mf = MediaFragmentBuilder::new(period_counter, u)
2675 .with_range(start_byte, end_byte)
2676 .build();
2677 fragments.push(mf);
2678 }
2679 }
2680 } else if audio_repr.SegmentTemplate.is_some() ||
2681 audio_adaptation.SegmentTemplate.is_some()
2682 {
2683 let st;
2686 if let Some(it) = &audio_repr.SegmentTemplate {
2687 st = it;
2688 } else if let Some(it) = &audio_adaptation.SegmentTemplate {
2689 st = it;
2690 } else {
2691 panic!("unreachable");
2692 }
2693 if let Some(i) = &st.initialization {
2694 opt_init = Some(i.clone());
2695 }
2696 if let Some(m) = &st.media {
2697 opt_media = Some(m.clone());
2698 }
2699 if let Some(ts) = st.timescale {
2700 timescale = ts;
2701 }
2702 if let Some(sn) = st.startNumber {
2703 start_number = sn;
2704 }
2705 if let Some(stl) = &audio_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
2706 .or(audio_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
2707 {
2708 if downloader.verbosity > 1 {
2711 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for audio representation");
2712 }
2713 if let Some(init) = opt_init {
2714 let path = resolve_url_template(&init, &dict);
2715 let u = merge_baseurls(&base_url, &path)?;
2716 let mf = MediaFragmentBuilder::new(period_counter, u)
2717 .set_init()
2718 .build();
2719 fragments.push(mf);
2720 }
2721 let mut elapsed_seconds = 0.0;
2722 if let Some(media) = opt_media {
2723 let audio_path = resolve_url_template(&media, &dict);
2724 let mut segment_time = 0;
2725 let mut segment_duration;
2726 let mut number = start_number;
2727 let mut target_duration = period_duration_secs;
2728 if let Some(target) = downloader.force_duration {
2729 if target > period_duration_secs {
2730 warn!(" Requested forced duration exceeds available content");
2731 } else {
2732 target_duration = target;
2733 }
2734 }
2735 'segment_loop: for s in &stl.segments {
2736 if let Some(t) = s.t {
2737 segment_time = t;
2738 }
2739 segment_duration = s.d;
2740 let dict = HashMap::from([("Time", segment_time.to_string()),
2742 ("Number", number.to_string())]);
2743 let path = resolve_url_template(&audio_path, &dict);
2744 let u = merge_baseurls(&base_url, &path)?;
2745 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2746 number += 1;
2747 elapsed_seconds += segment_duration as f64 / timescale as f64;
2748 if downloader.force_duration.is_some() &&
2749 target_duration > 0.0 &&
2750 elapsed_seconds > target_duration {
2751 break 'segment_loop;
2752 }
2753 if let Some(r) = s.r {
2754 let mut count = 0i64;
2755 loop {
2756 count += 1;
2757 if r >= 0 && count > r {
2762 break;
2763 }
2764 if downloader.force_duration.is_some() &&
2765 target_duration > 0.0 &&
2766 elapsed_seconds > target_duration {
2767 break 'segment_loop;
2768 }
2769 segment_time += segment_duration;
2770 elapsed_seconds += segment_duration as f64 / timescale as f64;
2771 let dict = HashMap::from([("Time", segment_time.to_string()),
2772 ("Number", number.to_string())]);
2773 let path = resolve_url_template(&audio_path, &dict);
2774 let u = merge_baseurls(&base_url, &path)?;
2775 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2776 number += 1;
2777 }
2778 }
2779 segment_time += segment_duration;
2780 }
2781 } else {
2782 return Err(DashMpdError::UnhandledMediaStream(
2783 "SegmentTimeline without a media attribute".to_string()));
2784 }
2785 } else { if downloader.verbosity > 1 {
2790 info!(" Using SegmentTemplate addressing mode for audio representation");
2791 }
2792 let mut total_number = 0i64;
2793 if let Some(init) = opt_init {
2794 let path = resolve_url_template(&init, &dict);
2795 let u = merge_baseurls(&base_url, &path)?;
2796 let mf = MediaFragmentBuilder::new(period_counter, u)
2797 .set_init()
2798 .build();
2799 fragments.push(mf);
2800 }
2801 if let Some(media) = opt_media {
2802 let audio_path = resolve_url_template(&media, &dict);
2803 let timescale = st.timescale.unwrap_or(timescale);
2804 let mut segment_duration: f64 = -1.0;
2805 if let Some(d) = opt_duration {
2806 segment_duration = d;
2808 }
2809 if let Some(std) = st.duration {
2810 if timescale == 0 {
2811 return Err(DashMpdError::UnhandledMediaStream(
2812 "SegmentTemplate@duration attribute cannot be zero".to_string()));
2813 }
2814 segment_duration = std / timescale as f64;
2815 }
2816 if segment_duration < 0.0 {
2817 return Err(DashMpdError::UnhandledMediaStream(
2818 "Audio representation is missing SegmentTemplate@duration attribute".to_string()));
2819 }
2820 total_number += (period_duration_secs / segment_duration).round() as i64;
2821 let mut number = start_number;
2822 if mpd_is_dynamic(mpd) {
2825 if let Some(start_time) = mpd.availabilityStartTime {
2826 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
2827 number = (elapsed + number as f64 - 1f64).floor() as u64;
2828 } else {
2829 return Err(DashMpdError::UnhandledMediaStream(
2830 "dynamic manifest is missing @availabilityStartTime".to_string()));
2831 }
2832 }
2833 for _ in 1..=total_number {
2834 let dict = HashMap::from([("Number", number.to_string())]);
2835 let path = resolve_url_template(&audio_path, &dict);
2836 let u = merge_baseurls(&base_url, &path)?;
2837 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2838 number += 1;
2839 }
2840 }
2841 }
2842 } else if let Some(sb) = &audio_repr.SegmentBase {
2843 if downloader.verbosity > 1 {
2845 info!(" Using SegmentBase@indexRange addressing mode for audio representation");
2846 }
2847 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
2848 fragments.extend(mf);
2849 } else if fragments.is_empty() {
2850 if let Some(bu) = audio_repr.BaseURL.first() {
2851 if downloader.verbosity > 1 {
2853 info!(" Using BaseURL addressing mode for audio representation");
2854 }
2855 let u = merge_baseurls(&base_url, &bu.base)?;
2856 fragments.push(MediaFragmentBuilder::new(period_counter, u).build());
2857 }
2858 }
2859 if fragments.is_empty() {
2860 return Err(DashMpdError::UnhandledMediaStream(
2861 "no usable addressing mode identified for audio representation".to_string()));
2862 }
2863 }
2864 Ok(PeriodOutputs {
2865 fragments, diagnostics, subtitle_formats: Vec::new(),
2866 selected_audio_language: String::from(selected_audio_language)
2867 })
2868}
2869
2870
2871#[tracing::instrument(level="trace", skip_all)]
2872async fn do_period_video(
2873 downloader: &DashDownloader,
2874 mpd: &MPD,
2875 period: &Period,
2876 period_counter: u8,
2877 base_url: Url
2878 ) -> Result<PeriodOutputs, DashMpdError>
2879{
2880 let mut fragments = Vec::new();
2881 let mut diagnostics = Vec::new();
2882 let mut period_duration_secs: f64 = 0.0;
2883 let mut opt_init: Option<String> = None;
2884 let mut opt_media: Option<String> = None;
2885 let mut opt_duration: Option<f64> = None;
2886 let mut timescale = 1;
2887 let mut start_number = 1;
2888 if let Some(d) = mpd.mediaPresentationDuration {
2889 period_duration_secs = d.as_secs_f64();
2890 }
2891 if let Some(d) = period.duration {
2892 period_duration_secs = d.as_secs_f64();
2893 }
2894 if let Some(s) = downloader.force_duration {
2895 period_duration_secs = s;
2896 }
2897 if let Some(st) = &period.SegmentTemplate {
2901 if let Some(i) = &st.initialization {
2902 opt_init = Some(i.clone());
2903 }
2904 if let Some(m) = &st.media {
2905 opt_media = Some(m.clone());
2906 }
2907 if let Some(d) = st.duration {
2908 opt_duration = Some(d);
2909 }
2910 if let Some(ts) = st.timescale {
2911 timescale = ts;
2912 }
2913 if let Some(s) = st.startNumber {
2914 start_number = s;
2915 }
2916 }
2917 let video_adaptations: Vec<&AdaptationSet> = period.adaptations.iter()
2934 .filter(is_video_adaptation)
2935 .collect();
2936 let representations: Vec<&Representation> = select_preferred_adaptations(video_adaptations, downloader)
2937 .iter()
2938 .flat_map(|a| a.representations.iter())
2939 .collect();
2940 trace!("Before filtering we have {} Representations", representations.len());
2941 let representations = representation_filter_video_id(representations, downloader);
2942 trace!("After video_id filter we have {} Representations", representations.len());
2943 let representations = representation_filter_video_width(representations, downloader);
2944 trace!("After width filter we have {} Representations", representations.len());
2945 let representations = representation_filter_video_height(representations, downloader);
2946 trace!("After height filter we have {} Representations", representations.len());
2947 let representations = representation_filter_video_codec(representations, downloader);
2948 trace!("After video codec filter we have {} Representations", representations.len());
2949 let representations = representation_filter_video_quality(representations, downloader);
2950 trace!("After quality filter we have {} Representations", representations.len());
2951 if let Some(video_repr) = representations.first() {
2952 let video_adaptation = period.adaptations.iter()
2956 .find(|a| a.representations.iter().any(|r| r.eq(video_repr)))
2957 .unwrap();
2958 let mut base_url = base_url.clone();
2961 if let Some(bu) = &video_adaptation.BaseURL.first() {
2962 base_url = merge_baseurls(&base_url, &bu.base)?;
2963 }
2964 if let Some(bu) = &video_repr.BaseURL.first() {
2965 base_url = merge_baseurls(&base_url, &bu.base)?;
2966 }
2967 if downloader.verbosity > 0 {
2968 let bw = if let Some(bw) = video_repr.bandwidth.or(video_adaptation.maxBandwidth) {
2969 format!("bw={} Kbps ", bw / 1024)
2970 } else {
2971 String::from("")
2972 };
2973 let unknown = String::from("?");
2974 let w = video_repr.width.unwrap_or(video_adaptation.width.unwrap_or(0));
2975 let h = video_repr.height.unwrap_or(video_adaptation.height.unwrap_or(0));
2976 let fmt = if w == 0 || h == 0 {
2977 String::from("")
2978 } else {
2979 format!("resolution={w}x{h} ")
2980 };
2981 let codec = video_repr.codecs.as_ref()
2982 .unwrap_or(video_adaptation.codecs.as_ref().unwrap_or(&unknown));
2983 let maybe_id = if let Some(rid) = &video_repr.id {
2984 format!(" (id={rid})")
2985 } else {
2986 String::from("")
2987 };
2988 diagnostics.push(format!(" Video stream selected: {bw}{fmt}codec={codec}{maybe_id}"));
2989 for cp in video_repr.ContentProtection.iter()
2991 .chain(video_adaptation.ContentProtection.iter())
2992 {
2993 diagnostics.push(format!(" ContentProtection: {}", content_protection_type(cp)));
2994 if let Some(kid) = &cp.default_KID {
2995 diagnostics.push(format!(" KID: {}", kid.replace('-', "")));
2996 }
2997 for pssh_element in &cp.cenc_pssh {
2998 if let Some(pssh_b64) = &pssh_element.content {
2999 diagnostics.push(format!(" PSSH (from manifest): {pssh_b64}"));
3000 if let Ok(pssh) = pssh_box::from_base64(pssh_b64) {
3001 diagnostics.push(format!(" {pssh}"));
3002 }
3003 }
3004 }
3005 }
3006 }
3007 let mut dict = HashMap::new();
3008 if let Some(rid) = &video_repr.id {
3009 dict.insert("RepresentationID", rid.clone());
3010 }
3011 if let Some(b) = &video_repr.bandwidth {
3012 dict.insert("Bandwidth", b.to_string());
3013 }
3014 if let Some(st) = &video_adaptation.SegmentTemplate {
3019 if let Some(i) = &st.initialization {
3020 opt_init = Some(i.clone());
3021 }
3022 if let Some(m) = &st.media {
3023 opt_media = Some(m.clone());
3024 }
3025 if let Some(d) = st.duration {
3026 opt_duration = Some(d);
3027 }
3028 if let Some(ts) = st.timescale {
3029 timescale = ts;
3030 }
3031 if let Some(s) = st.startNumber {
3032 start_number = s;
3033 }
3034 }
3035 if let Some(sl) = &video_adaptation.SegmentList {
3039 if downloader.verbosity > 1 {
3041 info!(" Using AdaptationSet>SegmentList addressing mode for video representation");
3042 }
3043 let mut start_byte: Option<u64> = None;
3044 let mut end_byte: Option<u64> = None;
3045 if let Some(init) = &sl.Initialization {
3046 if let Some(range) = &init.range {
3047 let (s, e) = parse_range(range)?;
3048 start_byte = Some(s);
3049 end_byte = Some(e);
3050 }
3051 if let Some(su) = &init.sourceURL {
3052 let path = resolve_url_template(su, &dict);
3053 let u = merge_baseurls(&base_url, &path)?;
3054 let mf = MediaFragmentBuilder::new(period_counter, u)
3055 .with_range(start_byte, end_byte)
3056 .set_init()
3057 .build();
3058 fragments.push(mf);
3059 }
3060 } else {
3061 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3062 .with_range(start_byte, end_byte)
3063 .set_init()
3064 .build();
3065 fragments.push(mf);
3066 }
3067 for su in &sl.segment_urls {
3068 start_byte = None;
3069 end_byte = None;
3070 if let Some(range) = &su.mediaRange {
3072 let (s, e) = parse_range(range)?;
3073 start_byte = Some(s);
3074 end_byte = Some(e);
3075 }
3076 if let Some(m) = &su.media {
3077 let u = merge_baseurls(&base_url, m)?;
3078 let mf = MediaFragmentBuilder::new(period_counter, u)
3079 .with_range(start_byte, end_byte)
3080 .build();
3081 fragments.push(mf);
3082 } else if let Some(bu) = video_adaptation.BaseURL.first() {
3083 let u = merge_baseurls(&base_url, &bu.base)?;
3084 let mf = MediaFragmentBuilder::new(period_counter, u)
3085 .with_range(start_byte, end_byte)
3086 .build();
3087 fragments.push(mf);
3088 }
3089 }
3090 }
3091 if let Some(sl) = &video_repr.SegmentList {
3092 if downloader.verbosity > 1 {
3094 info!(" Using Representation>SegmentList addressing mode for video representation");
3095 }
3096 let mut start_byte: Option<u64> = None;
3097 let mut end_byte: Option<u64> = None;
3098 if let Some(init) = &sl.Initialization {
3099 if let Some(range) = &init.range {
3100 let (s, e) = parse_range(range)?;
3101 start_byte = Some(s);
3102 end_byte = Some(e);
3103 }
3104 if let Some(su) = &init.sourceURL {
3105 let path = resolve_url_template(su, &dict);
3106 let u = merge_baseurls(&base_url, &path)?;
3107 let mf = MediaFragmentBuilder::new(period_counter, u)
3108 .with_range(start_byte, end_byte)
3109 .set_init()
3110 .build();
3111 fragments.push(mf);
3112 } else {
3113 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3114 .with_range(start_byte, end_byte)
3115 .set_init()
3116 .build();
3117 fragments.push(mf);
3118 }
3119 }
3120 for su in &sl.segment_urls {
3121 start_byte = None;
3122 end_byte = None;
3123 if let Some(range) = &su.mediaRange {
3125 let (s, e) = parse_range(range)?;
3126 start_byte = Some(s);
3127 end_byte = Some(e);
3128 }
3129 if let Some(m) = &su.media {
3130 let u = merge_baseurls(&base_url, m)?;
3131 let mf = MediaFragmentBuilder::new(period_counter, u)
3132 .with_range(start_byte, end_byte)
3133 .build();
3134 fragments.push(mf);
3135 } else if let Some(bu) = video_repr.BaseURL.first() {
3136 let u = merge_baseurls(&base_url, &bu.base)?;
3137 let mf = MediaFragmentBuilder::new(period_counter, u)
3138 .with_range(start_byte, end_byte)
3139 .build();
3140 fragments.push(mf);
3141 }
3142 }
3143 } else if video_repr.SegmentTemplate.is_some() ||
3144 video_adaptation.SegmentTemplate.is_some() {
3145 let st;
3148 if let Some(it) = &video_repr.SegmentTemplate {
3149 st = it;
3150 } else if let Some(it) = &video_adaptation.SegmentTemplate {
3151 st = it;
3152 } else {
3153 panic!("impossible");
3154 }
3155 if let Some(i) = &st.initialization {
3156 opt_init = Some(i.clone());
3157 }
3158 if let Some(m) = &st.media {
3159 opt_media = Some(m.clone());
3160 }
3161 if let Some(ts) = st.timescale {
3162 timescale = ts;
3163 }
3164 if let Some(sn) = st.startNumber {
3165 start_number = sn;
3166 }
3167 if let Some(stl) = &video_repr.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
3168 .or(video_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
3169 {
3170 if downloader.verbosity > 1 {
3172 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for video representation");
3173 }
3174 if let Some(init) = opt_init {
3175 let path = resolve_url_template(&init, &dict);
3176 let u = merge_baseurls(&base_url, &path)?;
3177 let mf = MediaFragmentBuilder::new(period_counter, u)
3178 .set_init()
3179 .build();
3180 fragments.push(mf);
3181 }
3182 let mut elapsed_seconds = 0.0;
3183 if let Some(media) = opt_media {
3184 let video_path = resolve_url_template(&media, &dict);
3185 let mut segment_time = 0;
3186 let mut segment_duration;
3187 let mut number = start_number;
3188 let mut target_duration = period_duration_secs;
3189 if let Some(target) = downloader.force_duration {
3190 if target > period_duration_secs {
3191 warn!(" Requested forced duration exceeds available content");
3192 } else {
3193 target_duration = target;
3194 }
3195 }
3196 'segment_loop: for s in &stl.segments {
3197 if let Some(t) = s.t {
3198 segment_time = t;
3199 }
3200 segment_duration = s.d;
3201 let dict = HashMap::from([("Time", segment_time.to_string()),
3203 ("Number", number.to_string())]);
3204 let path = resolve_url_template(&video_path, &dict);
3205 let u = merge_baseurls(&base_url, &path)?;
3206 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3207 fragments.push(mf);
3208 number += 1;
3209 elapsed_seconds += segment_duration as f64 / timescale as f64;
3210 if downloader.force_duration.is_some() &&
3211 target_duration > 0.0 &&
3212 elapsed_seconds > target_duration
3213 {
3214 break 'segment_loop;
3215 }
3216 if let Some(r) = s.r {
3217 let mut count = 0i64;
3218 loop {
3219 count += 1;
3220 if r >= 0 && count > r {
3226 break;
3227 }
3228 if downloader.force_duration.is_some() &&
3229 target_duration > 0.0 &&
3230 elapsed_seconds > target_duration
3231 {
3232 break 'segment_loop;
3233 }
3234 segment_time += segment_duration;
3235 elapsed_seconds += segment_duration as f64 / timescale as f64;
3236 let dict = HashMap::from([("Time", segment_time.to_string()),
3237 ("Number", number.to_string())]);
3238 let path = resolve_url_template(&video_path, &dict);
3239 let u = merge_baseurls(&base_url, &path)?;
3240 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3241 fragments.push(mf);
3242 number += 1;
3243 }
3244 }
3245 segment_time += segment_duration;
3246 }
3247 } else {
3248 return Err(DashMpdError::UnhandledMediaStream(
3249 "SegmentTimeline without a media attribute".to_string()));
3250 }
3251 } else { if downloader.verbosity > 1 {
3254 info!(" Using SegmentTemplate addressing mode for video representation");
3255 }
3256 let mut total_number = 0i64;
3257 if let Some(init) = opt_init {
3258 let path = resolve_url_template(&init, &dict);
3259 let u = merge_baseurls(&base_url, &path)?;
3260 let mf = MediaFragmentBuilder::new(period_counter, u)
3261 .set_init()
3262 .build();
3263 fragments.push(mf);
3264 }
3265 if let Some(media) = opt_media {
3266 let video_path = resolve_url_template(&media, &dict);
3267 let timescale = st.timescale.unwrap_or(timescale);
3268 let mut segment_duration: f64 = -1.0;
3269 if let Some(d) = opt_duration {
3270 segment_duration = d;
3272 }
3273 if let Some(std) = st.duration {
3274 if timescale == 0 {
3275 return Err(DashMpdError::UnhandledMediaStream(
3276 "SegmentTemplate@duration attribute cannot be zero".to_string()));
3277 }
3278 segment_duration = std / timescale as f64;
3279 }
3280 if segment_duration < 0.0 {
3281 return Err(DashMpdError::UnhandledMediaStream(
3282 "Video representation is missing SegmentTemplate@duration attribute".to_string()));
3283 }
3284 total_number += (period_duration_secs / segment_duration).round() as i64;
3285 let mut number = start_number;
3286 if mpd_is_dynamic(mpd) {
3296 if let Some(start_time) = mpd.availabilityStartTime {
3297 let elapsed = Utc::now().signed_duration_since(start_time).as_seconds_f64() / segment_duration;
3298 number = (elapsed + number as f64 - 1f64).floor() as u64;
3299 } else {
3300 return Err(DashMpdError::UnhandledMediaStream(
3301 "dynamic manifest is missing @availabilityStartTime".to_string()));
3302 }
3303 }
3304 for _ in 1..=total_number {
3305 let dict = HashMap::from([("Number", number.to_string())]);
3306 let path = resolve_url_template(&video_path, &dict);
3307 let u = merge_baseurls(&base_url, &path)?;
3308 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3309 fragments.push(mf);
3310 number += 1;
3311 }
3312 }
3313 }
3314 } else if let Some(sb) = &video_repr.SegmentBase {
3315 if downloader.verbosity > 1 {
3317 info!(" Using SegmentBase@indexRange addressing mode for video representation");
3318 }
3319 let mf = do_segmentbase_indexrange(downloader, period_counter, base_url, sb, &dict).await?;
3320 fragments.extend(mf);
3321 } else if fragments.is_empty() {
3322 if let Some(bu) = video_repr.BaseURL.first() {
3323 if downloader.verbosity > 1 {
3325 info!(" Using BaseURL addressing mode for video representation");
3326 }
3327 let u = merge_baseurls(&base_url, &bu.base)?;
3328 let mf = MediaFragmentBuilder::new(period_counter, u)
3329 .with_timeout(Duration::new(10000, 0))
3330 .build();
3331 fragments.push(mf);
3332 }
3333 }
3334 if fragments.is_empty() {
3335 return Err(DashMpdError::UnhandledMediaStream(
3336 "no usable addressing mode identified for video representation".to_string()));
3337 }
3338 }
3339 Ok(PeriodOutputs {
3342 fragments,
3343 diagnostics,
3344 subtitle_formats: Vec::new(),
3345 selected_audio_language: String::from("unk")
3346 })
3347}
3348
3349#[tracing::instrument(level="trace", skip_all)]
3350async fn do_period_subtitles(
3351 downloader: &DashDownloader,
3352 mpd: &MPD,
3353 period: &Period,
3354 period_counter: u8,
3355 base_url: Url
3356 ) -> Result<PeriodOutputs, DashMpdError>
3357{
3358 let client = downloader.http_client.as_ref().unwrap();
3359 let output_path = &downloader.output_path.as_ref().unwrap().clone();
3360 let period_output_path = output_path_for_period(output_path, period_counter);
3361 let mut fragments = Vec::new();
3362 let mut subtitle_formats = Vec::new();
3363 let mut period_duration_secs: f64 = 0.0;
3364 if let Some(d) = mpd.mediaPresentationDuration {
3365 period_duration_secs = d.as_secs_f64();
3366 }
3367 if let Some(d) = period.duration {
3368 period_duration_secs = d.as_secs_f64();
3369 }
3370 let maybe_subtitle_adaptation = if let Some(ref lang) = downloader.language_preference_subtitles {
3371 period.adaptations.iter().filter(is_subtitle_adaptation)
3372 .min_by_key(|a| adaptation_lang_distance(a, lang))
3373 } else {
3374 period.adaptations.iter().find(is_subtitle_adaptation)
3376 };
3377 if downloader.fetch_subtitles {
3378 if let Some(subtitle_adaptation) = maybe_subtitle_adaptation {
3379 let subtitle_format = subtitle_type(&subtitle_adaptation);
3380 subtitle_formats.push(subtitle_format);
3381 if downloader.verbosity > 1 && downloader.fetch_subtitles {
3382 info!(" Retrieving subtitles in format {subtitle_format:?}");
3383 }
3384 let mut base_url = base_url.clone();
3387 if let Some(bu) = &subtitle_adaptation.BaseURL.first() {
3388 base_url = merge_baseurls(&base_url, &bu.base)?;
3389 }
3390 if let Some(rep) = subtitle_adaptation.representations.first() {
3393 if !rep.BaseURL.is_empty() {
3394 for st_bu in &rep.BaseURL {
3395 let st_url = merge_baseurls(&base_url, &st_bu.base)?;
3396 let mut req = client.get(st_url.clone());
3397 if let Some(referer) = &downloader.referer {
3398 req = req.header("Referer", referer);
3399 } else {
3400 req = req.header("Referer", base_url.to_string());
3401 }
3402 let rqw = req.build()
3403 .map_err(|e| network_error("building request", &e))?;
3404 let subs = reqwest_bytes_with_retries(client, rqw, 5).await
3405 .map_err(|e| network_error("fetching subtitles", &e))?;
3406 let mut subs_path = period_output_path.clone();
3407 let subtitle_format = subtitle_type(&subtitle_adaptation);
3408 match subtitle_format {
3409 SubtitleType::Vtt => subs_path.set_extension("vtt"),
3410 SubtitleType::Srt => subs_path.set_extension("srt"),
3411 SubtitleType::Ttml => subs_path.set_extension("ttml"),
3412 SubtitleType::Sami => subs_path.set_extension("sami"),
3413 SubtitleType::Wvtt => subs_path.set_extension("wvtt"),
3414 SubtitleType::Stpp => subs_path.set_extension("stpp"),
3415 _ => subs_path.set_extension("sub"),
3416 };
3417 subtitle_formats.push(subtitle_format);
3418 let mut subs_file = File::create(subs_path.clone()).await
3419 .map_err(|e| DashMpdError::Io(e, String::from("creating subtitle file")))?;
3420 if downloader.verbosity > 2 {
3421 info!(" Subtitle {st_url} -> {} octets", subs.len());
3422 }
3423 match subs_file.write_all(&subs).await {
3424 Ok(()) => {
3425 if downloader.verbosity > 0 {
3426 info!(" Downloaded subtitles ({subtitle_format:?}) to {}",
3427 subs_path.display());
3428 }
3429 },
3430 Err(e) => {
3431 error!("Unable to write subtitle file: {e:?}");
3432 return Err(DashMpdError::Io(e, String::from("writing subtitle data")));
3433 },
3434 }
3435 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
3436 subtitle_formats.contains(&SubtitleType::Ttxt)
3437 {
3438 if downloader.verbosity > 0 {
3439 info!(" Converting subtitles to SRT format with MP4Box ");
3440 }
3441 let out = subs_path.with_extension("srt");
3442 let out_str = out.to_string_lossy();
3449 let subs_str = subs_path.to_string_lossy();
3450 let args = vec![
3451 "-srt", "1",
3452 "-out", &out_str,
3453 &subs_str];
3454 if downloader.verbosity > 0 {
3455 info!(" Running MPBox {}", args.join(" "));
3456 }
3457 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
3458 .args(args)
3459 .output()
3460 {
3461 let msg = partial_process_output(&mp4box.stdout);
3462 if !msg.is_empty() {
3463 info!("MP4Box stdout: {msg}");
3464 }
3465 let msg = partial_process_output(&mp4box.stderr);
3466 if !msg.is_empty() {
3467 info!("MP4Box stderr: {msg}");
3468 }
3469 if mp4box.status.success() {
3470 info!(" Converted subtitles to SRT");
3471 } else {
3472 warn!("Error running MP4Box to convert subtitles");
3473 }
3474 }
3475 }
3476 }
3477 } else if rep.SegmentTemplate.is_some() || subtitle_adaptation.SegmentTemplate.is_some() {
3478 let mut opt_init: Option<String> = None;
3479 let mut opt_media: Option<String> = None;
3480 let mut opt_duration: Option<f64> = None;
3481 let mut timescale = 1;
3482 let mut start_number = 1;
3483 if let Some(st) = &rep.SegmentTemplate {
3488 if let Some(i) = &st.initialization {
3489 opt_init = Some(i.clone());
3490 }
3491 if let Some(m) = &st.media {
3492 opt_media = Some(m.clone());
3493 }
3494 if let Some(d) = st.duration {
3495 opt_duration = Some(d);
3496 }
3497 if let Some(ts) = st.timescale {
3498 timescale = ts;
3499 }
3500 if let Some(s) = st.startNumber {
3501 start_number = s;
3502 }
3503 }
3504 let rid = match &rep.id {
3505 Some(id) => id,
3506 None => return Err(
3507 DashMpdError::UnhandledMediaStream(
3508 "Missing @id on Representation node".to_string())),
3509 };
3510 let mut dict = HashMap::from([("RepresentationID", rid.clone())]);
3511 if let Some(b) = &rep.bandwidth {
3512 dict.insert("Bandwidth", b.to_string());
3513 }
3514 if let Some(sl) = &rep.SegmentList {
3518 if downloader.verbosity > 1 {
3521 info!(" Using AdaptationSet>SegmentList addressing mode for subtitle representation");
3522 }
3523 let mut start_byte: Option<u64> = None;
3524 let mut end_byte: Option<u64> = None;
3525 if let Some(init) = &sl.Initialization {
3526 if let Some(range) = &init.range {
3527 let (s, e) = parse_range(range)?;
3528 start_byte = Some(s);
3529 end_byte = Some(e);
3530 }
3531 if let Some(su) = &init.sourceURL {
3532 let path = resolve_url_template(su, &dict);
3533 let u = merge_baseurls(&base_url, &path)?;
3534 let mf = MediaFragmentBuilder::new(period_counter, u)
3535 .with_range(start_byte, end_byte)
3536 .set_init()
3537 .build();
3538 fragments.push(mf);
3539 } else {
3540 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3541 .with_range(start_byte, end_byte)
3542 .set_init()
3543 .build();
3544 fragments.push(mf);
3545 }
3546 }
3547 for su in &sl.segment_urls {
3548 start_byte = None;
3549 end_byte = None;
3550 if let Some(range) = &su.mediaRange {
3552 let (s, e) = parse_range(range)?;
3553 start_byte = Some(s);
3554 end_byte = Some(e);
3555 }
3556 if let Some(m) = &su.media {
3557 let u = merge_baseurls(&base_url, m)?;
3558 let mf = MediaFragmentBuilder::new(period_counter, u)
3559 .with_range(start_byte, end_byte)
3560 .build();
3561 fragments.push(mf);
3562 } else if let Some(bu) = subtitle_adaptation.BaseURL.first() {
3563 let u = merge_baseurls(&base_url, &bu.base)?;
3564 let mf = MediaFragmentBuilder::new(period_counter, u)
3565 .with_range(start_byte, end_byte)
3566 .build();
3567 fragments.push(mf);
3568 }
3569 }
3570 }
3571 if let Some(sl) = &rep.SegmentList {
3572 if downloader.verbosity > 1 {
3574 info!(" Using Representation>SegmentList addressing mode for subtitle representation");
3575 }
3576 let mut start_byte: Option<u64> = None;
3577 let mut end_byte: Option<u64> = None;
3578 if let Some(init) = &sl.Initialization {
3579 if let Some(range) = &init.range {
3580 let (s, e) = parse_range(range)?;
3581 start_byte = Some(s);
3582 end_byte = Some(e);
3583 }
3584 if let Some(su) = &init.sourceURL {
3585 let path = resolve_url_template(su, &dict);
3586 let u = merge_baseurls(&base_url, &path)?;
3587 let mf = MediaFragmentBuilder::new(period_counter, u)
3588 .with_range(start_byte, end_byte)
3589 .set_init()
3590 .build();
3591 fragments.push(mf);
3592 } else {
3593 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3594 .with_range(start_byte, end_byte)
3595 .set_init()
3596 .build();
3597 fragments.push(mf);
3598 }
3599 }
3600 for su in &sl.segment_urls {
3601 start_byte = None;
3602 end_byte = None;
3603 if let Some(range) = &su.mediaRange {
3605 let (s, e) = parse_range(range)?;
3606 start_byte = Some(s);
3607 end_byte = Some(e);
3608 }
3609 if let Some(m) = &su.media {
3610 let u = merge_baseurls(&base_url, m)?;
3611 let mf = MediaFragmentBuilder::new(period_counter, u)
3612 .with_range(start_byte, end_byte)
3613 .build();
3614 fragments.push(mf);
3615 } else if let Some(bu) = &rep.BaseURL.first() {
3616 let u = merge_baseurls(&base_url, &bu.base)?;
3617 let mf = MediaFragmentBuilder::new(period_counter, u)
3618 .with_range(start_byte, end_byte)
3619 .build();
3620 fragments.push(mf);
3621 }
3622 }
3623 } else if rep.SegmentTemplate.is_some() ||
3624 subtitle_adaptation.SegmentTemplate.is_some()
3625 {
3626 let st;
3629 if let Some(it) = &rep.SegmentTemplate {
3630 st = it;
3631 } else if let Some(it) = &subtitle_adaptation.SegmentTemplate {
3632 st = it;
3633 } else {
3634 panic!("unreachable");
3635 }
3636 if let Some(i) = &st.initialization {
3637 opt_init = Some(i.clone());
3638 }
3639 if let Some(m) = &st.media {
3640 opt_media = Some(m.clone());
3641 }
3642 if let Some(ts) = st.timescale {
3643 timescale = ts;
3644 }
3645 if let Some(sn) = st.startNumber {
3646 start_number = sn;
3647 }
3648 if let Some(stl) = &rep.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone())
3649 .or(subtitle_adaptation.SegmentTemplate.as_ref().and_then(|st| st.SegmentTimeline.clone()))
3650 {
3651 if downloader.verbosity > 1 {
3654 info!(" Using SegmentTemplate+SegmentTimeline addressing mode for subtitle representation");
3655 }
3656 if let Some(init) = opt_init {
3657 let path = resolve_url_template(&init, &dict);
3658 let u = merge_baseurls(&base_url, &path)?;
3659 let mf = MediaFragmentBuilder::new(period_counter, u)
3660 .set_init()
3661 .build();
3662 fragments.push(mf);
3663 }
3664 if let Some(media) = opt_media {
3665 let sub_path = resolve_url_template(&media, &dict);
3666 let mut segment_time = 0;
3667 let mut segment_duration;
3668 let mut number = start_number;
3669 for s in &stl.segments {
3670 if let Some(t) = s.t {
3671 segment_time = t;
3672 }
3673 segment_duration = s.d;
3674 let dict = HashMap::from([("Time", segment_time.to_string()),
3676 ("Number", number.to_string())]);
3677 let path = resolve_url_template(&sub_path, &dict);
3678 let u = merge_baseurls(&base_url, &path)?;
3679 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3680 fragments.push(mf);
3681 number += 1;
3682 if let Some(r) = s.r {
3683 let mut count = 0i64;
3684 let end_time = period_duration_secs * timescale as f64;
3686 loop {
3687 count += 1;
3688 if r >= 0 {
3694 if count > r {
3695 break;
3696 }
3697 if downloader.force_duration.is_some() &&
3698 segment_time as f64 > end_time
3699 {
3700 break;
3701 }
3702 } else if segment_time as f64 > end_time {
3703 break;
3704 }
3705 segment_time += segment_duration;
3706 let dict = HashMap::from([("Time", segment_time.to_string()),
3707 ("Number", number.to_string())]);
3708 let path = resolve_url_template(&sub_path, &dict);
3709 let u = merge_baseurls(&base_url, &path)?;
3710 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3711 fragments.push(mf);
3712 number += 1;
3713 }
3714 }
3715 segment_time += segment_duration;
3716 }
3717 } else {
3718 return Err(DashMpdError::UnhandledMediaStream(
3719 "SegmentTimeline without a media attribute".to_string()));
3720 }
3721 } else { if downloader.verbosity > 0 {
3726 info!(" Using SegmentTemplate addressing mode for stpp subtitles");
3727 }
3728 if let Some(i) = &st.initialization {
3729 opt_init = Some(i.clone());
3730 }
3731 if let Some(m) = &st.media {
3732 opt_media = Some(m.clone());
3733 }
3734 if let Some(d) = st.duration {
3735 opt_duration = Some(d);
3736 }
3737 if let Some(ts) = st.timescale {
3738 timescale = ts;
3739 }
3740 if let Some(s) = st.startNumber {
3741 start_number = s;
3742 }
3743 let rid = match &rep.id {
3744 Some(id) => id,
3745 None => return Err(
3746 DashMpdError::UnhandledMediaStream(
3747 "Missing @id on Representation node".to_string())),
3748 };
3749 let mut dict = HashMap::from([("RepresentationID", rid.clone())]);
3750 if let Some(b) = &rep.bandwidth {
3751 dict.insert("Bandwidth", b.to_string());
3752 }
3753 let mut total_number = 0i64;
3754 if let Some(init) = opt_init {
3755 let path = resolve_url_template(&init, &dict);
3756 let u = merge_baseurls(&base_url, &path)?;
3757 let mf = MediaFragmentBuilder::new(period_counter, u)
3758 .set_init()
3759 .build();
3760 fragments.push(mf);
3761 }
3762 if let Some(media) = opt_media {
3763 let sub_path = resolve_url_template(&media, &dict);
3764 let mut segment_duration: f64 = -1.0;
3765 if let Some(d) = opt_duration {
3766 segment_duration = d;
3768 }
3769 if let Some(std) = st.duration {
3770 if timescale == 0 {
3771 return Err(DashMpdError::UnhandledMediaStream(
3772 "SegmentTemplate@duration attribute cannot be zero".to_string()));
3773 }
3774 segment_duration = std / timescale as f64;
3775 }
3776 if segment_duration < 0.0 {
3777 return Err(DashMpdError::UnhandledMediaStream(
3778 "Subtitle representation is missing SegmentTemplate@duration".to_string()));
3779 }
3780 total_number += (period_duration_secs / segment_duration).ceil() as i64;
3781 let mut number = start_number;
3782 #[allow(clippy::explicit_counter_loop)]
3783 for _ in 1..=total_number {
3784 let dict = HashMap::from([("Number", number.to_string())]);
3785 let path = resolve_url_template(&sub_path, &dict);
3786 let u = merge_baseurls(&base_url, &path)?;
3787 let mf = MediaFragmentBuilder::new(period_counter, u).build();
3788 fragments.push(mf);
3789 number += 1;
3790 }
3791 }
3792 }
3793 } else if let Some(sb) = &rep.SegmentBase {
3794 info!(" Using SegmentBase@indexRange for subs");
3796 if downloader.verbosity > 1 {
3797 info!(" Using SegmentBase@indexRange addressing mode for subtitle representation");
3798 }
3799 let mut start_byte: Option<u64> = None;
3800 let mut end_byte: Option<u64> = None;
3801 if let Some(init) = &sb.Initialization {
3802 if let Some(range) = &init.range {
3803 let (s, e) = parse_range(range)?;
3804 start_byte = Some(s);
3805 end_byte = Some(e);
3806 }
3807 if let Some(su) = &init.sourceURL {
3808 let path = resolve_url_template(su, &dict);
3809 let u = merge_baseurls(&base_url, &path)?;
3810 let mf = MediaFragmentBuilder::new(period_counter, u)
3811 .with_range(start_byte, end_byte)
3812 .set_init()
3813 .build();
3814 fragments.push(mf);
3815 }
3816 }
3817 let mf = MediaFragmentBuilder::new(period_counter, base_url.clone())
3818 .set_init()
3819 .build();
3820 fragments.push(mf);
3821 }
3824 }
3825 }
3826 }
3827 }
3828 Ok(PeriodOutputs {
3829 fragments,
3830 diagnostics: Vec::new(),
3831 subtitle_formats,
3832 selected_audio_language: String::from("unk")
3833 })
3834}
3835
3836
3837struct DownloadState {
3840 period_counter: u8,
3841 segment_count: usize,
3842 segment_counter: usize,
3843 download_errors: u32
3844}
3845
3846#[tracing::instrument(level="trace", skip_all)]
3853async fn fetch_fragment(
3854 downloader: &mut DashDownloader,
3855 frag: &MediaFragment,
3856 fragment_type: &str,
3857 progress_percent: u32) -> Result<File, DashMpdError>
3858{
3859 let send_request = || async {
3860 trace!("send_request {}", frag.url.clone());
3861 let mut req = downloader.http_client.as_ref().unwrap()
3864 .get(frag.url.clone())
3865 .header("Accept", format!("{fragment_type}/*;q=0.9,*/*;q=0.5"))
3866 .header("Sec-Fetch-Mode", "navigate");
3867 if let Some(sb) = &frag.start_byte {
3868 if let Some(eb) = &frag.end_byte {
3869 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
3870 }
3871 }
3872 if let Some(ts) = &frag.timeout {
3873 req = req.timeout(*ts);
3874 }
3875 if let Some(referer) = &downloader.referer {
3876 req = req.header("Referer", referer);
3877 } else {
3878 req = req.header("Referer", downloader.redirected_url.to_string());
3879 }
3880 if let Some(username) = &downloader.auth_username {
3881 if let Some(password) = &downloader.auth_password {
3882 req = req.basic_auth(username, Some(password));
3883 }
3884 }
3885 if let Some(token) = &downloader.auth_bearer_token {
3886 req = req.bearer_auth(token);
3887 }
3888 req.send().await?
3889 .error_for_status()
3890 };
3891 match send_request
3892 .retry(ExponentialBuilder::default())
3893 .when(reqwest_error_transient_p)
3894 .notify(notify_transient)
3895 .await
3896 {
3897 Ok(response) => {
3898 match response.error_for_status() {
3899 Ok(mut resp) => {
3900 let tmp_out_std = tempfile::tempfile()
3901 .map_err(|e| DashMpdError::Io(e, String::from("creating tmpfile for fragment")))?;
3902 let mut tmp_out = tokio::fs::File::from_std(tmp_out_std);
3903 let content_type_checker = if fragment_type.eq("audio") {
3904 content_type_audio_p
3905 } else if fragment_type.eq("video") {
3906 content_type_video_p
3907 } else {
3908 panic!("fragment_type not audio or video");
3909 };
3910 if !downloader.content_type_checks || content_type_checker(&resp) {
3911 let mut fragment_out: Option<File> = None;
3912 if let Some(ref fragment_path) = downloader.fragment_path {
3913 if let Some(path) = frag.url.path_segments()
3914 .unwrap_or_else(|| "".split(' '))
3915 .next_back()
3916 {
3917 let vf_file = fragment_path.clone().join(fragment_type).join(path);
3918 if let Ok(f) = File::create(vf_file).await {
3919 fragment_out = Some(f);
3920 }
3921 }
3922 }
3923 let mut segment_size = 0;
3924 while let Some(chunk) = resp.chunk().await
3930 .map_err(|e| network_error(&format!("fetching DASH {fragment_type} segment"), &e))?
3931 {
3932 segment_size += chunk.len();
3933 downloader.bw_estimator_bytes += chunk.len();
3934 let size = min((chunk.len()/1024+1) as u32, u32::MAX);
3935 throttle_download_rate(downloader, size).await?;
3936 if let Err(e) = tmp_out.write_all(&chunk).await {
3937 return Err(DashMpdError::Io(e, format!("writing DASH {fragment_type} data")));
3938 }
3939 if let Some(ref mut fout) = fragment_out {
3940 fout.write_all(&chunk)
3941 .map_err(|e| DashMpdError::Io(e, format!("writing {fragment_type} fragment")))
3942 .await?;
3943 }
3944 let elapsed = downloader.bw_estimator_started.elapsed().as_secs_f64();
3945 if (elapsed > 0.5) || (downloader.bw_estimator_bytes > 50_000) {
3946 let bw = downloader.bw_estimator_bytes as f64 / elapsed;
3947 for observer in &downloader.progress_observers {
3948 observer.update(progress_percent, bw as u64, &format!("Fetching {fragment_type} segments"));
3949 }
3950 downloader.bw_estimator_started = Instant::now();
3951 downloader.bw_estimator_bytes = 0;
3952 }
3953 }
3954 if downloader.verbosity > 2 {
3955 if let Some(sb) = &frag.start_byte {
3956 if let Some(eb) = &frag.end_byte {
3957 info!(" {fragment_type} segment {} range {sb}-{eb} -> {} octets",
3958 frag.url, segment_size);
3959 }
3960 } else {
3961 info!(" {fragment_type} segment {} -> {segment_size} octets", &frag.url);
3962 }
3963 }
3964 } else {
3965 warn!("Ignoring segment {} with non-{fragment_type} content-type", frag.url);
3966 }
3967 tmp_out.sync_all().await
3968 .map_err(|e| DashMpdError::Io(e, format!("syncing {fragment_type} fragment")))?;
3969 Ok(tmp_out)
3970 },
3971 Err(e) => Err(network_error("HTTP error", &e)),
3972 }
3973 },
3974 Err(e) => Err(network_error(&format!("{e:?}"), &e)),
3975 }
3976}
3977
3978
3979#[tracing::instrument(level="trace", skip_all)]
3981async fn fetch_period_audio(
3982 downloader: &mut DashDownloader,
3983 tmppath: &Path,
3984 audio_fragments: &[MediaFragment],
3985 ds: &mut DownloadState) -> Result<bool, DashMpdError>
3986{
3987 let start_download = Instant::now();
3988 let mut have_audio = false;
3989 {
3990 let tmpfile_audio = File::create(tmppath).await
3994 .map_err(|e| DashMpdError::Io(e, String::from("creating audio tmpfile")))?;
3995 ensure_permissions_readable(tmppath).await?;
3996 let mut tmpfile_audio = BufWriter::new(tmpfile_audio);
3997 if let Some(ref fragment_path) = downloader.fragment_path {
3999 let audio_fragment_dir = fragment_path.join("audio");
4000 if !audio_fragment_dir.exists() {
4001 fs::create_dir_all(audio_fragment_dir).await
4002 .map_err(|e| DashMpdError::Io(e, String::from("creating audio fragment dir")))?;
4003 }
4004 }
4005 for frag in audio_fragments.iter().filter(|f| f.period == ds.period_counter) {
4009 ds.segment_counter += 1;
4010 let progress_percent = min(98, (100.0 * ds.segment_counter as f32 / (2.0 + ds.segment_count as f32)).ceil() as u32);
4013 let url = &frag.url;
4014 if url.scheme() == "data" {
4018 let us = &url.to_string();
4019 let du = DataUrl::process(us)
4020 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
4021 if du.mime_type().type_ != "audio" {
4022 return Err(DashMpdError::UnhandledMediaStream(
4023 String::from("expecting audio content in data URL")));
4024 }
4025 let (body, _fragment) = du.decode_to_vec()
4026 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
4027 if downloader.verbosity > 2 {
4028 info!(" Audio segment data URL -> {} octets", body.len());
4029 }
4030 tmpfile_audio.write_all(&body)
4031 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH audio data")))
4032 .await?;
4033 have_audio = true;
4034 } else {
4035 'done: for _ in 0..downloader.fragment_retry_count {
4037 match fetch_fragment(downloader, frag, "audio", progress_percent).await {
4038 Ok(mut frag_file) => {
4039 frag_file.rewind().await
4040 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
4041 let mut buf = Vec::new();
4042 frag_file.read_to_end(&mut buf).await
4043 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
4044 tmpfile_audio.write_all(&buf)
4045 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH audio data")))
4046 .await?;
4047 have_audio = true;
4048 break 'done;
4049 },
4050 Err(e) => {
4051 if downloader.verbosity > 0 {
4052 error!("Error fetching audio segment {url}: {e:?}");
4053 }
4054 ds.download_errors += 1;
4055 if ds.download_errors > downloader.max_error_count {
4056 error!("max_error_count network errors encountered");
4057 return Err(DashMpdError::Network(
4058 String::from("more than max_error_count network errors")));
4059 }
4060 },
4061 }
4062 info!(" Retrying audio segment {url}");
4063 if downloader.sleep_between_requests > 0 {
4064 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
4065 }
4066 }
4067 }
4068 }
4069 tmpfile_audio.flush().map_err(|e| {
4070 error!("Couldn't flush DASH audio file: {e}");
4071 DashMpdError::Io(e, String::from("flushing DASH audio file"))
4072 }).await?;
4073 } if !downloader.decryption_keys.is_empty() {
4075 if downloader.verbosity > 0 {
4076 let metadata = fs::metadata(tmppath).await
4077 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted audio metadata")))?;
4078 info!(" Attempting to decrypt audio stream ({} kB) with {}",
4079 metadata.len() / 1024,
4080 downloader.decryptor_preference);
4081 }
4082 let out_ext = downloader.output_path.as_ref().unwrap()
4083 .extension()
4084 .unwrap_or(OsStr::new("mp4"));
4085 let decrypted = tmp_file_path("dashmpd-decrypted-audio", out_ext)?;
4086 if downloader.decryptor_preference.eq("mp4decrypt") {
4087 decrypt_mp4decrypt(downloader, tmppath, &decrypted, "audio").await?;
4088 } else if downloader.decryptor_preference.eq("shaka") {
4089 decrypt_shaka(downloader, tmppath, &decrypted, "audio").await?;
4090 } else if downloader.decryptor_preference.eq("shaka-container") {
4091 decrypt_shaka_container(downloader, tmppath, &decrypted, "audio").await?;
4092 } else if downloader.decryptor_preference.eq("mp4box") {
4093 decrypt_mp4box(downloader, tmppath, &decrypted, "audio").await?;
4094 } else if downloader.decryptor_preference.eq("mp4box-container") {
4095 decrypt_mp4box_container(downloader, tmppath, &decrypted, "audio").await?;
4096 } else {
4097 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
4098 }
4099 if let Err(e) = fs::metadata(&decrypted).await {
4100 return Err(DashMpdError::Decrypting(format!("missing decrypted audio file: {e:?}")));
4101 }
4102 fs::remove_file(&tmppath).await
4103 .map_err(|e| DashMpdError::Io(e, String::from("deleting encrypted audio tmpfile")))?;
4104 fs::rename(&decrypted, &tmppath).await
4105 .map_err(|e| {
4106 let dbg = Command::new("bash")
4107 .args(["-c", &format!("id;ls -l {}", decrypted.display())])
4108 .output()
4109 .unwrap();
4110 warn!("debugging ls: {}", String::from_utf8_lossy(&dbg.stdout));
4111 DashMpdError::Io(e, format!("renaming decrypted audio {}->{}", decrypted.display(), tmppath.display()))
4112 })?;
4113 }
4114 if let Ok(metadata) = fs::metadata(&tmppath).await {
4115 if downloader.verbosity > 1 {
4116 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4117 let elapsed = start_download.elapsed();
4118 info!(" Wrote {mbytes:.1}MB to DASH audio file ({:.1} MB/s)",
4119 mbytes / elapsed.as_secs_f64());
4120 }
4121 }
4122 Ok(have_audio)
4123}
4124
4125
4126#[tracing::instrument(level="trace", skip_all)]
4128async fn fetch_period_video(
4129 downloader: &mut DashDownloader,
4130 tmppath: &Path,
4131 video_fragments: &[MediaFragment],
4132 ds: &mut DownloadState) -> Result<bool, DashMpdError>
4133{
4134 let start_download = Instant::now();
4135 let mut have_video = false;
4136 {
4137 let tmpfile_video = File::create(tmppath).await
4141 .map_err(|e| DashMpdError::Io(e, String::from("creating video tmpfile")))?;
4142 ensure_permissions_readable(tmppath).await?;
4143 let mut tmpfile_video = BufWriter::new(tmpfile_video);
4144 if let Some(ref fragment_path) = downloader.fragment_path {
4146 let video_fragment_dir = fragment_path.join("video");
4147 if !video_fragment_dir.exists() {
4148 fs::create_dir_all(video_fragment_dir).await
4149 .map_err(|e| DashMpdError::Io(e, String::from("creating video fragment dir")))?;
4150 }
4151 }
4152 for frag in video_fragments.iter().filter(|f| f.period == ds.period_counter) {
4153 ds.segment_counter += 1;
4154 let progress_percent = min(98, (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32);
4157 if frag.url.scheme() == "data" {
4158 let us = &frag.url.to_string();
4159 let du = DataUrl::process(us)
4160 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
4161 if du.mime_type().type_ != "video" {
4162 return Err(DashMpdError::UnhandledMediaStream(
4163 String::from("expecting video content in data URL")));
4164 }
4165 let (body, _fragment) = du.decode_to_vec()
4166 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
4167 if downloader.verbosity > 2 {
4168 info!(" Video segment data URL -> {} octets", body.len());
4169 }
4170 tmpfile_video.write_all(&body)
4171 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH video data")))
4172 .await?;
4173 have_video = true;
4174 } else {
4175 'done: for _ in 0..downloader.fragment_retry_count {
4176 match fetch_fragment(downloader, frag, "video", progress_percent).await {
4177 Ok(mut frag_file) => {
4178 frag_file.rewind().await
4179 .map_err(|e| DashMpdError::Io(e, String::from("rewinding fragment tempfile")))?;
4180 let mut buf = Vec::new();
4181 frag_file.read_to_end(&mut buf).await
4182 .map_err(|e| DashMpdError::Io(e, String::from("reading fragment tempfile")))?;
4183 tmpfile_video.write_all(&buf)
4184 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH video data")))
4185 .await?;
4186 have_video = true;
4187 break 'done;
4188 },
4189 Err(e) => {
4190 if downloader.verbosity > 0 {
4191 error!(" Error fetching video segment {}: {e:?}", frag.url);
4192 }
4193 ds.download_errors += 1;
4194 if ds.download_errors > downloader.max_error_count {
4195 return Err(DashMpdError::Network(
4196 String::from("more than max_error_count network errors")));
4197 }
4198 },
4199 }
4200 info!(" Retrying video segment {}", frag.url);
4201 if downloader.sleep_between_requests > 0 {
4202 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
4203 }
4204 }
4205 }
4206 }
4207 tmpfile_video.flush().map_err(|e| {
4208 error!(" Couldn't flush video file: {e}");
4209 DashMpdError::Io(e, String::from("flushing video file"))
4210 }).await?;
4211 } if !downloader.decryption_keys.is_empty() {
4213 if downloader.verbosity > 0 {
4214 let metadata = fs::metadata(tmppath).await
4215 .map_err(|e| DashMpdError::Io(e, String::from("reading encrypted video metadata")))?;
4216 info!(" Attempting to decrypt video stream ({} kB) with {}",
4217 metadata.len() / 1024,
4218 downloader.decryptor_preference);
4219 }
4220 let out_ext = downloader.output_path.as_ref().unwrap()
4221 .extension()
4222 .unwrap_or(OsStr::new("mp4"));
4223 let decrypted = tmp_file_path("dashmpd-decrypted-video", out_ext)?;
4224 if downloader.decryptor_preference.eq("mp4decrypt") {
4225 decrypt_mp4decrypt(downloader, tmppath, &decrypted, "video").await?;
4226 } else if downloader.decryptor_preference.eq("shaka") {
4227 decrypt_shaka(downloader, tmppath, &decrypted, "video").await?;
4228 } else if downloader.decryptor_preference.eq("shaka-container") {
4229 decrypt_shaka_container(downloader, tmppath, &decrypted, "video").await?;
4230 } else if downloader.decryptor_preference.eq("mp4box") {
4231 decrypt_mp4box(downloader, tmppath, &decrypted, "video").await?;
4232 } else if downloader.decryptor_preference.eq("mp4box-container") {
4233 decrypt_mp4box_container(downloader, tmppath, &decrypted, "video").await?;
4234 } else {
4235 return Err(DashMpdError::Decrypting(String::from("unknown decryption application")));
4236 }
4237 if let Err(e) = fs::metadata(&decrypted).await {
4238 return Err(DashMpdError::Decrypting(format!("missing decrypted video file: {e:?}")));
4239 }
4240 fs::remove_file(&tmppath).await
4241 .map_err(|e| DashMpdError::Io(e, String::from("deleting encrypted video tmpfile")))?;
4242 fs::rename(&decrypted, &tmppath).await
4243 .map_err(|e| DashMpdError::Io(e, String::from("renaming decrypted video")))?;
4244 }
4245 if let Ok(metadata) = fs::metadata(&tmppath).await {
4246 if downloader.verbosity > 1 {
4247 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4248 let elapsed = start_download.elapsed();
4249 info!(" Wrote {mbytes:.1}MB to DASH video file ({:.1} MB/s)",
4250 mbytes / elapsed.as_secs_f64());
4251 }
4252 }
4253 Ok(have_video)
4254}
4255
4256
4257#[tracing::instrument(level="trace", skip_all)]
4259async fn fetch_period_subtitles(
4260 downloader: &DashDownloader,
4261 tmppath: &Path,
4262 subtitle_fragments: &[MediaFragment],
4263 subtitle_formats: &[SubtitleType],
4264 ds: &mut DownloadState) -> Result<bool, DashMpdError>
4265{
4266 let client = downloader.http_client.clone().unwrap();
4267 let start_download = Instant::now();
4268 let mut have_subtitles = false;
4269 {
4270 let tmpfile_subs = File::create(tmppath).await
4271 .map_err(|e| DashMpdError::Io(e, String::from("creating subs tmpfile")))?;
4272 ensure_permissions_readable(tmppath).await?;
4273 let mut tmpfile_subs = BufWriter::new(tmpfile_subs);
4274 for frag in subtitle_fragments {
4275 ds.segment_counter += 1;
4277 let progress_percent = min(98, (100.0 * ds.segment_counter as f32 / ds.segment_count as f32).ceil() as u32);
4278 for observer in &downloader.progress_observers {
4279 observer.update(progress_percent, 1, "Fetching subtitle segments");
4280 }
4281 if frag.url.scheme() == "data" {
4282 let us = &frag.url.to_string();
4283 let du = DataUrl::process(us)
4284 .map_err(|_| DashMpdError::Parsing(String::from("parsing data URL")))?;
4285 if du.mime_type().type_ != "video" {
4286 return Err(DashMpdError::UnhandledMediaStream(
4287 String::from("expecting video content in data URL")));
4288 }
4289 let (body, _fragment) = du.decode_to_vec()
4290 .map_err(|_| DashMpdError::Parsing(String::from("decoding data URL")))?;
4291 if downloader.verbosity > 2 {
4292 info!(" Subtitle segment data URL -> {} octets", body.len());
4293 }
4294 tmpfile_subs.write_all(&body)
4295 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH subtitle data")))
4296 .await?;
4297 have_subtitles = true;
4298 } else {
4299 let fetch = || async {
4300 let mut req = client.get(frag.url.clone())
4301 .header("Sec-Fetch-Mode", "navigate");
4302 if let Some(sb) = &frag.start_byte {
4303 if let Some(eb) = &frag.end_byte {
4304 req = req.header(RANGE, format!("bytes={sb}-{eb}"));
4305 }
4306 }
4307 if let Some(referer) = &downloader.referer {
4308 req = req.header("Referer", referer);
4309 } else {
4310 req = req.header("Referer", downloader.redirected_url.to_string());
4311 }
4312 if let Some(username) = &downloader.auth_username {
4313 if let Some(password) = &downloader.auth_password {
4314 req = req.basic_auth(username, Some(password));
4315 }
4316 }
4317 if let Some(token) = &downloader.auth_bearer_token {
4318 req = req.bearer_auth(token);
4319 }
4320 req.send().await?
4321 .error_for_status()
4322 };
4323 let mut failure = None;
4324 match fetch
4325 .retry(ExponentialBuilder::default())
4326 .when(reqwest_error_transient_p)
4327 .notify(notify_transient)
4328 .await
4329 {
4330 Ok(response) => {
4331 if response.status().is_success() {
4332 let dash_bytes = response.bytes().await
4333 .map_err(|e| network_error("fetching DASH subtitle segment", &e))?;
4334 if downloader.verbosity > 2 {
4335 if let Some(sb) = &frag.start_byte {
4336 if let Some(eb) = &frag.end_byte {
4337 info!(" Subtitle segment {} range {sb}-{eb} -> {} octets",
4338 &frag.url, dash_bytes.len());
4339 }
4340 } else {
4341 info!(" Subtitle segment {} -> {} octets", &frag.url, dash_bytes.len());
4342 }
4343 }
4344 let size = min((dash_bytes.len()/1024 + 1) as u32, u32::MAX);
4345 throttle_download_rate(downloader, size).await?;
4346 tmpfile_subs.write_all(&dash_bytes)
4347 .map_err(|e| DashMpdError::Io(e, String::from("writing DASH subtitle data")))
4348 .await?;
4349 have_subtitles = true;
4350 } else {
4351 failure = Some(format!("HTTP error {}", response.status().as_str()));
4352 }
4353 },
4354 Err(e) => failure = Some(format!("{e}")),
4355 }
4356 if let Some(f) = failure {
4357 if downloader.verbosity > 0 {
4358 error!("{f} fetching subtitle segment {}", &frag.url);
4359 }
4360 ds.download_errors += 1;
4361 if ds.download_errors > downloader.max_error_count {
4362 return Err(DashMpdError::Network(
4363 String::from("more than max_error_count network errors")));
4364 }
4365 }
4366 }
4367 if downloader.sleep_between_requests > 0 {
4368 tokio::time::sleep(Duration::new(downloader.sleep_between_requests.into(), 0)).await;
4369 }
4370 }
4371 tmpfile_subs.flush().map_err(|e| {
4372 error!("Couldn't flush subs file: {e}");
4373 DashMpdError::Io(e, String::from("flushing subtitle file"))
4374 }).await?;
4375 } if have_subtitles {
4377 if let Ok(metadata) = fs::metadata(tmppath).await {
4378 if downloader.verbosity > 1 {
4379 let mbytes = metadata.len() as f64 / (1024.0 * 1024.0);
4380 let elapsed = start_download.elapsed();
4381 info!(" Wrote {mbytes:.1}MB to DASH subtitle file ({:.1} MB/s)",
4382 mbytes / elapsed.as_secs_f64());
4383 }
4384 }
4385 if subtitle_formats.contains(&SubtitleType::Wvtt) ||
4388 subtitle_formats.contains(&SubtitleType::Ttxt)
4389 {
4390 if downloader.verbosity > 0 {
4392 if let Some(fmt) = subtitle_formats.first() {
4393 info!(" Downloaded media contains subtitles in {fmt:?} format");
4394 }
4395 info!(" Running MP4Box to extract subtitles");
4396 }
4397 let out = downloader.output_path.as_ref().unwrap()
4398 .with_extension("srt");
4399 let out_str = out.to_string_lossy();
4400 let tmp_str = tmppath.to_string_lossy();
4401 let args = vec![
4402 "-srt", "1",
4403 "-out", &out_str,
4404 &tmp_str];
4405 if downloader.verbosity > 0 {
4406 info!(" Running MP4Box {}", args.join(" "));
4407 }
4408 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4409 .args(args)
4410 .output()
4411 {
4412 let msg = partial_process_output(&mp4box.stdout);
4413 if !msg.is_empty() {
4414 info!(" MP4Box stdout: {msg}");
4415 }
4416 let msg = partial_process_output(&mp4box.stderr);
4417 if !msg.is_empty() {
4418 info!(" MP4Box stderr: {msg}");
4419 }
4420 if mp4box.status.success() {
4421 info!(" Extracted subtitles as SRT");
4422 } else {
4423 warn!(" Error running MP4Box to extract subtitles");
4424 }
4425 } else {
4426 warn!(" Failed to spawn MP4Box to extract subtitles");
4427 }
4428 }
4429 if subtitle_formats.contains(&SubtitleType::Stpp) {
4430 if downloader.verbosity > 0 {
4431 info!(" Converting STPP subtitles to TTML format with ffmpeg");
4432 }
4433 let out = downloader.output_path.as_ref().unwrap()
4434 .with_extension("ttml");
4435 let tmppath_arg = tmppath.to_string_lossy();
4436 let out_arg = &out.to_string_lossy();
4437 let ffmpeg_args = vec![
4438 "-hide_banner",
4439 "-nostats",
4440 "-loglevel", "error",
4441 "-y", "-nostdin",
4443 "-i", &tmppath_arg,
4444 "-f", "data",
4445 "-map", "0",
4446 "-c", "copy",
4447 out_arg];
4448 if downloader.verbosity > 0 {
4449 info!(" Running ffmpeg {}", ffmpeg_args.join(" "));
4450 }
4451 if let Ok(ffmpeg) = Command::new(downloader.ffmpeg_location.clone())
4452 .args(ffmpeg_args)
4453 .output()
4454 {
4455 let msg = partial_process_output(&ffmpeg.stdout);
4456 if !msg.is_empty() {
4457 info!(" ffmpeg stdout: {msg}");
4458 }
4459 let msg = partial_process_output(&ffmpeg.stderr);
4460 if !msg.is_empty() {
4461 info!(" ffmpeg stderr: {msg}");
4462 }
4463 if ffmpeg.status.success() {
4464 info!(" Converted STPP subtitles to TTML format");
4465 } else {
4466 warn!(" Error running ffmpeg to convert subtitles");
4467 }
4468 }
4469 }
4473
4474 }
4475 Ok(have_subtitles)
4476}
4477
4478
4479async fn fetch_mpd_http(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4481 let client = &downloader.http_client.clone().unwrap();
4482 let send_request = || async {
4483 let mut req = client.get(&downloader.mpd_url)
4484 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4485 .header("Accept-Language", "en-US,en")
4486 .header("Upgrade-Insecure-Requests", "1")
4487 .header("Sec-Fetch-Mode", "navigate");
4488 if let Some(referer) = &downloader.referer {
4489 req = req.header("Referer", referer);
4490 }
4491 if let Some(username) = &downloader.auth_username {
4492 if let Some(password) = &downloader.auth_password {
4493 req = req.basic_auth(username, Some(password));
4494 }
4495 }
4496 if let Some(token) = &downloader.auth_bearer_token {
4497 req = req.bearer_auth(token);
4498 }
4499 req.send().await?
4500 .error_for_status()
4501 };
4502 for observer in &downloader.progress_observers {
4503 observer.update(1, 1, "Fetching DASH manifest");
4504 }
4505 if downloader.verbosity > 0 {
4506 if !downloader.fetch_audio && !downloader.fetch_video && !downloader.fetch_subtitles {
4507 info!("Only simulating media downloads");
4508 }
4509 info!("Fetching the DASH manifest");
4510 }
4511 let response = send_request
4512 .retry(ExponentialBuilder::default())
4513 .when(reqwest_error_transient_p)
4514 .notify(notify_transient)
4515 .await
4516 .map_err(|e| network_error("requesting DASH manifest", &e))?;
4517 if !response.status().is_success() {
4518 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4519 return Err(DashMpdError::Network(msg));
4520 }
4521 downloader.redirected_url = response.url().clone();
4522 response.bytes().await
4523 .map_err(|e| network_error("fetching DASH manifest", &e))
4524}
4525
4526async fn fetch_mpd_file(downloader: &mut DashDownloader) -> Result<Bytes, DashMpdError> {
4529 if ! &downloader.mpd_url.starts_with("file://") {
4530 return Err(DashMpdError::Other(String::from("expecting file:// URL scheme")));
4531 }
4532 let url = Url::parse(&downloader.mpd_url)
4533 .map_err(|_| DashMpdError::Other(String::from("parsing MPD URL")))?;
4534 let path = url.to_file_path()
4535 .map_err(|_| DashMpdError::Other(String::from("extracting path from file:// URL")))?;
4536 let octets = fs::read(path).await
4537 .map_err(|_| DashMpdError::Other(String::from("reading from file:// URL")))?;
4538 Ok(Bytes::from(octets))
4539}
4540
4541
4542#[tracing::instrument(level="trace", skip_all)]
4543async fn fetch_mpd(downloader: &mut DashDownloader) -> Result<PathBuf, DashMpdError> {
4544 #[cfg(all(feature = "sandbox", target_os = "linux"))]
4545 if downloader.sandbox {
4546 if let Err(e) = restrict_thread(downloader) {
4547 warn!("Sandboxing failed: {e:?}");
4548 }
4549 }
4550 let xml = if downloader.mpd_url.starts_with("file://") {
4551 fetch_mpd_file(downloader).await?
4552 } else {
4553 fetch_mpd_http(downloader).await?
4554 };
4555 let mut mpd: MPD = parse_resolving_xlinks(downloader, &xml).await
4556 .map_err(|e| parse_error("parsing DASH XML", e))?;
4557 let client = &downloader.http_client.clone().unwrap();
4560 if let Some(new_location) = &mpd.locations.first() {
4561 let new_url = &new_location.url;
4562 if downloader.verbosity > 0 {
4563 info!("Redirecting to new manifest <Location> {new_url}");
4564 }
4565 let send_request = || async {
4566 let mut req = client.get(new_url)
4567 .header("Accept", "application/dash+xml,video/vnd.mpeg.dash.mpd")
4568 .header("Accept-Language", "en-US,en")
4569 .header("Sec-Fetch-Mode", "navigate");
4570 if let Some(referer) = &downloader.referer {
4571 req = req.header("Referer", referer);
4572 } else {
4573 req = req.header("Referer", downloader.redirected_url.to_string());
4574 }
4575 if let Some(username) = &downloader.auth_username {
4576 if let Some(password) = &downloader.auth_password {
4577 req = req.basic_auth(username, Some(password));
4578 }
4579 }
4580 if let Some(token) = &downloader.auth_bearer_token {
4581 req = req.bearer_auth(token);
4582 }
4583 req.send().await?
4584 .error_for_status()
4585 };
4586 let response = send_request
4587 .retry(ExponentialBuilder::default())
4588 .when(reqwest_error_transient_p)
4589 .notify(notify_transient)
4590 .await
4591 .map_err(|e| network_error("requesting relocated DASH manifest", &e))?;
4592 if !response.status().is_success() {
4593 let msg = format!("fetching DASH manifest (HTTP {})", response.status().as_str());
4594 return Err(DashMpdError::Network(msg));
4595 }
4596 downloader.redirected_url = response.url().clone();
4597 let xml = response.bytes().await
4598 .map_err(|e| network_error("fetching relocated DASH manifest", &e))?;
4599 mpd = parse_resolving_xlinks(downloader, &xml).await
4600 .map_err(|e| parse_error("parsing relocated DASH XML", e))?;
4601 }
4602 if mpd_is_dynamic(&mpd) {
4603 if downloader.allow_live_streams {
4606 if downloader.verbosity > 0 {
4607 warn!("Attempting to download from live stream (this may not work).");
4608 }
4609 } else {
4610 return Err(DashMpdError::UnhandledMediaStream("Don't know how to download dynamic MPD".to_string()));
4611 }
4612 }
4613 let mut toplevel_base_url = downloader.redirected_url.clone();
4614 if let Some(bu) = &mpd.base_url.first() {
4616 toplevel_base_url = merge_baseurls(&downloader.redirected_url, &bu.base)?;
4617 }
4618 if let Some(base) = &downloader.base_url {
4621 toplevel_base_url = merge_baseurls(&downloader.redirected_url, base)?;
4622 }
4623 if downloader.verbosity > 0 {
4624 let pcount = mpd.periods.len();
4625 info!("DASH manifest has {pcount} period{}", if pcount > 1 { "s" } else { "" });
4626 print_available_streams(&mpd);
4627 }
4628 let mut pds: Vec<PeriodDownloads> = Vec::new();
4636 let mut period_counter = 0;
4637 for mpd_period in &mpd.periods {
4638 let period = mpd_period.clone();
4639 period_counter += 1;
4640 if let Some(min) = downloader.minimum_period_duration {
4641 if let Some(duration) = period.duration {
4642 if duration < min {
4643 if let Some(id) = period.id.as_ref() {
4644 info!("Skipping period {id} (#{period_counter}): duration is less than requested minimum");
4645 } else {
4646 info!("Skipping period #{period_counter}: duration is less than requested minimum");
4647 }
4648 continue;
4649 }
4650 }
4651 }
4652 let mut pd = PeriodDownloads { period_counter, ..Default::default() };
4653 if let Some(id) = period.id.as_ref() {
4654 pd.id = Some(id.clone());
4655 }
4656 if downloader.verbosity > 0 {
4657 if let Some(id) = period.id.as_ref() {
4658 info!("Preparing download for period {id} (#{period_counter})");
4659 } else {
4660 info!("Preparing download for period #{period_counter}");
4661 }
4662 }
4663 let mut base_url = toplevel_base_url.clone();
4664 if let Some(bu) = period.BaseURL.first() {
4666 base_url = merge_baseurls(&base_url, &bu.base)?;
4667 }
4668 let mut audio_outputs = PeriodOutputs::default();
4669 if downloader.fetch_audio {
4670 audio_outputs = do_period_audio(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4671 for f in audio_outputs.fragments {
4672 pd.audio_fragments.push(f);
4673 }
4674 pd.selected_audio_language = audio_outputs.selected_audio_language;
4675 }
4676 let mut video_outputs = PeriodOutputs::default();
4677 if downloader.fetch_video {
4678 video_outputs = do_period_video(downloader, &mpd, &period, period_counter, base_url.clone()).await?;
4679 for f in video_outputs.fragments {
4680 pd.video_fragments.push(f);
4681 }
4682 }
4683 match do_period_subtitles(downloader, &mpd, &period, period_counter, base_url.clone()).await {
4684 Ok(subtitle_outputs) => {
4685 for f in subtitle_outputs.fragments {
4686 pd.subtitle_fragments.push(f);
4687 }
4688 for f in subtitle_outputs.subtitle_formats {
4689 pd.subtitle_formats.push(f);
4690 }
4691 },
4692 Err(e) => warn!(" Ignoring error triggered while processing subtitles: {e}"),
4693 }
4694 if downloader.verbosity > 0 {
4696 use base64::prelude::{Engine as _, BASE64_STANDARD};
4697
4698 audio_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4699 for f in pd.audio_fragments.iter().filter(|f| f.is_init) {
4700 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4701 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4702 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4703 info!(" {}", pssh.to_string());
4704 }
4705 }
4706 }
4707 video_outputs.diagnostics.iter().for_each(|msg| info!("{}", msg));
4708 for f in pd.video_fragments.iter().filter(|f| f.is_init) {
4709 if let Some(pssh_bytes) = extract_init_pssh(downloader, f.url.clone()).await {
4710 info!(" PSSH (from init segment): {}", BASE64_STANDARD.encode(&pssh_bytes));
4711 if let Ok(pssh) = pssh_box::from_bytes(&pssh_bytes) {
4712 info!(" {}", pssh.to_string());
4713 }
4714 }
4715 }
4716 }
4717 pds.push(pd);
4718 } let output_path = &downloader.output_path.as_ref().unwrap().clone();
4723 let mut period_output_pathbufs: Vec<PathBuf> = Vec::new();
4724 let mut ds = DownloadState {
4725 period_counter: 0,
4726 segment_count: pds.iter().map(period_fragment_count).sum(),
4728 segment_counter: 0,
4729 download_errors: 0
4730 };
4731 for pd in pds {
4732 let mut have_audio = false;
4733 let mut have_video = false;
4734 let mut have_subtitles = false;
4735 ds.period_counter = pd.period_counter;
4736 let period_output_path = output_path_for_period(output_path, pd.period_counter);
4737 #[allow(clippy::collapsible_if)]
4738 if downloader.verbosity > 0 {
4739 if downloader.fetch_audio || downloader.fetch_video || downloader.fetch_subtitles {
4740 let idnum = if let Some(id) = pd.id {
4741 format!("id={} (#{})", id, pd.period_counter)
4742 } else {
4743 format!("#{}", pd.period_counter)
4744 };
4745 info!("Period {idnum}: fetching {} audio, {} video and {} subtitle segments",
4746 pd.audio_fragments.len(),
4747 pd.video_fragments.len(),
4748 pd.subtitle_fragments.len());
4749 }
4750 }
4751 let output_ext = downloader.output_path.as_ref().unwrap()
4752 .extension()
4753 .unwrap_or(OsStr::new("mp4"));
4754 let tmppath_audio = if let Some(ref path) = downloader.keep_audio {
4755 path.clone()
4756 } else {
4757 tmp_file_path("dashmpd-audio", output_ext)?
4758 };
4759 let tmppath_video = if let Some(ref path) = downloader.keep_video {
4760 path.clone()
4761 } else {
4762 tmp_file_path("dashmpd-video", output_ext)?
4763 };
4764 let tmppath_subs = tmp_file_path("dashmpd-subs", OsStr::new("sub"))?;
4765 if downloader.fetch_audio && !pd.audio_fragments.is_empty() {
4766 have_audio = fetch_period_audio(downloader,
4770 &tmppath_audio, &pd.audio_fragments,
4771 &mut ds).await?;
4772 }
4773 if downloader.fetch_video && !pd.video_fragments.is_empty() {
4774 have_video = fetch_period_video(downloader,
4775 &tmppath_video, &pd.video_fragments,
4776 &mut ds).await?;
4777 }
4778 if downloader.fetch_subtitles && !pd.subtitle_fragments.is_empty() {
4782 have_subtitles = fetch_period_subtitles(downloader,
4783 &tmppath_subs,
4784 &pd.subtitle_fragments,
4785 &pd.subtitle_formats,
4786 &mut ds).await?;
4787 }
4788
4789 if have_audio && have_video {
4792 for observer in &downloader.progress_observers {
4793 observer.update(99, 1, "Muxing audio and video");
4794 }
4795 if downloader.verbosity > 1 {
4796 info!(" Muxing audio and video streams");
4797 }
4798 let audio_tracks = vec![
4799 AudioTrack {
4800 language: pd.selected_audio_language,
4801 path: tmppath_audio.clone()
4802 }];
4803 mux_audio_video(downloader, &period_output_path, &audio_tracks, &tmppath_video).await?;
4804 if pd.subtitle_formats.contains(&SubtitleType::Stpp) {
4805 let container = match &period_output_path.extension() {
4806 Some(ext) => ext.to_str().unwrap_or("mp4"),
4807 None => "mp4",
4808 };
4809 if container.eq("mp4") {
4810 if downloader.verbosity > 1 {
4811 if let Some(fmt) = &pd.subtitle_formats.first() {
4812 info!(" Downloaded media contains subtitles in {fmt:?} format");
4813 }
4814 info!(" Running MP4Box to merge subtitles with output MP4 container");
4815 }
4816 let tmp_str = tmppath_subs.to_string_lossy();
4819 let period_output_str = period_output_path.to_string_lossy();
4820 let args = vec!["-add", &tmp_str, &period_output_str];
4821 if downloader.verbosity > 0 {
4822 info!(" Running MP4Box {}", args.join(" "));
4823 }
4824 if let Ok(mp4box) = Command::new(downloader.mp4box_location.clone())
4825 .args(args)
4826 .output()
4827 {
4828 let msg = partial_process_output(&mp4box.stdout);
4829 if !msg.is_empty() {
4830 info!(" MP4Box stdout: {msg}");
4831 }
4832 let msg = partial_process_output(&mp4box.stderr);
4833 if !msg.is_empty() {
4834 info!(" MP4Box stderr: {msg}");
4835 }
4836 if mp4box.status.success() {
4837 info!(" Merged subtitles with MP4 container");
4838 } else {
4839 warn!(" Error running MP4Box to merge subtitles");
4840 }
4841 } else {
4842 warn!(" Failed to spawn MP4Box to merge subtitles");
4843 }
4844 } else if container.eq("mkv") || container.eq("webm") {
4845 let srt = period_output_path.with_extension("srt");
4857 if srt.exists() {
4858 if downloader.verbosity > 0 {
4859 info!(" Running mkvmerge to merge subtitles with output Matroska container");
4860 }
4861 let tmppath = temporary_outpath(".mkv")?;
4862 let pop_arg = &period_output_path.to_string_lossy();
4863 let srt_arg = &srt.to_string_lossy();
4864 let mkvmerge_args = vec!["-o", &tmppath, pop_arg, srt_arg];
4865 if downloader.verbosity > 0 {
4866 info!(" Running mkvmerge {}", mkvmerge_args.join(" "));
4867 }
4868 if let Ok(mkvmerge) = Command::new(downloader.mkvmerge_location.clone())
4869 .args(mkvmerge_args)
4870 .output()
4871 {
4872 let msg = partial_process_output(&mkvmerge.stdout);
4873 if !msg.is_empty() {
4874 info!(" mkvmerge stdout: {msg}");
4875 }
4876 let msg = partial_process_output(&mkvmerge.stderr);
4877 if !msg.is_empty() {
4878 info!(" mkvmerge stderr: {msg}");
4879 }
4880 if mkvmerge.status.success() {
4881 info!(" Merged subtitles with Matroska container");
4882 {
4885 let tmpfile = File::open(tmppath.clone()).await
4886 .map_err(|e| DashMpdError::Io(
4887 e, String::from("opening mkvmerge output")))?;
4888 let mut merged = BufReader::new(tmpfile);
4889 let outfile = File::create(period_output_path.clone()).await
4891 .map_err(|e| DashMpdError::Io(
4892 e, String::from("creating output file")))?;
4893 let mut sink = BufWriter::new(outfile);
4894 io::copy(&mut merged, &mut sink).await
4895 .map_err(|e| DashMpdError::Io(
4896 e, String::from("copying mkvmerge output to output file")))?;
4897 }
4898 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4899 if let Err(e) = fs::remove_file(tmppath).await {
4900 warn!(" Error deleting temporary mkvmerge output: {e}");
4901 }
4902 }
4903 } else {
4904 warn!(" Error running mkvmerge to merge subtitles");
4905 }
4906 }
4907 }
4908 }
4909 }
4910 } else if have_audio {
4911 copy_audio_to_container(downloader, &period_output_path, &tmppath_audio).await?;
4912 } else if have_video {
4913 copy_video_to_container(downloader, &period_output_path, &tmppath_video).await?;
4914 } else if downloader.fetch_video && downloader.fetch_audio {
4915 return Err(DashMpdError::UnhandledMediaStream("no audio or video streams found".to_string()));
4916 } else if downloader.fetch_video {
4917 return Err(DashMpdError::UnhandledMediaStream("no video streams found".to_string()));
4918 } else if downloader.fetch_audio {
4919 return Err(DashMpdError::UnhandledMediaStream("no audio streams found".to_string()));
4920 }
4921 #[allow(clippy::collapsible_if)]
4922 if downloader.keep_audio.is_none() && downloader.fetch_audio {
4923 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4924 if tmppath_audio.exists() && fs::remove_file(tmppath_audio).await.is_err() {
4925 info!(" Failed to delete temporary file for audio stream");
4926 }
4927 }
4928 }
4929 #[allow(clippy::collapsible_if)]
4930 if downloader.keep_video.is_none() && downloader.fetch_video {
4931 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4932 if tmppath_video.exists() && fs::remove_file(tmppath_video).await.is_err() {
4933 info!(" Failed to delete temporary file for video stream");
4934 }
4935 }
4936 }
4937 #[allow(clippy::collapsible_if)]
4938 if env::var("DASHMPD_PERSIST_FILES").is_err() {
4939 if downloader.fetch_subtitles && tmppath_subs.exists() &&
4940 fs::remove_file(tmppath_subs).await.is_err() {
4941 info!(" Failed to delete temporary file for subtitles");
4942 }
4943 }
4944 if downloader.verbosity > 1 && (downloader.fetch_audio || downloader.fetch_video || have_subtitles) {
4945 if let Ok(metadata) = fs::metadata(&period_output_path).await {
4946 info!(" Wrote {:.1}MB to media file", metadata.len() as f64 / (1024.0 * 1024.0));
4947 }
4948 }
4949 if have_audio || have_video {
4950 period_output_pathbufs.push(period_output_path);
4951 }
4952 } let period_output_paths: Vec<&Path> = period_output_pathbufs
4954 .iter()
4955 .map(PathBuf::as_path)
4956 .collect();
4957 #[allow(clippy::comparison_chain)]
4958 if period_output_paths.len() == 1 {
4959 maybe_record_metainformation(output_path, downloader, &mpd);
4961 } else if period_output_paths.len() > 1 {
4962 #[allow(unused_mut)]
4967 let mut concatenated = false;
4968 #[cfg(not(feature = "libav"))]
4969 if downloader.concatenate_periods && video_containers_concatable(downloader, &period_output_paths) {
4971 info!("Preparing to concatenate multiple Periods into one output file");
4972 concat_output_files(downloader, &period_output_paths).await?;
4973 for p in &period_output_paths[1..] {
4974 if fs::remove_file(p).await.is_err() {
4975 warn!(" Failed to delete temporary file {}", p.display());
4976 }
4977 }
4978 concatenated = true;
4979 if let Some(pop) = period_output_paths.first() {
4980 maybe_record_metainformation(pop, downloader, &mpd);
4981 }
4982 }
4983 if !concatenated {
4984 info!("Media content has been saved in a separate file for each period:");
4985 period_counter = 0;
4987 for p in period_output_paths {
4988 period_counter += 1;
4989 info!(" Period #{period_counter}: {}", p.display());
4990 maybe_record_metainformation(p, downloader, &mpd);
4991 }
4992 }
4993 }
4994 let have_content_protection = mpd.periods.iter().any(
4995 |p| p.adaptations.iter().any(
4996 |a| (!a.ContentProtection.is_empty()) ||
4997 a.representations.iter().any(
4998 |r| !r.ContentProtection.is_empty())));
4999 if have_content_protection && downloader.decryption_keys.is_empty() {
5000 warn!("Manifest seems to use ContentProtection (DRM), but you didn't provide decryption keys.");
5001 }
5002 for observer in &downloader.progress_observers {
5003 observer.update(100, 1, "Done");
5004 }
5005 Ok(PathBuf::from(output_path))
5006}
5007
5008
5009#[cfg(test)]
5010mod tests {
5011 #[test]
5012 fn test_resolve_url_template() {
5013 use std::collections::HashMap;
5014 use super::resolve_url_template;
5015
5016 assert_eq!(resolve_url_template("AA$Time$BB", &HashMap::from([("Time", "ZZZ".to_string())])),
5017 "AAZZZBB");
5018 assert_eq!(resolve_url_template("AA$Number%06d$BB", &HashMap::from([("Number", "42".to_string())])),
5019 "AA000042BB");
5020 let dict = HashMap::from([("RepresentationID", "640x480".to_string()),
5021 ("Number", "42".to_string()),
5022 ("Time", "ZZZ".to_string())]);
5023 assert_eq!(resolve_url_template("AA/$RepresentationID$/segment-$Number%05d$.mp4", &dict),
5024 "AA/640x480/segment-00042.mp4");
5025 }
5026}