use std::path::Path;
use std::ptr;
use ff::codec::context::Context as CodecContext;
use ff::ffi;
use ff::media::Type as Media;
use ffmpeg_next as ff;
use super::cuda::{CuContext, Mem};
use super::motion::{self, FlowField, Motion};
use super::nvof::{OpticalFlow, PerfLevel};
use super::{DetectParams, Detection, FlowQuality, SourceRange};
use ffcine_core::{Error, Result};
pub fn analysis_size(w: u32, h: u32, width: u32) -> (u32, u32) {
let aw = (width.min(w) / 2 * 2).max(2);
let ah = ((f64::from(h) * f64::from(aw) / f64::from(w) / 2.0).round() as u32 * 2).max(2);
(aw, ah)
}
struct HwDevice(*mut ffi::AVBufferRef);
impl HwDevice {
fn cuda() -> Result<Self> {
let mut dev = ptr::null_mut();
let r = unsafe {
ffi::av_hwdevice_ctx_create(
&raw mut dev,
ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA,
ptr::null(),
ptr::null_mut(),
0,
)
};
if r < 0 {
return Err(Error::Gpu {
api: "CUDA",
msg: format!("av_hwdevice_ctx_create: {}", ff::Error::from(r)),
});
}
Ok(Self(dev))
}
#[allow(clippy::cast_ptr_alignment)]
fn cuda_ctx(&self) -> CuContext {
unsafe {
let dev = (*self.0).data.cast::<ffi::AVHWDeviceContext>();
(*(*dev).hwctx.cast::<ffi::AVCUDADeviceContext>())
.cuda_ctx
.cast()
}
}
}
impl Drop for HwDevice {
fn drop(&mut self) {
unsafe { ffi::av_buffer_unref(&raw mut self.0) };
}
}
unsafe extern "C" fn cuda_format(
_ctx: *mut ffi::AVCodecContext,
mut fmts: *const ffi::AVPixelFormat,
) -> ffi::AVPixelFormat {
unsafe {
while *fmts != ffi::AVPixelFormat::AV_PIX_FMT_NONE {
if *fmts == ffi::AVPixelFormat::AV_PIX_FMT_CUDA {
return *fmts;
}
fmts = fmts.add(1);
}
}
ffi::AVPixelFormat::AV_PIX_FMT_NONE
}
fn build_graph(
first: &ff::frame::Video,
tb: ff::Rational,
range: &SourceRange,
(aw, ah): (u32, u32),
) -> Result<ff::filter::Graph> {
let mut g = ff::filter::Graph::new();
unsafe {
let filt = ffi::avfilter_get_by_name(c"buffer".as_ptr());
let src = if filt.is_null() {
ptr::null_mut()
} else {
ffi::avfilter_graph_alloc_filter(g.as_mut_ptr(), filt, c"in".as_ptr())
};
let par = ffi::av_buffersrc_parameters_alloc();
if src.is_null() || par.is_null() {
ffi::av_free(par.cast());
return Err(Error::Unimplemented("libavfilter buffer source"));
}
(*par).format = ffi::AVPixelFormat::AV_PIX_FMT_CUDA as i32;
(*par).time_base = tb.into();
(*par).width = (*first.as_ptr()).width;
(*par).height = (*first.as_ptr()).height;
(*par).sample_aspect_ratio = ffi::AVRational { num: 1, den: 1 };
(*par).hw_frames_ctx = (*first.as_ptr()).hw_frames_ctx;
let r = ffi::av_buffersrc_parameters_set(src, par);
ffi::av_free(par.cast());
let r = if r < 0 {
r
} else {
ffi::avfilter_init_str(src, ptr::null())
};
if r < 0 {
return Err(ff::Error::from(r).into());
}
}
let sink =
ff::filter::find("buffersink").ok_or(Error::Unimplemented("libavfilter buffersink"))?;
g.add(&sink, "out", "")?;
let spec = format!(
"setpts=PTS-STARTPTS,fps={},scale_cuda={aw}:{ah}:format=nv12",
range.rate
);
g.output("in", 0)?.input("out", 0)?.parse(&spec)?;
g.validate()?;
Ok(g)
}
struct Analyser {
of: OpticalFlow,
params: DetectParams,
scale: (f64, f64),
want: usize,
motions: Vec<Motion>,
flow: Vec<[i16; 2]>,
flow_px: Vec<[f32; 2]>,
cost: Vec<u8>,
}
impl Analyser {
fn done(&self) -> bool {
self.motions.len() >= self.want
}
fn frame(&mut self, f: &ff::frame::Video) -> Result<()> {
if self.done() {
return Ok(());
}
let k = self.motions.len();
unsafe {
let raw = f.as_ptr();
let pitch = usize::try_from((*raw).linesize[0]).map_err(|_| Error::Invalid {
what: "CUDA frame pitch",
value: (*raw).linesize[0].to_string(),
})?;
self.of
.upload(k % 2, Mem::Device((*raw).data[0] as u64), pitch)?;
}
if k == 0 {
self.motions.push(Motion::IDENTITY);
return Ok(());
}
self.of
.run(k % 2, (k - 1) % 2, &mut self.flow, &mut self.cost)?;
self.flow_px.clear();
self.flow_px.extend(
self.flow
.iter()
.map(|v| [f32::from(v[0]) / 32.0, f32::from(v[1]) / 32.0]),
);
let field = FlowField {
width: self.of.width,
height: self.of.height,
grid: self.of.grid,
cols: self.of.grid_w as usize,
rows: self.of.grid_h as usize,
flow: &self.flow_px,
cost: &self.cost,
};
let m = motion::estimate(&field, &self.params.estimate);
self.motions.push(m.rescaled(self.scale.0, self.scale.1));
Ok(())
}
fn pull(&mut self, graph: &mut ff::filter::Graph, out: &mut ff::frame::Video) -> Result<()> {
let Some(mut sink) = graph.get("out") else {
return Ok(());
};
loop {
match sink.sink().frame(out) {
Ok(()) => self.frame(out)?,
Err(
ff::Error::Other {
errno: ff::error::EAGAIN,
}
| ff::Error::Eof,
) => return Ok(()),
Err(e) => return Err(e.into()),
}
}
}
}
struct Source {
ictx: ff::format::context::Input,
dec: ff::decoder::Video,
index: usize,
tb: ff::Rational,
start_pts: i64,
device: HwDevice,
}
fn open(path: &Path, range: &SourceRange) -> Result<Source> {
let mut ictx = ff::format::input(path)?;
let (index, tb, codecpar) = {
let s = ictx.streams().best(Media::Video).ok_or(Error::Invalid {
what: "input",
value: format!("{}: no video stream", path.display()),
})?;
(s.index(), s.time_base(), s.parameters())
};
let device = HwDevice::cuda()?;
let mut cctx = CodecContext::from_parameters(codecpar)?;
unsafe {
let c = cctx.as_mut_ptr();
(*c).hw_device_ctx = ffi::av_buffer_ref(device.0);
(*c).get_format = Some(cuda_format);
}
let dec = cctx.decoder().video()?;
let file_start = match unsafe { (*ictx.as_ptr()).start_time } {
ffi::AV_NOPTS_VALUE => 0,
t => t,
};
let target = file_start + (range.start * f64::from(ffi::AV_TIME_BASE)).round() as i64;
if range.start > 0.0 {
let r =
unsafe { ffi::avformat_seek_file(ictx.as_mut_ptr(), -1, i64::MIN, target, target, 0) };
if r < 0 {
return Err(ff::Error::from(r).into());
}
}
let av_time_base = ffi::AVRational {
num: 1,
den: ffi::AV_TIME_BASE,
};
let start_pts = unsafe { ffi::av_rescale_q(target, av_time_base, tb.into()) };
Ok(Source {
ictx,
dec,
index,
tb,
start_pts,
device,
})
}
pub fn detect(path: &Path, range: &SourceRange, params: &DetectParams) -> Result<Detection> {
ff::init()?;
if range.frames == 0 || range.rate.num <= 0 || range.rate.den <= 0 {
return Err(Error::Invalid {
what: "stabilize range",
value: format!("{range:?}"),
});
}
let Source {
mut ictx,
mut dec,
index,
tb,
start_pts,
device,
} = open(path, range)?;
let (w, h) = (dec.width(), dec.height());
let (aw, ah) = analysis_size(w, h, params.analysis_width);
let perf = match params.quality {
FlowQuality::Slow => PerfLevel::Slow,
FlowQuality::Medium => PerfLevel::Medium,
FlowQuality::Fast => PerfLevel::Fast,
};
let of = unsafe { OpticalFlow::new(device.cuda_ctx(), aw, ah, params.grid, perf)? };
let mut an = Analyser {
of,
params: *params,
scale: (f64::from(w) / f64::from(aw), f64::from(h) / f64::from(ah)),
want: range.frames,
motions: Vec::with_capacity(range.frames),
flow: Vec::new(),
flow_px: Vec::new(),
cost: Vec::new(),
};
let mut graph: Option<ff::filter::Graph> = None;
let mut decoded = ff::frame::Video::empty();
let mut out = ff::frame::Video::empty();
let mut push = |dec: &mut ff::decoder::Video,
graph: &mut Option<ff::filter::Graph>,
an: &mut Analyser|
-> Result<()> {
while dec.receive_frame(&mut decoded).is_ok() {
if decoded.timestamp().or(decoded.pts()).unwrap_or(0) < start_pts {
continue;
}
if graph.is_none() {
*graph = Some(build_graph(&decoded, tb, range, (aw, ah))?);
}
let Some(g) = graph.as_mut() else { continue };
g.get("in")
.ok_or(Error::Unimplemented("filter graph input"))?
.source()
.add(&decoded)?;
an.pull(g, &mut out)?;
}
Ok(())
};
for (s, packet) in ictx.packets() {
if an.done() {
break;
}
if s.index() != index {
continue;
}
dec.send_packet(&packet)?;
push(&mut dec, &mut graph, &mut an)?;
}
if !an.done() {
dec.send_eof()?;
push(&mut dec, &mut graph, &mut an)?;
if let Some(g) = graph.as_mut() {
if let Some(mut src) = g.get("in") {
src.source().flush()?;
}
an.pull(g, &mut out)?;
}
}
if an.motions.is_empty() {
return Err(Error::Invalid {
what: "stabilize range",
value: format!("no frames at {} s in {}", range.start, path.display()),
});
}
an.motions.resize(range.frames, Motion::IDENTITY);
Ok(Detection {
width: w,
height: h,
analysis_width: aw,
analysis_height: ah,
motions: std::mem::take(&mut an.motions),
})
}
#[cfg(test)]
mod tests {
use super::analysis_size;
#[test]
fn analysis_sizes() {
assert_eq!(analysis_size(3840, 2160, 1920), (1920, 1080));
assert_eq!(analysis_size(1920, 1080, 4000), (1920, 1080));
assert_eq!(analysis_size(3840, 1606, 1280), (1280, 536));
assert_eq!(analysis_size(1080, 1920, 960), (960, 1706));
}
}