Skip to content

egocap, sensing: align camera, glove and controller time; show latency and fps - #1437

Open
gareth-morgan-nv wants to merge 57 commits into
gmorgan/Argusfrom
gmorgan/TimestampAlignment
Open

gareth-morgan-nv wants to merge 57 commits into
gmorgan/Argusfrom
gmorgan/TimestampAlignment

Conversation

@gareth-morgan-nv

Copy link
Copy Markdown
Contributor

Summary

Puts camera video, glove and controller data on one PC timeline for recording, and shows end-to-end latency and fps on the page.

  • One clock path. The PC (recorder) and the browser each run a four-timestamp exchange with the camera publisher over LiveKit data messages; the publisher answers with its monotonic clock. A mapping carries its uncertainty (half the round trip plus a drift allowance that grows with age at read time). A take ends on a missing, stale, jumped or over-budget mapping (EGOCAP_CLOCK_UNCERTAINTY_BUDGET_MS, default 5 ms, a policy choice).
  • Recorded timestamps. Camera frames are recorded only on a monotonic basis: monotonic_us (the publisher's frame-ready time, the native sender's default) or capture_monotonic_us (sensor capture event). Tracking keeps the existing host poll timestamp. The take's timing attributes save the mapping evidence, the budget, what each camera time means (frame_ready / sensor_capture) and what each tracking time means.
  • Page. Latency runs from the publisher's per-frame stamp (matched by RTP timestamp) to the expected display time; fps comes from presented frames.
  • Native sender (camera_plugin_sensing). Answers the clock exchange; --timestamp-basis selects monotonic_us (default), capture_monotonic_us (converts the SIPL counter or Argus sensor timestamp through a 1 Hz anchor, failing the stream on an implausible value) or processing_unix_us. SENSING_ENCODER_TIMESTAMP_PROBE=1 logs encoder returned stamps.
  • Agent-only plan notes are no longer checked in (agentPlans/ is git-ignored).

Limits

  • Frame-ready time is later than the sensor's capture event; neither basis proves physical alignment between camera, glove and controllers.
  • The encoder returns no submitted timestamp, so units are paired with captures by submission order; this can shift after an encoder-internal drop, so per-frame times are not hardware-qualified.
  • Tracking device times (controller pose-evaluation time, Manus receipt/publishTime) are not carried into recordings; that needs changes outside egocap.

Validation

Run (mock stack, no hardware):

  • pytest over tests/backend, tests/capture_server, tests/tools, tests/recording_service: 1181 passed, 100 skipped, 1 failed (tests/capture_server/test_gmsl_ipc.py::test_pipeline_serves_each_announced_frame_once_downscaled_as_jpeg, in code this PR does not touch; not checked against a clean checkout).
  • Frontend: vitest 386 passed, tsc --noEmit clean.
  • A take recorded through the local LiveKit server and the mock Orin emulator (fake clock offset, drift and pipeline delay) came out KEPT with the mapping evidence in its attributes. Shared-truth timeline tests read saved timestamp artifacts for both bases, with drop and clock-jump cases.
  • pre-commit hooks (with SKIP=check-copyright-year) pass on the touched files; touched C++ was formatted with clang-format-14.

Not run: the native sender and Argus/SIPL paths were not built against the SDKs, nothing ran on an Orin, the Manus/controller paths were not exercised, and the page was not verified in a real browser by the author's tooling. The sensor-counter conversion (capture_monotonic_us) is unverified on hardware.

Stacked on #1429 (Argus), which stacks on #1428 (RearchBackend) and #1426 (FastLiveKit).

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@ivany-nv ivany-nv added the NVIDIA label Nov 5, 2026
@gareth-morgan-nv
gareth-morgan-nv force-pushed the gmorgan/TimestampAlignment branch from ba2d1d7 to b7d8f99 Compare November 6, 2026 01:42
@gareth-morgan-nv
gareth-morgan-nv force-pushed the gmorgan/TimestampAlignment branch from b7d8f99 to be24085 Compare November 6, 2026 05:39
@gareth-morgan-nv
gareth-morgan-nv force-pushed the gmorgan/TimestampAlignment branch from be24085 to fbdc7e3 Compare November 6, 2026 05:42
@gareth-morgan-nv
gareth-morgan-nv force-pushed the gmorgan/TimestampAlignment branch from c6f0635 to d9d408f Compare November 6, 2026 18:04
@gareth-morgan-nv
gareth-morgan-nv force-pushed the gmorgan/TimestampAlignment branch from d9d408f to 0dc1c08 Compare November 6, 2026 20:31
gareth-morgan-nv and others added 11 commits November 6, 2026 14:37
UnixClockMap keeps the bracket uncertainty of its pair and flags a change in
the offset between measurements that exceeds the larger of 5 ms and four times
that uncertainty. The recording feed then fails the take instead of recording
frames with a guessed mapping; frames already mapped are never remapped.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
…sh delay), shared by all tracks

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
…hetic camera/tracking timeline

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
The rate is the change in the browser's cumulative presented-frame count over
a one-second window, not a tally of callbacks; a hidden tab shows paused, a
stalled stream 0 FPS, and a browser without frame callbacks 'FPS unavailable'.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
Each camera stamp is truth + offset + elapsed_s * drift_ppm over a 60 s span,
and the recorded residual is asserted per frame and to increase.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
gareth-morgan-nv and others added 28 commits November 6, 2026 14:37
…mp and a four-timestamp clock mapping

