use std::cell::RefCell;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use anyhow::Context as _;
use bpaf::Bpaf;
use ffcine::assembly;
use ffcine::{Decode, Device, backend};
#[derive(Debug, Clone, Bpaf)]
#[allow(clippy::doc_markdown)]
pub(crate) struct RenderArgs {
#[bpaf(argument("NAME"), fallback("preview".to_owned()), display_fallback)]
profile: String,
#[bpaf(argument("ASPECT"))]
render_profile: Option<String>,
#[bpaf(argument("DIR"))]
cache: Option<PathBuf>,
dry_run: bool,
no_text: bool,
#[bpaf(external(crate::audio::mix_flags))]
mix: crate::audio::MixFlags,
json: bool,
#[bpaf(argument("N"))]
cuda: Option<u32>,
#[bpaf(argument("API"), fallback(Decode::Vulkan), display_fallback)]
decode: Decode,
#[bpaf(short('o'), long("output"), argument("FILE"))]
output: PathBuf,
#[bpaf(positional("TIMELINE"))]
timeline: PathBuf,
}
fn profile(
t: &ffcine::timeline::Timeline,
name: &str,
aspect: Option<&str>,
) -> anyhow::Result<assembly::Profile> {
let output = t.project().transpose()?.unwrap_or_default().output;
assembly::Profile::resolve(name, &output, aspect).map_err(|e| anyhow::anyhow!(e))
}
fn plan_file(
path: &Path,
profile_name: &str,
aspect: Option<&str>,
no_text: bool,
) -> anyhow::Result<(assembly::Plan, Option<assembly::TextLayer>)> {
let t = ffcine::timeline::Timeline::from_file(path)
.with_context(|| format!("reading {}", path.display()))?;
let profile = &profile(&t, profile_name, aspect)?;
let dir = path.parent().unwrap_or(Path::new("."));
let text = if no_text {
None
} else {
Some(assembly::TextLayer::from_timeline(&t, dir).context("text clips")?)
};
let p = assembly::plan(&t, dir, profile, &assembly::FsFingerprinter, text.as_ref())
.with_context(|| format!("planning {}", path.display()))?;
for w in &p.warnings {
eprintln!("warning: {w}");
}
Ok((p, text))
}
pub(crate) fn diff(
profile: &str,
aspect: Option<&str>,
all: bool,
old: &Path,
new: &Path,
) -> anyhow::Result<()> {
let (old, new) = (
plan_file(old, profile, aspect, false)?.0,
plan_file(new, profile, aspect, false)?.0,
);
print!("{}", assembly::Diff::new(&old, &new).report(&new, all));
Ok(())
}
impl RenderArgs {
fn plan(&self) -> anyhow::Result<(assembly::Plan, Option<assembly::TextLayer>)> {
let (mut plan, text) = plan_file(
&self.timeline,
&self.profile,
self.render_profile.as_deref(),
self.no_text,
)?;
if let Some(a) = &mut plan.audio {
let mut o = a.options.clone();
self.mix.apply(&mut o);
a.set_options(o);
}
Ok((plan, text))
}
pub(crate) fn run(self, ff: &backend::Ffmpeg) -> anyhow::Result<()> {
let (plan, mut text) = self.plan()?;
let profile = &plan.profile;
let cache = self.cache.unwrap_or_else(|| {
self.timeline
.parent()
.unwrap_or(Path::new("."))
.join(".ffcine-cache")
});
if self.dry_run {
let cached = |k: &assembly::Key| cache.join("seg").join(format!("{k}.mp4")).exists();
let empty = assembly::Plan {
segments: plan
.segments
.iter()
.filter(|s| cached(&s.key))
.cloned()
.collect(),
audio: plan.audio.clone().filter(|a| {
cache
.join("audio")
.join(format!("{}-aac{}.m4a", a.key, profile.audio_bitrate))
.exists()
}),
..plan.clone()
};
print!("{}", assembly::Diff::new(&empty, &plan).report(&plan, true));
return Ok(());
}
let t = ffcine::timeline::Timeline::from_file(&self.timeline)?;
let project = t.project().transpose()?.unwrap_or_default();
let dir = self.timeline.parent().unwrap_or(Path::new("."));
let luts = RefCell::new(super::looks::Luts::default());
let warned = RefCell::new(HashSet::new());
let grader = |s: &assembly::Shot| {
let (g, skipped) =
super::looks::grade_for(&project, dir, &s.grade, &mut luts.borrow_mut())
.map_err(|e| format!("{e:#}"))?;
for w in skipped.into_iter().filter(|w| !w.starts_with("denoise")) {
if warned.borrow_mut().insert(w.clone()) {
eprintln!("warning: {w}");
}
}
Ok(g)
};
let detect = |p: &Path, r: &ffcine::stabilize::SourceRange| {
let t = std::time::Instant::now();
let (det, cached) =
ffcine::stabilize::detect_cached(p, r, &ffcine::stabilize::DetectParams::default())
.map_err(|e| format!("{e:#}"))?;
if !self.json {
eprintln!(
"stabilize: {} frames of {}, detect {:.1} s{}",
r.frames,
p.display(),
t.elapsed().as_secs_f64(),
if cached { " (cached)" } else { "" }
);
}
Ok(det)
};
let r = assembly::Renderer {
grader: Some(&grader),
detect: Some(&detect),
backend: ff,
cache,
ffmpeg: std::env::var_os("FFMPEG").unwrap_or_else(|| "ffmpeg".into()),
device: self.cuda.map_or(Device::Auto, Device::Cuda),
decode: self.decode,
progress: if self.json {
assembly::Progress::Json
} else {
assembly::Progress::Human
},
};
let rep = r.render(&plan, text.as_mut(), &self.output)?;
if !self.json {
let s = |d: Option<std::time::Duration>| {
d.map_or_else(
|| "cached".to_owned(),
|d| format!("{:.1} s", d.as_secs_f64()),
)
};
eprintln!(
"segments {:.1} s ({} of {}, {} frames), audio {}, join+mux {}, total {:.1} s",
rep.render.as_secs_f64(),
rep.rendered,
rep.segments,
rep.rendered_frames,
s(rep.audio),
s(rep.mux),
rep.total.as_secs_f64()
);
}
Ok(())
}
}