extern crate blockhash;
extern crate ffmpeg_next;
extern crate image;
use std::path::Path;
use std::time::Duration;
use crate::{Error, Result};
struct VideoDecoder {
decoder: ffmpeg_next::codec::decoder::Video,
converter: Option<ffmpeg_next::software::scaling::context::Context>,
}
impl VideoDecoder {
fn build_threading_config() -> ffmpeg_next::codec::threading::Config {
let mut config = ffmpeg_next::codec::threading::Config::default();
config.count = std::thread::available_parallelism()
.expect("unable to determine available parallelism")
.get();
config.kind = ffmpeg_next::codec::threading::Type::Frame;
config
}
fn from_stream(stream: ffmpeg_next::format::stream::Stream, threaded: bool) -> Result<Self> {
let ctx = ffmpeg_next::codec::context::Context::from_parameters(stream.parameters())?;
let mut decoder = ctx.decoder();
if threaded {
decoder.set_threading(Self::build_threading_config());
}
Ok(Self {
decoder: decoder.video()?,
converter: None,
})
}
fn format(&self) -> ffmpeg_next::format::Pixel {
self.decoder.format()
}
fn height(&self) -> u32 {
self.decoder.height()
}
fn width(&self) -> u32 {
self.decoder.width()
}
fn send_packet(&mut self, packet: &ffmpeg_next::packet::Packet) -> Result<()> {
Ok(self.decoder.send_packet(packet)?)
}
fn set_converter(&mut self, format: ffmpeg_next::format::Pixel) -> Result<()> {
self.converter = Some(self.decoder.converter(format)?);
Ok(())
}
fn convert_frame(
&mut self,
frame: &ffmpeg_next::frame::Video,
converted_frame: &mut ffmpeg_next::frame::Video,
) -> Result<()> {
if let Some(converter) = &mut self.converter {
converter.run(frame, converted_frame)?;
}
Ok(())
}
fn receive_frame(&mut self, frame: &mut ffmpeg_next::frame::Video) -> Result<()> {
Ok(self.decoder.receive_frame(frame)?)
}
}
struct GrayFrameView<'a> {
width: u32,
height: u32,
inner: &'a [u8],
}
impl<'a> blockhash::Image for GrayFrameView<'a> {
#[inline(always)]
fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
#[inline(always)]
fn get_pixel(&self, x: u32, y: u32) -> [u8; 4] {
let (x, y, width) = (x as usize, y as usize, self.width as usize);
let mut data = [0xFF; 4]; data[0] = self.inner[y * width + x];
data[1] = data[0];
data[2] = data[0];
data
}
}
pub struct VideoComparator {
src_ctx: ffmpeg_next::format::context::Input,
dst_ctx: ffmpeg_next::format::context::Input,
}
impl VideoComparator {
pub fn new<P, Q>(src_path: P, dst_path: Q) -> Result<Self>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let src_ctx = ffmpeg_next::format::input(&src_path)?;
let dst_ctx = ffmpeg_next::format::input(&dst_path)?;
Ok(Self { src_ctx, dst_ctx })
}
fn src_stream(&self) -> ffmpeg_next::format::stream::Stream {
self.src_ctx
.streams()
.best(ffmpeg_next::media::Type::Video)
.expect("unable to find a video stream")
}
fn dst_stream(&self) -> ffmpeg_next::format::stream::Stream {
self.dst_ctx
.streams()
.best(ffmpeg_next::media::Type::Video)
.expect("unable to find a video stream")
}
fn src_decoder(&mut self) -> Result<VideoDecoder> {
VideoDecoder::from_stream(self.src_stream(), false)
}
fn dst_decoder(&mut self) -> Result<VideoDecoder> {
VideoDecoder::from_stream(self.dst_stream(), false)
}
#[inline(always)]
fn hash_frame(f: &ffmpeg_next::frame::Video) -> blockhash::Blockhash144 {
let frame_view = GrayFrameView {
width: f.width(),
height: f.height(),
inner: f.data(0),
};
blockhash::blockhash144(&frame_view)
}
#[allow(unused)]
#[inline(always)]
fn compare_two_frames(f1: &ffmpeg_next::frame::Video, f2: &ffmpeg_next::frame::Video) -> u32 {
let d1 = Self::hash_frame(f1);
let d2 = Self::hash_frame(f2);
return d1.distance(&d2);
}
#[allow(unused)]
fn frame_timestamp(
ctx: &mut ffmpeg_next::format::context::Input,
stream_idx: usize,
frame: &ffmpeg_next::frame::Video,
) -> Option<Duration> {
ctx.stream(stream_idx)
.map(|s| f64::from(s.time_base()))
.and_then(|time_base| frame.timestamp().map(|t| t as f64 * time_base * 1000.0))
.map(|ts| Duration::from_millis(ts as u64))
}
fn process_frames<T, F>(
ctx: &mut ffmpeg_next::format::context::Input,
decoder: &mut VideoDecoder,
stream_idx: usize,
count: Option<usize>,
skip_by: Option<usize>,
map_frame_fn: F,
) -> Vec<T>
where
F: Fn(&ffmpeg_next::frame::Video, &ffmpeg_next::format::stream::Stream) -> T,
{
let _g = tracing::span!(tracing::Level::TRACE, "process_frames", count);
let skip_by = skip_by.unwrap_or(1);
let mut output: Vec<T> = Vec::new();
let mut frame =
ffmpeg_next::frame::Video::new(decoder.format(), decoder.width(), decoder.height());
let mut frame_gray = ffmpeg_next::frame::Video::new(
ffmpeg_next::format::Pixel::GRAY8,
decoder.width(),
decoder.height(),
);
ctx.packets()
.filter(|(s, _)| s.index() == stream_idx)
.take(count.unwrap_or(usize::MAX))
.enumerate()
.map(|(i, (s, mut p))| {
if i % skip_by != 0 {
p.set_flags(ffmpeg_next::codec::packet::Flags::DISCARD);
}
(s, p)
})
.for_each(|(s, p)| {
decoder.send_packet(&p).unwrap();
while decoder.receive_frame(&mut frame).is_ok() {
decoder.convert_frame(&frame, &mut frame_gray).unwrap();
frame_gray.set_pts(frame.pts());
output.push(map_frame_fn(&frame_gray, &s));
}
});
output
}
fn get_all_packets(
ctx: &mut ffmpeg_next::format::context::Input,
stream_idx: usize,
) -> Vec<ffmpeg_next::codec::packet::Packet> {
ctx.packets()
.filter(|(s, _)| s.index() == stream_idx)
.map(|(_, p)| p)
.collect()
}
pub fn compare(&mut self, _count: usize) -> Result<()> {
let (src_stream, dst_stream) = (self.src_stream(), self.dst_stream());
let src_stream_idx = src_stream.index();
let dst_stream_idx = dst_stream.index();
let mut src_decoder = self.src_decoder()?;
let mut dst_decoder = self.dst_decoder()?;
src_decoder.set_converter(ffmpeg_next::format::Pixel::GRAY8)?;
dst_decoder.set_converter(ffmpeg_next::format::Pixel::GRAY8)?;
let packets = Self::get_all_packets(&mut self.src_ctx, src_stream_idx);
tracing::debug!(num_packets = packets.len());
let map_frame_fn = |output_prefix: Option<&'static str>| {
move |f: &ffmpeg_next::frame::video::Video, s: &ffmpeg_next::format::stream::Stream| {
let time_base = f64::from(s.time_base());
let pts = f.pts().unwrap();
let ts = Duration::from_millis((pts as f64 * time_base * 1000.0) as u64);
if let Some(output_prefix) = output_prefix {
let path = format!("frames/{}_{}.png", output_prefix, pts);
save_frame(f, &path).unwrap();
tracing::debug!(output = output_prefix, pts = pts);
}
(Self::hash_frame(f), ts)
}
};
let src_frame_hashes = Self::process_frames(
&mut self.src_ctx,
&mut src_decoder,
src_stream_idx,
Some(1000),
Some(5),
map_frame_fn(Some("src_gray")),
);
let dst_frame_hashes = Self::process_frames(
&mut self.dst_ctx,
&mut dst_decoder,
dst_stream_idx,
Some(1000),
Some(5),
map_frame_fn(Some("dst_gray")),
);
for ((h1, t1), (h2, t2)) in src_frame_hashes.iter().zip(dst_frame_hashes.iter().skip(1)) {
tracing::debug!(
t1 = t1.as_millis() as u64,
t2 = t2.as_millis() as u64,
similarity = h1.distance(h2)
);
}
Ok(())
}
}
fn save_frame<P: AsRef<Path>>(
frame: &ffmpeg_next::frame::video::Video,
path: P,
) -> std::result::Result<(), std::io::Error> {
let data = frame.data(0);
let img_buf: image::ImageBuffer<image::Luma<u8>, &[u8]> =
image::ImageBuffer::from_raw(frame.width(), frame.height(), data).unwrap();
img_buf.save(path).unwrap();
Ok(())
}
#[allow(unused)]
fn load_frame<P: AsRef<Path>>(path: P) -> Result<Vec<u8>> {
let img_buf = image::io::Reader::open(path)?.decode().unwrap();
Ok(img_buf.to_luma8().to_vec())
}