Skip to content

sensing: stream sensors to LiveKit from the hardware encoder - #1342

Draft
gareth-morgan-nv wants to merge 4 commits into
gmorgan/sensing-mock-backendfrom
gmorgan/sensing-livekit
Draft

gareth-morgan-nv wants to merge 4 commits into
gmorgan/sensing-mock-backendfrom
gmorgan/sensing-livekit

Conversation

@gareth-morgan-nv

@gareth-morgan-nv gareth-morgan-nv commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Streams SENSING sensors to a LiveKit room as named camera tracks. Each track carries H.264 produced by the plugin's hardware encoder and published through the LiveKit SDK's pre-encoded source, so the SDK forwards the access units and does not encode. Optional, off unless built with -DSENSING_LIVEKIT=ON.

Stacked on the mock backend PR (#1340, gmorgan/sensing-mock-backend). Draft until that merges and the Orin items below are checked. Review this PR's own commits only.

What changed

  • Hardware encoder, passed through. livekit=<track> builds a JetsonEncoder for the track (the V4L2 engine on a Jetson, NVENC through libavcodec in the mock) and publishes with EncodedVideoSource and VideoEncoderBackend::PreEncoded. There is no software encoder and no fallback: if the hardware encoder cannot start, construction fails.
  • Encoder API. JetsonEncoder gains a per-unit drain() (one unit per frame with its own stamp), request_keyframe(), set_bitrate(), a keyframe flag on each unit and backend_name(). File recording is unchanged.
  • Output worker. Per sensor: a GPU snapshot of the borrowed camera frame is finished before update() returns, then an optional integer reduction, the encode, and one SDK call per encoded unit, off the polling loop. Two snapshot slots bound memory; a newer capture replaces the waiting one.
  • Feedback. Keyframe requests from the receiving side make the next frame an IDR with SPS/PPS. A frame the SDK refuses is followed by a keyframe, and nothing is published in between. Bitrate targets are clamped to the configured range and applied only for a real change, at most every few seconds.
  • Metadata. Each frame carries the camera's sequence as its frame ID, the host capture time as its user timestamp, and the sensor's capture TSC as eight bytes of user data, matched to the encoded unit it came from.
  • CLI. --livekit-url, --livekit-token-file (the token never appears on the command line or in output), --livekit-fps, --livekit-scale-div, --livekit-bitrate, and livekit=<track> on --add-stream.
  • Jetson encoder tuning. The V4L2 FAST hardware preset is selected explicitly (a control separate from max-performance clocks), and submit() acquires an input buffer before the RGBA-to-NV12 conversion, so a frame that will be dropped costs no conversion.
  • Receiver page. livekit/receiver/index.html is a static page using the LiveKit browser SDK (CDN, integrity-checked): server URL and subscriber token, Connect/Disconnect, status, and each subscribed video track labelled with its size in its own aspect ratio. No backend or token generator. Installed under receiver/.
  • Packaging. The pinned SDK package (x64 or arm64, SHA-256 checked) is fetched at configure time. cmake --install copies liblivekit.so and liblivekit_ffi.so beside the plugin and the SDK's own LICENSE and NOTICE under licenses/livekit-sdk/.

Validation

On a desktop GPU, against a local livekit-server with a second participant decoding what arrives:

  • sensing_mock_tests: 70 cases pass with LIVEKIT_SERVER set (the server cases skip without it).
  • Tracks, sizes and picture content; frame IDs and exact capture TSCs; a viewer joining late and rejoining; keyframe requests; recovery after a refused frame; the end-of-stream drain; lower bitrate targets; server loss, a bad token, unpublish on shutdown.
  • The transport's stats name the pre-encoded passthrough source, and the output reports the upstream encoder; neither mentions OpenH264.
  • The arm64 SDK package matches its pinned hash and ships the same pre-encoded API header as the x64 one.
  • clang-format-14 and pre-commit are clean.

Known limitations / follow-ups

  • Nothing here ran on an Orin. Not real cameras, not the V4L2 encoder feeding LiveKit, not the arm64 package, not the JETSON build with LiveKit.
  • The Jetson encoder's new code was not compiled off the rig. forceIDR(), runtime setBitrate(), the keyframe flag and the drain are written against the Multimedia API but unbuilt here.
  • WebRTC acceptance of the Jetson engine's H.264 (High profile, Level 5.2) is unproven. The mock's NVENC output with the same settings decoded in the tests.
  • The mock reopens its encoder session to change bitrate, because the libavcodec NVENC wrapper takes its rate control at open; the stream continues from an IDR.
  • Notices. The SDK's LICENSE and NOTICE are installed, but liblivekit_ffi.so bundles third-party components (WebRTC among them) whose notices the SDK package does not ship. That needs a compliance review before the binaries are distributed.
  • Jetson capacity is calculated, not measured (README has the table): head stereo at native size and 30 fps is a plausible first target; four native images at 30 fps depend on the preset and exceed some modules' listed ceilings; four native at 60 fps is not a supported target. Orin Nano has no NVENC. A stream with both output= and livekit= runs two encoders on the one engine, so their pixel rates add. The FAST preset and the input-buffer reordering are unbuilt off the rig.

Test plan

  • sensing_mock_tests with LIVEKIT_SERVER set, on a desktop GPU.
  • On an Orin with SENSING cameras: build with -DSENSING_LIVEKIT=ON (arm64 package) and confirm a subscriber decodes the tracks.
  • Confirm the V4L2 encoder's preset, keyframe, bitrate and drain paths on the rig, and measure the capacity table.
  • Receiver page renders and loads the SDK with its integrity hash (desktop Chrome, headless); media not yet seen in a browser.

🤖 Generated with Claude Code

gareth-morgan-nv and others added 3 commits October 25, 2026 00:46
An --add-stream with livekit=<track> publishes that sensor as an H.264 LiveKit
camera track, from the real cameras or the mock alike. It is built only with
-DSENSING_LIVEKIT=ON, which downloads the pinned LiveKit C++ SDK package for
Ubuntu 24.04 (x64 or arm64) and checks it against its hash. With it off, the
plugin builds and behaves as before and has no LiveKit dependency.

SensingCamera::update() takes a GPU snapshot of the borrowed camera frame and
finishes that copy before returning, since the camera may rewrite its slot at
the second following poll. One worker per sensor then reduces the snapshot if
asked, converts it with the existing RGBA-to-NV12 kernel, copies it to the
host and submits it. Two snapshot slots bound the memory: a newer capture
replaces the one waiting, so a slow network cannot stall capture or the other
sensor. Publication is paced on the capture clock (--livekit-fps), and
--livekit-scale-div reduces every track by an integer divisor that must divide
the sensor exactly and leave even sizes.

Colours are sent as BT.601 limited range, which is what the video pipeline
decodes NV12 as (measured: a full-range gray decodes two levels high and a
BT.709 red comes back at 233). Each frame carries the camera sequence as its
frame ID, the CLOCK_MONOTONIC capture time as its user timestamp, and the
sensor's capture TSC as user data. Under bandwidth pressure the encoder lowers
the frame rate, not the size.

A stream with only livekit= builds no file writer and no H.264 encoder. Losing
the room, or a failed publish, ends capture with an error; there is no
fallback. Settings that need no network (duplicate track names, a divisor that
does not fit) are refused before any connection is made, and the join token,
read from a file, is never printed.

The SDK is reference counted through LiveKitSdkReference, so ending the
plugin's session does not shut it down under another room in the same process.

Tests cover the output against a fake track (pacing, bounded replacement,
ownership of the borrowed frame, conversion, reduction, failure, teardown),
the plugin's command line, and, when LIVEKIT_SERVER names a livekit-server
binary, a real local server with a second participant decoding the tracks:
names, geometry, colours, frame IDs, capture TSC, late join and rejoin, losing
the server, a bad token, and shutdown.

Not verified: the encoder in every run was the SDK's software OpenH264, also
when NVENC or hardware encoding was requested. The arm64 package and the
JETSON backend with LiveKit were not built or run.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Free CUDA allocations when the output's constructor throws, keep the
stats request off the polling loop, take the SDK reference only after
initialisation succeeds, close the test subscriber's streams outside
its mutex, and install the LiveKit runtime beside the plugin.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Each livekit= track now carries H.264 produced by the plugin's
JetsonEncoder (the V4L2 engine on a Jetson, NVENC through libavcodec
in the mock) and published through the SDK's pre-encoded source. There
is no software encoder and no fallback.

The encoder gains a per-unit drain, keyframe requests and a bitrate
setter. The output worker applies the receiving side's keyframe and
bitrate requests, recovers from a refused frame with a keyframe, and
matches each unit to its capture's sequence and TSC. The SDK's LICENSE
and NOTICE install beside its libraries.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
@coderabbitai

coderabbitai Bot commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Select the V4L2 FAST hardware preset explicitly, and acquire an input
buffer before the RGBA-to-NV12 conversion so a frame that will be
dropped costs no conversion. Add a static receiver page for LiveKit
tracks, and document the Jetson capacity limits as unmeasured.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>

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