Skip to content

Commit 527aba5

Browse files
chenosauruscursoragentgithub-actions[bot]
authored
Add support for Nvidia Jetson V4L2 M2M video encoder (#1161)
- Add support for Nvidia Jetson V4L2 M2M video encoder - Support H264, H265, AV1 encoding - Update the local_video example with --source flag that can be `uvc` or `argus` --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 5ebced2 commit 527aba5

35 files changed

Lines changed: 5729 additions & 102 deletions

.changeset/jetson-video-publish.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
webrtc-sys: patch
3+
libwebrtc: patch
4+
---
5+
6+
Add Jetson DMA-buffer video publishing support for libargus MIPI capture and the Jetson hardware encoder, including AV1 hardware encoding on supported Jetson Orin devices.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/local_video/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,6 @@ nokhwa = { git = "https://github.com/l1npengtul/nokhwa", rev = "4923ecab7cf26f9d
6060

6161
[target.'cfg(target_os = "windows")'.dependencies]
6262
nokhwa = { git = "https://github.com/l1npengtul/nokhwa", rev = "4923ecab7cf26f9dba83867a15a9d8662d021296", default-features = false, features = ["input-msmf"] }
63+
64+
[build-dependencies]
65+
cc = { workspace = true }

examples/local_video/README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,29 @@ Publisher usage:
5858
--room-name demo \
5959
--identity cam-1
6060
61+
# request MJPEG camera capture to reduce USB bandwidth
62+
cargo run -p local_video -F desktop --bin publisher -- \
63+
--camera-index 0 \
64+
--format mjpeg \
65+
--room-name demo \
66+
--identity cam-1
67+
68+
# publish from a Jetson MIPI CSI camera through libargus and the Jetson hardware encoder
69+
cargo run -p local_video -F desktop --bin publisher -- \
70+
--source argus \
71+
--camera-index 0 \
72+
--codec h265 \
73+
--room-name demo \
74+
--identity jetson-cam-1
75+
76+
# publish AV1 through the Jetson hardware encoder (Orin only)
77+
cargo run -p local_video -F desktop --bin publisher -- \
78+
--source argus \
79+
--camera-index 0 \
80+
--codec av1 \
81+
--room-name demo \
82+
--identity jetson-cam-1
83+
6184
# publish a static SMPTE color-bar test pattern (no camera required)
6285
cargo run -p local_video -F desktop --bin publisher -- \
6386
--test-pattern \
@@ -99,11 +122,13 @@ The clock draws a 3x9 grid below the time. The top row fills from `0` to `9` for
99122

100123
Publisher flags (in addition to the common connection flags above):
101124
- `--camera-index <n>`: Camera index to use (default: `0`). Use `--list-cameras` to see available indices.
125+
- `--source <uvc|argus>`: Camera backend to use (default: `uvc`). `argus` uses NVIDIA libargus for MIPI CSI cameras and is available only on Linux aarch64 Jetson builds.
126+
- `--format <auto|yuv|mjpeg>`: UVC camera capture format (default: `auto`). `auto` tries uncompressed YUYV first and falls back to MJPEG; `mjpeg` can reduce USB bandwidth when running multiple cameras.
102127
- `--test-pattern`: Generate a standard SMPTE 75% color-bar test pattern instead of capturing from a camera. `--camera-index` is ignored when this is set; `--width`, `--height`, and `--fps` still control the output resolution and frame rate.
103128
- `--width <px>`: Desired capture width (default: `1280`).
104129
- `--height <px>`: Desired capture height (default: `720`).
105130
- `--fps <n>`: Desired capture framerate (default: `30`).
106-
- `--codec <codec>`: Video codec to use for publishing: `h264`, `h265`, `vp8`, `vp9`, or `av1` (default: `h264`). H.265 falls back to H.264 on failure.
131+
- `--codec <codec>`: Video codec to use for publishing: `h264`, `h265`, `vp8`, `vp9`, or `av1` (default: `h264`). H.265 falls back to H.264 on failure. On Jetson Orin, `h264`, `h265`, and `av1` use the hardware encoder; elsewhere `av1` is encoded in software via libaom.
107132
- `--simulcast`: Publish simulcast video (multiple layers when the resolution is large enough).
108133
- `--max-bitrate <bps>`: Max video bitrate for the main (highest) layer in bits per second (e.g. `1500000`).
109134
- `--attach-timestamp`: Attach the current wall-clock time (microseconds since UNIX epoch) as the user timestamp on each published frame. The subscriber can display this to measure end-to-end latency.
@@ -154,3 +179,6 @@ Notes:
154179
- If the active video track is unsubscribed or unpublished, the app clears its state and will automatically attach to the next matching video track when it appears.
155180
- For E2EE to work, both publisher and subscriber must specify the same `--e2ee-key` value. If the keys don't match, the subscriber will not be able to decode the video.
156181
- The timestamp overlay updates at ~2 Hz so the latency value is readable rather than flickering every frame.
182+
- On Jetson, `--source argus` requires the Jetson Multimedia API headers under `/usr/src/jetson_multimedia_api`. It publishes NV12 DMA buffers through the Jetson hardware encoder; local publisher preview and burned timestamps are not supported on that path.
183+
- Jetson AV1 hardware encoding requires an Orin-class device (e.g. Orin NX or AGX Orin on JetPack 5+); the encoder is probed at startup and on devices without AV1 support (e.g. Xavier) `--codec av1` automatically falls back to the software libaom encoder. The Jetson AV1 encoder produces a single L1T1 stream (no SVC).
184+
- On Linux, preview windows use the Vulkan `wgpu` backend by default to avoid GLES/EGL conflicts on Jetson desktops. Set `WGPU_BACKEND=gl` or another supported `wgpu` backend to override this.

examples/local_video/build.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use std::path::PathBuf;
2+
3+
fn main() {
4+
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
5+
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
6+
7+
// Only compile the Argus shim on aarch64 Linux (Jetson).
8+
if target_os != "linux" || target_arch != "aarch64" {
9+
return;
10+
}
11+
12+
let argus_include = PathBuf::from("/usr/src/jetson_multimedia_api/argus/include");
13+
let mmapi_include = PathBuf::from("/usr/src/jetson_multimedia_api/include");
14+
15+
if !argus_include.exists() {
16+
println!(
17+
"cargo:warning=Argus headers not found at {}; skipping lk_argus build",
18+
argus_include.display()
19+
);
20+
return;
21+
}
22+
23+
println!("cargo:rerun-if-changed=src/lk_argus.cpp");
24+
25+
cc::Build::new()
26+
.cpp(true)
27+
.file("src/lk_argus.cpp")
28+
.include(&argus_include)
29+
.include(&mmapi_include)
30+
.flag("-std=c++14")
31+
.flag("-Wno-deprecated-declarations")
32+
.compile("lk_argus");
33+
34+
// Link Argus client library (talks to nvargus-daemon) and NvBufSurface
35+
println!("cargo:rustc-link-lib=dylib=nvargus_socketclient");
36+
println!("cargo:rustc-link-lib=dylib=nvbufsurface");
37+
38+
// Tegra library path
39+
let tegra_lib_dir = PathBuf::from("/usr/lib/aarch64-linux-gnu/tegra");
40+
if tegra_lib_dir.exists() {
41+
println!("cargo:rustc-link-search=native={}", tegra_lib_dir.display());
42+
}
43+
44+
// Standard aarch64 library path
45+
println!("cargo:rustc-link-search=native=/usr/lib/aarch64-linux-gnu");
46+
}

examples/local_video/src/argus.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
//! Thin FFI wrapper around NVIDIA Argus/libargus for MIPI CSI camera capture.
2+
//!
3+
//! This module provides DMA-buffer frame acquisition from MIPI cameras on Jetson
4+
//! platforms. Frames are blitted from Argus' EGLStream frame into NvBufSurface
5+
//! DMA file descriptors that can be passed to the hardware encoder without
6+
//! CPU-side pixel copies.
7+
//!
8+
//! The Argus API is C++, so we use a small C shim (linked via build.rs on
9+
//! Jetson) to expose the capture session lifecycle.
10+
11+
use std::ffi::c_int;
12+
use std::io;
13+
14+
/// Opaque handle to an Argus capture session.
15+
pub struct ArgusCaptureSession {
16+
handle: *mut std::ffi::c_void,
17+
width: u32,
18+
height: u32,
19+
}
20+
21+
/// A captured Argus frame backed by a DMA buffer.
22+
pub struct ArgusFrame {
23+
/// DMA buffer file descriptor containing an NV12 frame.
24+
pub dmabuf_fd: i32,
25+
/// Argus sensor start timestamp in nanoseconds, when available.
26+
pub sensor_timestamp_ns: Option<u64>,
27+
/// Time spent waiting for `FrameConsumer::acquireFrame` to return.
28+
pub acquire_wait_ns: u64,
29+
/// Time spent copying the acquired EGLStream frame into the DMA buffer.
30+
pub blit_ns: u64,
31+
}
32+
33+
// The C++ session is single-threaded but we move it across the tokio runtime.
34+
unsafe impl Send for ArgusCaptureSession {}
35+
36+
extern "C" {
37+
fn lk_argus_create_session(
38+
sensor_index: c_int,
39+
width: c_int,
40+
height: c_int,
41+
fps: c_int,
42+
) -> *mut std::ffi::c_void;
43+
44+
fn lk_argus_destroy_session(session: *mut std::ffi::c_void);
45+
46+
/// Acquire the next frame and optionally return the Argus sensor timestamp.
47+
/// Returns the NvBufSurface DMA fd, or -1 on error.
48+
/// The fd is valid until the next acquire call or `lk_argus_release_frame`.
49+
fn lk_argus_acquire_frame_with_metadata(
50+
session: *mut std::ffi::c_void,
51+
sensor_timestamp_ns: *mut u64,
52+
acquire_wait_ns: *mut u64,
53+
blit_ns: *mut u64,
54+
) -> c_int;
55+
56+
/// Release the most recently acquired frame back to the Argus buffer pool.
57+
fn lk_argus_release_frame(session: *mut std::ffi::c_void);
58+
}
59+
60+
impl ArgusCaptureSession {
61+
/// Open an Argus capture session on the given MIPI CSI sensor.
62+
///
63+
/// `sensor_index` selects the camera (0 for the first CSI camera).
64+
/// The session negotiates the given resolution and framerate with the ISP.
65+
pub fn new(sensor_index: u32, width: u32, height: u32, fps: u32) -> io::Result<Self> {
66+
let handle = unsafe {
67+
lk_argus_create_session(
68+
sensor_index as c_int,
69+
width as c_int,
70+
height as c_int,
71+
fps as c_int,
72+
)
73+
};
74+
if handle.is_null() {
75+
return Err(io::Error::new(
76+
io::ErrorKind::Other,
77+
"Failed to create Argus capture session",
78+
));
79+
}
80+
Ok(Self { handle, width, height })
81+
}
82+
83+
/// Acquire the next captured frame as a DMA buffer.
84+
///
85+
/// The returned fd refers to an NvBufSurface in NV12 format. It remains
86+
/// valid until [`release_frame`](Self::release_frame) is called or the
87+
/// next `acquire_frame` implicitly releases the previous one.
88+
pub fn acquire_frame(&mut self) -> io::Result<ArgusFrame> {
89+
let mut sensor_timestamp_ns = 0;
90+
let mut acquire_wait_ns = 0;
91+
let mut blit_ns = 0;
92+
let fd = unsafe {
93+
lk_argus_acquire_frame_with_metadata(
94+
self.handle,
95+
&mut sensor_timestamp_ns,
96+
&mut acquire_wait_ns,
97+
&mut blit_ns,
98+
)
99+
};
100+
if fd < 0 {
101+
return Err(io::Error::new(io::ErrorKind::Other, "Argus frame acquisition failed"));
102+
}
103+
Ok(ArgusFrame {
104+
dmabuf_fd: fd,
105+
sensor_timestamp_ns: (sensor_timestamp_ns > 0).then_some(sensor_timestamp_ns),
106+
acquire_wait_ns,
107+
blit_ns,
108+
})
109+
}
110+
111+
/// Release the most recently acquired frame back to the buffer pool.
112+
pub fn release_frame(&mut self) {
113+
unsafe { lk_argus_release_frame(self.handle) };
114+
}
115+
116+
pub fn width(&self) -> u32 {
117+
self.width
118+
}
119+
120+
pub fn height(&self) -> u32 {
121+
self.height
122+
}
123+
}
124+
125+
impl Drop for ArgusCaptureSession {
126+
fn drop(&mut self) {
127+
if !self.handle.is_null() {
128+
unsafe { lk_argus_destroy_session(self.handle) };
129+
self.handle = std::ptr::null_mut();
130+
}
131+
}
132+
}
133+
134+
/// Convert an Argus `CLOCK_MONOTONIC` sensor timestamp into a UNIX-epoch microsecond value
135+
/// by computing the offset between the current monotonic clock and the supplied wall time.
136+
pub fn sensor_monotonic_ns_to_unix_us(sensor_timestamp_ns: u64, wall_time_us: u64) -> Option<u64> {
137+
let monotonic_now_ns = monotonic_time_ns_now()?;
138+
let monotonic_delta_us = monotonic_now_ns.abs_diff(sensor_timestamp_ns) / 1_000;
139+
if sensor_timestamp_ns <= monotonic_now_ns {
140+
Some(wall_time_us.saturating_sub(monotonic_delta_us))
141+
} else {
142+
Some(wall_time_us.saturating_add(monotonic_delta_us))
143+
}
144+
}
145+
146+
/// Current `CLOCK_MONOTONIC` value in nanoseconds, used to translate Argus sensor
147+
/// timestamps into wall time.
148+
fn monotonic_time_ns_now() -> Option<u64> {
149+
#[repr(C)]
150+
struct Timespec {
151+
tv_sec: i64,
152+
tv_nsec: i64,
153+
}
154+
155+
extern "C" {
156+
fn clock_gettime(clk_id: i32, tp: *mut Timespec) -> i32;
157+
}
158+
159+
const CLOCK_MONOTONIC: i32 = 1;
160+
let mut ts = Timespec { tv_sec: 0, tv_nsec: 0 };
161+
let ret = unsafe {
162+
// SAFETY: `ts` is a valid, writable `Timespec` for the duration of the call.
163+
clock_gettime(CLOCK_MONOTONIC, &mut ts)
164+
};
165+
if ret != 0 || ts.tv_sec < 0 || ts.tv_nsec < 0 {
166+
return None;
167+
}
168+
Some(ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64)
169+
}

0 commit comments

Comments
 (0)