use anyhow::{Context, Result};
use axum::body::Bytes;
use serde::Deserialize;
use crate::settings::TranscodeSettings;
use super::ApiError;
#[derive(Deserialize, Default, Clone)]
pub(super) struct TranscodeParams {
pub(super) mode: Option<String>,
pub(super) codec: Option<String>,
pub(super) rungs: Option<String>,
pub(super) ladder: Option<bool>,
pub(super) max_short_side: Option<u32>,
pub(super) segment_seconds: Option<f32>,
pub(super) crf: Option<u8>,
pub(super) speed: Option<u8>,
pub(super) audio: Option<String>,
pub(super) color: Option<String>,
pub(super) pixel_format: Option<String>,
pub(super) seam: Option<String>,
pub(super) max_fps: Option<f64>,
pub(super) gpu: Option<u32>,
pub(super) filter: Option<String>,
pub(super) sync: Option<bool>,
}
impl TranscodeParams {
pub(super) fn into_settings(&self) -> Result<TranscodeSettings> {
use crate::settings::{
parse_audio, parse_bit_depth, parse_color, parse_mode, parse_rung, parse_seam,
parse_video_codec,
};
let mut s = TranscodeSettings::default();
if let Some(m) = &self.mode {
s.mode = Some(parse_mode(m)?);
}
if let Some(c) = &self.codec {
s.video_codec = Some(parse_video_codec(c)?);
}
if let Some(r) = &self.rungs {
for part in r.split(',').map(str::trim).filter(|p| !p.is_empty()) {
s.rungs.push(parse_rung(part)?);
}
}
s.ladder = self.ladder.unwrap_or(false);
s.max_short_side = self.max_short_side;
s.segment_seconds = self.segment_seconds;
s.crf = self.crf;
s.speed = self.speed;
if let Some(a) = &self.audio {
s.audio = Some(parse_audio(a)?);
}
if let Some(c) = &self.color {
s.color = Some(parse_color(c)?);
}
if let Some(p) = &self.pixel_format {
s.bit_depth = Some(parse_bit_depth(p)?);
}
if let Some(sm) = &self.seam {
s.seam = Some(parse_seam(sm)?);
}
s.max_fps = self.max_fps;
s.gpu = self.gpu;
if let Some(f) = &self.filter {
s.filters = codec::filter::parse_chain(f).context("parsing filter")?;
}
Ok(s)
}
}
#[derive(Deserialize)]
pub(super) struct TranscodeRequest {
pub(super) input: InputSource,
#[serde(default)]
pub(super) output: Option<OutputTarget>,
#[serde(default)]
pub(super) spec: SpecBody,
#[serde(default)]
pub(super) sync: bool,
}
#[derive(Deserialize)]
pub(super) struct InputSource {
#[serde(default)]
path: Option<String>,
#[serde(default)]
base64: Option<String>,
}
#[derive(Deserialize)]
pub(super) struct OutputTarget {
pub(super) path: String,
}
#[derive(Deserialize, Default)]
pub(super) struct SpecBody {
mode: Option<String>,
codec: Option<String>,
#[serde(default)]
rungs: Vec<String>,
ladder: Option<bool>,
max_short_side: Option<u32>,
segment_seconds: Option<f32>,
crf: Option<u8>,
speed: Option<u8>,
audio: Option<String>,
color: Option<String>,
#[serde(alias = "pixel_format")]
bit_depth: Option<String>,
seam: Option<String>,
max_fps: Option<f64>,
gpu: Option<u32>,
filter: Option<codec::filter::FilterSpec>,
}
impl SpecBody {
pub(super) fn into_params(self) -> TranscodeParams {
TranscodeParams {
mode: self.mode,
codec: self.codec,
rungs: (!self.rungs.is_empty()).then(|| self.rungs.join(",")),
ladder: self.ladder,
max_short_side: self.max_short_side,
segment_seconds: self.segment_seconds,
crf: self.crf,
speed: self.speed,
audio: self.audio,
color: self.color,
pixel_format: self.bit_depth,
seam: self.seam,
max_fps: self.max_fps,
gpu: self.gpu,
filter: self.filter.map(|f| f.to_chain()),
sync: None,
}
}
}
pub(super) fn read_input(src: &InputSource) -> Result<Bytes, ApiError> {
match (&src.path, &src.base64) {
(Some(p), None) => {
let path = resolve_path(p, true)?;
let bytes = std::fs::read(&path)
.map_err(|e| ApiError::bad_request(anyhow::anyhow!("reading input {p}: {e}")))?;
Ok(Bytes::from(bytes))
}
(None, Some(b)) => {
let bytes = base64_decode(b.trim())
.map_err(|e| ApiError::bad_request(anyhow::anyhow!("input.base64: {e}")))?;
Ok(Bytes::from(bytes))
}
(Some(_), Some(_)) => Err(ApiError::bad_request(anyhow::anyhow!(
"input: set exactly one of `path` or `base64`"
))),
(None, None) => Err(ApiError::bad_request(anyhow::anyhow!(
"input: set `path` or `base64`"
))),
}
}
pub(super) fn resolve_path(p: &str, must_exist: bool) -> Result<std::path::PathBuf, ApiError> {
let path = std::path::PathBuf::from(p);
let root = std::env::var_os("RIVET_FILE_ROOT").map(std::path::PathBuf::from);
let resolved = if must_exist {
std::fs::canonicalize(&path)
.map_err(|_| ApiError::bad_request(anyhow::anyhow!("input path not found: {p}")))?
} else {
let parent = path.parent().filter(|s| !s.as_os_str().is_empty());
let file = path
.file_name()
.ok_or_else(|| ApiError::bad_request(anyhow::anyhow!("invalid output path: {p}")))?;
let cparent = match parent {
Some(par) => std::fs::canonicalize(par).map_err(|_| {
ApiError::bad_request(anyhow::anyhow!("output directory not found: {}", par.display()))
})?,
None => std::env::current_dir()
.map_err(|e| ApiError::internal(anyhow::anyhow!("cwd: {e}")))?,
};
cparent.join(file)
};
if let Some(root) = root {
let croot = std::fs::canonicalize(&root).unwrap_or(root);
if !resolved.starts_with(&croot) {
return Err(ApiError::bad_request(anyhow::anyhow!(
"path escapes RIVET_FILE_ROOT sandbox"
)));
}
}
Ok(resolved)
}
pub(super) fn base64_decode(s: &str) -> Result<Vec<u8>> {
fn val(c: u8) -> Option<u8> {
match c {
b'A'..=b'Z' => Some(c - b'A'),
b'a'..=b'z' => Some(c - b'a' + 26),
b'0'..=b'9' => Some(c - b'0' + 52),
b'+' => Some(62),
b'/' => Some(63),
_ => None,
}
}
let mut out = Vec::with_capacity(s.len() / 4 * 3);
let mut acc: u32 = 0;
let mut bits = 0u32;
for &c in s.as_bytes() {
if c == b'=' || c.is_ascii_whitespace() {
continue;
}
let v = val(c).context("invalid base64 character")? as u32;
acc = (acc << 6) | v;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((acc >> bits) as u8);
}
}
Ok(out)
}