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.iter() {
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.iter() {
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.iter() {
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 let mut args = Vec::new();
244 let drmfile = tmp_file_path("mp4boxcrypt", OsStr::new("xml"))?;
245 let mut drmfile_contents = String::from("<GPACDRM>\n <CrypTrack>\n");
246 for (k, v) in downloader.decryption_keys.iter() {
247 drmfile_contents += &format!(" <key KID=\"0x{k}\" value=\"0x{v}\"/>\n");
248 }
249 drmfile_contents += " </CrypTrack>\n</GPACDRM>\n";
250 fs::write(&drmfile, drmfile_contents).await
251 .map_err(|e| DashMpdError::Io(e, String::from("writing to MP4Box decrypt file")))?;
252 args.push("-decrypt".to_string());
253 args.push(drmfile.display().to_string());
254 args.push(String::from(inpath.to_string_lossy()));
255 args.push("-out".to_string());
256 args.push(String::from(outpath.to_string_lossy()));
257 if downloader.verbosity > 1 {
258 info!(" Running decryption application MP4Box {}", args.join(" "));
259 }
260 let out = Command::new(downloader.mp4box_location.clone())
261 .args(args)
262 .output()
263 .map_err(|e| DashMpdError::Decrypting(format!("spawning MP4Box: {e:?}")))?;
264 if env::var("DASHMPD_PERSIST_FILES").is_err() {
265 if let Err(e) = fs::remove_file(drmfile).await {
266 warn!(" Error deleting temporary mp4boxcrypt file: {e}");
267 }
268 }
269 let mut no_output = false;
270 if let Ok(metadata) = fs::metadata(outpath).await {
271 if downloader.verbosity > 0 {
272 info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
273 }
274 if metadata.len() == 0 {
275 no_output = true;
276 }
277 } else {
278 no_output = true;
279 }
280 if !out.status.success() || no_output {
281 warn!(" MP4Box decryption subprocess failed");
282 let msg = partial_process_output(&out.stdout);
283 if !msg.is_empty() {
284 warn!(" MP4Box stdout: {msg}");
285 }
286 let msg = partial_process_output(&out.stderr);
287 if !msg.is_empty() {
288 warn!(" MP4Box stderr: {msg}");
289 }
290 }
291 if no_output {
292 error!(" Failed to decrypt {media_type} with MP4Box");
293 warn!(" Undecrypted {media_type} stream left in {}", inpath.display());
294 return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
295 }
296 Ok(())
297}
298
299
300pub async fn decrypt_mp4box_container(
302 downloader: &DashDownloader,
303 inpath: &Path,
304 outpath: &Path,
305 media_type: &str) -> Result<(), DashMpdError>
306{
307 let inpath_dir = inpath.parent()
308 .ok_or_else(|| DashMpdError::Decrypting(String::from("inpath parent")))?;
309 let inpath_nondir = inpath.file_name()
310 .ok_or_else(|| DashMpdError::Decrypting(String::from("inpath file name")))?;
311 let outpath_nondir = outpath.file_name()
312 .ok_or_else(|| DashMpdError::Decrypting(String::from("outpath file name")))?;
313 let mut args = Vec::new();
314 let drmpath = tmp_file_path("mp4boxcrypt", OsStr::new("xml"))?;
315 let drmpath_nondir = drmpath.file_name()
316 .ok_or_else(|| DashMpdError::Decrypting(String::from("drmpath file name")))?;
317 let mut drm_contents = String::from("<GPACDRM>\n <CrypTrack>\n");
318 for (k, v) in downloader.decryption_keys.iter() {
319 drm_contents += &format!(" <key KID=\"0x{k}\" value=\"0x{v}\"/>\n");
320 }
321 drm_contents += " </CrypTrack>\n</GPACDRM>\n";
322 fs::write(&drmpath, drm_contents).await
323 .map_err(|e| DashMpdError::Io(e, String::from("writing to MP4Box decrypt file")))?;
324 args.push(String::from("run"));
325 args.push(String::from("--rm"));
326 args.push(String::from("--network=none"));
327 args.push(String::from("--userns=keep-id"));
328 args.push(String::from("-v"));
329 args.push(format!("{}:/tmp", inpath_dir.display()));
330 args.push(String::from("docker.io/gpac/ubuntu:latest"));
331 args.push(String::from("MP4Box"));
332 args.push("-decrypt".to_string());
333 args.push(format!("/tmp/{}", drmpath_nondir.display()));
334 args.push(format!("/tmp/{}", inpath_nondir.display()));
335 args.push("-out".to_string());
336 args.push(format!("/tmp/{}", outpath_nondir.display()));
337 if downloader.verbosity > 1 {
338 info!(" Running decryption container GPAC/MP4Box {}", args.join(" "));
339 }
340 let container_runtime = env::var("DOCKER").unwrap_or(String::from("podman"));
341 let pull = Command::new(&container_runtime)
342 .args(["pull", "docker.io/gpac/ubuntu:latest"])
343 .output()
344 .map_err(|e| DashMpdError::Decrypting(format!("pulling MP4Box container: {e:?}")))?;
345 if !pull.status.success() {
346 warn!(" Unable to pull MP4Box decryption container");
347 return Err(DashMpdError::Decrypting(String::from("pulling container docker.io/gpac/ubuntu:latest")));
348 }
349 let runner = Command::new(&container_runtime)
350 .args(args)
351 .output()
352 .map_err(|e| DashMpdError::Decrypting(format!("spawning MP4Box container: {e:?}")))?;
353 let mut no_output = false;
354 if let Ok(metadata) = fs::metadata(&outpath).await {
355 if downloader.verbosity > 0 {
356 info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
357 }
358 if metadata.len() == 0 {
359 no_output = true;
360 }
361 } else {
362 no_output = true;
363 }
364 if !runner.status.success() || no_output {
365 warn!(" MP4Box decryption container failed");
366 let msg = partial_process_output(&runner.stdout);
367 if !msg.is_empty() {
368 warn!(" MP4Box stdout: {msg}");
369 }
370 let msg = partial_process_output(&runner.stderr);
371 if !msg.is_empty() {
372 warn!(" MP4Box stderr: {msg}");
373 }
374 }
375 if no_output {
376 error!(" Failed to decrypt {media_type} with MP4Box container");
377 error!(" Undecrypted {media_type} stream left in {}", inpath.display());
378 return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
379 }
380 Ok(())
381}