The preview now asks livekit-client for the per-frame packet-trailer metadata
(worker option), runs a four-timestamp exchange with the publisher over the
room's data channel (viewer tokens may publish data, never media), and each
tile matches the displayed frame's RTP timestamp to the publisher's capture
stamp, maps the frame's expected display time onto the publisher's clock, and
shows the window's median, 95th percentile and the mapping's uncertainty. A
frame with no stamp, a stream whose basis is not a capture event on the
publisher's monotonic clock, a stale mapping, or a window spanning a mapping
discontinuity has no figure. Tests follow.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
…e recording tests to the four-timestamp path

A mapping is stale when no exchange has arrived within the limit, not when the
best exchange in the window is old, which made a healthy jittery link go
stale (Python and browser estimators). The recording, source, app, receiver,
loader and emulator tests move to the monotonic capture basis and the clock
mapping, UnixClockMap's tests are removed, and the synthetic timeline is one
shared event timeline for a camera and a glove-like source, each with its own
clock and delays, mapped by the real estimator.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
The tile's two labels are the displayed FPS (frames presented over the
one-second window) and the mean capture-to-display latency of the window in
ms; '– FPS' and '– ms' stand in when a number is not available. The
uncertainty and 95th percentile move into the tooltip. Tests for the
newest-exchange staleness fix are included; the tile tests follow.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
…rowser latency path

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
… clock mapping

The displayed rate now divides the change in presented frames by the
presentation times of the two frames that bound the window, and a counter
reset becomes the new baseline. The browser clock sync assumes a conservative
1 ms timer resolution instead of spinning the UI thread to measure it. Both
estimators pick the exchange with the smallest uncertainty now, half its
round trip plus the drift its age allows, and report that uncertainty, so a
low-round-trip exchange stops being preferred once it is old. Tests follow.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
…nd the tracking host-poll boundary

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
The LiveKit session parses four-timestamp requests (topic
isaaccapture.clock-sync, 13 bytes) in onUserPacketReceived, stamps t2 first on
CLOCK_MONOTONIC, and a small responder thread sends the 29-byte reply to the
requester only, unreliable, with t3 written just before the send. The queue is
bounded; anything else is ignored. The wire format is in clock_sync.hpp. The
frames still declare processing time. Syntax-checked against the pinned SDK
v1.12.4 headers here; not built or run on a Jetson.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
… the Python side

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
…utes

The recording service takes timing evidence for the active episode
(set_episode_timing, in memory, written under 'timing' in the kept episode's
attributes; a take recovered after a crash has none). The coordinator gathers it
from the LiveKit source before the room is left: the method, the clock names,
each stream's timestamp basis, the mappings adopted (epoch, offset, round trip,
uncertainty, time accepted; the newest 64), whether a discontinuity occurred,
rejected exchanges, and that tracking time is the host poll time. The
estimator's history is bounded. Tests follow.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
…th tests

The estimator re-reports one exchange with a growing age allowance while no
newer exchange arrives; the take's evidence now keeps the first report of each
exchange. Adds tests for set_episode_timing, timing_evidence(), the coordinator
wiring and the bounded estimator history.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
…e the encoder's returned stamps

--timestamp-basis=capture_monotonic_us converts the backend's sensor timestamp
(SIPL capture counter as generic-counter ticks, Argus sensor timestamp as
CLOCK_MONOTONIC_RAW ns) through a low-rate anchor pair and fails the stream on
an implausible conversion; the default stays processing time. The units are
assumptions checked per frame, not verified on hardware.
SENSING_ENCODER_TIMESTAMP_PROBE=1 logs returned unit stamps beside the last
submitted one. Host tests cover the mapping.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
EGOCAP_CLOCK_UNCERTAINTY_BUDGET_MS (default 5 ms, a policy choice) bounds the
mapping uncertainty a take may be recorded with; the budget is saved in the
take's timing evidence. Docs describe the native capture-time option and the
remaining open items.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
…racking times mean

ClockEstimator.mapping(now) grows the uncertainty with the age of the exchange,
so the recording budget cannot pass on a figure from before a reply gap. The
take's timing evidence names what each tracking source's time is (host poll;
controller pose-evaluation time; Manus host time at push) and that physical
alignment is unavailable. The native capture_monotonic_us option is described as
unverified.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
…and aging tests

The browser estimator recomputes the mapping's uncertainty from the exchange's
age, as the PC estimator does. New tests: a camera, glove-like and
controller-like source with independent clocks and delays through real video
and numeric ingestion into a saved take, with drop and clock-jump cases; a feed
that fails once aging pushes the mapping over budget. Existing estimator tests
now expect the aged figure.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
Each SDK callback reads CLOCK_MONOTONIC on entry; the skeleton and raw-sensor
samples keep that receipt time with the cached data and are injected or pushed
with it, once. The SDK publishTime stays unused: its sampling event is not
specified by the vendor. Not built or run here (Manus SDK and OpenXR runtime
needed).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
… default

The native sender publishes its monotonic frame-ready time (monotonic_us) by
default; recording accepts that basis alongside the sensor-capture basis, the
take's timing evidence names which, and the page computes latency from either.
The agent-only plan notes move to the git-ignored agentPlans/ folder. Tests
extend the saved-artifact timeline fixture over both bases.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
… as the code now has them

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
… one docstring line

The ingress and readiness tests built streams on a basis that is no longer
recordable, so no frame counted toward the budget. Use the sensor-capture
monotonic basis, which is.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
… no longer exists

FrameMeta lost stamp_us when the application timestamp moved to unix_us; the
frame's own timestamp is the unit's.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
The effect runs once per track but the clock arrives after it, so it closed over
a null clock and never showed latency. It now reads both through refs.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
…pping

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>

This branch was successfully deployed

1 active (outdated) deployment
dev — d9d408fa Deployed Nov 6, 2026 by gareth-morgan-nv via publish-wheel #6574
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants