use std::env;
use std::path::Path;
use std::process::Command;
use std::ffi::OsStr;
use tokio::fs;
use tracing::{info, warn, error};
use crate::DashMpdError;
use crate::fetch::{DashDownloader, partial_process_output, tmp_file_path};
pub async fn decrypt_mp4decrypt(
downloader: &DashDownloader,
inpath: &Path,
outpath: &Path,
media_type: &str) -> Result<(), DashMpdError>
{
let mut args = Vec::new();
for (k, v) in &downloader.decryption_keys {
args.push("--key".to_string());
args.push(format!("{k}:{v}"));
}
args.push(inpath.to_string_lossy().to_string());
args.push(outpath.to_string_lossy().to_string());
if downloader.verbosity > 1 {
info!(" Running mp4decrypt {}", args.join(" "));
}
let out = Command::new(downloader.mp4decrypt_location.clone())
.args(args)
.output()
.map_err(|e| DashMpdError::Io(e, String::from("spawning mp4decrypt")))?;
let mut no_output = false;
if let Ok(metadata) = fs::metadata(outpath).await {
if downloader.verbosity > 0 {
info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
}
if metadata.len() == 0 {
no_output = true;
}
} else {
no_output = true;
}
if !out.status.success() || no_output {
error!(" mp4decrypt subprocess failed");
let msg = partial_process_output(&out.stdout);
if !msg.is_empty() {
warn!(" mp4decrypt stdout: {msg}");
}
let msg = partial_process_output(&out.stderr);
if !msg.is_empty() {
warn!(" mp4decrypt stderr: {msg}");
}
}
if no_output {
error!(" Failed to decrypt {media_type} stream with mp4decrypt");
warn!(" Undecrypted {media_type} stream left in {}", inpath.display());
return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
}
Ok(())
}
pub async fn decrypt_shaka(
downloader: &DashDownloader,
inpath: &Path,
outpath: &Path,
media_type: &str) -> Result<(), DashMpdError>
{
let mut args = Vec::new();
let mut keys = Vec::new();
if downloader.verbosity < 1 {
args.push("--quiet".to_string());
}
args.push(format!("in={},stream={media_type},output={}", inpath.display(), outpath.display()));
let mut drm_label = 0;
#[allow(clippy::explicit_counter_loop)]
for (k, v) in &downloader.decryption_keys {
keys.push(format!("label=lbl{drm_label}:key_id={k}:key={v}"));
drm_label += 1;
}
args.push("--enable_raw_key_decryption".to_string());
args.push("--keys".to_string());
args.push(keys.join(","));
if downloader.verbosity > 1 {
info!(" Running shaka-packager {}", args.join(" "));
}
let out = Command::new(downloader.shaka_packager_location.clone())
.args(args)
.output()
.map_err(|e| DashMpdError::Io(e, String::from("spawning shaka-packager")))?;
let mut no_output = true;
if let Ok(metadata) = fs::metadata(outpath).await {
if downloader.verbosity > 0 {
info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
}
no_output = false;
}
if !out.status.success() || no_output {
warn!(" shaka-packager subprocess failed");
let msg = partial_process_output(&out.stdout);
if !msg.is_empty() {
warn!(" shaka-packager stdout: {msg}");
}
let msg = partial_process_output(&out.stderr);
if !msg.is_empty() {
warn!(" shaka-packager stderr: {msg}");
}
}
if no_output {
error!(" Failed to decrypt {media_type} stream with shaka-packager");
warn!(" Undecrypted {media_type} left in {}", inpath.display());
return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
}
Ok(())
}
pub async fn decrypt_shaka_container(
downloader: &DashDownloader,
inpath: &Path,
outpath: &Path,
media_type: &str) -> Result<(), DashMpdError>
{
let inpath_dir = inpath.parent()
.ok_or_else(|| DashMpdError::Decrypting(String::from("inpath parent")))?;
let inpath_nondir = inpath.file_name()
.ok_or_else(|| DashMpdError::Decrypting(String::from("inpath file name")))?;
let outpath_nondir = outpath.file_name()
.ok_or_else(|| DashMpdError::Decrypting(String::from("outpath file name")))?;
let mut args = Vec::new();
let mut keys = Vec::new();
args.push(String::from("run"));
args.push(String::from("--rm"));
args.push(String::from("--network=none"));
args.push(String::from("--userns=keep-id"));
args.push(String::from("-v"));
args.push(format!("{}:/tmp", inpath_dir.display()));
args.push(String::from("docker.io/google/shaka-packager:latest"));
args.push(String::from("packager"));
args.push("--quiet".to_string());
args.push(format!("in=/tmp/{},stream={media_type},output=/tmp/{}",
inpath_nondir.display(), outpath_nondir.display()));
let mut drm_label = 0;
#[allow(clippy::explicit_counter_loop)]
for (k, v) in &downloader.decryption_keys {
keys.push(format!("label=lbl{drm_label}:key_id={k}:key={v}"));
drm_label += 1;
}
args.push("--enable_raw_key_decryption".to_string());
args.push("--keys".to_string());
args.push(keys.join(","));
if downloader.verbosity > 1 {
info!(" Running shaka-packager container {}", args.join(" "));
}
let container_runtime = env::var("DOCKER").unwrap_or(String::from("podman"));
let pull = Command::new(&container_runtime)
.args(["pull", "docker.io/google/shaka-packager:latest"])
.output()
.map_err(|e| DashMpdError::Decrypting(format!("pulling shaka-packager container: {e:?}")))?;
if !pull.status.success() {
error!(" Unable to pull shaka-packager decryption container with {container_runtime}");
let msg = partial_process_output(&pull.stdout);
if !msg.is_empty() {
info!(" {container_runtime} stdout: {msg}");
}
let msg = partial_process_output(&pull.stderr);
if !msg.is_empty() {
info!(" {container_runtime} stderr: {msg}");
}
return Err(DashMpdError::Decrypting(String::from("pulling container docker.io/google/shaka-packager:latest")));
}
let runner = Command::new(&container_runtime)
.args(args)
.output()
.map_err(|e| DashMpdError::Decrypting(format!("running shaka-packager container: {e:?}")))?;
let mut no_output = false;
if let Ok(metadata) = fs::metadata(outpath).await {
if downloader.verbosity > 0 {
info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
}
no_output = false;
}
if !runner.status.success() || no_output {
warn!(" shaka-packager container failed");
let msg = partial_process_output(&runner.stdout);
if !msg.is_empty() {
warn!(" shaka-packager stdout: {msg}");
}
let msg = partial_process_output(&runner.stderr);
if !msg.is_empty() {
warn!(" shaka-packager stderr: {msg}");
}
}
if no_output {
error!(" Failed to decrypt {media_type} stream with shaka-packager container");
error!(" Undecrypted {media_type} left in {}", inpath.display());
return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
}
Ok(())
}
pub async fn decrypt_mp4box(
downloader: &DashDownloader,
inpath: &Path,
outpath: &Path,
media_type: &str) -> Result<(), DashMpdError>
{
use std::fmt::Write;
let mut args = Vec::new();
let drmfile = tmp_file_path("mp4boxcrypt", OsStr::new("xml"))?;
let mut drmfile_contents = String::from("<GPACDRM>\n <CrypTrack>\n");
for (k, v) in &downloader.decryption_keys {
let _ = writeln!(drmfile_contents, " <key KID=\"0x{k}\" value=\"0x{v}\"/>");
}
drmfile_contents += " </CrypTrack>\n</GPACDRM>\n";
fs::write(&drmfile, drmfile_contents).await
.map_err(|e| DashMpdError::Io(e, String::from("writing to MP4Box decrypt file")))?;
args.push("-decrypt".to_string());
args.push(drmfile.display().to_string());
args.push(String::from(inpath.to_string_lossy()));
args.push("-out".to_string());
args.push(String::from(outpath.to_string_lossy()));
if downloader.verbosity > 1 {
info!(" Running decryption application MP4Box {}", args.join(" "));
}
let out = Command::new(downloader.mp4box_location.clone())
.args(args)
.output()
.map_err(|e| DashMpdError::Decrypting(format!("spawning MP4Box: {e:?}")))?;
if env::var("DASHMPD_PERSIST_FILES").is_err() {
if let Err(e) = fs::remove_file(drmfile).await {
warn!(" Error deleting temporary mp4boxcrypt file: {e}");
}
}
let mut no_output = false;
if let Ok(metadata) = fs::metadata(outpath).await {
if downloader.verbosity > 0 {
info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
}
if metadata.len() == 0 {
no_output = true;
}
} else {
no_output = true;
}
if !out.status.success() || no_output {
warn!(" MP4Box decryption subprocess failed");
let msg = partial_process_output(&out.stdout);
if !msg.is_empty() {
warn!(" MP4Box stdout: {msg}");
}
let msg = partial_process_output(&out.stderr);
if !msg.is_empty() {
warn!(" MP4Box stderr: {msg}");
}
}
if no_output {
error!(" Failed to decrypt {media_type} with MP4Box");
warn!(" Undecrypted {media_type} stream left in {}", inpath.display());
return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
}
Ok(())
}
pub async fn decrypt_mp4box_container(
downloader: &DashDownloader,
inpath: &Path,
outpath: &Path,
media_type: &str) -> Result<(), DashMpdError>
{
use std::fmt::Write;
let inpath_dir = inpath.parent()
.ok_or_else(|| DashMpdError::Decrypting(String::from("inpath parent")))?;
let inpath_nondir = inpath.file_name()
.ok_or_else(|| DashMpdError::Decrypting(String::from("inpath file name")))?;
let outpath_nondir = outpath.file_name()
.ok_or_else(|| DashMpdError::Decrypting(String::from("outpath file name")))?;
let mut args = Vec::new();
let drmpath = tmp_file_path("mp4boxcrypt", OsStr::new("xml"))?;
let drmpath_nondir = drmpath.file_name()
.ok_or_else(|| DashMpdError::Decrypting(String::from("drmpath file name")))?;
let mut drm_contents = String::from("<GPACDRM>\n <CrypTrack>\n");
for (k, v) in &downloader.decryption_keys {
let _ = writeln!(drm_contents, " <key KID=\"0x{k}\" value=\"0x{v}\"/>");
}
drm_contents += " </CrypTrack>\n</GPACDRM>\n";
fs::write(&drmpath, drm_contents).await
.map_err(|e| DashMpdError::Io(e, String::from("writing to MP4Box decrypt file")))?;
args.push(String::from("run"));
args.push(String::from("--rm"));
args.push(String::from("--network=none"));
args.push(String::from("--userns=keep-id"));
args.push(String::from("-v"));
args.push(format!("{}:/tmp", inpath_dir.display()));
args.push(String::from("docker.io/gpac/ubuntu:latest"));
args.push(String::from("MP4Box"));
args.push("-decrypt".to_string());
args.push(format!("/tmp/{}", drmpath_nondir.display()));
args.push(format!("/tmp/{}", inpath_nondir.display()));
args.push("-out".to_string());
args.push(format!("/tmp/{}", outpath_nondir.display()));
if downloader.verbosity > 1 {
info!(" Running decryption container GPAC/MP4Box {}", args.join(" "));
}
let container_runtime = env::var("DOCKER").unwrap_or(String::from("podman"));
let pull = Command::new(&container_runtime)
.args(["pull", "docker.io/gpac/ubuntu:latest"])
.output()
.map_err(|e| DashMpdError::Decrypting(format!("pulling MP4Box container: {e:?}")))?;
if !pull.status.success() {
warn!(" Unable to pull MP4Box decryption container");
return Err(DashMpdError::Decrypting(String::from("pulling container docker.io/gpac/ubuntu:latest")));
}
let runner = Command::new(&container_runtime)
.args(args)
.output()
.map_err(|e| DashMpdError::Decrypting(format!("spawning MP4Box container: {e:?}")))?;
let mut no_output = false;
if let Ok(metadata) = fs::metadata(&outpath).await {
if downloader.verbosity > 0 {
info!(" Decrypted {media_type} stream of size {} kB.", metadata.len() / 1024);
}
if metadata.len() == 0 {
no_output = true;
}
} else {
no_output = true;
}
if !runner.status.success() || no_output {
warn!(" MP4Box decryption container failed");
let msg = partial_process_output(&runner.stdout);
if !msg.is_empty() {
warn!(" MP4Box stdout: {msg}");
}
let msg = partial_process_output(&runner.stderr);
if !msg.is_empty() {
warn!(" MP4Box stderr: {msg}");
}
}
if no_output {
error!(" Failed to decrypt {media_type} with MP4Box container");
error!(" Undecrypted {media_type} stream left in {}", inpath.display());
return Err(DashMpdError::Decrypting(format!("{media_type} stream")));
}
Ok(())
}