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)
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("--v".to_string());
91 args.push(format!("{}", downloader.verbosity));
92 args.push(format!("in={},stream={media_type},output={}", inpath.display(), outpath.display()));
93 let mut drm_label = 0;
94 #[allow(clippy::explicit_counter_loop)]
95 for (k, v) in &downloader.decryption_keys {
96 keys.push(format!("label=lbl{drm_label}:key_id={k}:key={v}"));
97 drm_label += 1;
98 }
99 args.push("--enable_raw_key_decryption".to_string());
100 args.push("--keys".to_string());
101 args.push(keys.join(","));
102 if downloader.verbosity > 1 {
103 info!(" Running shaka-packager {}", args.join(" "));
104 }
105 let out = Command::new(&downloader.shaka_packager_location)
106 .args(args)
107 .output()
108 .map_err(|e| DashMpdError::Io(e, String::from("spawning shaka-packager")))?;
109 let mut no_output = true;
110 if let Ok(metadata) = fs::metadata(outpath).await {
111 if downloader.verbosity > 0 {
112 info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
113 }
114 no_output = false;
115 }
116 if !out.status.success() || no_output {
117 warn!(" shaka-packager subprocess failed");
118 let msg = partial_process_output(&out.stdout);
119 if !msg.is_empty() {
120 warn!(" shaka-packager stdout: {msg}");
121 }
122 let msg = partial_process_output(&out.stderr);
123 if !msg.is_empty() {
124 warn!(" shaka-packager stderr: {msg}");
125 }
126 }
127 if no_output {
128 error!(" Failed to decrypt {media_type} stream with shaka-packager");
129 warn!(" Undecrypted {media_type} left in {}", inpath.display());
130 return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
131 }
132 Ok(())
133}
134
135
136pub async fn decrypt_shaka_container(
142 downloader: &DashDownloader,
143 inpath: &Path,
144 outpath: &Path,
145 media_type: &str) -> Result<(), DashMpdError>
146{
147 let inpath_dir = inpath.parent()
156 .ok_or_else(|| DashMpdError::Decrypting(String::from("inpath parent")))?;
157 let inpath_nondir = inpath.file_name()
158 .ok_or_else(|| DashMpdError::Decrypting(String::from("inpath file name")))?;
159 let outpath_nondir = outpath.file_name()
160 .ok_or_else(|| DashMpdError::Decrypting(String::from("outpath file name")))?;
161 let mut args = Vec::new();
162 let mut keys = Vec::new();
163 args.push(String::from("run"));
164 args.push(String::from("--rm"));
165 args.push(String::from("--network=none"));
166 args.push(String::from("--userns=keep-id"));
167 args.push(String::from("-v"));
168 args.push(format!("{}:/tmp", inpath_dir.display()));
169 args.push(String::from("docker.io/google/shaka-packager:latest"));
170 args.push(String::from("packager"));
171 if downloader.verbosity < 1 {
173 args.push("--quiet".to_string());
174 }
175 args.push("--v".to_string());
176 args.push(format!("{}", downloader.verbosity));
177 args.push(format!("in=/tmp/{},stream={media_type},output=/tmp/{}",
178 inpath_nondir.display(), outpath_nondir.display()));
179 let mut drm_label = 0;
180 #[allow(clippy::explicit_counter_loop)]
181 for (k, v) in &downloader.decryption_keys {
182 keys.push(format!("label=lbl{drm_label}:key_id={k}:key={v}"));
183 drm_label += 1;
184 }
185 args.push("--enable_raw_key_decryption".to_string());
186 args.push("--keys".to_string());
187 args.push(keys.join(","));
188 if downloader.verbosity > 1 {
189 info!(" Running shaka-packager container {}", args.join(" "));
190 }
191 let container_runtime = env::var("DOCKER").unwrap_or(String::from("podman"));
194 let pull = Command::new(&container_runtime)
195 .args(["pull", "docker.io/google/shaka-packager:latest"])
196 .output()
197 .map_err(|e| DashMpdError::Decrypting(format!("pulling shaka-packager container: {e:?}")))?;
198 if !pull.status.success() {
199 error!(" Unable to pull shaka-packager decryption container with {container_runtime}");
200 let msg = partial_process_output(&pull.stdout);
201 if !msg.is_empty() {
202 info!(" {container_runtime} stdout: {msg}");
203 }
204 let msg = partial_process_output(&pull.stderr);
205 if !msg.is_empty() {
206 info!(" {container_runtime} stderr: {msg}");
207 }
208 return Err(DashMpdError::Decrypting(String::from("pulling container docker.io/google/shaka-packager:latest")));
209 }
210 let runner = Command::new(&container_runtime)
211 .args(args)
212 .output()
213 .map_err(|e| DashMpdError::Decrypting(format!("running shaka-packager container: {e:?}")))?;
214 let mut no_output = false;
215 if let Ok(metadata) = fs::metadata(outpath).await {
216 if downloader.verbosity > 0 {
217 info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
218 }
219 no_output = false;
220 }
221 if !runner.status.success() || no_output {
222 warn!(" shaka-packager container failed");
223 let msg = partial_process_output(&runner.stdout);
224 if !msg.is_empty() {
225 warn!(" shaka-packager stdout: {msg}");
226 }
227 let msg = partial_process_output(&runner.stderr);
228 if !msg.is_empty() {
229 warn!(" shaka-packager stderr: {msg}");
230 }
231 }
232 if no_output {
233 error!(" Failed to decrypt {media_type} stream with shaka-packager container");
234 error!(" Undecrypted {media_type} left in {}", inpath.display());
235 return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
236 }
237 Ok(())
238}
239
240
241pub async fn decrypt_mp4box(
244 downloader: &DashDownloader,
245 inpath: &Path,
246 outpath: &Path,
247 media_type: &str) -> Result<(), DashMpdError>
248{
249 use std::fmt::Write;
250
251 let mut args = Vec::new();
252 let drmfile = tmp_file_path("mp4boxcrypt", OsStr::new("xml"))?;
253 let mut drmfile_contents = String::from("<GPACDRM>\n <CrypTrack>\n");
254 for (k, v) in &downloader.decryption_keys {
255 let _ = writeln!(drmfile_contents, " <key KID=\"0x{k}\" value=\"0x{v}\"/>");
256 }
257 drmfile_contents += " </CrypTrack>\n</GPACDRM>\n";
258 fs::write(&drmfile, drmfile_contents).await
259 .map_err(|e| DashMpdError::Io(e, String::from("writing to MP4Box decrypt file")))?;
260 let verbosity = match downloader.verbosity {
261 0 => "all@error",
262 1 => "all@warning",
263 2 => "all@info",
264 _ => "all@debug",
265 };
266 args.push("-logs".to_string());
267 args.push(verbosity.to_string());
268 args.push("-decrypt".to_string());
269 args.push(drmfile.display().to_string());
270 args.push(String::from(inpath.to_string_lossy()));
271 args.push("-out".to_string());
272 args.push(String::from(outpath.to_string_lossy()));
273 if downloader.verbosity > 1 {
274 info!(" Running decryption application MP4Box {}", args.join(" "));
275 }
276 let out = Command::new(&downloader.mp4box_location)
277 .args(args)
278 .output()
279 .map_err(|e| DashMpdError::Decrypting(format!("spawning MP4Box: {e:?}")))?;
280 if env::var("DASHMPD_PERSIST_FILES").is_err() {
281 if let Err(e) = fs::remove_file(drmfile).await {
282 warn!(" Error deleting temporary mp4boxcrypt file: {e}");
283 }
284 }
285 let mut no_output = false;
286 if let Ok(metadata) = fs::metadata(outpath).await {
287 if downloader.verbosity > 0 {
288 info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
289 }
290 if metadata.len() == 0 {
291 no_output = true;
292 }
293 } else {
294 no_output = true;
295 }
296 if !out.status.success() || no_output {
297 warn!(" MP4Box decryption subprocess failed");
298 let msg = partial_process_output(&out.stdout);
299 if !msg.is_empty() {
300 warn!(" MP4Box stdout: {msg}");
301 }
302 let msg = partial_process_output(&out.stderr);
303 if !msg.is_empty() {
304 warn!(" MP4Box stderr: {msg}");
305 }
306 }
307 if no_output {
308 error!(" Failed to decrypt {media_type} with MP4Box");
309 warn!(" Undecrypted {media_type} stream left in {}", inpath.display());
310 return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
311 }
312 Ok(())
313}
314
315
316pub async fn decrypt_mp4box_container(
318 downloader: &DashDownloader,
319 inpath: &Path,
320 outpath: &Path,
321 media_type: &str) -> Result<(), DashMpdError>
322{
323 use std::fmt::Write;
324
325 let inpath_dir = inpath.parent()
326 .ok_or_else(|| DashMpdError::Decrypting(String::from("inpath parent")))?;
327 let inpath_nondir = inpath.file_name()
328 .ok_or_else(|| DashMpdError::Decrypting(String::from("inpath file name")))?;
329 let outpath_nondir = outpath.file_name()
330 .ok_or_else(|| DashMpdError::Decrypting(String::from("outpath file name")))?;
331 let mut args = Vec::new();
332 let drmpath = tmp_file_path("mp4boxcrypt", OsStr::new("xml"))?;
333 let drmpath_nondir = drmpath.file_name()
334 .ok_or_else(|| DashMpdError::Decrypting(String::from("drmpath file name")))?;
335 let mut drm_contents = String::from("<GPACDRM>\n <CrypTrack>\n");
336 for (k, v) in &downloader.decryption_keys {
337 let _ = writeln!(drm_contents, " <key KID=\"0x{k}\" value=\"0x{v}\"/>");
338 }
339 drm_contents += " </CrypTrack>\n</GPACDRM>\n";
340 fs::write(&drmpath, drm_contents).await
341 .map_err(|e| DashMpdError::Io(e, String::from("writing to MP4Box decrypt file")))?;
342 args.push(String::from("run"));
343 args.push(String::from("--rm"));
344 args.push(String::from("--network=none"));
345 args.push(String::from("--userns=keep-id"));
346 args.push(String::from("-v"));
347 args.push(format!("{}:/tmp", inpath_dir.display()));
348 args.push(String::from("docker.io/gpac/ubuntu:latest"));
349 args.push(String::from("MP4Box"));
350 args.push("-decrypt".to_string());
351 args.push(format!("/tmp/{}", drmpath_nondir.display()));
352 args.push(format!("/tmp/{}", inpath_nondir.display()));
353 args.push("-out".to_string());
354 args.push(format!("/tmp/{}", outpath_nondir.display()));
355 if downloader.verbosity > 1 {
356 info!(" Running decryption container GPAC/MP4Box {}", args.join(" "));
357 }
358 let container_runtime = env::var("DOCKER").unwrap_or(String::from("podman"));
359 let pull = Command::new(&container_runtime)
360 .args(["pull", "docker.io/gpac/ubuntu:latest"])
361 .output()
362 .map_err(|e| DashMpdError::Decrypting(format!("pulling MP4Box container: {e:?}")))?;
363 if !pull.status.success() {
364 warn!(" Unable to pull MP4Box decryption container");
365 return Err(DashMpdError::Decrypting(String::from("pulling container docker.io/gpac/ubuntu:latest")));
366 }
367 let runner = Command::new(&container_runtime)
368 .args(args)
369 .output()
370 .map_err(|e| DashMpdError::Decrypting(format!("spawning MP4Box container: {e:?}")))?;
371 if env::var("DASHMPD_PERSIST_FILES").is_err() {
372 if let Err(e) = fs::remove_file(drmpath).await {
373 warn!(" Error deleting temporary mp4boxcrypt file: {e}");
374 }
375 }
376 let mut no_output = false;
377 if let Ok(metadata) = fs::metadata(&outpath).await {
378 if downloader.verbosity > 0 {
379 info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
380 }
381 if metadata.len() == 0 {
382 no_output = true;
383 }
384 } else {
385 no_output = true;
386 }
387 if !runner.status.success() || no_output {
388 warn!(" MP4Box decryption container failed");
389 let msg = partial_process_output(&runner.stdout);
390 if !msg.is_empty() {
391 warn!(" MP4Box stdout: {msg}");
392 }
393 let msg = partial_process_output(&runner.stderr);
394 if !msg.is_empty() {
395 warn!(" MP4Box stderr: {msg}");
396 }
397 }
398 if no_output {
399 error!(" Failed to decrypt {media_type} with MP4Box container");
400 error!(" Undecrypted {media_type} stream left in {}", inpath.display());
401 return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
402 }
403 Ok(())
404}