1use std::env;
19use std::path::Path;
20use std::process::Command;
21use std::ffi::OsStr;
22use tokio::fs;
23use tracing::{info, warn, error};
24use crate::DashMpdError;
25use crate::fetch::{DashDownloader, partial_process_output, tmp_file_path};
26
27
28pub async fn decrypt_mp4decrypt(
29 downloader: &DashDownloader,
30 inpath: &Path,
31 outpath: &Path,
32 media_type: &str) -> Result<(), DashMpdError>
33{
34 let mut args = Vec::new();
35 for (k, v) in &downloader.decryption_keys {
36 args.push("--key".to_string());
37 args.push(format!("{k}:{v}"));
38 }
39 args.push(inpath.to_string_lossy().to_string());
40 args.push(outpath.to_string_lossy().to_string());
41 if downloader.verbosity > 1 {
42 info!(" Running mp4decrypt {}", args.join(" "));
43 }
44 let out = Command::new(downloader.mp4decrypt_location.clone())
45 .args(args)
46 .output()
47 .map_err(|e| DashMpdError::Io(e, String::from("spawning mp4decrypt")))?;
48 let mut no_output = false;
49 if let Ok(metadata) = fs::metadata(outpath).await {
50 if downloader.verbosity > 0 {
51 info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
52 }
53 if metadata.len() == 0 {
54 no_output = true;
55 }
56 } else {
57 no_output = true;
58 }
59 if !out.status.success() || no_output {
60 error!(" mp4decrypt subprocess failed");
61 let msg = partial_process_output(&out.stdout);
62 if !msg.is_empty() {
63 warn!(" mp4decrypt stdout: {msg}");
64 }
65 let msg = partial_process_output(&out.stderr);
66 if !msg.is_empty() {
67 warn!(" mp4decrypt stderr: {msg}");
68 }
69 }
70 if no_output {
71 error!(" Failed to decrypt {media_type} stream with mp4decrypt");
72 warn!(" Undecrypted {media_type} stream left in {}", inpath.display());
73 return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
74 }
75 Ok(())
76}
77
78
79pub async fn decrypt_shaka(
80 downloader: &DashDownloader,
81 inpath: &Path,
82 outpath: &Path,
83 media_type: &str) -> Result<(), DashMpdError>
84{
85 let mut args = Vec::new();
86 let mut keys = Vec::new();
87 if downloader.verbosity < 1 {
88 args.push("--quiet".to_string());
89 }
90 args.push(format!("in={},stream={media_type},output={}", inpath.display(), outpath.display()));
91 let mut drm_label = 0;
92 #[allow(clippy::explicit_counter_loop)]
93 for (k, v) in &downloader.decryption_keys {
94 keys.push(format!("label=lbl{drm_label}:key_id={k}:key={v}"));
95 drm_label += 1;
96 }
97 args.push("--enable_raw_key_decryption".to_string());
98 args.push("--keys".to_string());
99 args.push(keys.join(","));
100 if downloader.verbosity > 1 {
101 info!(" Running shaka-packager {}", args.join(" "));
102 }
103 let out = Command::new(downloader.shaka_packager_location.clone())
104 .args(args)
105 .output()
106 .map_err(|e| DashMpdError::Io(e, String::from("spawning shaka-packager")))?;
107 let mut no_output = true;
108 if let Ok(metadata) = fs::metadata(outpath).await {
109 if downloader.verbosity > 0 {
110 info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
111 }
112 no_output = false;
113 }
114 if !out.status.success() || no_output {
115 warn!(" shaka-packager subprocess failed");
116 let msg = partial_process_output(&out.stdout);
117 if !msg.is_empty() {
118 warn!(" shaka-packager stdout: {msg}");
119 }
120 let msg = partial_process_output(&out.stderr);
121 if !msg.is_empty() {
122 warn!(" shaka-packager stderr: {msg}");
123 }
124 }
125 if no_output {
126 error!(" Failed to decrypt {media_type} stream with shaka-packager");
127 warn!(" Undecrypted {media_type} left in {}", inpath.display());
128 return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
129 }
130 Ok(())
131}
132
133
134pub async fn decrypt_shaka_container(
140 downloader: &DashDownloader,
141 inpath: &Path,
142 outpath: &Path,
143 media_type: &str) -> Result<(), DashMpdError>
144{
145 let inpath_dir = inpath.parent()
154 .ok_or_else(|| DashMpdError::Decrypting(String::from("inpath parent")))?;
155 let inpath_nondir = inpath.file_name()
156 .ok_or_else(|| DashMpdError::Decrypting(String::from("inpath file name")))?;
157 let outpath_nondir = outpath.file_name()
158 .ok_or_else(|| DashMpdError::Decrypting(String::from("outpath file name")))?;
159 let mut args = Vec::new();
160 let mut keys = Vec::new();
161 args.push(String::from("run"));
162 args.push(String::from("--rm"));
163 args.push(String::from("--network=none"));
164 args.push(String::from("--userns=keep-id"));
165 args.push(String::from("-v"));
166 args.push(format!("{}:/tmp", inpath_dir.display()));
167 args.push(String::from("docker.io/google/shaka-packager:latest"));
168 args.push(String::from("packager"));
169 args.push("--quiet".to_string());
171 args.push(format!("in=/tmp/{},stream={media_type},output=/tmp/{}",
172 inpath_nondir.display(), outpath_nondir.display()));
173 let mut drm_label = 0;
174 #[allow(clippy::explicit_counter_loop)]
175 for (k, v) in &downloader.decryption_keys {
176 keys.push(format!("label=lbl{drm_label}:key_id={k}:key={v}"));
177 drm_label += 1;
178 }
179 args.push("--enable_raw_key_decryption".to_string());
180 args.push("--keys".to_string());
181 args.push(keys.join(","));
182 if downloader.verbosity > 1 {
183 info!(" Running shaka-packager container {}", args.join(" "));
184 }
185 let container_runtime = env::var("DOCKER").unwrap_or(String::from("podman"));
188 let pull = Command::new(&container_runtime)
189 .args(["pull", "docker.io/google/shaka-packager:latest"])
190 .output()
191 .map_err(|e| DashMpdError::Decrypting(format!("pulling shaka-packager container: {e:?}")))?;
192 if !pull.status.success() {
193 error!(" Unable to pull shaka-packager decryption container with {container_runtime}");
194 let msg = partial_process_output(&pull.stdout);
195 if !msg.is_empty() {
196 info!(" {container_runtime} stdout: {msg}");
197 }
198 let msg = partial_process_output(&pull.stderr);
199 if !msg.is_empty() {
200 info!(" {container_runtime} stderr: {msg}");
201 }
202 return Err(DashMpdError::Decrypting(String::from("pulling container docker.io/google/shaka-packager:latest")));
203 }
204 let runner = Command::new(&container_runtime)
205 .args(args)
206 .output()
207 .map_err(|e| DashMpdError::Decrypting(format!("running shaka-packager container: {e:?}")))?;
208 let mut no_output = false;
209 if let Ok(metadata) = fs::metadata(outpath).await {
210 if downloader.verbosity > 0 {
211 info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
212 }
213 no_output = false;
214 }
215 if !runner.status.success() || no_output {
216 warn!(" shaka-packager container failed");
217 let msg = partial_process_output(&runner.stdout);
218 if !msg.is_empty() {
219 warn!(" shaka-packager stdout: {msg}");
220 }
221 let msg = partial_process_output(&runner.stderr);
222 if !msg.is_empty() {
223 warn!(" shaka-packager stderr: {msg}");
224 }
225 }
226 if no_output {
227 error!(" Failed to decrypt {media_type} stream with shaka-packager container");
228 error!(" Undecrypted {media_type} left in {}", inpath.display());
229 return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
230 }
231 Ok(())
232}
233
234
235pub async fn decrypt_mp4box(
238 downloader: &DashDownloader,
239 inpath: &Path,
240 outpath: &Path,
241 media_type: &str) -> Result<(), DashMpdError>
242{
243 use std::fmt::Write;
244
245 let mut args = Vec::new();
246 let drmfile = tmp_file_path("mp4boxcrypt", OsStr::new("xml"))?;
247 let mut drmfile_contents = String::from("<GPACDRM>\n <CrypTrack>\n");
248 for (k, v) in &downloader.decryption_keys {
249 let _ = writeln!(drmfile_contents, " <key KID=\"0x{k}\" value=\"0x{v}\"/>");
250 }
251 drmfile_contents += " </CrypTrack>\n</GPACDRM>\n";
252 fs::write(&drmfile, drmfile_contents).await
253 .map_err(|e| DashMpdError::Io(e, String::from("writing to MP4Box decrypt file")))?;
254 args.push("-decrypt".to_string());
255 args.push(drmfile.display().to_string());
256 args.push(String::from(inpath.to_string_lossy()));
257 args.push("-out".to_string());
258 args.push(String::from(outpath.to_string_lossy()));
259 if downloader.verbosity > 1 {
260 info!(" Running decryption application MP4Box {}", args.join(" "));
261 }
262 let out = Command::new(downloader.mp4box_location.clone())
263 .args(args)
264 .output()
265 .map_err(|e| DashMpdError::Decrypting(format!("spawning MP4Box: {e:?}")))?;
266 if env::var("DASHMPD_PERSIST_FILES").is_err() {
267 if let Err(e) = fs::remove_file(drmfile).await {
268 warn!(" Error deleting temporary mp4boxcrypt file: {e}");
269 }
270 }
271 let mut no_output = false;
272 if let Ok(metadata) = fs::metadata(outpath).await {
273 if downloader.verbosity > 0 {
274 info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
275 }
276 if metadata.len() == 0 {
277 no_output = true;
278 }
279 } else {
280 no_output = true;
281 }
282 if !out.status.success() || no_output {
283 warn!(" MP4Box decryption subprocess failed");
284 let msg = partial_process_output(&out.stdout);
285 if !msg.is_empty() {
286 warn!(" MP4Box stdout: {msg}");
287 }
288 let msg = partial_process_output(&out.stderr);
289 if !msg.is_empty() {
290 warn!(" MP4Box stderr: {msg}");
291 }
292 }
293 if no_output {
294 error!(" Failed to decrypt {media_type} with MP4Box");
295 warn!(" Undecrypted {media_type} stream left in {}", inpath.display());
296 return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
297 }
298 Ok(())
299}
300
301
302pub async fn decrypt_mp4box_container(
304 downloader: &DashDownloader,
305 inpath: &Path,
306 outpath: &Path,
307 media_type: &str) -> Result<(), DashMpdError>
308{
309 use std::fmt::Write;
310
311 let inpath_dir = inpath.parent()
312 .ok_or_else(|| DashMpdError::Decrypting(String::from("inpath parent")))?;
313 let inpath_nondir = inpath.file_name()
314 .ok_or_else(|| DashMpdError::Decrypting(String::from("inpath file name")))?;
315 let outpath_nondir = outpath.file_name()
316 .ok_or_else(|| DashMpdError::Decrypting(String::from("outpath file name")))?;
317 let mut args = Vec::new();
318 let drmpath = tmp_file_path("mp4boxcrypt", OsStr::new("xml"))?;
319 let drmpath_nondir = drmpath.file_name()
320 .ok_or_else(|| DashMpdError::Decrypting(String::from("drmpath file name")))?;
321 let mut drm_contents = String::from("<GPACDRM>\n <CrypTrack>\n");
322 for (k, v) in &downloader.decryption_keys {
323 let _ = writeln!(drm_contents, " <key KID=\"0x{k}\" value=\"0x{v}\"/>");
324 }
325 drm_contents += " </CrypTrack>\n</GPACDRM>\n";
326 fs::write(&drmpath, drm_contents).await
327 .map_err(|e| DashMpdError::Io(e, String::from("writing to MP4Box decrypt file")))?;
328 args.push(String::from("run"));
329 args.push(String::from("--rm"));
330 args.push(String::from("--network=none"));
331 args.push(String::from("--userns=keep-id"));
332 args.push(String::from("-v"));
333 args.push(format!("{}:/tmp", inpath_dir.display()));
334 args.push(String::from("docker.io/gpac/ubuntu:latest"));
335 args.push(String::from("MP4Box"));
336 args.push("-decrypt".to_string());
337 args.push(format!("/tmp/{}", drmpath_nondir.display()));
338 args.push(format!("/tmp/{}", inpath_nondir.display()));
339 args.push("-out".to_string());
340 args.push(format!("/tmp/{}", outpath_nondir.display()));
341 if downloader.verbosity > 1 {
342 info!(" Running decryption container GPAC/MP4Box {}", args.join(" "));
343 }
344 let container_runtime = env::var("DOCKER").unwrap_or(String::from("podman"));
345 let pull = Command::new(&container_runtime)
346 .args(["pull", "docker.io/gpac/ubuntu:latest"])
347 .output()
348 .map_err(|e| DashMpdError::Decrypting(format!("pulling MP4Box container: {e:?}")))?;
349 if !pull.status.success() {
350 warn!(" Unable to pull MP4Box decryption container");
351 return Err(DashMpdError::Decrypting(String::from("pulling container docker.io/gpac/ubuntu:latest")));
352 }
353 let runner = Command::new(&container_runtime)
354 .args(args)
355 .output()
356 .map_err(|e| DashMpdError::Decrypting(format!("spawning MP4Box container: {e:?}")))?;
357 let mut no_output = false;
358 if let Ok(metadata) = fs::metadata(&outpath).await {
359 if downloader.verbosity > 0 {
360 info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
361 }
362 if metadata.len() == 0 {
363 no_output = true;
364 }
365 } else {
366 no_output = true;
367 }
368 if !runner.status.success() || no_output {
369 warn!(" MP4Box decryption container failed");
370 let msg = partial_process_output(&runner.stdout);
371 if !msg.is_empty() {
372 warn!(" MP4Box stdout: {msg}");
373 }
374 let msg = partial_process_output(&runner.stderr);
375 if !msg.is_empty() {
376 warn!(" MP4Box stderr: {msg}");
377 }
378 }
379 if no_output {
380 error!(" Failed to decrypt {media_type} with MP4Box container");
381 error!(" Undecrypted {media_type} stream left in {}", inpath.display());
382 return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
383 }
384 Ok(())
385}