extern crate chromaprint_rust;
extern crate ffmpeg_next;
#[cfg(feature = "rayon")]
extern crate rayon;
use chromaprint_rust as chromaprint;
use std::path::Path;
use std::time::Duration;
#[cfg(feature = "rayon")]
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use crate::{Error, Result};
#[derive(Debug, Deserialize, Serialize)]
pub struct FrameHashes {
pub(crate) hash_period: f32,
pub(crate) hash_duration: f32,
pub(crate) data: Vec<(u32, Duration)>,
pub(crate) video_size: usize,
}
impl FrameHashes {
fn from_path(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
if !path.exists() {
return Err(Error::FrameHashDataNotFound(path.to_owned()).into());
}
let f = std::fs::File::open(path)?;
Ok(bincode::deserialize_from(&f)?)
}
pub fn from_video(video: impl AsRef<Path>, analyze: bool) -> Result<Self> {
let video = video.as_ref();
if !analyze {
let path = video
.to_owned()
.with_extension(super::FRAME_HASH_DATA_FILE_EXT);
Self::from_path(&path)
} else {
tracing::debug!(
"starting in-place video analysis for {}...",
video.display()
);
let analyzer = super::Analyzer::<&Path>::default().with_force(true);
let frame_hashes = analyzer.run_single(
video,
super::DEFAULT_HASH_PERIOD,
super::DEFAULT_HASH_DURATION,
false,
)?;
tracing::debug!("completed in-place video analysis for {}", video.display());
Ok(frame_hashes)
}
}
}
struct Decoder {
decoder: ffmpeg_next::codec::decoder::Audio,
}
impl Decoder {
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());
}
let decoder = decoder.audio()?;
Ok(Self { decoder })
}
fn send_packet(&mut self, packet: &ffmpeg_next::packet::Packet) -> Result<()> {
Ok(self.decoder.send_packet(packet)?)
}
fn receive_frame(&mut self, frame: &mut ffmpeg_next::frame::Audio) -> Result<()> {
Ok(self.decoder.receive_frame(frame)?)
}
}
#[derive(Debug)]
pub struct Analyzer<P: AsRef<Path>> {
pub(crate) videos: Vec<P>,
threaded_decoding: bool,
force: bool,
}
impl<P: AsRef<Path>> Default for Analyzer<P> {
fn default() -> Self {
Self {
videos: Default::default(),
threaded_decoding: false,
force: false,
}
}
}
impl<P: AsRef<Path>> Analyzer<P> {
pub fn from_files(videos: impl Into<Vec<P>>, threaded_decoding: bool, force: bool) -> Self {
Self {
videos: videos.into(),
threaded_decoding,
force,
}
}
pub fn videos(&self) -> &[P] {
&self.videos
}
pub fn with_force(mut self, force: bool) -> Self {
self.force = force;
self
}
pub fn with_threaded_decoding(mut self, threaded_decoding: bool) -> Self {
self.threaded_decoding = threaded_decoding;
self
}
fn find_best_audio_stream(
input: &ffmpeg_next::format::context::Input,
) -> ffmpeg_next::format::stream::Stream {
input
.streams()
.best(ffmpeg_next::media::Type::Audio)
.expect("unable to find an audio stream")
}
fn process_frames(
ctx: &mut ffmpeg_next::format::context::Input,
stream_idx: usize,
hash_duration: Duration,
hash_period: Duration,
threaded: bool,
) -> Result<Vec<(u32, Duration)>> {
let span = tracing::span!(tracing::Level::TRACE, "process_frames");
let _enter = span.enter();
let stream = ctx.stream(stream_idx).unwrap();
let mut decoder = Decoder::from_stream(stream, threaded).unwrap();
let mut hashes = Vec::new();
let mut frame = ffmpeg_next::frame::Audio::empty();
let mut frame_resampled = ffmpeg_next::frame::Audio::empty();
let n = f32::ceil(hash_duration.as_secs_f32() / hash_period.as_secs_f32()) as usize;
let mut fingerprinter =
chromaprint::DelayedFingerprinter::new(n, hash_duration, hash_period, None, 2, None);
let target_sample_rate = fingerprinter.sample_rate();
let mut resampler = decoder
.decoder
.resampler(
ffmpeg_next::format::Sample::I16(ffmpeg_next::format::sample::Type::Packed),
ffmpeg_next::ChannelLayout::STEREO,
target_sample_rate,
)
.unwrap();
let audio_packets = ctx
.packets()
.filter(|(s, _)| s.index() == stream_idx)
.map(|(_, p)| p);
for p in audio_packets {
decoder.send_packet(&p).unwrap();
while decoder.receive_frame(&mut frame).is_ok() {
let mut delay = match resampler.run(&frame, &mut frame_resampled) {
Ok(v) => v,
Err(ffmpeg_next::Error::InputChanged) => {
let mut local_resampler = frame
.resampler(
ffmpeg_next::format::Sample::I16(
ffmpeg_next::format::sample::Type::Packed,
),
ffmpeg_next::ChannelLayout::STEREO,
target_sample_rate,
)
.unwrap();
let delay = local_resampler
.run(&frame, &mut frame_resampled)
.expect("failed to resample frame");
resampler = local_resampler;
delay
}
Err(_) => panic!("unexpected error"),
};
loop {
let raw_samples = &frame_resampled.data(0)
[..frame_resampled.samples() * frame_resampled.channels() as usize * 2];
let (_, samples, _) = unsafe { raw_samples.align_to() };
for (raw_fingerprint, ts) in fingerprinter.feed(samples).unwrap() {
let hash = chromaprint::simhash::simhash32(raw_fingerprint.get());
hashes.push((hash, ts));
}
if delay.is_none() {
break;
} else {
delay = resampler.flush(&mut frame_resampled).unwrap();
}
}
}
}
Ok(hashes)
}
pub(crate) fn run_single(
&self,
path: impl AsRef<Path>,
hash_period: f32,
hash_duration: f32,
persist: bool,
) -> Result<FrameHashes> {
let span = tracing::span!(tracing::Level::TRACE, "run");
let _enter = span.enter();
let path = path.as_ref();
let frame_hash_path = path.with_extension(super::FRAME_HASH_DATA_FILE_EXT);
let video_size = std::fs::File::open(&path)?.metadata()?.len() as usize;
if !self.force {
if let Ok(f) = std::fs::File::open(&frame_hash_path) {
let data: FrameHashes = bincode::deserialize_from(&f).unwrap();
if data.video_size == video_size {
println!("Skipping analysis for {}...", path.display());
return Ok(data);
}
}
}
let mut ctx = ffmpeg_next::format::input(&path)?;
let stream = Self::find_best_audio_stream(&ctx);
let stream_idx = stream.index();
let threaded = self.threaded_decoding;
tracing::debug!("starting frame processing for {}", path.display());
let frame_hashes = Self::process_frames(
&mut ctx,
stream_idx,
Duration::from_secs_f32(hash_duration),
Duration::from_secs_f32(hash_period),
threaded,
)?;
tracing::debug!(
num_hashes = frame_hashes.len(),
"completed frame processing for {}",
path.display(),
);
let frame_hashes = FrameHashes {
hash_period,
hash_duration,
data: frame_hashes,
video_size,
};
if persist {
let mut f = std::fs::File::create(&frame_hash_path)?;
bincode::serialize_into(&mut f, &frame_hashes)?;
}
Ok(frame_hashes)
}
}
impl<P: AsRef<Path> + Sync> Analyzer<P> {
pub fn run(
&self,
hash_period: f32,
hash_duration: f32,
persist: bool,
threading: bool,
) -> Result<Vec<FrameHashes>> {
if self.videos.len() == 0 {
return Err(Error::AnalyzerMissingPaths.into());
}
let mut data = Vec::new();
if cfg!(feature = "rayon") && threading {
#[cfg(feature = "rayon")]
{
data = self
.videos
.par_iter()
.map(|path| {
self.run_single(path, hash_period, hash_duration, persist)
.unwrap()
})
.collect::<Vec<_>>();
}
} else {
data.extend(self.videos.iter().map(|path| {
self.run_single(path, hash_period, hash_duration, persist)
.unwrap()
}));
}
Ok(data)
}
}
#[cfg(test)]
mod test {
use std::path::PathBuf;
use super::*;
fn get_sample_paths() -> Vec<PathBuf> {
let resources = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources");
vec![
resources.join("sample-5s.mp4"),
resources.join("sample-shifted-4s.mp4"),
]
}
#[test]
fn test_analyzer() {
let paths = get_sample_paths();
let analyzer = Analyzer::from_files(paths.clone(), false, false);
let data = analyzer.run(0.3, 3.0, false, false).unwrap();
insta::assert_debug_snapshot!(data);
}
}