mirror of
https://github.com/azaion/autopilot.git
synced 2026-06-22 08:11:09 +00:00
251ebed1c2
Wires a real ffmpeg-next 8.1 decoder into the frame_ingest lifecycle loop. NVDEC is probed at runtime via h264_cuvid / hevc_cuvid; CUDA-less hosts transparently fall back to software h264 / hevc. Each decoded frame is stamped with capture_ts (taken at packet receipt) and decode_ts (taken after decode returns) so movement_detector sees accurate frame-arrival times. Single-frame decode errors are counted toward decode_errors_total and dropped; the stream is never aborted. Adds new public API on FrameIngestHandle: decoder_backend(), decode_errors_total(), frames_decoded_total(), decode_ms_first_frame(), decode_ms_p50(), decode_ms_p99(). Integration tests under crates/frame_ingest/tests/decoder_pipeline.rs cover AC-1, AC-3, AC-4 end-to-end through the real FfmpegDecoder using libx264-encoded synthetic streams; AC-2 positive (NVDEC selection) is opt-in via --ignored on a CUDA host. AZ-657 lifecycle tests retained via a StubDecoder. Co-authored-by: Cursor <cursoragent@cursor.com>
154 lines
5.0 KiB
Rust
154 lines
5.0 KiB
Rust
//! AZ-658 — frame timestamping helpers.
|
|
//!
|
|
//! `description.md §4` requires every emitted [`Frame`] to carry a
|
|
//! monotonic capture timestamp stamped at the earliest practical
|
|
//! point in the pipeline (the moment the lifecycle loop receives an
|
|
//! RTSP packet from the transport). The decoder runs *after* that
|
|
//! point, so the [`Frame::decode_ts_monotonic_ns`] field records when
|
|
//! `FrameDecoder::decode` returned — the difference is the per-frame
|
|
//! decode latency that feeds the `decode_ms_p50` / `decode_ms_p99` /
|
|
//! `decode_ms_first_frame` health metrics.
|
|
//!
|
|
//! This module owns:
|
|
//! - [`SeqCounter`] — a strictly-monotonic `u64` sequence number used
|
|
//! as the frame's identity downstream of the decoder. Saturates at
|
|
//! `u64::MAX` so a session that never restarts cannot wrap and
|
|
//! produce duplicate IDs (saturating is preferred over wrapping
|
|
//! here because `movement_detector` keys per-frame state by `seq`
|
|
//! and a wrap would corrupt that map).
|
|
//! - [`FrameStamper`] — pairs a `MonoClock` and a `SeqCounter` so the
|
|
//! lifecycle loop has one place to read both timestamps for a
|
|
//! single packet → frame transition.
|
|
|
|
use shared::clock::MonoClock;
|
|
|
|
/// Strictly-monotonic frame sequence counter. Saturates at
|
|
/// `u64::MAX`; in practice a 30 fps stream takes ~19.5 billion years
|
|
/// to overflow `u64`, so saturation behaviour is observable only as a
|
|
/// post-condition for tests with `u64::MAX - 1` priming.
|
|
#[derive(Debug, Default)]
|
|
pub struct SeqCounter {
|
|
next: u64,
|
|
}
|
|
|
|
impl SeqCounter {
|
|
pub fn new() -> Self {
|
|
Self { next: 0 }
|
|
}
|
|
|
|
/// Returns the next sequence number and advances internal state.
|
|
/// Saturates at `u64::MAX` (subsequent calls keep returning
|
|
/// `u64::MAX`). Named `advance` rather than `next` so that the
|
|
/// type does not collide with `Iterator::next` semantics in
|
|
/// caller code (and to satisfy `clippy::should_implement_trait`
|
|
/// — `SeqCounter` is intentionally NOT an Iterator: an unbounded
|
|
/// monotonic counter has no natural `None` terminator).
|
|
pub fn advance(&mut self) -> u64 {
|
|
let s = self.next;
|
|
self.next = self.next.saturating_add(1);
|
|
s
|
|
}
|
|
}
|
|
|
|
/// Holds a clock + sequence counter so the lifecycle loop only has
|
|
/// to call [`FrameStamper::capture`] (immediately on packet receipt)
|
|
/// and [`FrameStamper::decoded`] (immediately after decode returns)
|
|
/// to produce both monotonic timestamps for the next frame.
|
|
#[derive(Debug)]
|
|
pub struct FrameStamper {
|
|
clock: MonoClock,
|
|
seq: SeqCounter,
|
|
}
|
|
|
|
impl FrameStamper {
|
|
pub fn new(clock: MonoClock) -> Self {
|
|
Self {
|
|
clock,
|
|
seq: SeqCounter::new(),
|
|
}
|
|
}
|
|
|
|
/// Snapshot the capture-side timestamp + sequence number. Call
|
|
/// this the moment the transport hands us the packet, BEFORE
|
|
/// invoking the decoder. The capture timestamp is the head of
|
|
/// the per-frame latency budget (`description.md §8`: ≤30 ms p99
|
|
/// from RTSP rx → publish on Jetson Orin Nano).
|
|
pub fn capture(&mut self) -> CaptureMark {
|
|
CaptureMark {
|
|
seq: self.seq.advance(),
|
|
ts_ns: self.clock.elapsed_ns(),
|
|
}
|
|
}
|
|
|
|
/// Read the decode-side timestamp at the moment
|
|
/// `FrameDecoder::decode` returned. Used both for the emitted
|
|
/// `Frame::decode_ts_monotonic_ns` field and to compute
|
|
/// `decode_duration = decode_ts - capture_ts` for the histogram.
|
|
pub fn decoded(&self) -> u64 {
|
|
self.clock.elapsed_ns()
|
|
}
|
|
}
|
|
|
|
/// One capture-side mark per packet. Carried through the decode call
|
|
/// so the emitted `Frame` keeps the timestamp from packet receipt,
|
|
/// not from after-decode.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct CaptureMark {
|
|
pub seq: u64,
|
|
pub ts_ns: u64,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn seq_counter_is_strictly_monotonic() {
|
|
// Arrange
|
|
let mut c = SeqCounter::new();
|
|
|
|
// Act
|
|
let a = c.advance();
|
|
let b = c.advance();
|
|
let d = c.advance();
|
|
|
|
// Assert
|
|
assert_eq!(a, 0);
|
|
assert_eq!(b, 1);
|
|
assert_eq!(d, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn seq_counter_saturates_at_max_instead_of_wrapping() {
|
|
// Arrange — prime to u64::MAX - 1 by direct field assignment
|
|
// so the test runs in O(1).
|
|
let mut c = SeqCounter { next: u64::MAX - 1 };
|
|
|
|
// Act
|
|
let a = c.advance();
|
|
let b = c.advance();
|
|
let d = c.advance();
|
|
|
|
// Assert — once we hit MAX, every subsequent call must keep
|
|
// returning MAX (no wrap to 0).
|
|
assert_eq!(a, u64::MAX - 1);
|
|
assert_eq!(b, u64::MAX);
|
|
assert_eq!(d, u64::MAX);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_stamper_capture_advances_seq_and_ts() {
|
|
// Arrange
|
|
let mut s = FrameStamper::new(MonoClock::new());
|
|
|
|
// Act
|
|
let m1 = s.capture();
|
|
let m2 = s.capture();
|
|
|
|
// Assert
|
|
assert_eq!(m1.seq, 0);
|
|
assert_eq!(m2.seq, 1);
|
|
assert!(m2.ts_ns >= m1.ts_ns, "monotonic clock went backwards");
|
|
}
|
|
}
|