Skip to content

video_stage: PC worker that forwards Orin cameras to LiveKit over a direct link - #1443

Open
gareth-morgan-nv wants to merge 23 commits into
gmorgan/TimestampAlignmentfrom
gmorgan/VideoStage
Open

gareth-morgan-nv wants to merge 23 commits into
gmorgan/TimestampAlignmentfrom
gmorgan/VideoStage

Conversation

@gareth-morgan-nv

@gareth-morgan-nv gareth-morgan-nv commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The Orin camera producer sends H.264 straight to a PC worker over a direct link, with no LiveKit on the Orin; the PC worker publishes to LiveKit so the egocap consumers (backend, recorder, page) are unchanged. Also adds a management channel and fault diagnosis to the producer, and a mock producer for testing without cameras.

Orin to PC link

  • The producer's DirectSender sends AVS1 H.264 records over a DATA connection and exchanges keyframe requests and a four-timestamp clock exchange over a CTRL connection. It has a bounded queue (size and age), reconnects, and shuts down promptly.
  • Each encoded unit is matched to its capture by the stamp the encoder returns (the encoder is asked to copy input timestamps). Inputs the encoder skipped are counted, and a unit that matches nothing is sent without a capture time. --frame-correlation=fifo keeps an approximate in-order mode as an explicit choice.
  • The producer lowers its bitrate while its direct link drops frames (not while no destination is connected), raises it again after a calm stretch, and spaces forced keyframes at least a second apart.

PC worker (video_stage_worker)

  • Default mode forwards access units untouched; no CUDA context is created. --operation=sine-warp runs NVDEC, an example GPU operation and NVENC; a VUI-less Orin stream's SPS is rewritten to declare no reordering.
  • It never withholds video for a timing problem: when the clock mapping is missing or over --max-upstream-uncertainty-ms, or the record has no source stamp, the frame is published without a capture timestamp and counted. A session reset rebuilds the decoder; a stream with non-default chroma siting is refused.
  • Counters are atomic; the receiver's descriptors are guarded against concurrent shutdown and replacement. Output is fixed-rate (a downstream bitrate request is logged once).

Management channel and fault diagnosis (producer)

  • Optional authenticated WebSocket control server (--control-token-file, off by default; port 5610) with get_status, set_direct_target and ping, plus tools/orin_control.py. One thread owns the camera session; retargets are recorded and applied by that thread.
  • A stall watchdog restarts a session after 5 s without frames. Camera start failures are matched against a table of known failure signatures (including driver log lines, captured around camera start) and reported in plain language, for example a camera power or cable fault.
  • sensing_mock_producer runs the same server, handler, supervisor and DirectSender over looped H.264 files.

Measurements (real Orin, Wi-Fi, two cameras)

Orin-to-publish p50 about 45-78 ms on a good link; clock mapping within about 4.5 ms. On a loaded Wi-Fi link round trips reached 150-300 ms, which is what produced mapping outside the budget; those frames now flow without timestamps instead of being dropped. With the timestamp copy enabled, the Orin reported every unit matched and none skipped. These are not webpage latency.

Validation

  • Run on the PC: unit tests for the correlator, AVS1, clock exchange, SPS parsing, the receiver, DirectSender, and the control server/handler/supervisor (the control server under sanitizers); a mock-producer end-to-end test; pre-commit. These were last run before the final encoder edit, which cannot be built on the PC.
  • Run on a real Orin: the producer builds, streams both cameras to the PC worker, and the stats report stamps matched N, unmatched 0, encoder skipped 0.
  • Not run: the full test suite after the final rebase; sine-warp mode in this revision; the management channel against a real client over a long session.

Base is gmorgan/TimestampAlignment.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Nov 6, 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 6, 2026
@gareth-morgan-nv
gareth-morgan-nv force-pushed the gmorgan/TimestampAlignment branch 2 times, most recently 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/VideoStage branch 2 times, most recently from 27a09c5 to 15bfa3f Compare November 6, 2026 16:58
@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 17 commits November 6, 2026 14:37
…xample sine warp and NVENC

A worker receives H.264 access units over TCP in the repository's AVS1 wire format
(codec 2 added for H.264), decodes on NVDEC into device memory, applies a
GpuOperation (an example sine-wave warp that Part 2 replaces with fisheye
rectification), and re-encodes on NVENC with low-latency settings. Frame identity
travels by key through decode and encode. Tests cover the warp on a GPU and the
wire codec. Measured on one RTX PRO 5000 at 2560x1984, 30 fps, local loopback:
decode p50 1.5 ms, warp+encode p50 ~4 ms, receive-to-output p50 ~5.5 ms.

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

The worker serves one Orin session per configured camera (--stream=PORT:TRACK),
all tracks published under one LiveKit participant, each with its own receiver,
control channel (keyframe requests and a four-timestamp clock exchange whose
replies are matched to outstanding requests), NVDEC -> GPU operation -> NVENC
pipeline and bounded intake queue. Orin stamps are converted onto the PC
monotonic clock before publication; frames are withheld when the mapping is
missing or worse than the budget, and a clock jump unpublishes the track.
The LiveKit publisher library moves to src/livekit_publisher; the Orin producer
gets a per-stream direct= destination.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
A new Orin session unpublishes the previous publication, resets frame numbering
and starts the encoder at an IDR, and its control channel gets an epoch that is
never reused across sessions. The DATA/CTRL hello is read with a timeout on a
socket a stop can reach, so a half-open peer cannot block shutdown.

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]>
…ct sender

