Skip to main content

oximedia_container/
seek.rs

1//! Seeking infrastructure for demuxers.
2//!
3//! This module provides types and utilities for seeking in media containers,
4//! including keyframe-based seeking, sample-accurate seeking, and a seek index
5//! for fast random access.
6//!
7//! # Sample-Accurate Seeking
8//!
9//! Sample-accurate seeking goes beyond simple keyframe-based seeking by:
10//! 1. Finding the nearest keyframe before the target
11//! 2. Tracking samples between that keyframe and the target
12//! 3. Providing a `SeekPlan` that tells the decoder which samples to decode
13//!    and which to discard
14//!
15//! ```ignore
16//! let index = SeekIndex::new(90000); // 90kHz timescale
17//! // ... populate with sample entries ...
18//! let plan = index.plan_seek(target_pts, SeekAccuracy::SampleAccurate)?;
19//! // plan.decode_from_pts: start decoding here (keyframe)
20//! // plan.discard_count: number of frames to decode but discard
21//! // plan.target_pts: the actual target presentation time
22//! ```
23
24use bitflags::bitflags;
25use std::cmp::Ordering;
26use std::collections::HashMap;
27
28bitflags! {
29    /// Flags controlling seek behavior.
30    ///
31    /// These flags allow fine-grained control over how a seek operation
32    /// is performed and what position is targeted.
33    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
34    pub struct SeekFlags: u32 {
35        /// Seek backward (to position <= target).
36        ///
37        /// Without this flag, seeks go forward (to position >= target).
38        /// This is useful for finding the keyframe before a target position.
39        const BACKWARD = 0x0001;
40
41        /// Allow seeking to any frame, not just keyframes.
42        ///
43        /// By default, seeks target keyframes for clean decoding.
44        /// Setting this flag allows seeking to any position, which may
45        /// require decoding from the previous keyframe.
46        const ANY = 0x0002;
47
48        /// Seek to the nearest keyframe.
49        ///
50        /// This is the default behavior and ensures the seek position
51        /// can be decoded immediately without reference frames.
52        const KEYFRAME = 0x0004;
53
54        /// Seek by bytes rather than time.
55        ///
56        /// When set, the seek target is interpreted as a byte offset
57        /// in the file rather than a timestamp.
58        const BYTE = 0x0008;
59
60        /// Seek to exact position (frame-accurate).
61        ///
62        /// Attempts to seek to the exact target timestamp, which may
63        /// require additional parsing and decoding.
64        const FRAME_ACCURATE = 0x0010;
65    }
66}
67
68/// Target for a seek operation.
69///
70/// Specifies where to seek and which stream to use as reference.
71#[derive(Clone, Copy, Debug, PartialEq)]
72pub struct SeekTarget {
73    /// Target timestamp in seconds, or byte offset if `SeekFlags::BYTE` is set.
74    pub position: f64,
75
76    /// Stream index to use for seeking, or `None` for the default stream.
77    ///
78    /// The default stream is typically the first video stream, or the
79    /// first audio stream if there are no video streams.
80    pub stream_index: Option<usize>,
81
82    /// Seek flags controlling behavior.
83    pub flags: SeekFlags,
84}
85
86impl SeekTarget {
87    /// Creates a new seek target to a timestamp in seconds.
88    ///
89    /// # Arguments
90    ///
91    /// * `position` - Target timestamp in seconds
92    #[must_use]
93    pub const fn time(position: f64) -> Self {
94        Self {
95            position,
96            stream_index: None,
97            flags: SeekFlags::KEYFRAME,
98        }
99    }
100
101    /// Creates a new seek target to a byte offset.
102    ///
103    /// # Arguments
104    ///
105    /// * `offset` - Target byte offset in the file
106    #[must_use]
107    #[allow(clippy::cast_precision_loss)]
108    pub fn byte(offset: u64) -> Self {
109        Self {
110            position: offset as f64,
111            stream_index: None,
112            flags: SeekFlags::BYTE,
113        }
114    }
115
116    /// Creates a sample-accurate seek target.
117    ///
118    /// This will seek to the exact position, decoding from the
119    /// preceding keyframe and discarding intermediate frames.
120    #[must_use]
121    pub const fn sample_accurate(position: f64) -> Self {
122        Self {
123            position,
124            stream_index: None,
125            flags: SeekFlags::from_bits_truncate(
126                SeekFlags::FRAME_ACCURATE.bits() | SeekFlags::BACKWARD.bits(),
127            ),
128        }
129    }
130
131    /// Sets the stream index for this seek target.
132    #[must_use]
133    pub const fn with_stream(mut self, stream_index: usize) -> Self {
134        self.stream_index = Some(stream_index);
135        self
136    }
137
138    /// Sets the seek flags for this seek target.
139    #[must_use]
140    pub const fn with_flags(mut self, flags: SeekFlags) -> Self {
141        self.flags = flags;
142        self
143    }
144
145    /// Adds additional flags to this seek target.
146    #[must_use]
147    pub const fn add_flags(mut self, flags: SeekFlags) -> Self {
148        self.flags = SeekFlags::from_bits_truncate(self.flags.bits() | flags.bits());
149        self
150    }
151
152    /// Returns true if this is a backward seek.
153    #[must_use]
154    pub const fn is_backward(&self) -> bool {
155        self.flags.contains(SeekFlags::BACKWARD)
156    }
157
158    /// Returns true if this allows seeking to any frame.
159    #[must_use]
160    pub const fn is_any(&self) -> bool {
161        self.flags.contains(SeekFlags::ANY)
162    }
163
164    /// Returns true if this seeks to a keyframe.
165    #[must_use]
166    pub const fn is_keyframe(&self) -> bool {
167        self.flags.contains(SeekFlags::KEYFRAME)
168    }
169
170    /// Returns true if this is a byte-based seek.
171    #[must_use]
172    pub const fn is_byte(&self) -> bool {
173        self.flags.contains(SeekFlags::BYTE)
174    }
175
176    /// Returns true if this is a frame-accurate seek.
177    #[must_use]
178    pub const fn is_frame_accurate(&self) -> bool {
179        self.flags.contains(SeekFlags::FRAME_ACCURATE)
180    }
181}
182
183// ─── Seek Accuracy ──────────────────────────────────────────────────────────
184
185/// Desired accuracy level for seeking.
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub enum SeekAccuracy {
188    /// Seek to the nearest keyframe (fastest, least accurate).
189    Keyframe,
190    /// Seek to the exact sample/frame (requires decoding from prior keyframe).
191    SampleAccurate,
192    /// Seek to within a specified tolerance in timescale ticks.
193    WithinTolerance(u64),
194}
195
196// ─── Seek Index Entry ───────────────────────────────────────────────────────
197
198/// An entry in the seek index representing one sample/frame.
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub struct SeekIndexEntry {
201    /// Presentation timestamp in timescale ticks.
202    pub pts: i64,
203    /// Decode timestamp in timescale ticks.
204    pub dts: i64,
205    /// Byte offset in the container file.
206    pub file_offset: u64,
207    /// Sample size in bytes.
208    pub size: u32,
209    /// Sample duration in timescale ticks.
210    pub duration: u32,
211    /// Whether this is a keyframe (sync sample).
212    pub is_keyframe: bool,
213    /// Sample number (0-based).
214    pub sample_number: u32,
215}
216
217impl SeekIndexEntry {
218    /// Creates a new keyframe entry.
219    #[must_use]
220    pub const fn keyframe(
221        pts: i64,
222        dts: i64,
223        file_offset: u64,
224        size: u32,
225        duration: u32,
226        sample_number: u32,
227    ) -> Self {
228        Self {
229            pts,
230            dts,
231            file_offset,
232            size,
233            duration,
234            is_keyframe: true,
235            sample_number,
236        }
237    }
238
239    /// Creates a new non-keyframe entry.
240    #[must_use]
241    pub const fn non_keyframe(
242        pts: i64,
243        dts: i64,
244        file_offset: u64,
245        size: u32,
246        duration: u32,
247        sample_number: u32,
248    ) -> Self {
249        Self {
250            pts,
251            dts,
252            file_offset,
253            size,
254            duration,
255            is_keyframe: false,
256            sample_number,
257        }
258    }
259
260    /// Returns the end PTS (pts + duration).
261    #[must_use]
262    pub const fn end_pts(&self) -> i64 {
263        self.pts + self.duration as i64
264    }
265}
266
267// ─── Seek Plan ──────────────────────────────────────────────────────────────
268
269/// A plan for executing a sample-accurate seek.
270///
271/// Contains information about where to start decoding (keyframe),
272/// how many samples to decode-and-discard, and the final target sample.
273#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct SeekPlan {
275    /// The keyframe entry to seek to in the container (start decoding here).
276    pub keyframe_entry: SeekIndexEntry,
277    /// Number of samples between the keyframe and the target to decode
278    /// but discard (not presented).
279    pub discard_count: u32,
280    /// The target sample entry.
281    pub target_entry: SeekIndexEntry,
282    /// File offset to seek to.
283    pub file_offset: u64,
284    /// The target PTS that was requested.
285    pub requested_pts: i64,
286    /// Whether the seek is exact (target PTS matches a sample boundary).
287    pub is_exact: bool,
288}
289
290// ─── Seek Index ─────────────────────────────────────────────────────────────
291
292/// Index of sample positions for fast seeking.
293///
294/// Maintains a sorted list of sample entries that enables both keyframe-based
295/// and sample-accurate seeking. Entries are sorted by DTS for efficient
296/// binary search.
297#[derive(Debug, Clone)]
298pub struct SeekIndex {
299    /// Timescale (ticks per second) for interpreting timestamps.
300    timescale: u32,
301    /// All sample entries sorted by DTS.
302    entries: Vec<SeekIndexEntry>,
303    /// Indices of keyframe entries within `entries` (for fast keyframe lookup).
304    keyframe_indices: Vec<usize>,
305}
306
307impl SeekIndex {
308    /// Creates a new empty seek index.
309    #[must_use]
310    pub fn new(timescale: u32) -> Self {
311        Self {
312            timescale,
313            entries: Vec::new(),
314            keyframe_indices: Vec::new(),
315        }
316    }
317
318    /// Creates a seek index with pre-allocated capacity.
319    #[must_use]
320    pub fn with_capacity(timescale: u32, capacity: usize) -> Self {
321        Self {
322            timescale,
323            entries: Vec::with_capacity(capacity),
324            keyframe_indices: Vec::new(),
325        }
326    }
327
328    /// Returns the timescale.
329    #[must_use]
330    pub const fn timescale(&self) -> u32 {
331        self.timescale
332    }
333
334    /// Returns the number of entries in the index.
335    #[must_use]
336    pub fn len(&self) -> usize {
337        self.entries.len()
338    }
339
340    /// Returns true if the index is empty.
341    #[must_use]
342    pub fn is_empty(&self) -> bool {
343        self.entries.is_empty()
344    }
345
346    /// Returns the number of keyframes in the index.
347    #[must_use]
348    pub fn keyframe_count(&self) -> usize {
349        self.keyframe_indices.len()
350    }
351
352    /// Returns all entries.
353    #[must_use]
354    pub fn entries(&self) -> &[SeekIndexEntry] {
355        &self.entries
356    }
357
358    /// Adds a sample entry to the index.
359    ///
360    /// Entries should be added in DTS order for optimal performance.
361    /// If entries are added out of order, call [`sort`](SeekIndex::sort)
362    /// before seeking.
363    pub fn add_entry(&mut self, entry: SeekIndexEntry) {
364        let idx = self.entries.len();
365        if entry.is_keyframe {
366            self.keyframe_indices.push(idx);
367        }
368        self.entries.push(entry);
369    }
370
371    /// Sorts entries by DTS and rebuilds the keyframe index.
372    pub fn sort(&mut self) {
373        self.entries.sort_by_key(|e| e.dts);
374        self.keyframe_indices.clear();
375        for (i, entry) in self.entries.iter().enumerate() {
376            if entry.is_keyframe {
377                self.keyframe_indices.push(i);
378            }
379        }
380    }
381
382    /// Finalizes the index after all entries have been added.
383    ///
384    /// Equivalent to [`SeekIndex::sort`]: sorts all entries by DTS and
385    /// rebuilds the keyframe lookup table.  Call this once after all calls
386    /// to [`SeekIndex::add_entry`] are complete.
387    pub fn finalize(&mut self) {
388        self.sort();
389    }
390
391    /// Converts a time in seconds to ticks in this index's timescale.
392    #[must_use]
393    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
394    pub fn seconds_to_ticks(&self, seconds: f64) -> i64 {
395        (seconds * f64::from(self.timescale)) as i64
396    }
397
398    /// Converts ticks to seconds.
399    #[must_use]
400    #[allow(clippy::cast_precision_loss)]
401    pub fn ticks_to_seconds(&self, ticks: i64) -> f64 {
402        if self.timescale == 0 {
403            return 0.0;
404        }
405        ticks as f64 / f64::from(self.timescale)
406    }
407
408    /// Finds the nearest keyframe at or before the given PTS.
409    ///
410    /// Returns `None` if the index is empty or has no keyframes.
411    #[must_use]
412    pub fn find_keyframe_before(&self, target_pts: i64) -> Option<&SeekIndexEntry> {
413        if self.keyframe_indices.is_empty() {
414            return None;
415        }
416
417        // Binary search for the last keyframe with pts <= target_pts
418        let mut best: Option<usize> = None;
419        let mut lo = 0usize;
420        let mut hi = self.keyframe_indices.len();
421
422        while lo < hi {
423            let mid = lo + (hi - lo) / 2;
424            let kf_idx = self.keyframe_indices[mid];
425            let kf = &self.entries[kf_idx];
426
427            match kf.pts.cmp(&target_pts) {
428                Ordering::Less | Ordering::Equal => {
429                    best = Some(kf_idx);
430                    lo = mid + 1;
431                }
432                Ordering::Greater => {
433                    hi = mid;
434                }
435            }
436        }
437
438        best.map(|idx| &self.entries[idx])
439    }
440
441    /// Finds the nearest keyframe at or after the given PTS.
442    ///
443    /// Returns `None` if no keyframe exists at or after the target.
444    #[must_use]
445    pub fn find_keyframe_after(&self, target_pts: i64) -> Option<&SeekIndexEntry> {
446        if self.keyframe_indices.is_empty() {
447            return None;
448        }
449
450        let mut lo = 0usize;
451        let mut hi = self.keyframe_indices.len();
452
453        while lo < hi {
454            let mid = lo + (hi - lo) / 2;
455            let kf_idx = self.keyframe_indices[mid];
456            let kf = &self.entries[kf_idx];
457
458            if kf.pts < target_pts {
459                lo = mid + 1;
460            } else {
461                hi = mid;
462            }
463        }
464
465        if lo < self.keyframe_indices.len() {
466            Some(&self.entries[self.keyframe_indices[lo]])
467        } else {
468            None
469        }
470    }
471
472    /// Finds the nearest keyframe (before or after) to the given PTS.
473    ///
474    /// Returns whichever keyframe is closer in PTS distance.
475    #[must_use]
476    pub fn find_nearest_keyframe(&self, target_pts: i64) -> Option<&SeekIndexEntry> {
477        let before = self.find_keyframe_before(target_pts);
478        let after = self.find_keyframe_after(target_pts);
479
480        match (before, after) {
481            (None, None) => None,
482            (Some(b), None) => Some(b),
483            (None, Some(a)) => Some(a),
484            (Some(b), Some(a)) => {
485                let dist_before = (target_pts - b.pts).unsigned_abs();
486                let dist_after = (a.pts - target_pts).unsigned_abs();
487                if dist_before <= dist_after {
488                    Some(b)
489                } else {
490                    Some(a)
491                }
492            }
493        }
494    }
495
496    /// Finds the exact sample entry whose PTS range contains the target.
497    ///
498    /// Returns `None` if no sample covers the target PTS.
499    #[must_use]
500    pub fn find_sample_at(&self, target_pts: i64) -> Option<&SeekIndexEntry> {
501        // Binary search for the sample whose pts <= target_pts < pts+duration
502        let result = self.entries.binary_search_by(|entry| {
503            if entry.pts > target_pts {
504                Ordering::Greater
505            } else if entry.end_pts() <= target_pts {
506                Ordering::Less
507            } else {
508                Ordering::Equal
509            }
510        });
511
512        match result {
513            Ok(idx) => Some(&self.entries[idx]),
514            Err(_) => {
515                // Fallback: find the last entry with pts <= target_pts
516                let mut best = None;
517                for entry in &self.entries {
518                    if entry.pts <= target_pts {
519                        best = Some(entry);
520                    } else {
521                        break;
522                    }
523                }
524                best
525            }
526        }
527    }
528
529    /// Plans a sample-accurate seek to the given PTS.
530    ///
531    /// Returns a `SeekPlan` describing how to execute the seek:
532    /// - Which keyframe to seek to in the container
533    /// - How many samples to decode and discard
534    /// - The target sample
535    ///
536    /// # Errors
537    ///
538    /// Returns `None` if the index is empty or the target is out of range.
539    #[must_use]
540    pub fn plan_seek(&self, target_pts: i64, accuracy: SeekAccuracy) -> Option<SeekPlan> {
541        if self.entries.is_empty() || self.keyframe_indices.is_empty() {
542            return None;
543        }
544
545        match accuracy {
546            SeekAccuracy::Keyframe => {
547                let kf = self.find_keyframe_before(target_pts)?;
548                Some(SeekPlan {
549                    keyframe_entry: *kf,
550                    discard_count: 0,
551                    target_entry: *kf,
552                    file_offset: kf.file_offset,
553                    requested_pts: target_pts,
554                    is_exact: kf.pts == target_pts,
555                })
556            }
557            SeekAccuracy::SampleAccurate => self.plan_sample_accurate_seek(target_pts),
558            SeekAccuracy::WithinTolerance(tolerance) => {
559                // First try exact, fall back to keyframe if within tolerance
560                if let Some(plan) = self.plan_sample_accurate_seek(target_pts) {
561                    let distance = (plan.target_entry.pts - target_pts).unsigned_abs();
562                    if distance <= tolerance {
563                        return Some(plan);
564                    }
565                }
566                // Fall back to nearest keyframe
567                let kf = self.find_nearest_keyframe(target_pts)?;
568                let distance = (kf.pts - target_pts).unsigned_abs();
569                if distance <= tolerance {
570                    Some(SeekPlan {
571                        keyframe_entry: *kf,
572                        discard_count: 0,
573                        target_entry: *kf,
574                        file_offset: kf.file_offset,
575                        requested_pts: target_pts,
576                        is_exact: kf.pts == target_pts,
577                    })
578                } else {
579                    None
580                }
581            }
582        }
583    }
584
585    fn plan_sample_accurate_seek(&self, target_pts: i64) -> Option<SeekPlan> {
586        // 1. Find the preceding keyframe
587        let kf = self.find_keyframe_before(target_pts)?;
588        let kf_copy = *kf;
589
590        // 2. Find the target sample
591        let target_sample = self.find_sample_at(target_pts);
592        let target = match target_sample {
593            Some(s) => *s,
594            None => {
595                // Target is past all samples; use the last sample
596                *self.entries.last()?
597            }
598        };
599
600        // 3. Count samples between keyframe and target (exclusive of keyframe, inclusive of target)
601        let mut discard_count: u32 = 0;
602        for entry in &self.entries {
603            if entry.dts > kf_copy.dts && entry.dts < target.dts {
604                discard_count += 1;
605            }
606        }
607
608        Some(SeekPlan {
609            keyframe_entry: kf_copy,
610            discard_count,
611            target_entry: target,
612            file_offset: kf_copy.file_offset,
613            requested_pts: target_pts,
614            is_exact: target.pts <= target_pts && target_pts < target.end_pts(),
615        })
616    }
617
618    /// Returns the duration of the indexed content in timescale ticks.
619    #[must_use]
620    pub fn duration_ticks(&self) -> i64 {
621        self.entries.last().map_or(0, |e| e.pts + e.duration as i64)
622    }
623
624    /// Returns the duration of the indexed content in seconds.
625    #[must_use]
626    pub fn duration_seconds(&self) -> f64 {
627        self.ticks_to_seconds(self.duration_ticks())
628    }
629
630    /// Returns the average keyframe interval in timescale ticks.
631    #[must_use]
632    pub fn average_keyframe_interval(&self) -> Option<f64> {
633        if self.keyframe_indices.len() < 2 {
634            return None;
635        }
636
637        let mut total_interval: i64 = 0;
638        for i in 1..self.keyframe_indices.len() {
639            let prev = &self.entries[self.keyframe_indices[i - 1]];
640            let curr = &self.entries[self.keyframe_indices[i]];
641            total_interval += curr.pts - prev.pts;
642        }
643
644        #[allow(clippy::cast_precision_loss)]
645        let avg = total_interval as f64 / (self.keyframe_indices.len() - 1) as f64;
646        Some(avg)
647    }
648}
649
650/// Type alias for [`SeekIndex`] used in pre-roll seeking contexts.
651///
652/// `SampleIndex` is the same type as `SeekIndex`; the alias provides a
653/// more descriptive name when building per-stream sample tables for
654/// pre-roll seek planning.
655pub type SampleIndex = SeekIndex;
656
657// ─── TrackIndex ─────────────────────────────────────────────────────────────
658
659/// A lightweight index of keyframe positions within a single track.
660///
661/// Used by [`SampleAccurateSeeker`] to locate the nearest keyframe before a
662/// target PTS and compute the number of samples that must be decoded and
663/// discarded to reach a sample-accurate position.
664#[derive(Debug, Clone)]
665pub struct TrackIndex {
666    /// The underlying seek index (sorted by DTS).
667    pub seek_index: SeekIndex,
668    /// Codec delay in samples (e.g. 512 for Opus, 0 for most video codecs).
669    /// Added to the `preroll_samples` field of the returned [`SeekResult`].
670    pub codec_delay_samples: u32,
671}
672
673impl TrackIndex {
674    /// Creates a `TrackIndex` from an existing [`SeekIndex`].
675    #[must_use]
676    pub fn new(seek_index: SeekIndex) -> Self {
677        Self {
678            seek_index,
679            codec_delay_samples: 0,
680        }
681    }
682
683    /// Creates a `TrackIndex` with an explicit codec delay.
684    #[must_use]
685    pub fn with_codec_delay(seek_index: SeekIndex, codec_delay_samples: u32) -> Self {
686        Self {
687            seek_index,
688            codec_delay_samples,
689        }
690    }
691}
692
693// ─── SeekResult ─────────────────────────────────────────────────────────────
694
695/// The result of a sample-accurate seek operation.
696///
697/// Returned by [`SampleAccurateSeeker::seek_to_sample`] when a suitable
698/// keyframe is found.
699#[derive(Debug, Clone, PartialEq, Eq)]
700pub struct SeekResult {
701    /// The PTS of the keyframe that the decoder must start from.
702    ///
703    /// The container should be seeked to the file offset corresponding to
704    /// this keyframe before decoding begins.
705    pub keyframe_pts: u64,
706    /// Byte offset of the keyframe in the container file.
707    pub sample_offset: u64,
708    /// Number of samples to decode and discard between `keyframe_pts` and the
709    /// target PTS, plus any codec delay.
710    ///
711    /// A value of 0 means the seek landed exactly on a keyframe boundary.
712    pub preroll_samples: u32,
713}
714
715/// A shared decode-and-skip cursor for sample-accurate seek planning.
716///
717/// Demuxers can return this when the caller must seek to `byte_offset`,
718/// begin decoding at `sample_index`, and discard `skip_samples` decoded
719/// samples before presenting the first target sample.
720#[derive(Debug, Clone, Copy, PartialEq, Eq)]
721pub struct DecodeSkipCursor {
722    /// Byte offset of the keyframe/sample where decoding should begin.
723    pub byte_offset: u64,
724    /// 0-based sample index where decoding should begin.
725    pub sample_index: usize,
726    /// Number of decoded samples to discard before presentation.
727    pub skip_samples: u32,
728    /// Requested target presentation timestamp in track timescale units.
729    pub target_pts: i64,
730}
731
732// ─── SampleAccurateSeeker ───────────────────────────────────────────────────
733
734/// Performs sample-accurate seeking on a single media track.
735///
736/// Wraps a [`TrackIndex`] and provides a high-level `seek_to_sample` method
737/// that finds the nearest keyframe at or before a target PTS, returning the
738/// exact byte offset and the number of samples that must be decoded and
739/// discarded to reach the target position.
740///
741/// # Example
742///
743/// ```ignore
744/// use oximedia_container::seek::{SeekIndex, SeekIndexEntry, TrackIndex, SampleAccurateSeeker};
745///
746/// let mut index = SeekIndex::new(90000);
747/// // … populate index …
748/// let track = TrackIndex::new(index);
749/// let seeker = SampleAccurateSeeker::with_track(track);
750///
751/// let result = seeker.seek_to_sample(45000, &seeker.track).expect("seek should succeed");
752/// println!("Seek to file offset {}", result.sample_offset);
753/// println!("Discard {} samples after decoding", result.preroll_samples);
754/// ```
755pub struct SampleAccurateSeeker {
756    /// The primary track index used for single-track seek operations.
757    ///
758    /// This field is set when constructed with [`SampleAccurateSeeker::with_track`].
759    /// It holds a default empty `TrackIndex` when constructed with
760    /// [`SampleAccurateSeeker::new`] (multi-stream mode).
761    pub track: TrackIndex,
762    /// Per-stream sample indices for multi-stream pre-roll seeking.
763    ///
764    /// Keys are stream IDs (typically matching `StreamInfo.index`).
765    streams: HashMap<u32, SampleIndex>,
766}
767
768impl SampleAccurateSeeker {
769    /// Creates a new multi-stream `SampleAccurateSeeker` with no pre-loaded
770    /// streams.
771    ///
772    /// Use [`SampleAccurateSeeker::add_stream`] to register stream indices,
773    /// then call [`SampleAccurateSeeker::plan_preroll_seek`] to plan seeks.
774    #[must_use]
775    pub fn new() -> Self {
776        let empty_index = SeekIndex::new(90_000);
777        let empty_track = TrackIndex::new(empty_index);
778        Self {
779            track: empty_track,
780            streams: HashMap::new(),
781        }
782    }
783
784    /// Creates a `SampleAccurateSeeker` from a single pre-built [`TrackIndex`].
785    ///
786    /// This is the single-track constructor.  Use `new()` instead for
787    /// multi-stream workflows.
788    #[must_use]
789    pub fn with_track(track: TrackIndex) -> Self {
790        Self {
791            track,
792            streams: HashMap::new(),
793        }
794    }
795
796    /// Registers a per-stream `SampleIndex` for multi-stream pre-roll seeking.
797    ///
798    /// The `stream_id` must match the stream identifier used when calling
799    /// [`SampleAccurateSeeker::plan_preroll_seek`].
800    pub fn add_stream(&mut self, stream_id: u32, index: SampleIndex) {
801        self.streams.insert(stream_id, index);
802    }
803
804    /// Plans a pre-roll seek for `stream_id` to `target_pts`.
805    ///
806    /// Returns a [`crate::preroll::PreRollSeekPlan`] describing the keyframe to
807    /// start decoding from and the chain of samples to decode (some discarded,
808    /// some presented) in order to reach `target_pts` sample-accurately.
809    ///
810    /// `max_preroll` optionally limits the maximum number of decode-and-discard
811    /// samples.  When the distance from keyframe to target exceeds this limit the
812    /// chain is truncated.
813    ///
814    /// Returns `None` if `stream_id` has not been registered or the index has
815    /// no keyframe at or before `target_pts`.
816    #[must_use]
817    pub fn plan_preroll_seek(
818        &self,
819        stream_id: u32,
820        target_pts: i64,
821        max_preroll: Option<u32>,
822    ) -> Option<crate::preroll::PreRollSeekPlan> {
823        use crate::preroll::{PreRollAction, PreRollSample, PreRollSeekPlan};
824
825        let index = self.streams.get(&stream_id)?;
826        let keyframe = index.find_keyframe_before(target_pts)?;
827
828        // Collect samples from the keyframe onwards.
829        // - Before target_pts: decode-and-discard (up to max_preroll).
830        // - At target_pts or after: present (stop after the first present sample
831        //   since the caller only needs to reach the target, not process all
832        //   future samples).
833        let all_from_kf: Vec<&SeekIndexEntry> = index
834            .entries()
835            .iter()
836            .filter(|e| e.pts >= keyframe.pts)
837            .collect();
838
839        // Separate discard candidates (before target) from present candidates.
840        let discard_candidates: Vec<&&SeekIndexEntry> =
841            all_from_kf.iter().filter(|e| e.pts < target_pts).collect();
842        let present_candidate: Option<&SeekIndexEntry> =
843            all_from_kf.iter().find(|e| e.pts >= target_pts).copied();
844
845        // Apply max_preroll cap: if capped, take the LAST N discard candidates
846        // (closest to target) so the decoder has the fewest samples to skip.
847        let capped_discards: Vec<&SeekIndexEntry> = if let Some(max) = max_preroll {
848            let max = max as usize;
849            if discard_candidates.len() > max {
850                discard_candidates[discard_candidates.len() - max..]
851                    .iter()
852                    .copied()
853                    .copied()
854                    .collect()
855            } else {
856                discard_candidates.iter().copied().copied().collect()
857            }
858        } else {
859            discard_candidates.iter().copied().copied().collect()
860        };
861
862        let mut samples: Vec<PreRollSample> = capped_discards
863            .iter()
864            .map(|e| PreRollSample {
865                entry: **e,
866                action: PreRollAction::Decode,
867            })
868            .collect();
869
870        let discard_count = samples.len() as u32;
871        let mut present_count: u32 = 0;
872
873        if let Some(entry) = present_candidate {
874            samples.push(PreRollSample {
875                entry: *entry,
876                action: PreRollAction::Present,
877            });
878            present_count = 1;
879        } else if discard_count == 0 {
880            // Nothing found — no useful plan.
881            return None;
882        } else {
883            // No explicit present sample found (target is beyond the index).
884            // Promote the last discard to present.
885            if let Some(last) = samples.last_mut() {
886                last.action = PreRollAction::Present;
887                present_count = 1;
888            }
889        }
890
891        let final_discard_count = samples
892            .iter()
893            .filter(|s| matches!(s.action, PreRollAction::Decode))
894            .count() as u32;
895
896        Some(PreRollSeekPlan {
897            keyframe: *keyframe,
898            target_pts,
899            samples,
900            discard_count: final_discard_count,
901            present_count,
902            file_offset: keyframe.file_offset,
903        })
904    }
905
906    /// Returns the number of samples that must be decoded and discarded
907    /// (pre-roll count) to achieve sample-accurate positioning at `target_pts`
908    /// in `stream_id`.
909    ///
910    /// Returns `None` if the stream is not registered or has no suitable
911    /// keyframe.
912    #[must_use]
913    pub fn preroll_count(&self, stream_id: u32, target_pts: i64) -> Option<u32> {
914        let plan = self.plan_preroll_seek(stream_id, target_pts, None)?;
915        Some(plan.discard_count)
916    }
917
918    /// Seeks to the sample-accurate position for `target_pts` within `track`.
919    ///
920    /// The algorithm:
921    /// 1. Finds the nearest keyframe at or before `target_pts` using binary
922    ///    search on the track's seek index.
923    /// 2. Counts every sample between the keyframe (exclusive) and the target
924    ///    sample (exclusive) — these must be decoded and discarded.
925    /// 3. Adds the track's `codec_delay_samples` to the discard count.
926    ///
927    /// # Returns
928    ///
929    /// `Some(SeekResult)` if a keyframe is found, or `None` if the index is
930    /// empty or no keyframe exists before `target_pts`.
931    ///
932    /// # Arguments
933    ///
934    /// * `target_pts` — desired presentation timestamp in the track's timescale.
935    /// * `track` — the [`TrackIndex`] to search (typically `&self.track`).
936    #[must_use]
937    pub fn seek_to_sample(&self, target_pts: u64, track: &TrackIndex) -> Option<SeekResult> {
938        let target_i64 = i64::try_from(target_pts).unwrap_or(i64::MAX);
939
940        let plan = track
941            .seek_index
942            .plan_seek(target_i64, SeekAccuracy::SampleAccurate)?;
943
944        let keyframe_pts = u64::try_from(plan.keyframe_entry.pts.max(0)).unwrap_or(0);
945        let sample_offset = plan.keyframe_entry.file_offset;
946        let preroll_samples = plan.discard_count.saturating_add(track.codec_delay_samples);
947
948        Some(SeekResult {
949            keyframe_pts,
950            sample_offset,
951            preroll_samples,
952        })
953    }
954}
955
956impl Default for SampleAccurateSeeker {
957    fn default() -> Self {
958        Self::new()
959    }
960}
961
962// ─── MultiTrackSeeker ────────────────────────────────────────────────────────
963
964/// A compact index entry describing a single sample within a track.
965///
966/// Used by [`MultiTrackSeeker`] to build a per-track PTS→byte-offset index.
967#[derive(Debug, Clone, Copy, PartialEq, Eq)]
968pub struct SampleIndexEntry {
969    /// Presentation timestamp in the track's timescale ticks.
970    pub pts: i64,
971    /// Byte offset of the sample within the container file.
972    pub byte_offset: u64,
973    /// Whether this sample is a sync (key) sample.
974    pub is_sync: bool,
975}
976
977impl SampleIndexEntry {
978    /// Creates a new keyframe [`SampleIndexEntry`].
979    #[must_use]
980    pub const fn keyframe(pts: i64, byte_offset: u64) -> Self {
981        Self {
982            pts,
983            byte_offset,
984            is_sync: true,
985        }
986    }
987
988    /// Creates a new non-keyframe [`SampleIndexEntry`].
989    #[must_use]
990    pub const fn delta(pts: i64, byte_offset: u64) -> Self {
991        Self {
992            pts,
993            byte_offset,
994            is_sync: false,
995        }
996    }
997}
998
999/// The result of a [`MultiTrackSeeker::seek_to_pts`] operation.
1000///
1001/// Contains the actual sample PTS found (which may differ from the requested
1002/// target if the target fell between samples), the byte offset of that sample
1003/// in the container file, and the 0-based index of the sample within the
1004/// track's index.
1005#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1006pub struct PtsSeekResult {
1007    /// Presentation timestamp of the sample that was found (in timescale ticks).
1008    pub found_pts: i64,
1009    /// Byte offset of the found sample in the container file.
1010    pub byte_offset: u64,
1011    /// 0-based index of the sample within the track's sorted index array.
1012    pub sample_idx: usize,
1013}
1014
1015/// Error type returned by [`MultiTrackSeeker`] operations.
1016#[derive(Debug, thiserror::Error)]
1017pub enum MultiTrackSeekerError {
1018    /// The requested track ID has not been indexed yet.
1019    #[error("no index for track {0}")]
1020    NoIndex(u32),
1021    /// The index for the given track is empty.
1022    #[error("empty index for track {0}")]
1023    EmptyIndex(u32),
1024    /// The requested PTS is before all samples in the track.
1025    #[error("pts {0} is before the first sample in track {1}")]
1026    BeforeFirstSample(i64, u32),
1027}
1028
1029/// Multi-track sample-accurate seeker with a per-track PTS→byte-offset index.
1030///
1031/// Unlike [`SampleAccurateSeeker`] (which wraps a single [`TrackIndex`]),
1032/// `MultiTrackSeeker` manages a separate sorted index for each track and
1033/// exposes an O(log n) [`seek_to_pts`](MultiTrackSeeker::seek_to_pts) lookup.
1034///
1035/// # Example
1036///
1037/// ```ignore
1038/// use oximedia_container::seek::{MultiTrackSeeker, SampleIndexEntry};
1039///
1040/// let mut seeker = MultiTrackSeeker::new();
1041///
1042/// let samples = vec![
1043///     SampleIndexEntry::keyframe(0,     1000),
1044///     SampleIndexEntry::delta(3000,  1200),
1045///     SampleIndexEntry::keyframe(6000,  1500),
1046/// ];
1047/// seeker.build_index(1, &samples).expect("index built");
1048///
1049/// let result = seeker.seek_to_pts(1, 4500).expect("seek ok");
1050/// println!("found_pts={} offset={} idx={}", result.found_pts, result.byte_offset, result.sample_idx);
1051/// ```
1052pub struct MultiTrackSeeker {
1053    /// Per-track index: track_id → sorted `Vec<SampleIndexEntry>`.
1054    indices: HashMap<u32, Vec<SampleIndexEntry>>,
1055}
1056
1057impl MultiTrackSeeker {
1058    /// Creates an empty [`MultiTrackSeeker`].
1059    #[must_use]
1060    pub fn new() -> Self {
1061        Self {
1062            indices: HashMap::new(),
1063        }
1064    }
1065
1066    /// Builds (or replaces) the index for `track_id` from the provided sample list.
1067    ///
1068    /// The entries are sorted by PTS so that [`seek_to_pts`](Self::seek_to_pts)
1069    /// can use binary search.
1070    ///
1071    /// # Errors
1072    ///
1073    /// This method currently always succeeds.  It is defined with `Result` for
1074    /// forward compatibility (e.g. if validation logic is added later).
1075    pub fn build_index(
1076        &mut self,
1077        track_id: u32,
1078        samples: &[SampleIndexEntry],
1079    ) -> Result<(), MultiTrackSeekerError> {
1080        let mut sorted = samples.to_vec();
1081        sorted.sort_unstable_by_key(|e| e.pts);
1082        self.indices.insert(track_id, sorted);
1083        Ok(())
1084    }
1085
1086    /// Seeks to the sample-accurate position for `target_pts` within `track_id`.
1087    ///
1088    /// Uses binary search (O(log n)) to locate the last sample whose PTS is ≤
1089    /// `target_pts`.  If the `target_pts` is exactly a sample boundary the
1090    /// result is exact; otherwise the sample immediately preceding the target
1091    /// is returned (the decoder must decode from that point).
1092    ///
1093    /// # Errors
1094    ///
1095    /// - [`MultiTrackSeekerError::NoIndex`] — the track has no index.
1096    /// - [`MultiTrackSeekerError::EmptyIndex`] — the index is empty.
1097    /// - [`MultiTrackSeekerError::BeforeFirstSample`] — `target_pts` is earlier
1098    ///   than the first indexed sample.
1099    pub fn seek_to_pts(
1100        &self,
1101        track_id: u32,
1102        target_pts: i64,
1103    ) -> Result<PtsSeekResult, MultiTrackSeekerError> {
1104        let entries = self
1105            .indices
1106            .get(&track_id)
1107            .ok_or(MultiTrackSeekerError::NoIndex(track_id))?;
1108
1109        if entries.is_empty() {
1110            return Err(MultiTrackSeekerError::EmptyIndex(track_id));
1111        }
1112
1113        // Binary search: find the last entry with pts <= target_pts.
1114        // `partition_point` returns the index of the first element where the
1115        // predicate is false, i.e. the first entry with pts > target_pts.
1116        let insertion = entries.partition_point(|e| e.pts <= target_pts);
1117
1118        if insertion == 0 {
1119            // All entries have pts > target_pts → before the first sample
1120            return Err(MultiTrackSeekerError::BeforeFirstSample(
1121                target_pts, track_id,
1122            ));
1123        }
1124
1125        let sample_idx = insertion - 1;
1126        let entry = &entries[sample_idx];
1127
1128        Ok(PtsSeekResult {
1129            found_pts: entry.pts,
1130            byte_offset: entry.byte_offset,
1131            sample_idx,
1132        })
1133    }
1134
1135    /// Returns the number of tracks that have been indexed.
1136    #[must_use]
1137    pub fn indexed_track_count(&self) -> usize {
1138        self.indices.len()
1139    }
1140
1141    /// Returns the number of indexed samples for `track_id`, or `None`.
1142    #[must_use]
1143    pub fn sample_count(&self, track_id: u32) -> Option<usize> {
1144        self.indices.get(&track_id).map(Vec::len)
1145    }
1146
1147    /// Clears the index for `track_id`.
1148    pub fn clear_index(&mut self, track_id: u32) {
1149        self.indices.remove(&track_id);
1150    }
1151
1152    /// Returns a sorted slice of index entries for `track_id`, or `None`.
1153    #[must_use]
1154    pub fn entries(&self, track_id: u32) -> Option<&[SampleIndexEntry]> {
1155        self.indices.get(&track_id).map(Vec::as_slice)
1156    }
1157}
1158
1159impl Default for MultiTrackSeeker {
1160    fn default() -> Self {
1161        Self::new()
1162    }
1163}
1164
1165#[cfg(test)]
1166mod tests {
1167    use super::*;
1168
1169    // ── SeekFlags tests ─────────────────────────────────────────────────
1170
1171    #[test]
1172    fn test_seek_flags() {
1173        let flags = SeekFlags::BACKWARD | SeekFlags::KEYFRAME;
1174        assert!(flags.contains(SeekFlags::BACKWARD));
1175        assert!(flags.contains(SeekFlags::KEYFRAME));
1176        assert!(!flags.contains(SeekFlags::ANY));
1177    }
1178
1179    // ── SeekTarget tests ────────────────────────────────────────────────
1180
1181    #[test]
1182    fn test_seek_target_time() {
1183        let target = SeekTarget::time(10.5);
1184        assert_eq!(target.position, 10.5);
1185        assert!(target.is_keyframe());
1186        assert!(!target.is_byte());
1187        assert_eq!(target.stream_index, None);
1188    }
1189
1190    #[test]
1191    fn test_seek_target_byte() {
1192        let target = SeekTarget::byte(1024);
1193        assert_eq!(target.position, 1024.0);
1194        assert!(target.is_byte());
1195        assert_eq!(target.stream_index, None);
1196    }
1197
1198    #[test]
1199    fn test_seek_target_sample_accurate() {
1200        let target = SeekTarget::sample_accurate(5.0);
1201        assert!(target.is_frame_accurate());
1202        assert!(target.is_backward());
1203        assert!(!target.is_keyframe());
1204    }
1205
1206    #[test]
1207    fn test_seek_target_with_stream() {
1208        let target = SeekTarget::time(5.0).with_stream(1);
1209        assert_eq!(target.stream_index, Some(1));
1210        assert_eq!(target.position, 5.0);
1211    }
1212
1213    #[test]
1214    fn test_seek_target_with_flags() {
1215        let target = SeekTarget::time(3.0)
1216            .with_flags(SeekFlags::BACKWARD)
1217            .add_flags(SeekFlags::ANY);
1218
1219        assert!(target.is_backward());
1220        assert!(target.is_any());
1221    }
1222
1223    #[test]
1224    fn test_seek_target_predicates() {
1225        let target =
1226            SeekTarget::time(1.0).add_flags(SeekFlags::BACKWARD | SeekFlags::FRAME_ACCURATE);
1227
1228        assert!(target.is_backward());
1229        assert!(!target.is_any());
1230        assert!(target.is_keyframe());
1231        assert!(!target.is_byte());
1232        assert!(target.is_frame_accurate());
1233    }
1234
1235    // ── SeekIndexEntry tests ────────────────────────────────────────────
1236
1237    #[test]
1238    fn test_entry_keyframe() {
1239        let e = SeekIndexEntry::keyframe(0, 0, 100, 500, 3000, 0);
1240        assert!(e.is_keyframe);
1241        assert_eq!(e.pts, 0);
1242        assert_eq!(e.file_offset, 100);
1243        assert_eq!(e.end_pts(), 3000);
1244    }
1245
1246    #[test]
1247    fn test_entry_non_keyframe() {
1248        let e = SeekIndexEntry::non_keyframe(3000, 3000, 600, 200, 3000, 1);
1249        assert!(!e.is_keyframe);
1250        assert_eq!(e.sample_number, 1);
1251        assert_eq!(e.end_pts(), 6000);
1252    }
1253
1254    // ── SeekIndex basic tests ───────────────────────────────────────────
1255
1256    fn build_test_index() -> SeekIndex {
1257        // 90kHz timescale, 30fps video (3000 ticks per frame)
1258        // GOP size = 5 frames (keyframe every 5th frame)
1259        let mut index = SeekIndex::new(90000);
1260        for i in 0u32..20 {
1261            let pts = i64::from(i) * 3000;
1262            let is_kf = i % 5 == 0;
1263            let offset = u64::from(i) * 500 + 1000; // arbitrary offsets
1264            if is_kf {
1265                index.add_entry(SeekIndexEntry::keyframe(pts, pts, offset, 500, 3000, i));
1266            } else {
1267                index.add_entry(SeekIndexEntry::non_keyframe(pts, pts, offset, 200, 3000, i));
1268            }
1269        }
1270        index
1271    }
1272
1273    #[test]
1274    fn test_index_new() {
1275        let index = SeekIndex::new(48000);
1276        assert_eq!(index.timescale(), 48000);
1277        assert!(index.is_empty());
1278        assert_eq!(index.len(), 0);
1279        assert_eq!(index.keyframe_count(), 0);
1280    }
1281
1282    #[test]
1283    fn test_index_add_entries() {
1284        let index = build_test_index();
1285        assert_eq!(index.len(), 20);
1286        assert_eq!(index.keyframe_count(), 4); // frames 0, 5, 10, 15
1287    }
1288
1289    #[test]
1290    fn test_seconds_to_ticks() {
1291        let index = SeekIndex::new(90000);
1292        assert_eq!(index.seconds_to_ticks(1.0), 90000);
1293        assert_eq!(index.seconds_to_ticks(0.5), 45000);
1294    }
1295
1296    #[test]
1297    fn test_ticks_to_seconds() {
1298        let index = SeekIndex::new(90000);
1299        let s = index.ticks_to_seconds(90000);
1300        assert!((s - 1.0).abs() < 1e-10);
1301    }
1302
1303    #[test]
1304    fn test_ticks_to_seconds_zero_timescale() {
1305        let index = SeekIndex::new(0);
1306        assert_eq!(index.ticks_to_seconds(12345), 0.0);
1307    }
1308
1309    // ── Keyframe search tests ───────────────────────────────────────────
1310
1311    #[test]
1312    fn test_find_keyframe_before_exact() {
1313        let index = build_test_index();
1314        // Keyframes at pts: 0, 15000, 30000, 45000
1315        let kf = index.find_keyframe_before(15000).expect("should find");
1316        assert_eq!(kf.pts, 15000);
1317        assert!(kf.is_keyframe);
1318    }
1319
1320    #[test]
1321    fn test_find_keyframe_before_between() {
1322        let index = build_test_index();
1323        // Target at 20000 (between kf@15000 and kf@30000)
1324        let kf = index.find_keyframe_before(20000).expect("should find");
1325        assert_eq!(kf.pts, 15000);
1326    }
1327
1328    #[test]
1329    fn test_find_keyframe_before_start() {
1330        let index = build_test_index();
1331        let kf = index.find_keyframe_before(0).expect("should find");
1332        assert_eq!(kf.pts, 0);
1333    }
1334
1335    #[test]
1336    fn test_find_keyframe_before_none() {
1337        let index = build_test_index();
1338        // Target before all entries
1339        let kf = index.find_keyframe_before(-1);
1340        assert!(kf.is_none());
1341    }
1342
1343    #[test]
1344    fn test_find_keyframe_after() {
1345        let index = build_test_index();
1346        let kf = index.find_keyframe_after(20000).expect("should find");
1347        assert_eq!(kf.pts, 30000);
1348    }
1349
1350    #[test]
1351    fn test_find_keyframe_after_exact() {
1352        let index = build_test_index();
1353        let kf = index.find_keyframe_after(15000).expect("should find");
1354        assert_eq!(kf.pts, 15000);
1355    }
1356
1357    #[test]
1358    fn test_find_keyframe_after_past_end() {
1359        let index = build_test_index();
1360        let kf = index.find_keyframe_after(99999);
1361        assert!(kf.is_none());
1362    }
1363
1364    #[test]
1365    fn test_find_nearest_keyframe_closer_before() {
1366        let index = build_test_index();
1367        // 16000 is closer to kf@15000 than kf@30000
1368        let kf = index.find_nearest_keyframe(16000).expect("should find");
1369        assert_eq!(kf.pts, 15000);
1370    }
1371
1372    #[test]
1373    fn test_find_nearest_keyframe_closer_after() {
1374        let index = build_test_index();
1375        // 28000 is closer to kf@30000 than kf@15000
1376        let kf = index.find_nearest_keyframe(28000).expect("should find");
1377        assert_eq!(kf.pts, 30000);
1378    }
1379
1380    #[test]
1381    fn test_find_nearest_keyframe_equidistant() {
1382        let index = build_test_index();
1383        // 22500 is equidistant between kf@15000 and kf@30000
1384        // Should prefer the earlier one (backward preference)
1385        let kf = index.find_nearest_keyframe(22500).expect("should find");
1386        assert_eq!(kf.pts, 15000);
1387    }
1388
1389    // ── Sample-at tests ─────────────────────────────────────────────────
1390
1391    #[test]
1392    fn test_find_sample_at_exact_start() {
1393        let index = build_test_index();
1394        let sample = index.find_sample_at(3000).expect("should find");
1395        assert_eq!(sample.pts, 3000);
1396    }
1397
1398    #[test]
1399    fn test_find_sample_at_mid_frame() {
1400        let index = build_test_index();
1401        // 4000 is within frame at pts=3000 (duration=3000, so [3000..6000))
1402        let sample = index.find_sample_at(4000).expect("should find");
1403        assert_eq!(sample.pts, 3000);
1404    }
1405
1406    #[test]
1407    fn test_find_sample_at_last() {
1408        let index = build_test_index();
1409        // Last frame: pts=57000
1410        let sample = index.find_sample_at(58000).expect("should find");
1411        assert_eq!(sample.pts, 57000);
1412    }
1413
1414    // ── Seek Plan tests ─────────────────────────────────────────────────
1415
1416    #[test]
1417    fn test_plan_seek_keyframe() {
1418        let index = build_test_index();
1419        let plan = index
1420            .plan_seek(20000, SeekAccuracy::Keyframe)
1421            .expect("should plan");
1422        // Should go to keyframe at 15000
1423        assert_eq!(plan.keyframe_entry.pts, 15000);
1424        assert_eq!(plan.discard_count, 0);
1425        assert_eq!(plan.target_entry.pts, 15000);
1426    }
1427
1428    #[test]
1429    fn test_plan_seek_sample_accurate() {
1430        let index = build_test_index();
1431        // Target: pts=21000 (frame 7 at pts=21000)
1432        let plan = index
1433            .plan_seek(21000, SeekAccuracy::SampleAccurate)
1434            .expect("should plan");
1435
1436        // Keyframe should be at pts=15000 (frame 5)
1437        assert_eq!(plan.keyframe_entry.pts, 15000);
1438        // Target should be the frame at pts=21000
1439        assert_eq!(plan.target_entry.pts, 21000);
1440        // Discard count: frames 6 (18000) between keyframe 5 (15000) and target 7 (21000)
1441        assert_eq!(plan.discard_count, 1); // frame at 18000
1442        assert!(plan.is_exact);
1443    }
1444
1445    #[test]
1446    fn test_plan_seek_sample_accurate_on_keyframe() {
1447        let index = build_test_index();
1448        let plan = index
1449            .plan_seek(15000, SeekAccuracy::SampleAccurate)
1450            .expect("should plan");
1451        assert_eq!(plan.keyframe_entry.pts, 15000);
1452        assert_eq!(plan.discard_count, 0);
1453        assert!(plan.is_exact);
1454    }
1455
1456    #[test]
1457    fn test_plan_seek_sample_accurate_first_frame() {
1458        let index = build_test_index();
1459        let plan = index
1460            .plan_seek(0, SeekAccuracy::SampleAccurate)
1461            .expect("should plan");
1462        assert_eq!(plan.keyframe_entry.pts, 0);
1463        assert_eq!(plan.target_entry.pts, 0);
1464        assert_eq!(plan.discard_count, 0);
1465    }
1466
1467    #[test]
1468    fn test_plan_seek_within_tolerance_exact() {
1469        let index = build_test_index();
1470        // Within 5000 ticks of a keyframe at 15000
1471        let plan = index
1472            .plan_seek(16000, SeekAccuracy::WithinTolerance(5000))
1473            .expect("should plan");
1474        assert_eq!(plan.target_entry.pts, 15000);
1475    }
1476
1477    #[test]
1478    fn test_plan_seek_within_tolerance_out_of_range() {
1479        let index = build_test_index();
1480        // Only 1 tick tolerance, and 20000 is not within 1 tick of any keyframe
1481        let plan = index.plan_seek(20000, SeekAccuracy::WithinTolerance(1));
1482        assert!(plan.is_none());
1483    }
1484
1485    #[test]
1486    fn test_plan_seek_empty_index() {
1487        let index = SeekIndex::new(90000);
1488        let plan = index.plan_seek(0, SeekAccuracy::Keyframe);
1489        assert!(plan.is_none());
1490    }
1491
1492    // ── Duration and statistics tests ───────────────────────────────────
1493
1494    #[test]
1495    fn test_duration_ticks() {
1496        let index = build_test_index();
1497        // 20 frames, each 3000 ticks, last frame ends at 20*3000 = 60000
1498        assert_eq!(index.duration_ticks(), 60000);
1499    }
1500
1501    #[test]
1502    fn test_duration_seconds() {
1503        let index = build_test_index();
1504        let dur = index.duration_seconds();
1505        // 60000 ticks at 90kHz = 0.6667 seconds
1506        assert!((dur - 60000.0 / 90000.0).abs() < 1e-6);
1507    }
1508
1509    #[test]
1510    fn test_average_keyframe_interval() {
1511        let index = build_test_index();
1512        let avg = index.average_keyframe_interval().expect("should calculate");
1513        // Keyframes at 0, 15000, 30000, 45000 -> intervals: 15000, 15000, 15000
1514        assert!((avg - 15000.0).abs() < 1e-6);
1515    }
1516
1517    #[test]
1518    fn test_average_keyframe_interval_single() {
1519        let mut index = SeekIndex::new(90000);
1520        index.add_entry(SeekIndexEntry::keyframe(0, 0, 0, 100, 3000, 0));
1521        assert!(index.average_keyframe_interval().is_none());
1522    }
1523
1524    // ── Sort test ───────────────────────────────────────────────────────
1525
1526    #[test]
1527    fn test_sort_reorders_entries() {
1528        let mut index = SeekIndex::new(90000);
1529        // Add out of order
1530        index.add_entry(SeekIndexEntry::non_keyframe(6000, 6000, 300, 100, 3000, 2));
1531        index.add_entry(SeekIndexEntry::keyframe(0, 0, 100, 100, 3000, 0));
1532        index.add_entry(SeekIndexEntry::non_keyframe(3000, 3000, 200, 100, 3000, 1));
1533
1534        index.sort();
1535
1536        assert_eq!(index.entries()[0].pts, 0);
1537        assert_eq!(index.entries()[1].pts, 3000);
1538        assert_eq!(index.entries()[2].pts, 6000);
1539        assert_eq!(index.keyframe_count(), 1);
1540    }
1541
1542    // ── Edge cases ──────────────────────────────────────────────────────
1543
1544    #[test]
1545    fn test_find_keyframe_empty() {
1546        let index = SeekIndex::new(90000);
1547        assert!(index.find_keyframe_before(0).is_none());
1548        assert!(index.find_keyframe_after(0).is_none());
1549        assert!(index.find_nearest_keyframe(0).is_none());
1550    }
1551
1552    #[test]
1553    fn test_single_keyframe_index() {
1554        let mut index = SeekIndex::new(90000);
1555        index.add_entry(SeekIndexEntry::keyframe(0, 0, 0, 100, 90000, 0));
1556
1557        let kf = index.find_keyframe_before(45000).expect("should find");
1558        assert_eq!(kf.pts, 0);
1559
1560        let plan = index
1561            .plan_seek(45000, SeekAccuracy::SampleAccurate)
1562            .expect("should plan");
1563        assert_eq!(plan.keyframe_entry.pts, 0);
1564    }
1565
1566    #[test]
1567    fn test_all_keyframes_index() {
1568        let mut index = SeekIndex::new(48000);
1569        // Audio: every frame is a keyframe
1570        for i in 0u32..100 {
1571            let pts = i64::from(i) * 960;
1572            index.add_entry(SeekIndexEntry::keyframe(
1573                pts,
1574                pts,
1575                u64::from(i) * 100,
1576                100,
1577                960,
1578                i,
1579            ));
1580        }
1581
1582        assert_eq!(index.keyframe_count(), 100);
1583
1584        let plan = index
1585            .plan_seek(48000, SeekAccuracy::SampleAccurate)
1586            .expect("should plan");
1587        // Should find the exact frame
1588        assert_eq!(plan.keyframe_entry.pts, 48000);
1589        assert_eq!(plan.discard_count, 0);
1590    }
1591
1592    #[test]
1593    fn test_with_capacity() {
1594        let index = SeekIndex::with_capacity(90000, 1000);
1595        assert_eq!(index.timescale(), 90000);
1596        assert!(index.is_empty());
1597    }
1598
1599    // ── SampleAccurateSeeker tests ───────────────────────────────────────
1600
1601    fn build_seeker_index() -> SeekIndex {
1602        // 90kHz, 30fps (3000 ticks/frame), GOP=5
1603        let mut index = SeekIndex::new(90000);
1604        for i in 0u32..20 {
1605            let pts = i64::from(i) * 3000;
1606            let is_kf = i % 5 == 0;
1607            let offset = u64::from(i) * 500 + 1000;
1608            if is_kf {
1609                index.add_entry(SeekIndexEntry::keyframe(pts, pts, offset, 500, 3000, i));
1610            } else {
1611                index.add_entry(SeekIndexEntry::non_keyframe(pts, pts, offset, 200, 3000, i));
1612            }
1613        }
1614        index
1615    }
1616
1617    #[test]
1618    fn test_sample_accurate_seeker_on_keyframe() {
1619        let track = TrackIndex::new(build_seeker_index());
1620        let seeker = SampleAccurateSeeker::with_track(TrackIndex::new(build_seeker_index()));
1621        let result = seeker.seek_to_sample(15000, &track).expect("should find");
1622        assert_eq!(result.keyframe_pts, 15000);
1623        assert_eq!(result.preroll_samples, 0);
1624        assert_eq!(result.sample_offset, 1000 + 5 * 500); // frame 5 offset
1625    }
1626
1627    #[test]
1628    fn test_sample_accurate_seeker_between_keyframes() {
1629        let track = TrackIndex::new(build_seeker_index());
1630        let seeker = SampleAccurateSeeker::with_track(TrackIndex::new(build_seeker_index()));
1631        // Target pts=21000 (frame 7) — keyframe is at pts=15000 (frame 5)
1632        // Frames 6 (pts=18000) must be discarded → preroll = 1
1633        let result = seeker.seek_to_sample(21000, &track).expect("should find");
1634        assert_eq!(result.keyframe_pts, 15000);
1635        assert_eq!(result.preroll_samples, 1);
1636    }
1637
1638    #[test]
1639    fn test_sample_accurate_seeker_codec_delay_added() {
1640        let track = TrackIndex::with_codec_delay(build_seeker_index(), 512);
1641        let seeker = SampleAccurateSeeker::with_track(TrackIndex::new(build_seeker_index()));
1642        // On a keyframe: preroll = 0 inter-frame + 512 codec_delay = 512
1643        let result = seeker.seek_to_sample(0, &track).expect("should find");
1644        assert_eq!(result.keyframe_pts, 0);
1645        assert_eq!(result.preroll_samples, 512);
1646    }
1647
1648    #[test]
1649    fn test_sample_accurate_seeker_empty_index() {
1650        let track = TrackIndex::new(SeekIndex::new(90000));
1651        let seeker = SampleAccurateSeeker::with_track(TrackIndex::new(SeekIndex::new(90000)));
1652        let result = seeker.seek_to_sample(0, &track);
1653        assert!(result.is_none());
1654    }
1655
1656    #[test]
1657    fn test_track_index_default_codec_delay() {
1658        let idx = SeekIndex::new(90000);
1659        let track = TrackIndex::new(idx);
1660        assert_eq!(track.codec_delay_samples, 0);
1661    }
1662
1663    #[test]
1664    fn test_seek_result_fields() {
1665        let track = TrackIndex::new(build_seeker_index());
1666        let seeker = SampleAccurateSeeker::with_track(TrackIndex::new(build_seeker_index()));
1667        let result = seeker.seek_to_sample(0, &track).expect("should find");
1668        // frame 0 is a keyframe at file offset 1000
1669        assert_eq!(result.keyframe_pts, 0);
1670        assert_eq!(result.sample_offset, 1000);
1671        assert_eq!(result.preroll_samples, 0);
1672    }
1673
1674    // ── MultiTrackSeeker tests ───────────────────────────────────────────
1675
1676    fn build_multi_track_samples() -> Vec<SampleIndexEntry> {
1677        // 30fps video at 90kHz: keyframe every 5 frames (GOP=5)
1678        (0u32..20)
1679            .map(|i| {
1680                let pts = i64::from(i) * 3000;
1681                let offset = 1000 + u64::from(i) * 500;
1682                if i % 5 == 0 {
1683                    SampleIndexEntry::keyframe(pts, offset)
1684                } else {
1685                    SampleIndexEntry::delta(pts, offset)
1686                }
1687            })
1688            .collect()
1689    }
1690
1691    #[test]
1692    fn test_multi_track_seeker_new() {
1693        let seeker = MultiTrackSeeker::new();
1694        assert_eq!(seeker.indexed_track_count(), 0);
1695    }
1696
1697    #[test]
1698    fn test_multi_track_build_index() {
1699        let mut seeker = MultiTrackSeeker::new();
1700        let samples = build_multi_track_samples();
1701        seeker.build_index(1, &samples).expect("build ok");
1702        assert_eq!(seeker.indexed_track_count(), 1);
1703        assert_eq!(seeker.sample_count(1), Some(20));
1704    }
1705
1706    #[test]
1707    fn test_multi_track_entries_sorted() {
1708        let mut seeker = MultiTrackSeeker::new();
1709        // Insert out of order
1710        let samples = vec![
1711            SampleIndexEntry::delta(9000, 3000),
1712            SampleIndexEntry::keyframe(0, 1000),
1713            SampleIndexEntry::delta(3000, 1500),
1714            SampleIndexEntry::delta(6000, 2000),
1715        ];
1716        seeker.build_index(1, &samples).expect("ok");
1717        let entries = seeker.entries(1).expect("entries exist");
1718        assert_eq!(entries[0].pts, 0);
1719        assert_eq!(entries[1].pts, 3000);
1720        assert_eq!(entries[2].pts, 6000);
1721        assert_eq!(entries[3].pts, 9000);
1722    }
1723
1724    #[test]
1725    fn test_seek_to_pts_exact() {
1726        let mut seeker = MultiTrackSeeker::new();
1727        seeker
1728            .build_index(1, &build_multi_track_samples())
1729            .expect("ok");
1730        // Seek exactly to frame 5 (pts=15000)
1731        let result = seeker.seek_to_pts(1, 15000).expect("seek ok");
1732        assert_eq!(result.found_pts, 15000);
1733        assert_eq!(result.byte_offset, 1000 + 5 * 500);
1734        assert_eq!(result.sample_idx, 5);
1735    }
1736
1737    #[test]
1738    fn test_seek_to_pts_between_samples() {
1739        let mut seeker = MultiTrackSeeker::new();
1740        seeker
1741            .build_index(1, &build_multi_track_samples())
1742            .expect("ok");
1743        // PTS 16000 falls between frame 5 (15000) and frame 6 (18000)
1744        let result = seeker.seek_to_pts(1, 16000).expect("seek ok");
1745        assert_eq!(
1746            result.found_pts, 15000,
1747            "should return the preceding sample"
1748        );
1749        assert_eq!(result.sample_idx, 5);
1750    }
1751
1752    #[test]
1753    fn test_seek_to_pts_first_sample() {
1754        let mut seeker = MultiTrackSeeker::new();
1755        seeker
1756            .build_index(1, &build_multi_track_samples())
1757            .expect("ok");
1758        let result = seeker.seek_to_pts(1, 0).expect("seek ok");
1759        assert_eq!(result.found_pts, 0);
1760        assert_eq!(result.sample_idx, 0);
1761    }
1762
1763    #[test]
1764    fn test_seek_to_pts_last_sample() {
1765        let mut seeker = MultiTrackSeeker::new();
1766        seeker
1767            .build_index(1, &build_multi_track_samples())
1768            .expect("ok");
1769        // PTS beyond the last sample should return the last sample
1770        let result = seeker.seek_to_pts(1, 99999).expect("seek ok");
1771        assert_eq!(result.found_pts, 19 * 3000); // last sample
1772        assert_eq!(result.sample_idx, 19);
1773    }
1774
1775    #[test]
1776    fn test_seek_to_pts_before_first_sample() {
1777        let mut seeker = MultiTrackSeeker::new();
1778        seeker
1779            .build_index(1, &build_multi_track_samples())
1780            .expect("ok");
1781        let err = seeker.seek_to_pts(1, -1);
1782        assert!(matches!(
1783            err,
1784            Err(MultiTrackSeekerError::BeforeFirstSample(-1, 1))
1785        ));
1786    }
1787
1788    #[test]
1789    fn test_seek_to_pts_no_index() {
1790        let seeker = MultiTrackSeeker::new();
1791        let err = seeker.seek_to_pts(42, 0);
1792        assert!(matches!(err, Err(MultiTrackSeekerError::NoIndex(42))));
1793    }
1794
1795    #[test]
1796    fn test_seek_to_pts_empty_index() {
1797        let mut seeker = MultiTrackSeeker::new();
1798        seeker.build_index(1, &[]).expect("ok");
1799        let err = seeker.seek_to_pts(1, 0);
1800        assert!(matches!(err, Err(MultiTrackSeekerError::EmptyIndex(1))));
1801    }
1802
1803    #[test]
1804    fn test_multi_track_multiple_tracks() {
1805        let mut seeker = MultiTrackSeeker::new();
1806        let video = build_multi_track_samples();
1807        // 51 audio frames: pts 0, 960, 1920, ..., 50*960=48000
1808        let audio: Vec<SampleIndexEntry> = (0u32..=50)
1809            .map(|i| SampleIndexEntry::keyframe(i64::from(i) * 960, u64::from(i) * 100 + 500))
1810            .collect();
1811
1812        seeker.build_index(1, &video).expect("video ok");
1813        seeker.build_index(2, &audio).expect("audio ok");
1814
1815        assert_eq!(seeker.indexed_track_count(), 2);
1816
1817        let v_result = seeker.seek_to_pts(1, 15000).expect("video seek ok");
1818        let a_result = seeker.seek_to_pts(2, 48000).expect("audio seek ok");
1819
1820        assert_eq!(v_result.found_pts, 15000);
1821        assert_eq!(a_result.found_pts, 48000);
1822    }
1823
1824    #[test]
1825    fn test_clear_index() {
1826        let mut seeker = MultiTrackSeeker::new();
1827        seeker
1828            .build_index(1, &build_multi_track_samples())
1829            .expect("ok");
1830        assert_eq!(seeker.indexed_track_count(), 1);
1831        seeker.clear_index(1);
1832        assert_eq!(seeker.indexed_track_count(), 0);
1833        let err = seeker.seek_to_pts(1, 0);
1834        assert!(matches!(err, Err(MultiTrackSeekerError::NoIndex(1))));
1835    }
1836
1837    #[test]
1838    fn test_build_index_replaces_existing() {
1839        let mut seeker = MultiTrackSeeker::new();
1840        let old: Vec<SampleIndexEntry> = vec![SampleIndexEntry::keyframe(0, 100)];
1841        let new: Vec<SampleIndexEntry> = vec![
1842            SampleIndexEntry::keyframe(0, 200),
1843            SampleIndexEntry::keyframe(3000, 300),
1844        ];
1845        seeker.build_index(1, &old).expect("ok");
1846        seeker.build_index(1, &new).expect("replace ok");
1847        assert_eq!(seeker.sample_count(1), Some(2));
1848        let result = seeker.seek_to_pts(1, 0).expect("ok");
1849        assert_eq!(result.byte_offset, 200);
1850    }
1851
1852    #[test]
1853    fn test_sample_index_entry_constructors() {
1854        let kf = SampleIndexEntry::keyframe(1000, 9999);
1855        assert!(kf.is_sync);
1856        assert_eq!(kf.pts, 1000);
1857        assert_eq!(kf.byte_offset, 9999);
1858
1859        let df = SampleIndexEntry::delta(2000, 8888);
1860        assert!(!df.is_sync);
1861        assert_eq!(df.pts, 2000);
1862    }
1863
1864    #[test]
1865    fn test_pts_seek_result_fields() {
1866        let mut seeker = MultiTrackSeeker::new();
1867        seeker
1868            .build_index(1, &[SampleIndexEntry::keyframe(5000, 12345)])
1869            .expect("ok");
1870        let r = seeker.seek_to_pts(1, 5000).expect("ok");
1871        assert_eq!(r.found_pts, 5000);
1872        assert_eq!(r.byte_offset, 12345);
1873        assert_eq!(r.sample_idx, 0);
1874    }
1875}