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}