Replaces the simple replay script. It mirrors DirectSender and its feed: Jetson-style
H.264 (no AUDs, IDR every 5 s, constant bitrate), frame ids stepping by 2, an offset
and drifting Orin clock, DATA then CTRL hellos with retry, a bounded queue that drops
to the next keyframe, keyframe requests, and clock answers stamped on arrival. The
worker now counts a frame-number gap only when the id does not increase.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
The first failed connection and then one every 30 s are logged with the reason, so
a producer that cannot connect is distinguishable from one that never tried.

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

The sender drops a queue whose oldest unit has waited more than 250 ms and resumes at a
keyframe, keeps its kernel send buffer small so a backlog stays visible, and both ends
notice a dead link in seconds. The worker prints live percentiles every few seconds,
including Orin frame-ready to PC arrival and to publication.

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

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

The real Orin stream makes NVDEC hold back about nine pictures (about 300 ms at 30 fps)
because its sequence header does not state that pictures are never reordered. The worker
now logs the source's SPS and, for a source whose contract is no B-frames, rewrites the
SPS to declare bitstream_restriction with no reordering before decoding. The decode timer
now uses each picture's own submission time, and the decoder lag is reported in frames.

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

Embedded SPS bytes from x264 (with and without B-frames, baseline and high, with CBR
HRD, full range) and NVENC check every parsed field, that the rewrite changes only the
reordering declaration, that the rest of the access unit is untouched, and that it is
idempotent and handles emulation prevention and truncated input.

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]>
The default operation is none: no decode, no GPU, no re-encode, so latency is the Orin,
the link and LiveKit only, with no quality loss. --operation=sine-warp enables the
example GPU stage. The CUDA context is created only when an operation needs it.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
DirectSender: connect is non-blocking and polled so a stop request is noticed
within 100 ms, TCP_USER_TIMEOUT is set before connect, and the destructor
shuts down any socket the worker thread is blocked on. Add a test that bounds
destruction time for a stalled connect, a blocked send and an idle link.

Mock sender: per-camera frame cursors, per-camera keyframe requests, and a
stop() that cancels blocked sockets and fails loudly if the thread is stuck.

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]>
… SDK block

The SDK block lives in src/livekit_publisher; the stray marker broke configure.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
The producer no longer exits when a camera cannot start: it reports a plain
message (for example an unpowered camera), retries, and keeps a WebSocket
management channel up. Clients with the shared token can read the status and
change where a stream's direct link sends its video; every bad request gets an
error reply. The direct sender moves to its own library so a mock producer can
run the same channel, supervisor and sender over looped H.264 files.

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

A fault table covers the producer's own failures (camera power and link,
Argus service, GPU mapping, encoder, LiveKit) and the capture driver's log
lines, which are collected while the cameras start and attached to the
failure. SIPL notifications are decoded by kind; repeated capture timeouts or
sensor-control failures and a stream with no new frame for 5 s fail the
session so it restarts. A direct link that cannot reach the PC is reported
with its reason in the status and as a one-time error event.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
gareth-morgan-nv and others added 6 commits November 6, 2026 14:37
…eKit on the Orin

camera_plugin_sensing no longer links or uses LiveKit: the video output encodes
and hands units to the direct sender, and the PC video stage publishes to
LiveKit. The wire library gains the encoded-frame type and the clock-sync
messages that both sides share, and the LiveKit publisher builds only with the
video stage. Options are --fps, --bitrate and --scale-div; a stream is
sensor=N,name=NAME[,direct=HOST:PORT].

Also: benign I2C NACK lines no longer read as a camera fault, and SIPL status
codes are named in errors.

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

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Signed-off-by: Gareth Morgan <[email protected]>
… space forced keyframes and follow the link

The worker no longer unpublishes a track because the Orin-to-PC clock mapping is
missing or over budget: frames are published without a capture timestamp, so a
viewer keeps its last picture and a recording that needs a timestamp says so.
The producer spaces forced keyframes at least a second apart and lowers its
video bitrate while the direct link has to drop frames, raising it again after a
calm stretch.

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

The management server is stopped before the objects its thread calls into. One
thread owns the camera session; the channel only records a destination change
and the owner applies it before the next frame, including one made while the
cameras were starting. The producer accepts only monotonic timestamp bases. The
PC receiver guards its session descriptors with one lock, and the worker's
counters are atomic. The control server bounds its reads, work per round and
output, and tracks fragmentation. A new source session rebuilds the decoder and
clears the identity ring. Non-default chroma siting is refused, and a downstream
bitrate request is logged once. Stale LiveKit fault text is removed.

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]>
…FIFO order

The Jetson encoder numbers units from its own frame clock unless asked to copy
input timestamps, so FIFO order is not frame identity. Ask the driver to copy
them, match units by stamp, count skipped inputs, and send an unmatched unit
without a capture time. --frame-correlation=fifo keeps the approximate mode as
an explicit choice. The bitrate controller no longer treats an unconnected link
as congestion.

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

This branch has not been deployed

No deployments
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