use bitflags::bitflags;
use std::cmp::Ordering;
use std::collections::HashMap;
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub struct SeekFlags: u32 {
const BACKWARD = 0x0001;
const ANY = 0x0002;
const KEYFRAME = 0x0004;
const BYTE = 0x0008;
const FRAME_ACCURATE = 0x0010;
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SeekTarget {
pub position: f64,
pub stream_index: Option<usize>,
pub flags: SeekFlags,
}
impl SeekTarget {
#[must_use]
pub const fn time(position: f64) -> Self {
Self {
position,
stream_index: None,
flags: SeekFlags::KEYFRAME,
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn byte(offset: u64) -> Self {
Self {
position: offset as f64,
stream_index: None,
flags: SeekFlags::BYTE,
}
}
#[must_use]
pub const fn sample_accurate(position: f64) -> Self {
Self {
position,
stream_index: None,
flags: SeekFlags::from_bits_truncate(
SeekFlags::FRAME_ACCURATE.bits() | SeekFlags::BACKWARD.bits(),
),
}
}
#[must_use]
pub const fn with_stream(mut self, stream_index: usize) -> Self {
self.stream_index = Some(stream_index);
self
}
#[must_use]
pub const fn with_flags(mut self, flags: SeekFlags) -> Self {
self.flags = flags;
self
}
#[must_use]
pub const fn add_flags(mut self, flags: SeekFlags) -> Self {
self.flags = SeekFlags::from_bits_truncate(self.flags.bits() | flags.bits());
self
}
#[must_use]
pub const fn is_backward(&self) -> bool {
self.flags.contains(SeekFlags::BACKWARD)
}
#[must_use]
pub const fn is_any(&self) -> bool {
self.flags.contains(SeekFlags::ANY)
}
#[must_use]
pub const fn is_keyframe(&self) -> bool {
self.flags.contains(SeekFlags::KEYFRAME)
}
#[must_use]
pub const fn is_byte(&self) -> bool {
self.flags.contains(SeekFlags::BYTE)
}
#[must_use]
pub const fn is_frame_accurate(&self) -> bool {
self.flags.contains(SeekFlags::FRAME_ACCURATE)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeekAccuracy {
Keyframe,
SampleAccurate,
WithinTolerance(u64),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SeekIndexEntry {
pub pts: i64,
pub dts: i64,
pub file_offset: u64,
pub size: u32,
pub duration: u32,
pub is_keyframe: bool,
pub sample_number: u32,
}
impl SeekIndexEntry {
#[must_use]
pub const fn keyframe(
pts: i64,
dts: i64,
file_offset: u64,
size: u32,
duration: u32,
sample_number: u32,
) -> Self {
Self {
pts,
dts,
file_offset,
size,
duration,
is_keyframe: true,
sample_number,
}
}
#[must_use]
pub const fn non_keyframe(
pts: i64,
dts: i64,
file_offset: u64,
size: u32,
duration: u32,
sample_number: u32,
) -> Self {
Self {
pts,
dts,
file_offset,
size,
duration,
is_keyframe: false,
sample_number,
}
}
#[must_use]
pub const fn end_pts(&self) -> i64 {
self.pts + self.duration as i64
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SeekPlan {
pub keyframe_entry: SeekIndexEntry,
pub discard_count: u32,
pub target_entry: SeekIndexEntry,
pub file_offset: u64,
pub requested_pts: i64,
pub is_exact: bool,
}
#[derive(Debug, Clone)]
pub struct SeekIndex {
timescale: u32,
entries: Vec<SeekIndexEntry>,
keyframe_indices: Vec<usize>,
}
impl SeekIndex {
#[must_use]
pub fn new(timescale: u32) -> Self {
Self {
timescale,
entries: Vec::new(),
keyframe_indices: Vec::new(),
}
}
#[must_use]
pub fn with_capacity(timescale: u32, capacity: usize) -> Self {
Self {
timescale,
entries: Vec::with_capacity(capacity),
keyframe_indices: Vec::new(),
}
}
#[must_use]
pub const fn timescale(&self) -> u32 {
self.timescale
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn keyframe_count(&self) -> usize {
self.keyframe_indices.len()
}
#[must_use]
pub fn entries(&self) -> &[SeekIndexEntry] {
&self.entries
}
pub fn add_entry(&mut self, entry: SeekIndexEntry) {
let idx = self.entries.len();
if entry.is_keyframe {
self.keyframe_indices.push(idx);
}
self.entries.push(entry);
}
pub fn sort(&mut self) {
self.entries.sort_by_key(|e| e.dts);
self.keyframe_indices.clear();
for (i, entry) in self.entries.iter().enumerate() {
if entry.is_keyframe {
self.keyframe_indices.push(i);
}
}
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
pub fn seconds_to_ticks(&self, seconds: f64) -> i64 {
(seconds * f64::from(self.timescale)) as i64
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn ticks_to_seconds(&self, ticks: i64) -> f64 {
if self.timescale == 0 {
return 0.0;
}
ticks as f64 / f64::from(self.timescale)
}
#[must_use]
pub fn find_keyframe_before(&self, target_pts: i64) -> Option<&SeekIndexEntry> {
if self.keyframe_indices.is_empty() {
return None;
}
let mut best: Option<usize> = None;
let mut lo = 0usize;
let mut hi = self.keyframe_indices.len();
while lo < hi {
let mid = lo + (hi - lo) / 2;
let kf_idx = self.keyframe_indices[mid];
let kf = &self.entries[kf_idx];
match kf.pts.cmp(&target_pts) {
Ordering::Less | Ordering::Equal => {
best = Some(kf_idx);
lo = mid + 1;
}
Ordering::Greater => {
hi = mid;
}
}
}
best.map(|idx| &self.entries[idx])
}
#[must_use]
pub fn find_keyframe_after(&self, target_pts: i64) -> Option<&SeekIndexEntry> {
if self.keyframe_indices.is_empty() {
return None;
}
let mut lo = 0usize;
let mut hi = self.keyframe_indices.len();
while lo < hi {
let mid = lo + (hi - lo) / 2;
let kf_idx = self.keyframe_indices[mid];
let kf = &self.entries[kf_idx];
if kf.pts < target_pts {
lo = mid + 1;
} else {
hi = mid;
}
}
if lo < self.keyframe_indices.len() {
Some(&self.entries[self.keyframe_indices[lo]])
} else {
None
}
}
#[must_use]
pub fn find_nearest_keyframe(&self, target_pts: i64) -> Option<&SeekIndexEntry> {
let before = self.find_keyframe_before(target_pts);
let after = self.find_keyframe_after(target_pts);
match (before, after) {
(None, None) => None,
(Some(b), None) => Some(b),
(None, Some(a)) => Some(a),
(Some(b), Some(a)) => {
let dist_before = (target_pts - b.pts).unsigned_abs();
let dist_after = (a.pts - target_pts).unsigned_abs();
if dist_before <= dist_after {
Some(b)
} else {
Some(a)
}
}
}
}
#[must_use]
pub fn find_sample_at(&self, target_pts: i64) -> Option<&SeekIndexEntry> {
let result = self.entries.binary_search_by(|entry| {
if entry.pts > target_pts {
Ordering::Greater
} else if entry.end_pts() <= target_pts {
Ordering::Less
} else {
Ordering::Equal
}
});
match result {
Ok(idx) => Some(&self.entries[idx]),
Err(_) => {
let mut best = None;
for entry in &self.entries {
if entry.pts <= target_pts {
best = Some(entry);
} else {
break;
}
}
best
}
}
}
#[must_use]
pub fn plan_seek(&self, target_pts: i64, accuracy: SeekAccuracy) -> Option<SeekPlan> {
if self.entries.is_empty() || self.keyframe_indices.is_empty() {
return None;
}
match accuracy {
SeekAccuracy::Keyframe => {
let kf = self.find_keyframe_before(target_pts)?;
Some(SeekPlan {
keyframe_entry: *kf,
discard_count: 0,
target_entry: *kf,
file_offset: kf.file_offset,
requested_pts: target_pts,
is_exact: kf.pts == target_pts,
})
}
SeekAccuracy::SampleAccurate => self.plan_sample_accurate_seek(target_pts),
SeekAccuracy::WithinTolerance(tolerance) => {
if let Some(plan) = self.plan_sample_accurate_seek(target_pts) {
let distance = (plan.target_entry.pts - target_pts).unsigned_abs();
if distance <= tolerance {
return Some(plan);
}
}
let kf = self.find_nearest_keyframe(target_pts)?;
let distance = (kf.pts - target_pts).unsigned_abs();
if distance <= tolerance {
Some(SeekPlan {
keyframe_entry: *kf,
discard_count: 0,
target_entry: *kf,
file_offset: kf.file_offset,
requested_pts: target_pts,
is_exact: kf.pts == target_pts,
})
} else {
None
}
}
}
}
fn plan_sample_accurate_seek(&self, target_pts: i64) -> Option<SeekPlan> {
let kf = self.find_keyframe_before(target_pts)?;
let kf_copy = *kf;
let target_sample = self.find_sample_at(target_pts);
let target = match target_sample {
Some(s) => *s,
None => {
*self.entries.last()?
}
};
let mut discard_count: u32 = 0;
for entry in &self.entries {
if entry.dts > kf_copy.dts && entry.dts < target.dts {
discard_count += 1;
}
}
Some(SeekPlan {
keyframe_entry: kf_copy,
discard_count,
target_entry: target,
file_offset: kf_copy.file_offset,
requested_pts: target_pts,
is_exact: target.pts <= target_pts && target_pts < target.end_pts(),
})
}
#[must_use]
pub fn duration_ticks(&self) -> i64 {
self.entries.last().map_or(0, |e| e.pts + e.duration as i64)
}
#[must_use]
pub fn duration_seconds(&self) -> f64 {
self.ticks_to_seconds(self.duration_ticks())
}
#[must_use]
pub fn average_keyframe_interval(&self) -> Option<f64> {
if self.keyframe_indices.len() < 2 {
return None;
}
let mut total_interval: i64 = 0;
for i in 1..self.keyframe_indices.len() {
let prev = &self.entries[self.keyframe_indices[i - 1]];
let curr = &self.entries[self.keyframe_indices[i]];
total_interval += curr.pts - prev.pts;
}
#[allow(clippy::cast_precision_loss)]
let avg = total_interval as f64 / (self.keyframe_indices.len() - 1) as f64;
Some(avg)
}
}
#[derive(Debug, Clone)]
pub struct TrackIndex {
pub seek_index: SeekIndex,
pub codec_delay_samples: u32,
}
impl TrackIndex {
#[must_use]
pub fn new(seek_index: SeekIndex) -> Self {
Self {
seek_index,
codec_delay_samples: 0,
}
}
#[must_use]
pub fn with_codec_delay(seek_index: SeekIndex, codec_delay_samples: u32) -> Self {
Self {
seek_index,
codec_delay_samples,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SeekResult {
pub keyframe_pts: u64,
pub sample_offset: u64,
pub preroll_samples: u32,
}
pub struct SampleAccurateSeeker {
pub track: TrackIndex,
}
impl SampleAccurateSeeker {
#[must_use]
pub fn new(track: TrackIndex) -> Self {
Self { track }
}
#[must_use]
pub fn seek_to_sample(&self, target_pts: u64, track: &TrackIndex) -> Option<SeekResult> {
let target_i64 = i64::try_from(target_pts).unwrap_or(i64::MAX);
let plan = track
.seek_index
.plan_seek(target_i64, SeekAccuracy::SampleAccurate)?;
let keyframe_pts = u64::try_from(plan.keyframe_entry.pts.max(0)).unwrap_or(0);
let sample_offset = plan.keyframe_entry.file_offset;
let preroll_samples = plan.discard_count.saturating_add(track.codec_delay_samples);
Some(SeekResult {
keyframe_pts,
sample_offset,
preroll_samples,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SampleIndexEntry {
pub pts: i64,
pub byte_offset: u64,
pub is_sync: bool,
}
impl SampleIndexEntry {
#[must_use]
pub const fn keyframe(pts: i64, byte_offset: u64) -> Self {
Self {
pts,
byte_offset,
is_sync: true,
}
}
#[must_use]
pub const fn delta(pts: i64, byte_offset: u64) -> Self {
Self {
pts,
byte_offset,
is_sync: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PtsSeekResult {
pub found_pts: i64,
pub byte_offset: u64,
pub sample_idx: usize,
}
#[derive(Debug, thiserror::Error)]
pub enum MultiTrackSeekerError {
#[error("no index for track {0}")]
NoIndex(u32),
#[error("empty index for track {0}")]
EmptyIndex(u32),
#[error("pts {0} is before the first sample in track {1}")]
BeforeFirstSample(i64, u32),
}
pub struct MultiTrackSeeker {
indices: HashMap<u32, Vec<SampleIndexEntry>>,
}
impl MultiTrackSeeker {
#[must_use]
pub fn new() -> Self {
Self {
indices: HashMap::new(),
}
}
pub fn build_index(
&mut self,
track_id: u32,
samples: &[SampleIndexEntry],
) -> Result<(), MultiTrackSeekerError> {
let mut sorted = samples.to_vec();
sorted.sort_unstable_by_key(|e| e.pts);
self.indices.insert(track_id, sorted);
Ok(())
}
pub fn seek_to_pts(
&self,
track_id: u32,
target_pts: i64,
) -> Result<PtsSeekResult, MultiTrackSeekerError> {
let entries = self
.indices
.get(&track_id)
.ok_or(MultiTrackSeekerError::NoIndex(track_id))?;
if entries.is_empty() {
return Err(MultiTrackSeekerError::EmptyIndex(track_id));
}
let insertion = entries.partition_point(|e| e.pts <= target_pts);
if insertion == 0 {
return Err(MultiTrackSeekerError::BeforeFirstSample(
target_pts, track_id,
));
}
let sample_idx = insertion - 1;
let entry = &entries[sample_idx];
Ok(PtsSeekResult {
found_pts: entry.pts,
byte_offset: entry.byte_offset,
sample_idx,
})
}
#[must_use]
pub fn indexed_track_count(&self) -> usize {
self.indices.len()
}
#[must_use]
pub fn sample_count(&self, track_id: u32) -> Option<usize> {
self.indices.get(&track_id).map(Vec::len)
}
pub fn clear_index(&mut self, track_id: u32) {
self.indices.remove(&track_id);
}
#[must_use]
pub fn entries(&self, track_id: u32) -> Option<&[SampleIndexEntry]> {
self.indices.get(&track_id).map(Vec::as_slice)
}
}
impl Default for MultiTrackSeeker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_seek_flags() {
let flags = SeekFlags::BACKWARD | SeekFlags::KEYFRAME;
assert!(flags.contains(SeekFlags::BACKWARD));
assert!(flags.contains(SeekFlags::KEYFRAME));
assert!(!flags.contains(SeekFlags::ANY));
}
#[test]
fn test_seek_target_time() {
let target = SeekTarget::time(10.5);
assert_eq!(target.position, 10.5);
assert!(target.is_keyframe());
assert!(!target.is_byte());
assert_eq!(target.stream_index, None);
}
#[test]
fn test_seek_target_byte() {
let target = SeekTarget::byte(1024);
assert_eq!(target.position, 1024.0);
assert!(target.is_byte());
assert_eq!(target.stream_index, None);
}
#[test]
fn test_seek_target_sample_accurate() {
let target = SeekTarget::sample_accurate(5.0);
assert!(target.is_frame_accurate());
assert!(target.is_backward());
assert!(!target.is_keyframe());
}
#[test]
fn test_seek_target_with_stream() {
let target = SeekTarget::time(5.0).with_stream(1);
assert_eq!(target.stream_index, Some(1));
assert_eq!(target.position, 5.0);
}
#[test]
fn test_seek_target_with_flags() {
let target = SeekTarget::time(3.0)
.with_flags(SeekFlags::BACKWARD)
.add_flags(SeekFlags::ANY);
assert!(target.is_backward());
assert!(target.is_any());
}
#[test]
fn test_seek_target_predicates() {
let target =
SeekTarget::time(1.0).add_flags(SeekFlags::BACKWARD | SeekFlags::FRAME_ACCURATE);
assert!(target.is_backward());
assert!(!target.is_any());
assert!(target.is_keyframe());
assert!(!target.is_byte());
assert!(target.is_frame_accurate());
}
#[test]
fn test_entry_keyframe() {
let e = SeekIndexEntry::keyframe(0, 0, 100, 500, 3000, 0);
assert!(e.is_keyframe);
assert_eq!(e.pts, 0);
assert_eq!(e.file_offset, 100);
assert_eq!(e.end_pts(), 3000);
}
#[test]
fn test_entry_non_keyframe() {
let e = SeekIndexEntry::non_keyframe(3000, 3000, 600, 200, 3000, 1);
assert!(!e.is_keyframe);
assert_eq!(e.sample_number, 1);
assert_eq!(e.end_pts(), 6000);
}
fn build_test_index() -> SeekIndex {
let mut index = SeekIndex::new(90000);
for i in 0u32..20 {
let pts = i64::from(i) * 3000;
let is_kf = i % 5 == 0;
let offset = u64::from(i) * 500 + 1000; if is_kf {
index.add_entry(SeekIndexEntry::keyframe(pts, pts, offset, 500, 3000, i));
} else {
index.add_entry(SeekIndexEntry::non_keyframe(pts, pts, offset, 200, 3000, i));
}
}
index
}
#[test]
fn test_index_new() {
let index = SeekIndex::new(48000);
assert_eq!(index.timescale(), 48000);
assert!(index.is_empty());
assert_eq!(index.len(), 0);
assert_eq!(index.keyframe_count(), 0);
}
#[test]
fn test_index_add_entries() {
let index = build_test_index();
assert_eq!(index.len(), 20);
assert_eq!(index.keyframe_count(), 4); }
#[test]
fn test_seconds_to_ticks() {
let index = SeekIndex::new(90000);
assert_eq!(index.seconds_to_ticks(1.0), 90000);
assert_eq!(index.seconds_to_ticks(0.5), 45000);
}
#[test]
fn test_ticks_to_seconds() {
let index = SeekIndex::new(90000);
let s = index.ticks_to_seconds(90000);
assert!((s - 1.0).abs() < 1e-10);
}
#[test]
fn test_ticks_to_seconds_zero_timescale() {
let index = SeekIndex::new(0);
assert_eq!(index.ticks_to_seconds(12345), 0.0);
}
#[test]
fn test_find_keyframe_before_exact() {
let index = build_test_index();
let kf = index.find_keyframe_before(15000).expect("should find");
assert_eq!(kf.pts, 15000);
assert!(kf.is_keyframe);
}
#[test]
fn test_find_keyframe_before_between() {
let index = build_test_index();
let kf = index.find_keyframe_before(20000).expect("should find");
assert_eq!(kf.pts, 15000);
}
#[test]
fn test_find_keyframe_before_start() {
let index = build_test_index();
let kf = index.find_keyframe_before(0).expect("should find");
assert_eq!(kf.pts, 0);
}
#[test]
fn test_find_keyframe_before_none() {
let index = build_test_index();
let kf = index.find_keyframe_before(-1);
assert!(kf.is_none());
}
#[test]
fn test_find_keyframe_after() {
let index = build_test_index();
let kf = index.find_keyframe_after(20000).expect("should find");
assert_eq!(kf.pts, 30000);
}
#[test]
fn test_find_keyframe_after_exact() {
let index = build_test_index();
let kf = index.find_keyframe_after(15000).expect("should find");
assert_eq!(kf.pts, 15000);
}
#[test]
fn test_find_keyframe_after_past_end() {
let index = build_test_index();
let kf = index.find_keyframe_after(99999);
assert!(kf.is_none());
}
#[test]
fn test_find_nearest_keyframe_closer_before() {
let index = build_test_index();
let kf = index.find_nearest_keyframe(16000).expect("should find");
assert_eq!(kf.pts, 15000);
}
#[test]
fn test_find_nearest_keyframe_closer_after() {
let index = build_test_index();
let kf = index.find_nearest_keyframe(28000).expect("should find");
assert_eq!(kf.pts, 30000);
}
#[test]
fn test_find_nearest_keyframe_equidistant() {
let index = build_test_index();
let kf = index.find_nearest_keyframe(22500).expect("should find");
assert_eq!(kf.pts, 15000);
}
#[test]
fn test_find_sample_at_exact_start() {
let index = build_test_index();
let sample = index.find_sample_at(3000).expect("should find");
assert_eq!(sample.pts, 3000);
}
#[test]
fn test_find_sample_at_mid_frame() {
let index = build_test_index();
let sample = index.find_sample_at(4000).expect("should find");
assert_eq!(sample.pts, 3000);
}
#[test]
fn test_find_sample_at_last() {
let index = build_test_index();
let sample = index.find_sample_at(58000).expect("should find");
assert_eq!(sample.pts, 57000);
}
#[test]
fn test_plan_seek_keyframe() {
let index = build_test_index();
let plan = index
.plan_seek(20000, SeekAccuracy::Keyframe)
.expect("should plan");
assert_eq!(plan.keyframe_entry.pts, 15000);
assert_eq!(plan.discard_count, 0);
assert_eq!(plan.target_entry.pts, 15000);
}
#[test]
fn test_plan_seek_sample_accurate() {
let index = build_test_index();
let plan = index
.plan_seek(21000, SeekAccuracy::SampleAccurate)
.expect("should plan");
assert_eq!(plan.keyframe_entry.pts, 15000);
assert_eq!(plan.target_entry.pts, 21000);
assert_eq!(plan.discard_count, 1); assert!(plan.is_exact);
}
#[test]
fn test_plan_seek_sample_accurate_on_keyframe() {
let index = build_test_index();
let plan = index
.plan_seek(15000, SeekAccuracy::SampleAccurate)
.expect("should plan");
assert_eq!(plan.keyframe_entry.pts, 15000);
assert_eq!(plan.discard_count, 0);
assert!(plan.is_exact);
}
#[test]
fn test_plan_seek_sample_accurate_first_frame() {
let index = build_test_index();
let plan = index
.plan_seek(0, SeekAccuracy::SampleAccurate)
.expect("should plan");
assert_eq!(plan.keyframe_entry.pts, 0);
assert_eq!(plan.target_entry.pts, 0);
assert_eq!(plan.discard_count, 0);
}
#[test]
fn test_plan_seek_within_tolerance_exact() {
let index = build_test_index();
let plan = index
.plan_seek(16000, SeekAccuracy::WithinTolerance(5000))
.expect("should plan");
assert_eq!(plan.target_entry.pts, 15000);
}
#[test]
fn test_plan_seek_within_tolerance_out_of_range() {
let index = build_test_index();
let plan = index.plan_seek(20000, SeekAccuracy::WithinTolerance(1));
assert!(plan.is_none());
}
#[test]
fn test_plan_seek_empty_index() {
let index = SeekIndex::new(90000);
let plan = index.plan_seek(0, SeekAccuracy::Keyframe);
assert!(plan.is_none());
}
#[test]
fn test_duration_ticks() {
let index = build_test_index();
assert_eq!(index.duration_ticks(), 60000);
}
#[test]
fn test_duration_seconds() {
let index = build_test_index();
let dur = index.duration_seconds();
assert!((dur - 60000.0 / 90000.0).abs() < 1e-6);
}
#[test]
fn test_average_keyframe_interval() {
let index = build_test_index();
let avg = index.average_keyframe_interval().expect("should calculate");
assert!((avg - 15000.0).abs() < 1e-6);
}
#[test]
fn test_average_keyframe_interval_single() {
let mut index = SeekIndex::new(90000);
index.add_entry(SeekIndexEntry::keyframe(0, 0, 0, 100, 3000, 0));
assert!(index.average_keyframe_interval().is_none());
}
#[test]
fn test_sort_reorders_entries() {
let mut index = SeekIndex::new(90000);
index.add_entry(SeekIndexEntry::non_keyframe(6000, 6000, 300, 100, 3000, 2));
index.add_entry(SeekIndexEntry::keyframe(0, 0, 100, 100, 3000, 0));
index.add_entry(SeekIndexEntry::non_keyframe(3000, 3000, 200, 100, 3000, 1));
index.sort();
assert_eq!(index.entries()[0].pts, 0);
assert_eq!(index.entries()[1].pts, 3000);
assert_eq!(index.entries()[2].pts, 6000);
assert_eq!(index.keyframe_count(), 1);
}
#[test]
fn test_find_keyframe_empty() {
let index = SeekIndex::new(90000);
assert!(index.find_keyframe_before(0).is_none());
assert!(index.find_keyframe_after(0).is_none());
assert!(index.find_nearest_keyframe(0).is_none());
}
#[test]
fn test_single_keyframe_index() {
let mut index = SeekIndex::new(90000);
index.add_entry(SeekIndexEntry::keyframe(0, 0, 0, 100, 90000, 0));
let kf = index.find_keyframe_before(45000).expect("should find");
assert_eq!(kf.pts, 0);
let plan = index
.plan_seek(45000, SeekAccuracy::SampleAccurate)
.expect("should plan");
assert_eq!(plan.keyframe_entry.pts, 0);
}
#[test]
fn test_all_keyframes_index() {
let mut index = SeekIndex::new(48000);
for i in 0u32..100 {
let pts = i64::from(i) * 960;
index.add_entry(SeekIndexEntry::keyframe(
pts,
pts,
u64::from(i) * 100,
100,
960,
i,
));
}
assert_eq!(index.keyframe_count(), 100);
let plan = index
.plan_seek(48000, SeekAccuracy::SampleAccurate)
.expect("should plan");
assert_eq!(plan.keyframe_entry.pts, 48000);
assert_eq!(plan.discard_count, 0);
}
#[test]
fn test_with_capacity() {
let index = SeekIndex::with_capacity(90000, 1000);
assert_eq!(index.timescale(), 90000);
assert!(index.is_empty());
}
fn build_seeker_index() -> SeekIndex {
let mut index = SeekIndex::new(90000);
for i in 0u32..20 {
let pts = i64::from(i) * 3000;
let is_kf = i % 5 == 0;
let offset = u64::from(i) * 500 + 1000;
if is_kf {
index.add_entry(SeekIndexEntry::keyframe(pts, pts, offset, 500, 3000, i));
} else {
index.add_entry(SeekIndexEntry::non_keyframe(pts, pts, offset, 200, 3000, i));
}
}
index
}
#[test]
fn test_sample_accurate_seeker_on_keyframe() {
let track = TrackIndex::new(build_seeker_index());
let seeker = SampleAccurateSeeker::new(TrackIndex::new(build_seeker_index()));
let result = seeker.seek_to_sample(15000, &track).expect("should find");
assert_eq!(result.keyframe_pts, 15000);
assert_eq!(result.preroll_samples, 0);
assert_eq!(result.sample_offset, 1000 + 5 * 500); }
#[test]
fn test_sample_accurate_seeker_between_keyframes() {
let track = TrackIndex::new(build_seeker_index());
let seeker = SampleAccurateSeeker::new(TrackIndex::new(build_seeker_index()));
let result = seeker.seek_to_sample(21000, &track).expect("should find");
assert_eq!(result.keyframe_pts, 15000);
assert_eq!(result.preroll_samples, 1);
}
#[test]
fn test_sample_accurate_seeker_codec_delay_added() {
let track = TrackIndex::with_codec_delay(build_seeker_index(), 512);
let seeker = SampleAccurateSeeker::new(TrackIndex::new(build_seeker_index()));
let result = seeker.seek_to_sample(0, &track).expect("should find");
assert_eq!(result.keyframe_pts, 0);
assert_eq!(result.preroll_samples, 512);
}
#[test]
fn test_sample_accurate_seeker_empty_index() {
let track = TrackIndex::new(SeekIndex::new(90000));
let seeker = SampleAccurateSeeker::new(TrackIndex::new(SeekIndex::new(90000)));
let result = seeker.seek_to_sample(0, &track);
assert!(result.is_none());
}
#[test]
fn test_track_index_default_codec_delay() {
let idx = SeekIndex::new(90000);
let track = TrackIndex::new(idx);
assert_eq!(track.codec_delay_samples, 0);
}
#[test]
fn test_seek_result_fields() {
let track = TrackIndex::new(build_seeker_index());
let seeker = SampleAccurateSeeker::new(TrackIndex::new(build_seeker_index()));
let result = seeker.seek_to_sample(0, &track).expect("should find");
assert_eq!(result.keyframe_pts, 0);
assert_eq!(result.sample_offset, 1000);
assert_eq!(result.preroll_samples, 0);
}
fn build_multi_track_samples() -> Vec<SampleIndexEntry> {
(0u32..20)
.map(|i| {
let pts = i64::from(i) * 3000;
let offset = 1000 + u64::from(i) * 500;
if i % 5 == 0 {
SampleIndexEntry::keyframe(pts, offset)
} else {
SampleIndexEntry::delta(pts, offset)
}
})
.collect()
}
#[test]
fn test_multi_track_seeker_new() {
let seeker = MultiTrackSeeker::new();
assert_eq!(seeker.indexed_track_count(), 0);
}
#[test]
fn test_multi_track_build_index() {
let mut seeker = MultiTrackSeeker::new();
let samples = build_multi_track_samples();
seeker.build_index(1, &samples).expect("build ok");
assert_eq!(seeker.indexed_track_count(), 1);
assert_eq!(seeker.sample_count(1), Some(20));
}
#[test]
fn test_multi_track_entries_sorted() {
let mut seeker = MultiTrackSeeker::new();
let samples = vec![
SampleIndexEntry::delta(9000, 3000),
SampleIndexEntry::keyframe(0, 1000),
SampleIndexEntry::delta(3000, 1500),
SampleIndexEntry::delta(6000, 2000),
];
seeker.build_index(1, &samples).expect("ok");
let entries = seeker.entries(1).expect("entries exist");
assert_eq!(entries[0].pts, 0);
assert_eq!(entries[1].pts, 3000);
assert_eq!(entries[2].pts, 6000);
assert_eq!(entries[3].pts, 9000);
}
#[test]
fn test_seek_to_pts_exact() {
let mut seeker = MultiTrackSeeker::new();
seeker
.build_index(1, &build_multi_track_samples())
.expect("ok");
let result = seeker.seek_to_pts(1, 15000).expect("seek ok");
assert_eq!(result.found_pts, 15000);
assert_eq!(result.byte_offset, 1000 + 5 * 500);
assert_eq!(result.sample_idx, 5);
}
#[test]
fn test_seek_to_pts_between_samples() {
let mut seeker = MultiTrackSeeker::new();
seeker
.build_index(1, &build_multi_track_samples())
.expect("ok");
let result = seeker.seek_to_pts(1, 16000).expect("seek ok");
assert_eq!(
result.found_pts, 15000,
"should return the preceding sample"
);
assert_eq!(result.sample_idx, 5);
}
#[test]
fn test_seek_to_pts_first_sample() {
let mut seeker = MultiTrackSeeker::new();
seeker
.build_index(1, &build_multi_track_samples())
.expect("ok");
let result = seeker.seek_to_pts(1, 0).expect("seek ok");
assert_eq!(result.found_pts, 0);
assert_eq!(result.sample_idx, 0);
}
#[test]
fn test_seek_to_pts_last_sample() {
let mut seeker = MultiTrackSeeker::new();
seeker
.build_index(1, &build_multi_track_samples())
.expect("ok");
let result = seeker.seek_to_pts(1, 99999).expect("seek ok");
assert_eq!(result.found_pts, 19 * 3000); assert_eq!(result.sample_idx, 19);
}
#[test]
fn test_seek_to_pts_before_first_sample() {
let mut seeker = MultiTrackSeeker::new();
seeker
.build_index(1, &build_multi_track_samples())
.expect("ok");
let err = seeker.seek_to_pts(1, -1);
assert!(matches!(
err,
Err(MultiTrackSeekerError::BeforeFirstSample(-1, 1))
));
}
#[test]
fn test_seek_to_pts_no_index() {
let seeker = MultiTrackSeeker::new();
let err = seeker.seek_to_pts(42, 0);
assert!(matches!(err, Err(MultiTrackSeekerError::NoIndex(42))));
}
#[test]
fn test_seek_to_pts_empty_index() {
let mut seeker = MultiTrackSeeker::new();
seeker.build_index(1, &[]).expect("ok");
let err = seeker.seek_to_pts(1, 0);
assert!(matches!(err, Err(MultiTrackSeekerError::EmptyIndex(1))));
}
#[test]
fn test_multi_track_multiple_tracks() {
let mut seeker = MultiTrackSeeker::new();
let video = build_multi_track_samples();
let audio: Vec<SampleIndexEntry> = (0u32..=50)
.map(|i| SampleIndexEntry::keyframe(i64::from(i) * 960, u64::from(i) * 100 + 500))
.collect();
seeker.build_index(1, &video).expect("video ok");
seeker.build_index(2, &audio).expect("audio ok");
assert_eq!(seeker.indexed_track_count(), 2);
let v_result = seeker.seek_to_pts(1, 15000).expect("video seek ok");
let a_result = seeker.seek_to_pts(2, 48000).expect("audio seek ok");
assert_eq!(v_result.found_pts, 15000);
assert_eq!(a_result.found_pts, 48000);
}
#[test]
fn test_clear_index() {
let mut seeker = MultiTrackSeeker::new();
seeker
.build_index(1, &build_multi_track_samples())
.expect("ok");
assert_eq!(seeker.indexed_track_count(), 1);
seeker.clear_index(1);
assert_eq!(seeker.indexed_track_count(), 0);
let err = seeker.seek_to_pts(1, 0);
assert!(matches!(err, Err(MultiTrackSeekerError::NoIndex(1))));
}
#[test]
fn test_build_index_replaces_existing() {
let mut seeker = MultiTrackSeeker::new();
let old: Vec<SampleIndexEntry> = vec![SampleIndexEntry::keyframe(0, 100)];
let new: Vec<SampleIndexEntry> = vec![
SampleIndexEntry::keyframe(0, 200),
SampleIndexEntry::keyframe(3000, 300),
];
seeker.build_index(1, &old).expect("ok");
seeker.build_index(1, &new).expect("replace ok");
assert_eq!(seeker.sample_count(1), Some(2));
let result = seeker.seek_to_pts(1, 0).expect("ok");
assert_eq!(result.byte_offset, 200);
}
#[test]
fn test_sample_index_entry_constructors() {
let kf = SampleIndexEntry::keyframe(1000, 9999);
assert!(kf.is_sync);
assert_eq!(kf.pts, 1000);
assert_eq!(kf.byte_offset, 9999);
let df = SampleIndexEntry::delta(2000, 8888);
assert!(!df.is_sync);
assert_eq!(df.pts, 2000);
}
#[test]
fn test_pts_seek_result_fields() {
let mut seeker = MultiTrackSeeker::new();
seeker
.build_index(1, &[SampleIndexEntry::keyframe(5000, 12345)])
.expect("ok");
let r = seeker.seek_to_pts(1, 5000).expect("ok");
assert_eq!(r.found_pts, 5000);
assert_eq!(r.byte_offset, 12345);
assert_eq!(r.sample_idx, 0);
}
}