use std::collections::BTreeSet;
use av_decoders::Decoder;
use ffms2_sys::{FFMS_GetFrameInfo, FFMS_GetNumFrames, FFMS_GetTrackFromVideo};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndexEntry {
pub pts: i64,
pub keyframe: bool,
}
pub fn read_index(decoder: &mut Decoder) -> Option<Vec<IndexEntry>> {
let total = decoder.get_video_details().total_frames?;
let source = decoder.get_ffms2_impl()?.video_source;
let track = unsafe { FFMS_GetTrackFromVideo(source) };
if track.is_null() {
return None;
}
let track_frames = unsafe { FFMS_GetNumFrames(track) };
if track_frames < 0 {
return None;
}
let total = total.min(track_frames as usize);
let mut index = Vec::with_capacity(total);
for i in 0..total {
let info = unsafe { FFMS_GetFrameInfo(track, i as i32) };
if info.is_null() {
return None;
}
let info = unsafe { &*info };
index.push(IndexEntry {
pts: info.PTS,
keyframe: info.KeyFrame != 0,
});
}
Some(index)
}
pub fn phantom_indices(index: &[IndexEntry]) -> BTreeSet<usize> {
let mut phantom = BTreeSet::new();
if index.is_empty() {
return phantom;
}
let lead = index.iter().position(|e| e.keyframe).unwrap_or(0);
if lead > 1 {
tracing::warn!(
dropped = lead,
"the index marks no keyframe until entry {lead}, dropping every entry before it",
);
}
phantom.extend(0..lead);
let gaps: Vec<i64> = (lead + 1..index.len())
.map(|i| index[i].pts.saturating_sub(index[i - 1].pts))
.collect();
if gaps.is_empty() {
return phantom;
}
let mut sorted = gaps.clone();
sorted.sort_unstable();
let median = sorted[sorted.len() / 2];
let regular = gaps
.iter()
.filter(|&&gap| gap.saturating_sub(median).saturating_abs().saturating_mul(4) <= median)
.count();
if regular * 10 < gaps.len() * 9 {
tracing::debug!(
regular,
gaps = gaps.len(),
"frame spacing is too irregular to tell phantom entries from variable frame rate pacing",
);
return phantom;
}
for (offset, &gap) in gaps.iter().enumerate() {
if gap.saturating_mul(2) < median {
phantom.insert(lead + offset);
}
}
phantom
}
pub fn remap_scene_starts(starts: &[usize], phantom: &BTreeSet<usize>) -> Vec<usize> {
let mut out: Vec<usize> = starts
.iter()
.map(|&raw| raw - phantom.range(..raw).count())
.collect();
out.dedup();
out
}
#[cfg(test)]
mod tests {
use super::*;
fn regular(count: usize) -> Vec<IndexEntry> {
(0..count)
.map(|i| IndexEntry {
pts: i as i64 * 42,
keyframe: i == 0,
})
.collect()
}
#[test]
fn a_regular_index_has_no_phantoms() {
assert!(phantom_indices(®ular(20)).is_empty());
}
#[test]
fn an_empty_index_has_no_phantoms() {
assert!(phantom_indices(&[]).is_empty());
}
#[test]
fn entries_before_the_first_keyframe_are_phantom() {
let mut index = vec![IndexEntry {
pts: 0,
keyframe: false,
}];
index.extend((1..20).map(|i| IndexEntry {
pts: 41 + (i as i64 - 1) * 42,
keyframe: i == 1,
}));
assert_eq!(phantom_indices(&index), BTreeSet::from([0]));
}
#[test]
fn the_earlier_entry_of_a_too_close_pair_is_phantom() {
let mut index = vec![
IndexEntry {
pts: 0,
keyframe: true,
},
IndexEntry {
pts: 41,
keyframe: false,
},
IndexEntry {
pts: 42,
keyframe: false,
},
IndexEntry {
pts: 82,
keyframe: false,
},
IndexEntry {
pts: 84,
keyframe: false,
},
];
index.extend((5..60).map(|i| IndexEntry {
pts: 125 + (i as i64 - 5) * 42,
keyframe: false,
}));
assert_eq!(phantom_indices(&index), BTreeSet::from([1, 3]));
}
#[test]
fn a_time_base_as_tight_as_the_frame_rate_still_finds_a_phantom() {
let mut index: Vec<IndexEntry> = (0..40)
.map(|i| IndexEntry {
pts: i as i64,
keyframe: i == 0,
})
.collect();
index[4].pts = index[3].pts;
assert_eq!(phantom_indices(&index), BTreeSet::from([3]));
}
#[test]
fn variable_frame_rate_pacing_keeps_every_entry() {
let mut pts = 0;
let index: Vec<IndexEntry> = (0..30)
.map(|i| {
let entry = IndexEntry {
pts,
keyframe: i == 0,
};
pts += if i % 3 == 2 { 10 } else { 30 };
entry
})
.collect();
assert!(phantom_indices(&index).is_empty());
}
#[test]
fn repeated_pictures_at_regular_spacing_are_kept() {
assert!(phantom_indices(®ular(2159)).is_empty());
}
#[test]
fn remap_shifts_boundaries_past_each_dropped_entry() {
let phantom = BTreeSet::from([1, 3]);
assert_eq!(remap_scene_starts(&[0, 2, 5], &phantom), vec![0, 1, 3]);
}
#[test]
fn remap_collapses_a_boundary_that_lands_on_a_dropped_entry() {
assert_eq!(remap_scene_starts(&[0, 1, 2], &BTreeSet::from([1])), vec![0, 1]);
}
}