av_scenechange/
lib.rs

1// Documentation lints
2// FIXME: add docs and turn this to warn
3#![allow(missing_docs)]
4#![warn(clippy::doc_link_with_quotes)]
5#![warn(clippy::doc_markdown)]
6#![warn(clippy::missing_errors_doc)]
7#![warn(clippy::missing_panics_doc)]
8
9pub mod decoder;
10
11mod analyze;
12#[macro_use]
13mod cpu;
14mod data;
15#[cfg(feature = "ffmpeg")]
16pub mod ffmpeg;
17#[cfg(feature = "vapoursynth")]
18pub mod vapoursynth;
19mod y4m;
20
21use std::{
22    collections::{BTreeMap, BTreeSet},
23    io::Read,
24    sync::Arc,
25    time::Instant,
26};
27
28pub use ::y4m::Decoder as Y4mDecoder;
29use decoder::Decoder;
30pub use num_rational::Rational32;
31use v_frame::pixel::Pixel;
32
33pub use crate::{analyze::SceneChangeDetector, cpu::CpuFeatureLevel};
34
35/// Options determining how to run scene change detection.
36#[derive(Debug, Clone, Copy)]
37pub struct DetectionOptions {
38    /// The speed of detection algorithm to use.
39    /// Slower algorithms are more accurate/better for use in encoders.
40    pub analysis_speed: SceneDetectionSpeed,
41    /// Enabling this will utilize heuristics to avoid scenecuts
42    /// that are too close to each other.
43    /// This is generally useful if you want scenecut detection
44    /// for use in an encoder.
45    /// If you want a raw list of scene changes, you should disable this.
46    pub detect_flashes: bool,
47    /// The minimum distance between two scene changes.
48    pub min_scenecut_distance: Option<usize>,
49    /// The maximum distance between two scene changes.
50    pub max_scenecut_distance: Option<usize>,
51    /// The distance to look ahead in the video
52    /// for scene flash detection.
53    ///
54    /// Not used if `detect_flashes` is `false`.
55    pub lookahead_distance: usize,
56}
57
58impl Default for DetectionOptions {
59    #[inline]
60    fn default() -> Self {
61        DetectionOptions {
62            analysis_speed: SceneDetectionSpeed::Standard,
63            detect_flashes: true,
64            lookahead_distance: 5,
65            min_scenecut_distance: None,
66            max_scenecut_distance: None,
67        }
68    }
69}
70
71/// Results from a scene change detection pass.
72#[derive(Debug, Clone)]
73#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
74pub struct DetectionResults {
75    /// The 0-indexed frame numbers where scene changes were detected.
76    pub scene_changes: Vec<usize>,
77    /// The total number of frames read.
78    pub frame_count: usize,
79    /// Average speed (FPS)
80    pub speed: f64,
81}
82
83/// # Errors
84///
85/// - If using a Vapoursynth script that contains an unsupported video format.
86#[inline]
87pub fn new_detector<R: Read, T: Pixel>(
88    dec: &mut Decoder<R>,
89    opts: DetectionOptions,
90) -> anyhow::Result<SceneChangeDetector<T>> {
91    let video_details = dec.get_video_details()?;
92
93    Ok(SceneChangeDetector::new(
94        (video_details.width, video_details.height),
95        video_details.bit_depth,
96        video_details.time_base.recip(),
97        video_details.chroma_sampling,
98        if opts.detect_flashes {
99            opts.lookahead_distance
100        } else {
101            1
102        },
103        opts.analysis_speed,
104        opts.min_scenecut_distance.map_or(0, |val| val),
105        opts.max_scenecut_distance
106            .map_or_else(|| u32::MAX as usize, |val| val),
107        CpuFeatureLevel::default(),
108    ))
109}
110
111/// Runs through a y4m video clip,
112/// detecting where scene changes occur.
113/// This is adjustable based on the `opts` parameters.
114///
115/// This is the preferred, simplified interface
116/// for analyzing a whole clip for scene changes.
117///
118/// # Arguments
119///
120/// - `progress_callback`: An optional callback that will fire after each frame
121///   is analyzed. Arguments passed in will be, in order, the number of frames
122///   analyzed, and the number of keyframes detected. This is generally useful
123///   for displaying progress, etc.
124///
125/// # Errors
126///
127/// - If using a Vapoursynth script that contains an unsupported video format.
128///
129/// # Panics
130///
131/// - If `opts.lookahead_distance` is 0.
132#[inline]
133pub fn detect_scene_changes<R: Read, T: Pixel>(
134    dec: &mut Decoder<R>,
135    opts: DetectionOptions,
136    frame_limit: Option<usize>,
137    progress_callback: Option<&dyn Fn(usize, usize)>,
138) -> anyhow::Result<DetectionResults> {
139    assert!(opts.lookahead_distance >= 1);
140
141    let mut detector = new_detector::<R, T>(dec, opts)?;
142    let video_details = dec.get_video_details()?;
143    let mut frame_queue = BTreeMap::new();
144    let mut keyframes = BTreeSet::new();
145    keyframes.insert(0);
146
147    let start_time = Instant::now();
148    let mut frameno = 0;
149    loop {
150        let mut next_input_frameno = frame_queue.keys().last().copied().map_or(0, |key| key + 1);
151        while next_input_frameno
152            < (frameno + opts.lookahead_distance + 1).min(frame_limit.unwrap_or(usize::MAX))
153        {
154            let frame = dec.read_video_frame(&video_details);
155            if let Ok(frame) = frame {
156                frame_queue.insert(next_input_frameno, Arc::new(frame));
157                next_input_frameno += 1;
158            } else {
159                // End of input
160                break;
161            }
162        }
163
164        // The frame_queue should start at whatever the previous frame was
165        let frame_set = frame_queue
166            .values()
167            .take(opts.lookahead_distance + 2)
168            .collect::<Vec<_>>();
169        if frame_set.len() < 2 {
170            // End of video
171            break;
172        }
173        if frameno == 0
174            || detector.analyze_next_frame(
175                &frame_set,
176                frameno,
177                *keyframes
178                    .iter()
179                    .last()
180                    .expect("at least 1 keyframe should exist"),
181            )
182        {
183            keyframes.insert(frameno);
184        };
185
186        if frameno > 0 {
187            frame_queue.remove(&(frameno - 1));
188        }
189
190        frameno += 1;
191        if let Some(progress_fn) = progress_callback {
192            progress_fn(frameno, keyframes.len());
193        }
194        if let Some(frame_limit) = frame_limit {
195            if frameno == frame_limit {
196                break;
197            }
198        }
199    }
200    Ok(DetectionResults {
201        scene_changes: keyframes.into_iter().collect(),
202        frame_count: frameno,
203        speed: frameno as f64 / start_time.elapsed().as_secs_f64(),
204    })
205}
206
207#[derive(Clone, Copy, Debug, PartialOrd, PartialEq, Eq)]
208pub enum SceneDetectionSpeed {
209    /// Fastest scene detection using pixel-wise comparison
210    Fast,
211    /// Scene detection using frame costs and motion vectors
212    Standard,
213    /// Do not perform scenecut detection, only place keyframes at fixed
214    /// intervals
215    None,
216}