Skip to main content

g2g_core/
segment.rs

1//! Seek requests and the playback segment (M79).
2//!
3//! The pure, `no_std` foundation of the seek track: a [`Seek`] request
4//! (`seek(rate, start, stop, flags)`, the GStreamer seek-event analog) and a
5//! [`Segment`] (the `GstSegment` analog, TIME format, nanoseconds) carrying the
6//! `rate` / `start` / `stop` / `base` / `time` decomposition plus the
7//! **running-time** and **stream-time** conversions that AV sync and trick-play
8//! depend on. This milestone is data + math only; wiring a `Segment` into the
9//! packet stream and a `Seekable` source into the runner (flush-and-resume) is
10//! the next milestone.
11//!
12//! Two timelines, mirroring GStreamer:
13//! - **running time** is pipeline-clock time. It is direction- and rate-aware:
14//!   playing twice as fast (`rate == 2.0`) advances running time half as much
15//!   per buffer, and reverse playback (`rate < 0`) measures from `stop` down.
16//!   This is the timeline a sink compares against the clock to schedule.
17//! - **stream time** is the position within the media, scaled by
18//!   `applied_rate` (the rate already baked into the buffers). Direction-
19//!   agnostic: it answers "how far into the asset is this?" for seeking and UI.
20
21/// Absolute value of an `f64` without pulling in `std` (`f64::abs` is a `std`
22/// method; the `no_std` core has no libm). Used for the rate magnitude.
23fn fabs(x: f64) -> f64 {
24    if x < 0.0 {
25        -x
26    } else {
27        x
28    }
29}
30
31/// What a seek's `start` / `stop` value means, mirroring `GstSeekType`.
32// Closed set: intentionally exhaustive (not #[non_exhaustive]); see STABILITY.md.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum SeekType {
35    /// Leave this edge of the segment unchanged.
36    None,
37    /// Set this edge to the absolute value carried by the [`Seek`].
38    Set,
39    /// Set this edge relative to the end of the stream (the value is an offset
40    /// back from the duration; `0` means the very end).
41    End,
42}
43
44/// Seek modifier flags, a subset of `GstSeekFlags`. A bitset over a `u32` so
45/// flags compose with `|` and are queried with [`SeekFlags::contains`].
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub struct SeekFlags(u32);
48
49impl SeekFlags {
50    /// No flags.
51    pub const NONE: SeekFlags = SeekFlags(0);
52    /// Flush the pipeline (discard in-flight data) before repositioning. The
53    /// common interactive-seek case; without it the seek accumulates after the
54    /// current data drains.
55    pub const FLUSH: SeekFlags = SeekFlags(1 << 0);
56    /// Seek to the exact position rather than the nearest cheap point.
57    pub const ACCURATE: SeekFlags = SeekFlags(1 << 1);
58    /// Snap to a key unit (keyframe) at or around the target.
59    pub const KEY_UNIT: SeekFlags = SeekFlags(1 << 2);
60    /// Emit a `SEGMENT`-done at `stop` instead of running to `Eos` (segment
61    /// playback / looping).
62    pub const SEGMENT: SeekFlags = SeekFlags(1 << 3);
63    /// Trick mode: allow dropping non-key frames for fast scrub.
64    pub const TRICKMODE: SeekFlags = SeekFlags(1 << 4);
65    /// With `KEY_UNIT`, snap to the key unit at or before the target.
66    pub const SNAP_BEFORE: SeekFlags = SeekFlags(1 << 5);
67    /// With `KEY_UNIT`, snap to the key unit at or after the target.
68    pub const SNAP_AFTER: SeekFlags = SeekFlags(1 << 6);
69
70    /// Whether every flag in `other` is set.
71    pub fn contains(self, other: SeekFlags) -> bool {
72        (self.0 & other.0) == other.0
73    }
74
75    /// The raw bits.
76    pub fn bits(self) -> u32 {
77        self.0
78    }
79}
80
81impl core::ops::BitOr for SeekFlags {
82    type Output = SeekFlags;
83    fn bitor(self, rhs: SeekFlags) -> SeekFlags {
84        SeekFlags(self.0 | rhs.0)
85    }
86}
87
88/// A seek request: change the playback `rate` and/or reposition the
89/// `[start, stop]` window. The GStreamer seek-event analog. Times are
90/// nanoseconds on the stream timeline.
91#[derive(Debug, Clone, Copy, PartialEq)]
92pub struct Seek {
93    /// Playback rate. `1.0` is normal; `> 1.0` faster, `0 < r < 1` slower,
94    /// `< 0` reverse. Must be non-zero.
95    pub rate: f64,
96    /// Modifier flags.
97    pub flags: SeekFlags,
98    /// How to interpret `start`.
99    pub start_type: SeekType,
100    /// New segment start (ns), interpreted per `start_type`.
101    pub start: u64,
102    /// How to interpret `stop`.
103    pub stop_type: SeekType,
104    /// New segment stop (ns), interpreted per `stop_type`.
105    pub stop: u64,
106}
107
108impl Seek {
109    /// A flushing seek to an absolute `position` (ns) at normal rate, leaving
110    /// `stop` unchanged. The everyday "scrub to here" request.
111    pub fn flush_to(position: u64) -> Seek {
112        Seek {
113            rate: 1.0,
114            flags: SeekFlags::FLUSH,
115            start_type: SeekType::Set,
116            start: position,
117            stop_type: SeekType::None,
118            stop: 0,
119        }
120    }
121
122    /// A flushing **reverse** seek over `[start, stop]` (ns) at rate `-1.0`:
123    /// playback runs from `stop` down to `start`. The source emits frames in
124    /// descending PTS order; the sink maps them to ascending running time
125    /// (measured from `stop`, see [`Segment::to_running_time`]). A reverse
126    /// segment needs a finite `stop` to measure from, so both edges are `Set`.
127    pub fn reverse(start: u64, stop: u64) -> Seek {
128        Seek {
129            rate: -1.0,
130            flags: SeekFlags::FLUSH,
131            start_type: SeekType::Set,
132            start,
133            stop_type: SeekType::Set,
134            stop,
135        }
136    }
137
138    /// Whether this is a flushing seek.
139    pub fn is_flush(self) -> bool {
140        self.flags.contains(SeekFlags::FLUSH)
141    }
142
143    /// Whether playback is reverse (`rate < 0`).
144    pub fn is_reverse(self) -> bool {
145        self.rate < 0.0
146    }
147}
148
149/// A playback segment (the `GstSegment` analog, TIME format). Describes the
150/// portion of the stream currently being played, the rate, and the mapping
151/// onto the pipeline running-time clock.
152#[derive(Debug, Clone, Copy, PartialEq)]
153pub struct Segment {
154    /// Playback rate (sign carries direction).
155    pub rate: f64,
156    /// Rate already applied to the buffers (scales stream time). Usually `1.0`.
157    pub applied_rate: f64,
158    /// Running time of the segment's playback start. Accumulates across
159    /// non-flushing seeks; resets on a flushing seek.
160    pub base: u64,
161    /// Earliest valid stream timestamp in the segment (ns).
162    pub start: u64,
163    /// Latest valid stream timestamp (ns), or `None` for an open-ended segment.
164    pub stop: Option<u64>,
165    /// Stream time of the segment start (ns).
166    pub time: u64,
167    /// Current playback position within the segment (ns).
168    pub position: u64,
169    /// Trick mode: only key units (keyframes) are to be presented, the rest
170    /// dropped (the `GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS` analog). Set from a
171    /// seek's `TRICKMODE` flag; a sink honoring it presents only frames whose
172    /// [`FrameTiming::keyframe`](crate::frame::FrameTiming) is set.
173    pub key_units_only: bool,
174}
175
176impl Segment {
177    /// An open-ended, normal-rate segment starting at `0`.
178    pub fn new() -> Segment {
179        Segment {
180            rate: 1.0,
181            applied_rate: 1.0,
182            base: 0,
183            start: 0,
184            stop: None,
185            time: 0,
186            position: 0,
187            key_units_only: false,
188        }
189    }
190
191    /// Whether `ts` (ns, stream timeline) lies within `[start, stop]`.
192    pub fn contains(&self, ts: u64) -> bool {
193        ts >= self.start && self.stop.is_none_or(|stop| ts <= stop)
194    }
195
196    /// Map a stream timestamp to **running time** (pipeline-clock ns), or
197    /// `None` if `ts` is outside the segment (or the segment is reverse with no
198    /// `stop` to measure from). Rate- and direction-aware:
199    /// - forward (`rate > 0`): `base + (ts - start) / |rate|`
200    /// - reverse (`rate < 0`): `base + (stop - ts) / |rate|`
201    pub fn to_running_time(&self, ts: u64) -> Option<u64> {
202        if !self.contains(ts) {
203            return None;
204        }
205        let abs_rate = fabs(self.rate);
206        if abs_rate == 0.0 {
207            return None;
208        }
209        let span = if self.rate < 0.0 {
210            // Reverse needs a finite stop to measure down from.
211            self.stop?.checked_sub(ts)?
212        } else {
213            ts.checked_sub(self.start)?
214        };
215        let scaled = (span as f64 / abs_rate) as u64;
216        Some(self.base.saturating_add(scaled))
217    }
218
219    /// Map a stream timestamp to **stream time** (ns), or `None` if `ts` is
220    /// outside the segment. Direction-agnostic, scaled by `applied_rate`:
221    /// `time + (ts - start) * |applied_rate|`.
222    pub fn to_stream_time(&self, ts: u64) -> Option<u64> {
223        if !self.contains(ts) {
224            return None;
225        }
226        let span = ts.checked_sub(self.start)?;
227        let scaled = (span as f64 * fabs(self.applied_rate)) as u64;
228        Some(self.time.saturating_add(scaled))
229    }
230
231    /// Clip a buffer's `[b_start, b_stop]` (ns, stream timeline) to the
232    /// segment, returning the visible sub-range, or `None` if the buffer falls
233    /// entirely outside. `b_stop` is exclusive-ish (a `None` open buffer end is
234    /// clipped to the segment `stop`). The GStreamer `gst_segment_clip` analog.
235    pub fn clip(&self, b_start: u64, b_stop: Option<u64>) -> Option<(u64, Option<u64>)> {
236        // Fully after the segment stop?
237        if let Some(seg_stop) = self.stop {
238            if b_start >= seg_stop {
239                return None;
240            }
241        }
242        // Fully before the segment start?
243        if let Some(bs) = b_stop {
244            if bs <= self.start {
245                return None;
246            }
247        }
248        let out_start = b_start.max(self.start);
249        let out_stop = match (b_stop, self.stop) {
250            (Some(bs), Some(ss)) => Some(bs.min(ss)),
251            (Some(bs), None) => Some(bs),
252            (None, Some(ss)) => Some(ss),
253            (None, None) => None,
254        };
255        Some((out_start, out_stop))
256    }
257
258    /// Build the fresh segment produced by applying a **flushing** `seek` from
259    /// a stream of total `duration` ns (used to resolve `SeekType::End`). The
260    /// flushing case resets `base` to `0` (running time restarts after a
261    /// flush). `SeekType::None` leaves that edge at its default (`start`
262    /// unchanged from `0`, `stop` open). For the non-flushing (accumulating)
263    /// seek, which keeps the running-time clock advancing, see
264    /// [`accumulate_seek`](Self::accumulate_seek).
265    pub fn for_flush_seek(seek: &Seek, duration: Option<u64>) -> Segment {
266        // A flushing seek restarts the running-time clock: base = 0. No prior
267        // segment is threaded here, so `None` edges resolve to the default
268        // (start 0, stop open) as documented above.
269        Segment::from_seek(seek, duration, 0, 0, None)
270    }
271
272    /// Build the segment produced by a **non-flushing (accumulating)** seek
273    /// applied to `self` (the segment in effect when the seek arrives). Unlike a
274    /// flushing seek, the running-time clock is NOT reset: the new segment's
275    /// `base` is the running time playback has already reached in `self` (its
276    /// `base` plus the time elapsed to `self.position`), so downstream running
277    /// time stays monotonic across the seek. This is the gapless / segment-seek
278    /// case (looping, playlists), the `gst_segment_do_seek` non-flush path. If
279    /// `self.position` is somehow outside `self`, `base` falls back to `self.base`
280    /// (no negative jump). A `SeekType::None` edge keeps `self`'s current
281    /// `start` / `stop` (the "leave this edge unchanged" contract).
282    pub fn accumulate_seek(&self, seek: &Seek, duration: Option<u64>) -> Segment {
283        let base = self.to_running_time(self.position).unwrap_or(self.base);
284        Segment::from_seek(seek, duration, base, self.start, self.stop)
285    }
286
287    /// Shared construction for [`for_flush_seek`](Self::for_flush_seek) and
288    /// [`accumulate_seek`](Self::accumulate_seek): resolve the seek's edges
289    /// (`End` relative to `duration`, `None` keeping the prior edge `prev_start`
290    /// / `prev_stop`) and place the segment at running-time `base`.
291    fn from_seek(
292        seek: &Seek,
293        duration: Option<u64>,
294        base: u64,
295        prev_start: u64,
296        prev_stop: Option<u64>,
297    ) -> Segment {
298        let start = match seek.start_type {
299            SeekType::None => prev_start,
300            SeekType::Set => seek.start,
301            SeekType::End => duration.unwrap_or(0).saturating_sub(seek.start),
302        };
303        let stop = match seek.stop_type {
304            SeekType::None => prev_stop,
305            SeekType::Set => Some(seek.stop),
306            SeekType::End => Some(duration.unwrap_or(0).saturating_sub(seek.stop)),
307        };
308        Segment {
309            rate: seek.rate,
310            applied_rate: 1.0,
311            base,
312            start,
313            stop,
314            time: start,
315            position: start,
316            // Trick-mode seeks ask the sink to present key units only.
317            key_units_only: seek.flags.contains(SeekFlags::TRICKMODE),
318        }
319    }
320}
321
322impl Default for Segment {
323    fn default() -> Self {
324        Segment::new()
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    #[test]
333    fn flags_compose_and_query() {
334        let f = SeekFlags::FLUSH | SeekFlags::KEY_UNIT;
335        assert!(f.contains(SeekFlags::FLUSH));
336        assert!(f.contains(SeekFlags::KEY_UNIT));
337        assert!(!f.contains(SeekFlags::ACCURATE));
338        // contains is subset, so it holds for NONE and for the exact set.
339        assert!(f.contains(SeekFlags::NONE));
340        assert!(f.contains(SeekFlags::FLUSH | SeekFlags::KEY_UNIT));
341    }
342
343    #[test]
344    fn flush_to_builds_a_flushing_forward_seek() {
345        let s = Seek::flush_to(5_000);
346        assert!(s.is_flush());
347        assert!(!s.is_reverse());
348        assert_eq!(s.start_type, SeekType::Set);
349        assert_eq!(s.start, 5_000);
350        assert_eq!(s.stop_type, SeekType::None);
351    }
352
353    #[test]
354    fn running_time_forward_normal_rate() {
355        let seg = Segment {
356            start: 1_000,
357            base: 100,
358            ..Segment::new()
359        };
360        // base + (ts - start) / 1.0
361        assert_eq!(seg.to_running_time(1_000), Some(100));
362        assert_eq!(seg.to_running_time(3_000), Some(2_100));
363        // Before the segment start: outside.
364        assert_eq!(seg.to_running_time(500), None);
365    }
366
367    #[test]
368    fn running_time_scales_with_rate() {
369        // 2x: running time advances half as fast.
370        let fast = Segment {
371            rate: 2.0,
372            ..Segment::new()
373        };
374        assert_eq!(fast.to_running_time(2_000), Some(1_000));
375        // 0.5x: running time advances twice as fast.
376        let slow = Segment {
377            rate: 0.5,
378            ..Segment::new()
379        };
380        assert_eq!(slow.to_running_time(2_000), Some(4_000));
381    }
382
383    #[test]
384    fn running_time_reverse_measures_from_stop() {
385        let seg = Segment {
386            rate: -1.0,
387            start: 0,
388            stop: Some(10_000),
389            base: 0,
390            ..Segment::new()
391        };
392        // At stop, running time is base (0); earlier positions are later in
393        // running time.
394        assert_eq!(seg.to_running_time(10_000), Some(0));
395        assert_eq!(seg.to_running_time(6_000), Some(4_000));
396        // Outside the segment.
397        assert_eq!(seg.to_running_time(11_000), None);
398        // Reverse with no stop cannot be measured.
399        let open = Segment {
400            rate: -1.0,
401            stop: None,
402            ..Segment::new()
403        };
404        assert_eq!(open.to_running_time(1_000), None);
405    }
406
407    #[test]
408    fn stream_time_uses_applied_rate_and_is_direction_agnostic() {
409        let seg = Segment {
410            start: 1_000,
411            time: 50_000,
412            applied_rate: 2.0,
413            ..Segment::new()
414        };
415        // time + (ts - start) * |applied_rate|
416        assert_eq!(seg.to_stream_time(1_000), Some(50_000));
417        assert_eq!(seg.to_stream_time(2_000), Some(52_000));
418        assert_eq!(seg.to_stream_time(500), None);
419    }
420
421    #[test]
422    fn clip_trims_to_segment_bounds() {
423        let seg = Segment {
424            start: 1_000,
425            stop: Some(5_000),
426            ..Segment::new()
427        };
428        // Fully inside: unchanged.
429        assert_eq!(seg.clip(2_000, Some(3_000)), Some((2_000, Some(3_000))));
430        // Straddles the start: trimmed up to start.
431        assert_eq!(seg.clip(500, Some(2_000)), Some((1_000, Some(2_000))));
432        // Straddles the stop: trimmed down to stop.
433        assert_eq!(seg.clip(4_000, Some(9_000)), Some((4_000, Some(5_000))));
434        // Open buffer end clips to segment stop.
435        assert_eq!(seg.clip(2_000, None), Some((2_000, Some(5_000))));
436        // Fully before / fully after: dropped.
437        assert_eq!(seg.clip(0, Some(1_000)), None);
438        assert_eq!(seg.clip(5_000, Some(6_000)), None);
439    }
440
441    #[test]
442    fn flush_seek_builds_reset_segment() {
443        // Set start, open stop, 2x rate.
444        let seek = Seek {
445            rate: 2.0,
446            flags: SeekFlags::FLUSH,
447            start_type: SeekType::Set,
448            start: 3_000,
449            stop_type: SeekType::None,
450            stop: 0,
451        };
452        let seg = Segment::for_flush_seek(&seek, Some(100_000));
453        assert_eq!(seg.rate, 2.0);
454        assert_eq!(seg.start, 3_000);
455        assert_eq!(seg.stop, None);
456        assert_eq!(seg.base, 0, "flushing seek restarts running time");
457        assert_eq!(seg.time, 3_000);
458        assert_eq!(seg.position, 3_000);
459
460        // SeekType::End resolves against the duration.
461        let to_end = Seek {
462            rate: 1.0,
463            flags: SeekFlags::FLUSH,
464            start_type: SeekType::End,
465            start: 10_000, // 10us back from the end
466            stop_type: SeekType::None,
467            stop: 0,
468        };
469        let seg = Segment::for_flush_seek(&to_end, Some(100_000));
470        assert_eq!(seg.start, 90_000);
471    }
472
473    #[test]
474    fn accumulate_seek_advances_base_by_running_time_reached() {
475        // An open-ended normal-rate segment that has played up to position 3_000
476        // (running time 3_000, since base=0, start=0, rate=1).
477        let current = Segment {
478            position: 3_000,
479            ..Segment::new()
480        };
481        assert_eq!(current.to_running_time(current.position), Some(3_000));
482
483        // A non-flushing seek to 8_000: running time must NOT reset.
484        let seek = Seek {
485            rate: 1.0,
486            flags: SeekFlags::NONE,
487            start_type: SeekType::Set,
488            start: 8_000,
489            stop_type: SeekType::None,
490            stop: 0,
491        };
492        let seg = current.accumulate_seek(&seek, None);
493        assert_eq!(seg.start, 8_000, "repositioned to the target");
494        assert_eq!(
495            seg.base, 3_000,
496            "base accumulates the running time already played"
497        );
498        // The first post-seek frame (pts == target) continues at running time
499        // 3_000, monotonic with the pre-seek timeline (gapless).
500        assert_eq!(seg.to_running_time(8_000), Some(3_000));
501    }
502
503    #[test]
504    fn accumulate_seek_none_edge_keeps_current_bounds() {
505        // A bounded segment [10_000, 100_000): a seek that only repositions
506        // start (stop_type None) must keep the existing stop, not open it.
507        let current = Segment {
508            start: 10_000,
509            stop: Some(100_000),
510            position: 20_000,
511            ..Segment::new()
512        };
513        let move_start = Seek {
514            rate: 1.0,
515            flags: SeekFlags::NONE,
516            start_type: SeekType::Set,
517            start: 50_000,
518            stop_type: SeekType::None,
519            stop: 0,
520        };
521        let seg = current.accumulate_seek(&move_start, None);
522        assert_eq!(seg.start, 50_000);
523        assert_eq!(
524            seg.stop,
525            Some(100_000),
526            "None stop keeps the current segment's stop"
527        );
528
529        // Symmetrically, a None start keeps the current start.
530        let move_stop = Seek {
531            rate: 1.0,
532            flags: SeekFlags::NONE,
533            start_type: SeekType::None,
534            start: 0,
535            stop_type: SeekType::Set,
536            stop: 80_000,
537        };
538        let seg2 = current.accumulate_seek(&move_stop, None);
539        assert_eq!(
540            seg2.start, 10_000,
541            "None start keeps the current segment's start"
542        );
543        assert_eq!(seg2.stop, Some(80_000));
544    }
545
546    #[test]
547    fn reverse_seek_builds_a_descending_segment_with_ascending_running_time() {
548        let seek = Seek::reverse(0, 100_000);
549        assert!(seek.is_reverse());
550        assert!(seek.is_flush());
551        let seg = Segment::for_flush_seek(&seek, None);
552        assert_eq!(seg.rate, -1.0);
553        assert_eq!(seg.start, 0);
554        assert_eq!(seg.stop, Some(100_000));
555        // Reverse: the highest PTS plays first (running time 0), the lowest last.
556        assert_eq!(seg.to_running_time(100_000), Some(0));
557        assert_eq!(seg.to_running_time(75_000), Some(25_000));
558        assert_eq!(seg.to_running_time(0), Some(100_000));
559        // Outside the range is clipped.
560        assert_eq!(seg.to_running_time(150_000), None);
561    }
562
563    #[test]
564    fn accumulate_seek_keeps_running_time_monotonic_across_a_segment() {
565        // Play [0, 5_000), reach the end (position 5_000, running time 5_000),
566        // then a non-flushing segment seek loops back to 0.
567        let current = Segment {
568            start: 0,
569            stop: Some(5_000),
570            position: 5_000,
571            ..Segment::new()
572        };
573        let loop_back = Seek {
574            rate: 1.0,
575            flags: SeekFlags::NONE,
576            start_type: SeekType::Set,
577            start: 0,
578            stop_type: SeekType::Set,
579            stop: 5_000,
580        };
581        let seg = current.accumulate_seek(&loop_back, None);
582        assert_eq!(
583            seg.base, 5_000,
584            "the second loop iteration starts at running time 5_000"
585        );
586        // Frame at stream-time 0 in the new iteration maps to running time 5_000,
587        // and stream-time 2_500 to 7_500: the running-time line never goes back.
588        assert_eq!(seg.to_running_time(0), Some(5_000));
589        assert_eq!(seg.to_running_time(2_500), Some(7_500));
590    }
591}