From db0a70a334a88ec5a0dfe46b48ac7e7436cc04b1 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:46:41 -0700 Subject: [PATCH 1/6] Create libargus-sys crate --- Cargo.lock | 7 +++++++ Cargo.toml | 2 ++ libargus-sys/Cargo.toml | 12 ++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 libargus-sys/Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index 56683ad70..67e10f8e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3766,6 +3766,13 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" +[[package]] +name = "libargus-sys" +version = "0.1.0" +dependencies = [ + "cc", +] + [[package]] name = "libc" version = "0.2.189" diff --git a/Cargo.toml b/Cargo.toml index dcdbc0140..bb18f53c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "livekit-wakeword", "tools/bindgens", "libwebrtc", + "libargus-sys", "soxr-sys", "yuv-sys", "imgproc", @@ -69,6 +70,7 @@ livekit-net = { version = "0.1.3", path = "livekit-net" } livekit-protocol = { version = "0.7.13", path = "livekit-protocol" } livekit-region = { version = "0.1.1", path = "livekit-region" } livekit-rpc = { version = "0.1.1", path = "livekit-rpc" } +libargus-sys = { version = "0.1.0", path = "libargus-sys" } soxr-sys = { version = "0.1.3", path = "soxr-sys" } webrtc-sys = { version = "0.3.45", path = "webrtc-sys" } webrtc-sys-build = { version = "0.3.19", path = "webrtc-sys/build" } diff --git a/libargus-sys/Cargo.toml b/libargus-sys/Cargo.toml new file mode 100644 index 000000000..0e45b7d15 --- /dev/null +++ b/libargus-sys/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "libargus-sys" +version = "0.1.0" +edition.workspace = true +license.workspace = true +description = "Native NVIDIA libargus (Jetson) capture shim and FFI bindings" +readme = "README.md" +repository.workspace = true +links = "lk_argus" + +[build-dependencies] +cc = { workspace = true } From 25c075ea086a60b8d4625728e11073ab01feb9f2 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:46:41 -0700 Subject: [PATCH 2/6] Configure release manager for new crate --- knope.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/knope.toml b/knope.toml index bf337b338..6b2607041 100644 --- a/knope.toml +++ b/knope.toml @@ -225,3 +225,11 @@ versioned_files = [ { path = "Cargo.toml", dependency = "livekit-capture" }, ] changelog = "livekit-capture/CHANGELOG.md" + +[packages.libargus-sys] +versioned_files = [ + "libargus-sys/Cargo.toml", + "Cargo.lock", + { path = "Cargo.toml", dependency = "libargus-sys" }, +] +changelog = "libargus-sys/CHANGELOG.md" From 1795b6bb07a351835ed72a14b674cdec66505272 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:46:42 -0700 Subject: [PATCH 3/6] Add C++ shim --- libargus-sys/src/lk_argus.cpp | 939 ++++++++++++++++++++++++++++++++++ libargus-sys/src/lk_argus.h | 213 ++++++++ 2 files changed, 1152 insertions(+) create mode 100644 libargus-sys/src/lk_argus.cpp create mode 100644 libargus-sys/src/lk_argus.h diff --git a/libargus-sys/src/lk_argus.cpp b/libargus-sys/src/lk_argus.cpp new file mode 100644 index 000000000..058d8a229 --- /dev/null +++ b/libargus-sys/src/lk_argus.cpp @@ -0,0 +1,939 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// C shim around NVIDIA libargus for MIPI CSI camera capture on Jetson. +// See lk_argus.h for the ABI and the thread-safety contract. + +#include "lk_argus.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include "NvBufSurface.h" + +namespace { + +constexpr int kDefaultNumDmaBufs = 4; +constexpr int kMinNumDmaBufs = 2; + +// --------------------------------------------------------------------------- +// Logging + +std::mutex g_log_mutex; +LkArgusLogFn g_log_fn = nullptr; +void* g_log_user_data = nullptr; + +#if defined(__GNUC__) +__attribute__((format(printf, 2, 3))) +#endif +void lk_log(int32_t level, const char* fmt, ...) { + char buf[512]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + std::lock_guard lock(g_log_mutex); + if (g_log_fn) { + g_log_fn(level, buf, g_log_user_data); + } else { + fprintf(stderr, "[lk_argus] %s\n", buf); + } +} + +// --------------------------------------------------------------------------- +// Process-wide CameraProvider +// +// Argus::CameraProvider is a process singleton in libargus, and repeatedly +// creating/destroying it across sessions is flaky on some JetPack releases. +// Create it lazily on first use and keep it for the life of the process +// (intentional leak); all open sessions and enumeration calls share it. + +std::mutex g_provider_mutex; +Argus::CameraProvider* g_provider = nullptr; + +// Requires g_provider_mutex to be held. +Argus::ICameraProvider* provider_locked() { + if (!g_provider) { + g_provider = Argus::CameraProvider::create(); + if (!g_provider) { + lk_log(LK_ARGUS_LOG_ERROR, + "failed to create CameraProvider (is nvargus-daemon running?)"); + return nullptr; + } + auto* i_provider = Argus::interface_cast(g_provider); + if (i_provider) { + lk_log(LK_ARGUS_LOG_INFO, "Argus version: %s", + i_provider->getVersion().c_str()); + } + } + return Argus::interface_cast(g_provider); +} + +// Requires g_provider_mutex to be held. +int32_t devices_locked(std::vector* devices) { + Argus::ICameraProvider* i_provider = provider_locked(); + if (!i_provider) { + return LK_ARGUS_ERR_NO_PROVIDER; + } + Argus::Status status = i_provider->getCameraDevices(devices); + if (status != Argus::STATUS_OK) { + lk_log(LK_ARGUS_LOG_ERROR, "getCameraDevices failed: %d", + static_cast(status)); + return LK_ARGUS_ERR_ARGUS; + } + return LK_ARGUS_OK; +} + +// --------------------------------------------------------------------------- +// Sensor timestamp helpers + +enum class SensorTimestampStatus { + Available, + InvalidArgs, + NoEventQueue, + EventWaitFailed, + NoCaptureCompleteEvent, + CaptureCompleteFailed, + NoEventMetadata, + NoOutputStream, + MetadataCreateFailed, + NoCaptureMetadata, + ZeroTimestamp, +}; + +const char* sensor_timestamp_status_name(SensorTimestampStatus status) { + switch (status) { + case SensorTimestampStatus::Available: + return "available"; + case SensorTimestampStatus::InvalidArgs: + return "invalid args"; + case SensorTimestampStatus::NoEventQueue: + return "no capture-complete event queue"; + case SensorTimestampStatus::EventWaitFailed: + return "capture-complete event wait failed"; + case SensorTimestampStatus::NoCaptureCompleteEvent: + return "no capture-complete event"; + case SensorTimestampStatus::CaptureCompleteFailed: + return "capture-complete event failed"; + case SensorTimestampStatus::NoEventMetadata: + return "no capture-complete metadata"; + case SensorTimestampStatus::NoOutputStream: + return "no EGL output stream"; + case SensorTimestampStatus::MetadataCreateFailed: + return "metadata container create failed"; + case SensorTimestampStatus::NoCaptureMetadata: + return "no capture metadata interface"; + case SensorTimestampStatus::ZeroTimestamp: + return "zero sensor timestamp"; + } + return "unknown"; +} + +} // namespace + +struct LkArgusSession { + Argus::UniqueObj session; + Argus::UniqueObj stream_settings; + Argus::UniqueObj stream; + Argus::UniqueObj request; + Argus::UniqueObj event_queue; + Argus::UniqueObj consumer; + + // DMA buffer ring. A slot is "leased" from lk_argus_frame_acquire until + // lk_argus_frame_release; the blit never targets a leased slot, so a + // frame's fd stays valid however long the consumer holds it. + int num_dma_bufs = 0; + int dmabuf_fds[LK_ARGUS_MAX_DMA_BUFS]; + NvBufSurface* dmabuf_surfaces[LK_ARGUS_MAX_DMA_BUFS]; + bool leased[LK_ARGUS_MAX_DMA_BUFS]; + int next_slot = 0; // ring scan start, only touched by the acquire thread + std::mutex lease_mutex; + + std::atomic interrupted{false}; + std::atomic last_argus_status{0}; + + int width = 0; + int height = 0; + bool metadata_enabled = false; + bool event_metadata_enabled = false; + + // Log rate-limiting state for sensor timestamp availability (per session, + // only touched by the acquire thread). + SensorTimestampStatus last_logged_ts_status = SensorTimestampStatus::Available; +}; + +namespace { + +SensorTimestampStatus read_sensor_timestamp_ns_from_event( + LkArgusSession* s, + uint64_t* sensor_timestamp_ns, + Argus::Status* metadata_status) { + if (metadata_status) *metadata_status = Argus::STATUS_OK; + if (!s || !sensor_timestamp_ns) return SensorTimestampStatus::InvalidArgs; + *sensor_timestamp_ns = 0; + + auto* i_event_provider = + Argus::interface_cast(s->session); + auto* i_event_queue = Argus::interface_cast(s->event_queue); + if (!i_event_provider || !i_event_queue) { + return SensorTimestampStatus::NoEventQueue; + } + + Argus::Status status = + i_event_provider->waitForEvents(s->event_queue.get(), 1000000); + if (metadata_status) *metadata_status = status; + if (status != Argus::STATUS_OK) { + return SensorTimestampStatus::EventWaitFailed; + } + + const Argus::Event* newest_capture_complete = nullptr; + for (uint32_t i = 0; i < i_event_queue->getSize(); i++) { + const Argus::Event* event = i_event_queue->getEvent(i); + auto* i_event = Argus::interface_cast(event); + if (i_event && i_event->getEventType() == Argus::EVENT_TYPE_CAPTURE_COMPLETE) { + newest_capture_complete = event; + } + } + if (!newest_capture_complete) { + return SensorTimestampStatus::NoCaptureCompleteEvent; + } + + auto* i_capture_complete = + Argus::interface_cast( + newest_capture_complete); + if (!i_capture_complete) { + return SensorTimestampStatus::NoCaptureCompleteEvent; + } + status = i_capture_complete->getStatus(); + if (metadata_status) *metadata_status = status; + if (status != Argus::STATUS_OK) { + return SensorTimestampStatus::CaptureCompleteFailed; + } + + const Argus::CaptureMetadata* metadata = i_capture_complete->getMetadata(); + if (!metadata) { + return SensorTimestampStatus::NoEventMetadata; + } + + auto* i_metadata = + Argus::interface_cast(metadata); + if (!i_metadata) { + return SensorTimestampStatus::NoCaptureMetadata; + } + + *sensor_timestamp_ns = i_metadata->getSensorTimestamp(); + if (*sensor_timestamp_ns == 0) { + return SensorTimestampStatus::ZeroTimestamp; + } + return SensorTimestampStatus::Available; +} + +SensorTimestampStatus read_sensor_timestamp_ns_from_egl_metadata( + LkArgusSession* s, + uint64_t* sensor_timestamp_ns, + Argus::Status* metadata_status) { + if (metadata_status) *metadata_status = Argus::STATUS_OK; + if (!s || !sensor_timestamp_ns) return SensorTimestampStatus::InvalidArgs; + *sensor_timestamp_ns = 0; + + auto* i_stream = Argus::interface_cast(s->stream); + if (!i_stream) return SensorTimestampStatus::NoOutputStream; + + Argus::Status status; + EGLStream::MetadataContainer* metadata = EGLStream::MetadataContainer::create( + i_stream->getEGLDisplay(), i_stream->getEGLStream(), + EGLStream::MetadataContainer::CONSUMER, &status); + if (metadata_status) *metadata_status = status; + if (status != Argus::STATUS_OK || !metadata) { + return SensorTimestampStatus::MetadataCreateFailed; + } + + auto* i_metadata = Argus::interface_cast(metadata); + if (!i_metadata) { + metadata->destroy(); + return SensorTimestampStatus::NoCaptureMetadata; + } + + *sensor_timestamp_ns = i_metadata->getSensorTimestamp(); + metadata->destroy(); + if (*sensor_timestamp_ns == 0) { + return SensorTimestampStatus::ZeroTimestamp; + } + return SensorTimestampStatus::Available; +} + +SensorTimestampStatus read_sensor_timestamp_ns(LkArgusSession* s, + uint64_t* sensor_timestamp_ns, + Argus::Status* metadata_status) { + SensorTimestampStatus status = read_sensor_timestamp_ns_from_egl_metadata( + s, sensor_timestamp_ns, metadata_status); + if (status == SensorTimestampStatus::Available) { + return status; + } + + // Fall back to capture-complete events only when embedded EGLStream + // metadata is unavailable. Event queues are session-scoped, so they can lag + // or lead the exact frame returned by FrameConsumer::acquireFrame(). + SensorTimestampStatus egl_status = status; + Argus::Status egl_metadata_status = + metadata_status ? *metadata_status : Argus::STATUS_OK; + + SensorTimestampStatus event_status = + read_sensor_timestamp_ns_from_event(s, sensor_timestamp_ns, metadata_status); + if (event_status == SensorTimestampStatus::Available) { + return event_status; + } + + if (metadata_status) *metadata_status = egl_metadata_status; + return egl_status; +} + +// Destroys the persistent NvBufSurface ring entries [0, count), releasing +// their DMA-BUF fds. Entries that were never created (nullptr) are skipped, +// so this is safe on a partially-initialized session. +void destroy_dmabuf_surfaces(LkArgusSession* s, int count) { + for (int i = 0; i < count; i++) { + if (s->dmabuf_surfaces[i]) { + NvBufSurfaceDestroy(s->dmabuf_surfaces[i]); + s->dmabuf_surfaces[i] = nullptr; + } + s->dmabuf_fds[i] = -1; + } +} + +} // namespace + +extern "C" { + +int32_t lk_argus_set_logger(LkArgusLogFn log_fn, void* user_data) { + std::lock_guard lock(g_log_mutex); + g_log_fn = log_fn; + g_log_user_data = user_data; + return LK_ARGUS_OK; +} + +int32_t lk_argus_version(char* buf, size_t buf_len) { + if (!buf || buf_len == 0) return LK_ARGUS_ERR_INVALID_ARG; + std::lock_guard lock(g_provider_mutex); + Argus::ICameraProvider* i_provider = provider_locked(); + if (!i_provider) return LK_ARGUS_ERR_NO_PROVIDER; + snprintf(buf, buf_len, "%s", i_provider->getVersion().c_str()); + return LK_ARGUS_OK; +} + +int32_t lk_argus_device_count(void) { + std::lock_guard lock(g_provider_mutex); + std::vector devices; + int32_t status = devices_locked(&devices); + if (status != LK_ARGUS_OK) return status; + return static_cast(devices.size()); +} + +int32_t lk_argus_device_info(int32_t device_index, LkArgusDeviceInfo* out) { + if (!out || device_index < 0) return LK_ARGUS_ERR_INVALID_ARG; + memset(out, 0, sizeof(*out)); + + std::lock_guard lock(g_provider_mutex); + std::vector devices; + int32_t status = devices_locked(&devices); + if (status != LK_ARGUS_OK) return status; + if (device_index >= static_cast(devices.size())) { + return LK_ARGUS_ERR_NO_DEVICE; + } + + auto* i_props = + Argus::interface_cast(devices[device_index]); + if (!i_props) return LK_ARGUS_ERR_ARGUS; + + const Argus::UUID uuid = i_props->getUUID(); + snprintf(out->uuid, sizeof(out->uuid), + "%08x-%04x-%04x-%04x-%02x%02x%02x%02x%02x%02x", + uuid.time_low, uuid.time_mid, uuid.time_hi_and_version, + uuid.clock_seq, uuid.node[0], uuid.node[1], uuid.node[2], + uuid.node[3], uuid.node[4], uuid.node[5]); + + // A human-readable module name is only exposed through version-specific + // extension interfaces; leave `name` empty and let callers synthesize one. + + std::vector modes; + if (i_props->getAllSensorModes(&modes) == Argus::STATUS_OK) { + out->sensor_mode_count = static_cast(modes.size()); + } + return LK_ARGUS_OK; +} + +int32_t lk_argus_sensor_mode_info(int32_t device_index, + int32_t mode_index, + LkArgusSensorModeInfo* out) { + if (!out || device_index < 0 || mode_index < 0) { + return LK_ARGUS_ERR_INVALID_ARG; + } + memset(out, 0, sizeof(*out)); + + std::lock_guard lock(g_provider_mutex); + std::vector devices; + int32_t status = devices_locked(&devices); + if (status != LK_ARGUS_OK) return status; + if (device_index >= static_cast(devices.size())) { + return LK_ARGUS_ERR_NO_DEVICE; + } + + auto* i_props = + Argus::interface_cast(devices[device_index]); + if (!i_props) return LK_ARGUS_ERR_ARGUS; + + std::vector modes; + if (i_props->getAllSensorModes(&modes) != Argus::STATUS_OK || + mode_index >= static_cast(modes.size())) { + return LK_ARGUS_ERR_INVALID_ARG; + } + + auto* i_mode = Argus::interface_cast(modes[mode_index]); + if (!i_mode) return LK_ARGUS_ERR_ARGUS; + + const Argus::Size2D res = i_mode->getResolution(); + const Argus::Range dur = i_mode->getFrameDurationRange(); + out->width = res.width(); + out->height = res.height(); + out->min_frame_duration_ns = dur.min(); + out->max_frame_duration_ns = dur.max(); + out->bit_depth = i_mode->getInputBitDepth(); + return LK_ARGUS_OK; +} + +int32_t lk_argus_session_create(const LkArgusSessionConfig* config, + LkArgusSession** out_session) { + if (!config || !out_session || config->width <= 0 || config->height <= 0 || + config->fps <= 0 || config->device_index < 0 || + config->num_dma_bufs < 0) { + return LK_ARGUS_ERR_INVALID_ARG; + } + *out_session = nullptr; + + const int width = config->width; + const int height = config->height; + const int fps = config->fps; + int num_dma_bufs = + config->num_dma_bufs == 0 ? kDefaultNumDmaBufs : config->num_dma_bufs; + if (num_dma_bufs < kMinNumDmaBufs) num_dma_bufs = kMinNumDmaBufs; + if (num_dma_bufs > LK_ARGUS_MAX_DMA_BUFS) num_dma_bufs = LK_ARGUS_MAX_DMA_BUFS; + + auto* s = new LkArgusSession(); + s->num_dma_bufs = num_dma_bufs; + for (int i = 0; i < LK_ARGUS_MAX_DMA_BUFS; i++) { + s->dmabuf_fds[i] = -1; + s->dmabuf_surfaces[i] = nullptr; + s->leased[i] = false; + } + s->width = width; + s->height = height; + + Argus::CameraDevice* device = nullptr; + Argus::Status status; + { + std::lock_guard lock(g_provider_mutex); + Argus::ICameraProvider* i_provider = provider_locked(); + if (!i_provider) { + delete s; + return LK_ARGUS_ERR_NO_PROVIDER; + } + + std::vector devices; + i_provider->getCameraDevices(&devices); + if (config->device_index >= static_cast(devices.size())) { + lk_log(LK_ARGUS_LOG_ERROR, "no camera device at index %d (found %zu)", + config->device_index, devices.size()); + delete s; + return LK_ARGUS_ERR_NO_DEVICE; + } + device = devices[config->device_index]; + + s->session = Argus::UniqueObj( + i_provider->createCaptureSession(device, &status)); + } + if (status != Argus::STATUS_OK) { + lk_log(LK_ARGUS_LOG_ERROR, "failed to create CaptureSession: %d", + static_cast(status)); + s->last_argus_status = static_cast(status); + delete s; + return LK_ARGUS_ERR_ARGUS; + } + auto* i_session = Argus::interface_cast(s->session); + if (!i_session) { + delete s; + return LK_ARGUS_ERR_ARGUS; + } + + // Capture-complete event queue (fallback source for sensor timestamps). + auto* i_event_provider = + Argus::interface_cast(s->session); + if (i_event_provider) { + std::vector event_types; + event_types.push_back(Argus::EVENT_TYPE_CAPTURE_COMPLETE); + s->event_queue = Argus::UniqueObj( + i_event_provider->createEventQueue(event_types, &status)); + if (status != Argus::STATUS_OK || !s->event_queue) { + lk_log(LK_ARGUS_LOG_WARN, + "failed to create capture-complete event queue: %d", + static_cast(status)); + } else { + s->event_metadata_enabled = true; + } + } else { + lk_log(LK_ARGUS_LOG_WARN, + "capture session has no event provider interface"); + } + + // EGLStream-backed OutputStream delivering ISP-scaled NV12. + s->stream_settings = Argus::UniqueObj( + i_session->createOutputStreamSettings(Argus::STREAM_TYPE_EGL, &status)); + auto* i_stream_settings = + Argus::interface_cast(s->stream_settings); + if (!i_stream_settings) { + lk_log(LK_ARGUS_LOG_ERROR, "failed to get IEGLOutputStreamSettings"); + delete s; + return LK_ARGUS_ERR_ARGUS; + } + i_stream_settings->setPixelFormat(Argus::PIXEL_FMT_YCbCr_420_888); + i_stream_settings->setResolution(Argus::Size2D(width, height)); + status = i_stream_settings->setMode(Argus::EGL_STREAM_MODE_MAILBOX); + if (status != Argus::STATUS_OK) { + lk_log(LK_ARGUS_LOG_WARN, "failed to set EGLStream mailbox mode: %d", + static_cast(status)); + } + status = i_stream_settings->setFifoLength(1); + if (status != Argus::STATUS_OK) { + lk_log(LK_ARGUS_LOG_WARN, "failed to set EGLStream FIFO length: %d", + static_cast(status)); + } + status = i_stream_settings->setMetadataEnable(true); + if (status != Argus::STATUS_OK) { + lk_log(LK_ARGUS_LOG_WARN, "failed to enable EGLStream metadata: %d", + static_cast(status)); + } + s->metadata_enabled = i_stream_settings->getMetadataEnable(); + + s->stream = Argus::UniqueObj( + i_session->createOutputStream(s->stream_settings.get(), &status)); + if (status != Argus::STATUS_OK) { + lk_log(LK_ARGUS_LOG_ERROR, "failed to create OutputStream: %d", + static_cast(status)); + s->last_argus_status = static_cast(status); + delete s; + return LK_ARGUS_ERR_ARGUS; + } + + s->consumer = Argus::UniqueObj( + EGLStream::FrameConsumer::create(s->stream.get())); + if (!Argus::interface_cast(s->consumer)) { + lk_log(LK_ARGUS_LOG_ERROR, "failed to create FrameConsumer"); + delete s; + return LK_ARGUS_ERR_ARGUS; + } + + s->request = Argus::UniqueObj( + i_session->createRequest(Argus::CAPTURE_INTENT_VIDEO_RECORD, &status)); + if (status != Argus::STATUS_OK) { + lk_log(LK_ARGUS_LOG_ERROR, "failed to create Request: %d", + static_cast(status)); + s->last_argus_status = static_cast(status); + delete s; + return LK_ARGUS_ERR_ARGUS; + } + auto* i_request = Argus::interface_cast(s->request); + i_request->enableOutputStream(s->stream.get()); + + // Sensor mode: use the explicitly requested mode, or auto-select the + // smallest mode that covers the requested resolution and frame rate + // (Argus's own auto-selection often picks the highest-resolution mode and + // runs at that mode's lower frame rate). + auto* i_props = Argus::interface_cast(device); + auto* i_source = + Argus::interface_cast(i_request->getSourceSettings()); + const uint64_t requested_dur_ns = 1000000000ULL / fps; + if (i_props) { + std::vector modes; + i_props->getAllSensorModes(&modes); + + Argus::SensorMode* selected = nullptr; + if (config->sensor_mode_index >= 0) { + if (config->sensor_mode_index >= static_cast(modes.size())) { + lk_log(LK_ARGUS_LOG_ERROR, "sensor mode index %d out of range (%zu)", + config->sensor_mode_index, modes.size()); + delete s; + return LK_ARGUS_ERR_INVALID_ARG; + } + selected = modes[config->sensor_mode_index]; + } else { + uint64_t best_pixels = UINT64_MAX; + for (size_t i = 0; i < modes.size(); i++) { + auto* i_mode = Argus::interface_cast(modes[i]); + if (!i_mode) continue; + const Argus::Size2D res = i_mode->getResolution(); + const Argus::Range dur = i_mode->getFrameDurationRange(); + // Compare frame durations instead of floating-point fps. Sensor + // durations are in nanoseconds and often off by 1 ns from the ideal + // value (e.g. 33333334 vs 33333333 for 30 fps); a 1 ms tolerance + // handles this rounding. + if (static_cast(res.width()) >= width && + static_cast(res.height()) >= height && + dur.min() <= requested_dur_ns + 1000000) { + const uint64_t pixels = + static_cast(res.width()) * res.height(); + if (pixels < best_pixels) { + best_pixels = pixels; + selected = modes[i]; + } + } + } + } + + if (selected) { + auto* i_selected = Argus::interface_cast(selected); + const Argus::Size2D res = i_selected->getResolution(); + lk_log(LK_ARGUS_LOG_INFO, "selected sensor mode %ux%u for %dx%d @ %d fps", + res.width(), res.height(), width, height, fps); + if (i_source) { + i_source->setSensorMode(selected); + } + } else { + lk_log(LK_ARGUS_LOG_WARN, + "no sensor mode found for %dx%d @ %d fps, using Argus default", + width, height, fps); + } + } else { + lk_log(LK_ARGUS_LOG_WARN, "could not query sensor modes"); + } + if (i_source) { + // Fix the frame duration and cap exposure so auto-exposure can never + // stretch the frame interval below the requested rate. + i_source->setFrameDurationRange( + Argus::Range(requested_dur_ns, requested_dur_ns)); + i_source->setExposureTimeRange(Argus::Range(0, requested_dur_ns)); + } + + // Persistent NvBufSurface ring the acquired frames are blitted into. + for (int i = 0; i < num_dma_bufs; i++) { + NvBufSurfaceCreateParams create_params = {}; + create_params.gpuId = 0; + create_params.width = static_cast(width); + create_params.height = static_cast(height); + create_params.size = 0; + create_params.colorFormat = NVBUF_COLOR_FORMAT_NV12; + create_params.layout = NVBUF_LAYOUT_PITCH; + create_params.memType = NVBUF_MEM_SURFACE_ARRAY; + + NvBufSurface* surface = nullptr; + if (NvBufSurfaceCreate(&surface, 1, &create_params) != 0 || !surface) { + lk_log(LK_ARGUS_LOG_ERROR, "failed to create NvBufSurface[%d]", i); + destroy_dmabuf_surfaces(s, i); + delete s; + return LK_ARGUS_ERR_NVBUF; + } + surface->numFilled = 1; + s->dmabuf_fds[i] = surface->surfaceList[0].bufferDesc; + s->dmabuf_surfaces[i] = surface; + } + + status = i_session->repeat(s->request.get()); + if (status != Argus::STATUS_OK) { + lk_log(LK_ARGUS_LOG_ERROR, "failed to start repeating capture: %d", + static_cast(status)); + s->last_argus_status = static_cast(status); + destroy_dmabuf_surfaces(s, num_dma_bufs); + delete s; + return LK_ARGUS_ERR_ARGUS; + } + + lk_log(LK_ARGUS_LOG_INFO, + "session created: %dx%d @ %d fps, device %d, %d DMA buffers", width, + height, fps, config->device_index, num_dma_bufs); + *out_session = s; + return LK_ARGUS_OK; +} + +int32_t lk_argus_session_interrupt(LkArgusSession* s) { + if (!s) return LK_ARGUS_ERR_INVALID_ARG; + s->interrupted = true; + return LK_ARGUS_OK; +} + +int32_t lk_argus_session_last_argus_status(const LkArgusSession* s) { + if (!s) return 0; + return s->last_argus_status.load(); +} + +int32_t lk_argus_frame_acquire(LkArgusSession* s, + uint64_t timeout_ns, + LkArgusFrame* out) { + using Clock = std::chrono::steady_clock; + + if (!s || !out) return LK_ARGUS_ERR_INVALID_ARG; + memset(out, 0, sizeof(*out)); + out->dmabuf_fd = -1; + out->buffer_index = -1; + + if (s->interrupted.load()) return LK_ARGUS_ERR_INTERRUPTED; + + auto* i_consumer = + Argus::interface_cast(s->consumer); + if (!i_consumer) return LK_ARGUS_ERR_ARGUS; + + // Reserve a free ring slot before consuming a frame so backpressure is + // visible without discarding sensor output. + int slot = -1; + { + std::lock_guard lock(s->lease_mutex); + for (int i = 0; i < s->num_dma_bufs; i++) { + const int candidate = (s->next_slot + i) % s->num_dma_bufs; + if (!s->leased[candidate]) { + slot = candidate; + break; + } + } + } + if (slot < 0) return LK_ARGUS_ERR_NO_FREE_BUFFER; + + const auto t0 = Clock::now(); + + Argus::Status status; + Argus::UniqueObj frame( + i_consumer->acquireFrame(timeout_ns, &status)); + if (s->interrupted.load()) return LK_ARGUS_ERR_INTERRUPTED; + if (status == Argus::STATUS_TIMEOUT) return LK_ARGUS_ERR_TIMEOUT; + if (status == Argus::STATUS_DISCONNECTED) { + lk_log(LK_ARGUS_LOG_ERROR, "EGLStream disconnected"); + return LK_ARGUS_ERR_DISCONNECTED; + } + if (status != Argus::STATUS_OK || !frame) { + s->last_argus_status = static_cast(status); + lk_log(LK_ARGUS_LOG_ERROR, "acquireFrame failed: %d", + static_cast(status)); + return LK_ARGUS_ERR_ARGUS; + } + + const auto t1 = Clock::now(); + + auto* i_frame = Argus::interface_cast(frame); + if (!i_frame) return LK_ARGUS_ERR_ARGUS; + + uint64_t sensor_timestamp_ns = 0; + Argus::Status metadata_status = Argus::STATUS_OK; + const SensorTimestampStatus ts_status = + read_sensor_timestamp_ns(s, &sensor_timestamp_ns, &metadata_status); + const bool has_sensor_timestamp = ts_status == SensorTimestampStatus::Available; + if (!has_sensor_timestamp && ts_status != s->last_logged_ts_status) { + lk_log(LK_ARGUS_LOG_WARN, + "sensor timestamp unavailable: %s (event metadata=%s, EGL " + "metadata=%s, status=%d)", + sensor_timestamp_status_name(ts_status), + s->event_metadata_enabled ? "yes" : "no", + s->metadata_enabled ? "yes" : "no", + static_cast(metadata_status)); + s->last_logged_ts_status = ts_status; + } else if (has_sensor_timestamp && + s->last_logged_ts_status != SensorTimestampStatus::Available) { + lk_log(LK_ARGUS_LOG_INFO, "sensor timestamp available"); + s->last_logged_ts_status = SensorTimestampStatus::Available; + } + + EGLStream::Image* image = i_frame->getImage(); + if (!image) return LK_ARGUS_ERR_ARGUS; + + auto* i_native = + Argus::interface_cast(image); + if (!i_native) { + lk_log(LK_ARGUS_LOG_ERROR, "image does not support IImageNativeBuffer"); + return LK_ARGUS_ERR_ARGUS; + } + + // Blit (VIC) the acquired frame into the reserved slot. The EGLStream + // frame is released when `frame` goes out of scope; the pixel data lives + // on in the persistent NvBufSurface. + const int fd = s->dmabuf_fds[slot]; + status = i_native->copyToNvBuffer(fd); + + const auto t2 = Clock::now(); + + if (status != Argus::STATUS_OK) { + s->last_argus_status = static_cast(status); + lk_log(LK_ARGUS_LOG_ERROR, "copyToNvBuffer failed: %d", + static_cast(status)); + return LK_ARGUS_ERR_ARGUS; + } + + { + std::lock_guard lock(s->lease_mutex); + s->leased[slot] = true; + } + s->next_slot = (slot + 1) % s->num_dma_bufs; + + const NvBufSurfaceParams& params = s->dmabuf_surfaces[slot]->surfaceList[0]; + out->dmabuf_fd = fd; + out->buffer_index = slot; + out->width = static_cast(s->width); + out->height = static_cast(s->height); + out->pitch[0] = params.planeParams.pitch[0]; + out->pitch[1] = params.planeParams.pitch[1]; + out->offset[0] = params.planeParams.offset[0]; + out->offset[1] = params.planeParams.offset[1]; + out->sensor_timestamp_ns = sensor_timestamp_ns; + out->acquire_wait_ns = static_cast( + std::chrono::duration_cast(t1 - t0).count()); + out->blit_ns = static_cast( + std::chrono::duration_cast(t2 - t1).count()); + return LK_ARGUS_OK; +} + +int32_t lk_argus_frame_release(LkArgusSession* s, int32_t buffer_index) { + if (!s || buffer_index < 0 || buffer_index >= s->num_dma_bufs) { + return LK_ARGUS_ERR_INVALID_ARG; + } + std::lock_guard lock(s->lease_mutex); + s->leased[buffer_index] = false; + return LK_ARGUS_OK; +} + +int32_t lk_argus_frame_copy_to_i420(LkArgusSession* s, + int32_t buffer_index, + uint8_t* dst_y, + int32_t dst_stride_y, + uint8_t* dst_u, + int32_t dst_stride_u, + uint8_t* dst_v, + int32_t dst_stride_v) { + if (!s || buffer_index < 0 || buffer_index >= s->num_dma_bufs || !dst_y || + !dst_u || !dst_v) { + return LK_ARGUS_ERR_INVALID_ARG; + } + + const int width = s->width; + const int height = s->height; + const int chroma_width = (width + 1) / 2; + const int chroma_height = (height + 1) / 2; + if (dst_stride_y < width || dst_stride_u < chroma_width || + dst_stride_v < chroma_width) { + return LK_ARGUS_ERR_INVALID_ARG; + } + + NvBufSurface* surface = s->dmabuf_surfaces[buffer_index]; + if (!surface || surface->batchSize < 1) { + return LK_ARGUS_ERR_NVBUF; + } + + int ret = NvBufSurfaceMap(surface, 0, -1, NVBUF_MAP_READ); + if (ret != 0) { + lk_log(LK_ARGUS_LOG_ERROR, "NvBufSurfaceMap failed: %d", ret); + return LK_ARGUS_ERR_NVBUF; + } + + ret = NvBufSurfaceSyncForCpu(surface, 0, -1); + if (ret != 0) { + NvBufSurfaceUnMap(surface, 0, -1); + lk_log(LK_ARGUS_LOG_ERROR, "NvBufSurfaceSyncForCpu failed: %d", ret); + return LK_ARGUS_ERR_NVBUF; + } + + const NvBufSurfaceParams& params = surface->surfaceList[0]; + const uint8_t* src_y = static_cast(params.mappedAddr.addr[0]); + const uint8_t* src_uv = static_cast(params.mappedAddr.addr[1]); + const int src_stride_y = static_cast(params.planeParams.pitch[0]); + const int src_stride_uv = static_cast(params.planeParams.pitch[1]); + + if (!src_y || !src_uv || src_stride_y < width || + src_stride_uv < chroma_width * 2) { + NvBufSurfaceUnMap(surface, 0, -1); + return LK_ARGUS_ERR_NVBUF; + } + + for (int row = 0; row < height; row++) { + memcpy(dst_y + row * dst_stride_y, src_y + row * src_stride_y, + static_cast(width)); + } + for (int row = 0; row < chroma_height; row++) { + const uint8_t* src_row = src_uv + row * src_stride_uv; + uint8_t* dst_u_row = dst_u + row * dst_stride_u; + uint8_t* dst_v_row = dst_v + row * dst_stride_v; + for (int col = 0; col < chroma_width; col++) { + dst_u_row[col] = src_row[col * 2]; + dst_v_row[col] = src_row[col * 2 + 1]; + } + } + + ret = NvBufSurfaceUnMap(surface, 0, -1); + if (ret != 0) { + lk_log(LK_ARGUS_LOG_ERROR, "NvBufSurfaceUnMap failed: %d", ret); + return LK_ARGUS_ERR_NVBUF; + } + return LK_ARGUS_OK; +} + +void lk_argus_session_destroy(LkArgusSession* s) { + if (!s) return; + + s->interrupted = true; + + auto* i_session = Argus::interface_cast(s->session); + if (i_session) { + i_session->stopRepeat(); + i_session->waitForIdle(); + } + + // Callers are expected to release every frame before destroying the + // session (the Rust wrapper keeps the session alive until they do); this + // bounded wait is defense in depth against a misbehaving consumer. + constexpr int kMaxWaitMs = 500; + constexpr int kPollMs = 10; + for (int waited_ms = 0; waited_ms < kMaxWaitMs; waited_ms += kPollMs) { + bool any_leased = false; + { + std::lock_guard lock(s->lease_mutex); + for (int i = 0; i < s->num_dma_bufs; i++) { + any_leased |= s->leased[i]; + } + } + if (!any_leased) break; + std::this_thread::sleep_for(std::chrono::milliseconds(kPollMs)); + } + { + std::lock_guard lock(s->lease_mutex); + for (int i = 0; i < s->num_dma_bufs; i++) { + if (s->leased[i]) { + lk_log(LK_ARGUS_LOG_ERROR, + "destroying session with ring slot %d still leased", i); + } + } + } + + destroy_dmabuf_surfaces(s, s->num_dma_bufs); + delete s; + lk_log(LK_ARGUS_LOG_INFO, "session destroyed"); +} + +} // extern "C" diff --git a/libargus-sys/src/lk_argus.h b/libargus-sys/src/lk_argus.h new file mode 100644 index 000000000..766a1f02f --- /dev/null +++ b/libargus-sys/src/lk_argus.h @@ -0,0 +1,213 @@ +/* + * Copyright 2026 LiveKit, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * C ABI of the LiveKit libargus capture shim. + * + * This header is the single source of truth for the FFI surface; the Rust + * declarations in lib.rs mirror it and must be kept in sync. + * + * Thread-safety contract: + * - Enumeration functions, lk_argus_set_logger, lk_argus_session_create, + * and lk_argus_session_destroy may be called from any thread. + * - Per session, lk_argus_frame_acquire and lk_argus_frame_copy_to_i420 + * must be driven by a single consumer thread at a time. + * - lk_argus_frame_release and lk_argus_session_interrupt may be called + * from any thread (e.g. from an encoder thread releasing a frame). + */ + +#ifndef LK_ARGUS_H_ +#define LK_ARGUS_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ABI version of this shim. Incremented on breaking changes. */ +#define LK_ARGUS_ABI_VERSION 1 + +/* Maximum supported DMA buffer ring size. */ +#define LK_ARGUS_MAX_DMA_BUFS 16 + +/* Status codes. 0 is success; negative values are errors. */ +enum { + LK_ARGUS_OK = 0, + /* Invalid arguments crossed the FFI boundary (caller bug). */ + LK_ARGUS_ERR_INVALID_ARG = -1, + /* CameraProvider creation failed (nvargus-daemon not running?). */ + LK_ARGUS_ERR_NO_PROVIDER = -2, + /* Device index out of range. */ + LK_ARGUS_ERR_NO_DEVICE = -3, + /* Generic Argus failure; see lk_argus_session_last_argus_status. */ + LK_ARGUS_ERR_ARGUS = -4, + /* Frame acquire timed out. Not fatal: retry. */ + LK_ARGUS_ERR_TIMEOUT = -5, + /* EGLStream disconnected (nvargus-daemon died or stream ended). The + * session is dead and must be destroyed and re-created. */ + LK_ARGUS_ERR_DISCONNECTED = -6, + /* NvBufSurface operation failed. */ + LK_ARGUS_ERR_NVBUF = -7, + /* Every ring slot is leased to an in-flight frame. Not fatal: + * backpressure; retry after frames are released. */ + LK_ARGUS_ERR_NO_FREE_BUFFER = -8, + /* lk_argus_session_interrupt was called. */ + LK_ARGUS_ERR_INTERRUPTED = -9, + /* Returned by the Rust stubs when the native shim was not compiled in; + * the shim itself never returns this. */ + LK_ARGUS_ERR_UNAVAILABLE = -10, +}; + +/* Log levels passed to LkArgusLogFn. */ +enum { + LK_ARGUS_LOG_ERROR = 0, + LK_ARGUS_LOG_WARN = 1, + LK_ARGUS_LOG_INFO = 2, + LK_ARGUS_LOG_DEBUG = 3, +}; + +/* Opaque capture session handle. */ +typedef struct LkArgusSession LkArgusSession; + +typedef struct LkArgusDeviceInfo { + /* ICameraProperties::getUUID(), formatted, NUL-terminated. */ + char uuid[37]; + /* Best-effort human-readable module name; empty string when the JetPack + * version exposes none. */ + char name[64]; + int32_t sensor_mode_count; +} LkArgusDeviceInfo; + +typedef struct LkArgusSensorModeInfo { + uint32_t width; + uint32_t height; + /* ISensorMode::getFrameDurationRange(). */ + uint64_t min_frame_duration_ns; + uint64_t max_frame_duration_ns; + /* ISensorMode::getInputBitDepth(). */ + uint32_t bit_depth; +} LkArgusSensorModeInfo; + +typedef struct LkArgusSessionConfig { + int32_t device_index; + /* Sensor mode to use, or -1 to auto-select the smallest mode covering the + * requested resolution and frame rate. */ + int32_t sensor_mode_index; + /* Output (ISP-scaled) resolution and frame rate. */ + int32_t width; + int32_t height; + int32_t fps; + /* DMA buffer ring depth; 0 selects the default (4). Clamped to + * [2, LK_ARGUS_MAX_DMA_BUFS]. */ + int32_t num_dma_bufs; +} LkArgusSessionConfig; + +typedef struct LkArgusFrame { + /* NV12 DMA buffer fd, BORROWED from the session ring. Valid until + * lk_argus_frame_release(buffer_index) is called; never close it. */ + int32_t dmabuf_fd; + /* Ring slot index; token for lk_argus_frame_release. */ + int32_t buffer_index; + uint32_t width; + uint32_t height; + /* Actual plane pitches/offsets (NvBufSurfaceParams.planeParams), Y then + * interleaved UV. */ + uint32_t pitch[2]; + uint32_t offset[2]; + /* Argus sensor timestamp (CLOCK_MONOTONIC domain), 0 when unavailable. */ + uint64_t sensor_timestamp_ns; + /* Diagnostics: time spent waiting in acquireFrame and blitting. */ + uint64_t acquire_wait_ns; + uint64_t blit_ns; +} LkArgusFrame; + +/* + * Installs a log callback, replacing stderr output. `msg` is only valid for + * the duration of the call. Pass a null `log_fn` to restore stderr logging. + */ +typedef void (*LkArgusLogFn)(int32_t level, const char* msg, void* user_data); +int32_t lk_argus_set_logger(LkArgusLogFn log_fn, void* user_data); + +/* + * Copies the Argus version string (ICameraProvider::getVersion) into `buf`, + * NUL-terminated and truncated to `buf_len`. + */ +int32_t lk_argus_version(char* buf, size_t buf_len); + +/* Returns the number of camera devices (>= 0), or a negative status. */ +int32_t lk_argus_device_count(void); + +int32_t lk_argus_device_info(int32_t device_index, LkArgusDeviceInfo* out); + +int32_t lk_argus_sensor_mode_info(int32_t device_index, + int32_t mode_index, + LkArgusSensorModeInfo* out); + +int32_t lk_argus_session_create(const LkArgusSessionConfig* config, + LkArgusSession** out_session); + +/* + * Tears down a session. Interrupts any pending acquire, stops the repeating + * capture, and destroys the DMA buffer ring. Waits a bounded time for + * outstanding frame leases to be released; leases still outstanding after + * the wait are logged and the buffers destroyed anyway, so callers must + * ensure all frames are released before destroying the session. + */ +void lk_argus_session_destroy(LkArgusSession* session); + +/* + * Makes the pending (and every subsequent) lk_argus_frame_acquire return + * LK_ARGUS_ERR_INTERRUPTED. Note the current acquireFrame wait itself cannot + * be cut short; interruption latency is bounded by the acquire timeout. + */ +int32_t lk_argus_session_interrupt(LkArgusSession* session); + +/* Raw Argus::Status of the session's last failing Argus call. */ +int32_t lk_argus_session_last_argus_status(const LkArgusSession* session); + +/* + * Acquires the next frame, blocking at most `timeout_ns`. On success blits + * the frame into a free ring slot, marks that slot leased, and fills `out`. + * The lease (and the fd's validity) lasts until lk_argus_frame_release is + * called with out->buffer_index. + */ +int32_t lk_argus_frame_acquire(LkArgusSession* session, + uint64_t timeout_ns, + LkArgusFrame* out); + +/* Returns a leased ring slot for reuse. Callable from any thread. */ +int32_t lk_argus_frame_release(LkArgusSession* session, int32_t buffer_index); + +/* + * CPU fallback: copies the NV12 contents of a leased ring slot into + * caller-owned I420 planes. + */ +int32_t lk_argus_frame_copy_to_i420(LkArgusSession* session, + int32_t buffer_index, + uint8_t* dst_y, + int32_t dst_stride_y, + uint8_t* dst_u, + int32_t dst_stride_u, + uint8_t* dst_v, + int32_t dst_stride_v); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* LK_ARGUS_H_ */ From d89a6366220ea90d733a7906e2b1530732071093 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:46:43 -0700 Subject: [PATCH 4/6] Implement bindings --- libargus-sys/build.rs | 98 +++++++++++ libargus-sys/src/lib.rs | 376 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 474 insertions(+) create mode 100644 libargus-sys/build.rs create mode 100644 libargus-sys/src/lib.rs diff --git a/libargus-sys/build.rs b/libargus-sys/build.rs new file mode 100644 index 000000000..a5acb674d --- /dev/null +++ b/libargus-sys/build.rs @@ -0,0 +1,98 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::path::PathBuf; + +/// Locates the directory containing the Tegra userspace libraries the shim +/// links against, requiring the libraries themselves to be present so that +/// headers alone (e.g. in a partially-provisioned container or on a +/// non-Jetson aarch64 board) never produce a build that fails to link. +fn find_tegra_lib_dir() -> Option { + const REQUIRED_LIBS: [&str; 2] = ["libnvargus_socketclient.so", "libnvbufsurface.so"]; + + let candidates: Vec = match std::env::var_os("JETSON_TEGRA_LIB_DIR") { + Some(dir) => vec![PathBuf::from(dir)], + None => vec![ + // JetPack 5+ (L4T r35+) + PathBuf::from("/usr/lib/aarch64-linux-gnu/tegra"), + // Symlink directory shipped on some releases + PathBuf::from("/usr/lib/aarch64-linux-gnu/nvidia"), + ], + }; + + candidates + .into_iter() + .find(|dir| REQUIRED_LIBS.iter().all(|lib| dir.join(lib).exists())) +} + +fn main() { + println!("cargo:rustc-check-cfg=cfg(libargus_available)"); + println!("cargo:rerun-if-env-changed=JETSON_MULTIMEDIA_API_DIR"); + println!("cargo:rerun-if-env-changed=JETSON_TEGRA_LIB_DIR"); + + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); + if target_os != "linux" || target_arch != "aarch64" { + return; + } + + let mmapi_root = std::env::var_os("JETSON_MULTIMEDIA_API_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/usr/src/jetson_multimedia_api")); + let argus_include = mmapi_root.join("argus/include"); + let mmapi_include = mmapi_root.join("include"); + + if !argus_include.exists() || !mmapi_include.exists() { + println!( + "cargo:warning=Argus headers not found under {}; building libargus-sys without the \ + native shim (set JETSON_MULTIMEDIA_API_DIR to override)", + mmapi_root.display() + ); + return; + } + + // Require the link libraries too: headers can be present without the + // Tegra runtime (containers, sysroots), and emitting link directives in + // that state would fail the final link instead of degrading gracefully. + let Some(tegra_lib_dir) = find_tegra_lib_dir() else { + println!( + "cargo:warning=Tegra libraries (libnvargus_socketclient.so, libnvbufsurface.so) not \ + found; building libargus-sys without the native shim (set JETSON_TEGRA_LIB_DIR to \ + override)" + ); + return; + }; + + println!("cargo:rerun-if-changed=src/lk_argus.cpp"); + println!("cargo:rerun-if-changed=src/lk_argus.h"); + + cc::Build::new() + .cpp(true) + .file("src/lk_argus.cpp") + .include("src") + .include(&argus_include) + .include(&mmapi_include) + .flag("-std=c++14") + .flag("-Wno-deprecated-declarations") + .compile("lk_argus"); + + println!("cargo:rustc-cfg=libargus_available"); + println!("cargo:rustc-link-search=native={}", tegra_lib_dir.display()); + println!("cargo:rustc-link-search=native=/usr/lib/aarch64-linux-gnu"); + println!("cargo:rustc-link-lib=dylib=nvargus_socketclient"); + println!("cargo:rustc-link-lib=dylib=nvbufsurface"); + + // Communicate availability to dependent crates via `DEP_LK_ARGUS_AVAILABLE`. + println!("cargo:available=1"); +} diff --git a/libargus-sys/src/lib.rs b/libargus-sys/src/lib.rs new file mode 100644 index 000000000..13716dbe7 --- /dev/null +++ b/libargus-sys/src/lib.rs @@ -0,0 +1,376 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Raw FFI bindings to the LiveKit NVIDIA libargus capture shim. +//! +//! The native shim (`lk_argus.cpp`) wraps NVIDIA's Argus/libargus API for +//! capturing frames from Jetson MIPI CSI cameras as NV12 DMA buffers. It is +//! only compiled and linked on aarch64 Linux targets where the Jetson +//! Multimedia API headers *and* Tegra userspace libraries are found at build +//! time (probed at `/usr/src/jetson_multimedia_api` and +//! `/usr/lib/aarch64-linux-gnu/tegra`; override with the +//! `JETSON_MULTIMEDIA_API_DIR` and `JETSON_TEGRA_LIB_DIR` environment +//! variables). +//! +//! On every other target the crate still builds and exposes the same API, +//! but every entry point is an inert stub returning +//! [`LK_ARGUS_ERR_UNAVAILABLE`]. [`AVAILABLE`] reports which variant was +//! built. This keeps consumers free of build scripts and custom `cfg`s: +//! "not a Jetson" degrades exactly like "Jetson with zero cameras". +//! +//! The C header `src/lk_argus.h` is the source of truth for the ABI; the +//! declarations here mirror it and must be kept in sync. +//! +//! # Thread safety +//! +//! - Enumeration functions, [`lk_argus_set_logger`], [`lk_argus_session_create`], +//! and [`lk_argus_session_destroy`] may be called from any thread. +//! - Per session, [`lk_argus_frame_acquire`] and +//! [`lk_argus_frame_copy_to_i420`] must be driven by a single consumer +//! thread at a time. +//! - [`lk_argus_frame_release`] and [`lk_argus_session_interrupt`] may be +//! called from any thread. + +use std::ffi::{c_char, c_void}; + +/// Whether the native libargus shim was compiled and linked into this build. +/// +/// When `false`, every function in this crate is an inert stub: session and +/// enumeration calls return [`LK_ARGUS_ERR_UNAVAILABLE`]. +pub const AVAILABLE: bool = cfg!(libargus_available); + +/// ABI version of the shim. Incremented on breaking changes. +pub const LK_ARGUS_ABI_VERSION: u32 = 1; + +/// Maximum supported DMA buffer ring size. +pub const LK_ARGUS_MAX_DMA_BUFS: i32 = 16; + +pub const LK_ARGUS_OK: i32 = 0; +/// Invalid arguments crossed the FFI boundary (caller bug). +pub const LK_ARGUS_ERR_INVALID_ARG: i32 = -1; +/// CameraProvider creation failed (nvargus-daemon not running?). +pub const LK_ARGUS_ERR_NO_PROVIDER: i32 = -2; +/// Device index out of range. +pub const LK_ARGUS_ERR_NO_DEVICE: i32 = -3; +/// Generic Argus failure; see [`lk_argus_session_last_argus_status`]. +pub const LK_ARGUS_ERR_ARGUS: i32 = -4; +/// Frame acquire timed out. Not fatal: retry. +pub const LK_ARGUS_ERR_TIMEOUT: i32 = -5; +/// EGLStream disconnected (nvargus-daemon died or stream ended). The session +/// is dead and must be destroyed and re-created. +pub const LK_ARGUS_ERR_DISCONNECTED: i32 = -6; +/// NvBufSurface operation failed. +pub const LK_ARGUS_ERR_NVBUF: i32 = -7; +/// Every ring slot is leased to an in-flight frame. Not fatal: backpressure; +/// retry after frames are released. +pub const LK_ARGUS_ERR_NO_FREE_BUFFER: i32 = -8; +/// [`lk_argus_session_interrupt`] was called. +pub const LK_ARGUS_ERR_INTERRUPTED: i32 = -9; +/// The native shim was not compiled into this build (see [`AVAILABLE`]). +pub const LK_ARGUS_ERR_UNAVAILABLE: i32 = -10; + +/// Log levels passed to [`LkArgusLogFn`]. +pub const LK_ARGUS_LOG_ERROR: i32 = 0; +pub const LK_ARGUS_LOG_WARN: i32 = 1; +pub const LK_ARGUS_LOG_INFO: i32 = 2; +pub const LK_ARGUS_LOG_DEBUG: i32 = 3; + +/// Returns a human-readable description of a status code. +pub fn lk_argus_status_string(status: i32) -> &'static str { + match status { + LK_ARGUS_OK => "ok", + LK_ARGUS_ERR_INVALID_ARG => "invalid argument", + LK_ARGUS_ERR_NO_PROVIDER => { + "failed to create camera provider (is nvargus-daemon running?)" + } + LK_ARGUS_ERR_NO_DEVICE => "camera device index out of range", + LK_ARGUS_ERR_ARGUS => "Argus operation failed", + LK_ARGUS_ERR_TIMEOUT => "frame acquire timed out", + LK_ARGUS_ERR_DISCONNECTED => "EGL stream disconnected", + LK_ARGUS_ERR_NVBUF => "NvBufSurface operation failed", + LK_ARGUS_ERR_NO_FREE_BUFFER => "all DMA buffers are in flight", + LK_ARGUS_ERR_INTERRUPTED => "session interrupted", + LK_ARGUS_ERR_UNAVAILABLE => "libargus shim not available in this build", + _ => "unknown status", + } +} + +/// Opaque capture session handle. +#[repr(C)] +pub struct LkArgusSession { + _private: [u8; 0], +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct LkArgusDeviceInfo { + /// Camera device UUID, formatted and NUL-terminated. + pub uuid: [c_char; 37], + /// Best-effort human-readable module name; empty string when the JetPack + /// version exposes none. + pub name: [c_char; 64], + pub sensor_mode_count: i32, +} + +impl Default for LkArgusDeviceInfo { + fn default() -> Self { + Self { uuid: [0; 37], name: [0; 64], sensor_mode_count: 0 } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct LkArgusSensorModeInfo { + pub width: u32, + pub height: u32, + pub min_frame_duration_ns: u64, + pub max_frame_duration_ns: u64, + pub bit_depth: u32, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct LkArgusSessionConfig { + pub device_index: i32, + /// Sensor mode to use, or -1 to auto-select the smallest mode covering + /// the requested resolution and frame rate. + pub sensor_mode_index: i32, + /// Output (ISP-scaled) resolution and frame rate. + pub width: i32, + pub height: i32, + pub fps: i32, + /// DMA buffer ring depth; 0 selects the default (4). Clamped to + /// [2, [`LK_ARGUS_MAX_DMA_BUFS`]]. + pub num_dma_bufs: i32, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, Default)] +pub struct LkArgusFrame { + /// NV12 DMA buffer fd, *borrowed* from the session ring. Valid until + /// [`lk_argus_frame_release`] is called with `buffer_index`; never close + /// it. + pub dmabuf_fd: i32, + /// Ring slot index; token for [`lk_argus_frame_release`]. + pub buffer_index: i32, + pub width: u32, + pub height: u32, + /// Actual plane pitches/offsets, Y then interleaved UV. + pub pitch: [u32; 2], + pub offset: [u32; 2], + /// Argus sensor timestamp (`CLOCK_MONOTONIC` domain), 0 when unavailable. + pub sensor_timestamp_ns: u64, + /// Diagnostics: time spent waiting in `acquireFrame` and blitting. + pub acquire_wait_ns: u64, + pub blit_ns: u64, +} + +/// Log callback type for [`lk_argus_set_logger`]. `msg` is only valid for the +/// duration of the call. +pub type LkArgusLogFn = + unsafe extern "C" fn(level: i32, msg: *const c_char, user_data: *mut c_void); + +#[cfg(libargus_available)] +mod ffi { + use super::*; + + extern "C" { + pub fn lk_argus_set_logger(log_fn: Option, user_data: *mut c_void) -> i32; + pub fn lk_argus_version(buf: *mut c_char, buf_len: usize) -> i32; + pub fn lk_argus_device_count() -> i32; + pub fn lk_argus_device_info(device_index: i32, out: *mut LkArgusDeviceInfo) -> i32; + pub fn lk_argus_sensor_mode_info( + device_index: i32, + mode_index: i32, + out: *mut LkArgusSensorModeInfo, + ) -> i32; + pub fn lk_argus_session_create( + config: *const LkArgusSessionConfig, + out_session: *mut *mut LkArgusSession, + ) -> i32; + pub fn lk_argus_session_destroy(session: *mut LkArgusSession); + pub fn lk_argus_session_interrupt(session: *mut LkArgusSession) -> i32; + pub fn lk_argus_session_last_argus_status(session: *const LkArgusSession) -> i32; + pub fn lk_argus_frame_acquire( + session: *mut LkArgusSession, + timeout_ns: u64, + out: *mut LkArgusFrame, + ) -> i32; + pub fn lk_argus_frame_release(session: *mut LkArgusSession, buffer_index: i32) -> i32; + pub fn lk_argus_frame_copy_to_i420( + session: *mut LkArgusSession, + buffer_index: i32, + dst_y: *mut u8, + dst_stride_y: i32, + dst_u: *mut u8, + dst_stride_u: i32, + dst_v: *mut u8, + dst_stride_v: i32, + ) -> i32; + } +} + +macro_rules! shim_fns { + ($( + $(#[$doc:meta])* + fn $name:ident($($arg:ident: $ty:ty),* $(,)?) $(-> $ret:ty)? = $stub:expr; + )*) => { + $( + $(#[$doc])* + /// + /// # Safety + /// Pointer arguments must be valid per the contract in + /// `lk_argus.h`, and the caller must respect the crate-level + /// thread-safety rules. + #[inline] + #[cfg(libargus_available)] + pub unsafe fn $name($($arg: $ty),*) $(-> $ret)? { + ffi::$name($($arg),*) + } + + $(#[$doc])* + /// + /// # Safety + /// Pointer arguments must be valid per the contract in + /// `lk_argus.h`, and the caller must respect the crate-level + /// thread-safety rules. + #[inline] + #[cfg(not(libargus_available))] + #[allow(unused_variables)] + pub unsafe fn $name($($arg: $ty),*) $(-> $ret)? { + $stub + } + )* + }; +} + +shim_fns! { + /// Installs a log callback, replacing stderr output. Pass `None` to + /// restore stderr logging. + fn lk_argus_set_logger(log_fn: Option, user_data: *mut c_void) -> i32 + = LK_ARGUS_ERR_UNAVAILABLE; + + /// Copies the Argus version string into `buf`, NUL-terminated and + /// truncated to `buf_len`. + fn lk_argus_version(buf: *mut c_char, buf_len: usize) -> i32 + = LK_ARGUS_ERR_UNAVAILABLE; + + /// Returns the number of camera devices (>= 0), or a negative status. + fn lk_argus_device_count() -> i32 + = LK_ARGUS_ERR_UNAVAILABLE; + + /// Fills `out` with information about a camera device. + fn lk_argus_device_info(device_index: i32, out: *mut LkArgusDeviceInfo) -> i32 + = LK_ARGUS_ERR_UNAVAILABLE; + + /// Fills `out` with information about one of a device's sensor modes. + fn lk_argus_sensor_mode_info( + device_index: i32, + mode_index: i32, + out: *mut LkArgusSensorModeInfo, + ) -> i32 + = LK_ARGUS_ERR_UNAVAILABLE; + + /// Opens a capture session. On success writes the session handle to + /// `out_session`. + fn lk_argus_session_create( + config: *const LkArgusSessionConfig, + out_session: *mut *mut LkArgusSession, + ) -> i32 + = LK_ARGUS_ERR_UNAVAILABLE; + + /// Tears down a session. Interrupts any pending acquire, stops the + /// repeating capture, and destroys the DMA buffer ring. All frames must + /// be released before calling this. + fn lk_argus_session_destroy(session: *mut LkArgusSession) + = (); + + /// Makes the pending (and every subsequent) [`lk_argus_frame_acquire`] + /// return [`LK_ARGUS_ERR_INTERRUPTED`]. Interruption latency is bounded + /// by the acquire timeout. + fn lk_argus_session_interrupt(session: *mut LkArgusSession) -> i32 + = LK_ARGUS_ERR_UNAVAILABLE; + + /// Raw `Argus::Status` of the session's last failing Argus call. + fn lk_argus_session_last_argus_status(session: *const LkArgusSession) -> i32 + = 0; + + /// Acquires the next frame, blocking at most `timeout_ns`. On success + /// blits the frame into a free ring slot, marks that slot leased, and + /// fills `out`. The lease (and the fd's validity) lasts until + /// [`lk_argus_frame_release`] is called with `out.buffer_index`. + fn lk_argus_frame_acquire( + session: *mut LkArgusSession, + timeout_ns: u64, + out: *mut LkArgusFrame, + ) -> i32 + = LK_ARGUS_ERR_UNAVAILABLE; + + /// Returns a leased ring slot for reuse. Callable from any thread. + fn lk_argus_frame_release(session: *mut LkArgusSession, buffer_index: i32) -> i32 + = LK_ARGUS_ERR_UNAVAILABLE; + + /// CPU fallback: copies the NV12 contents of a leased ring slot into + /// caller-owned I420 planes. + fn lk_argus_frame_copy_to_i420( + session: *mut LkArgusSession, + buffer_index: i32, + dst_y: *mut u8, + dst_stride_y: i32, + dst_u: *mut u8, + dst_stride_u: i32, + dst_v: *mut u8, + dst_stride_v: i32, + ) -> i32 + = LK_ARGUS_ERR_UNAVAILABLE; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn status_strings_cover_all_codes() { + for code in [ + LK_ARGUS_OK, + LK_ARGUS_ERR_INVALID_ARG, + LK_ARGUS_ERR_NO_PROVIDER, + LK_ARGUS_ERR_NO_DEVICE, + LK_ARGUS_ERR_ARGUS, + LK_ARGUS_ERR_TIMEOUT, + LK_ARGUS_ERR_DISCONNECTED, + LK_ARGUS_ERR_NVBUF, + LK_ARGUS_ERR_NO_FREE_BUFFER, + LK_ARGUS_ERR_INTERRUPTED, + LK_ARGUS_ERR_UNAVAILABLE, + ] { + assert_ne!(lk_argus_status_string(code), "unknown status"); + } + assert_eq!(lk_argus_status_string(-999), "unknown status"); + } + + #[cfg(not(libargus_available))] + #[test] + fn stubs_report_unavailable() { + assert!(!AVAILABLE); + unsafe { + assert_eq!(lk_argus_device_count(), LK_ARGUS_ERR_UNAVAILABLE); + let mut out = LkArgusFrame::default(); + assert_eq!( + lk_argus_frame_acquire(std::ptr::null_mut(), 0, &mut out), + LK_ARGUS_ERR_UNAVAILABLE + ); + } + } +} From 3e9ea4452dd728322a2e6bcbe65264e5a3e8e2b1 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:46:43 -0700 Subject: [PATCH 5/6] Create README.md --- libargus-sys/README.md | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 libargus-sys/README.md diff --git a/libargus-sys/README.md b/libargus-sys/README.md new file mode 100644 index 000000000..4ce1fc82f --- /dev/null +++ b/libargus-sys/README.md @@ -0,0 +1,72 @@ +# libargus-sys + +Native shim and raw FFI bindings for capturing NVIDIA Jetson MIPI CSI cameras +through [libargus] as NV12 DMA buffers, used by the `source-device-argus` +feature of `livekit-capture`. + +[libargus]: https://docs.nvidia.com/jetson/l4t-multimedia/group__LibargusAPI.html + +## How it builds + +The C++ shim (`src/lk_argus.cpp`, ABI in `src/lk_argus.h`) is compiled only +when **all** of the following hold at build time: + +- the target is aarch64 Linux, +- the Jetson Multimedia API headers are present + (`/usr/src/jetson_multimedia_api`, package + `nvidia-l4t-jetson-multimedia-api`), +- the Tegra userspace libraries `libnvargus_socketclient.so` and + `libnvbufsurface.so` are present (`/usr/lib/aarch64-linux-gnu/tegra`). + +On every other target — or when the probe fails — the crate still builds and +exposes the same API, but every entry point is an inert stub returning +`LK_ARGUS_ERR_UNAVAILABLE`, and the `AVAILABLE` constant is `false`. +Consumers therefore need no build script or `cfg` of their own: "not a +Jetson" degrades exactly like "Jetson with zero cameras". + +### Cross-compilation + +Point the probe at a sysroot copy of the Jetson filesystem: + +| Environment variable | Default | +| --- | --- | +| `JETSON_MULTIMEDIA_API_DIR` | `/usr/src/jetson_multimedia_api` | +| `JETSON_TEGRA_LIB_DIR` | `/usr/lib/aarch64-linux-gnu/tegra` | + +The C++ toolchain itself is configured through the standard [`cc` crate +variables][cc-env] (`CXX_aarch64_unknown_linux_gnu`, `CXXFLAGS=--sysroot=…`). + +[cc-env]: https://docs.rs/cc/latest/cc/#external-configuration-via-environment-variables + +## Supported JetPack versions + +JetPack 5 through 7 (L4T r35–r39). The Argus API has been stable across this +range; JetPack 6.1 rewrote the stack internals but kept the API. JetPack 4's +`nvbuf_utils`-era buffer API is not supported. NVIDIA's long-term successor +to Argus is SIPL (JetPack 7+); Argus is in sustaining mode but remains the +supported CSI capture path on Orin-class devices. + +## Runtime requirements + +- A Jetson device with the Argus stack: the `nvargus-daemon` service must be + running (the shim talks to it through `libnvargus_socketclient`). +- A CSI camera module with a device-tree entry and ISP tuning file. USB (UVC) + webcams do not go through Argus. + +If `nvargus-daemon` dies mid-capture, frame acquisition reports +`LK_ARGUS_ERR_DISCONNECTED`; destroy the session and re-create it once the +daemon has restarted (`systemctl restart nvargus-daemon`). + +## Design notes + +- Frames are ISP-processed NV12 delivered as DMA buffer fds: zero CPU copies, + one VIC hardware blit from the EGLStream frame into a persistent + `NvBufSurface` ring. +- Ring slots are *leased*: a slot's fd stays valid until + `lk_argus_frame_release`, however long a consumer (e.g. a hardware encoder) + holds the frame. An exhausted ring is reported as retryable backpressure + (`LK_ARGUS_ERR_NO_FREE_BUFFER`), never overwritten. +- The process keeps a single shared `CameraProvider` (created lazily, + intentionally never destroyed): repeated create/destroy cycles are flaky on + some JetPack releases. +- See `src/lk_argus.h` for the full ABI and the thread-safety contract. From f7de04c990e6155762235c362a659ea3932fa5ed Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:46:44 -0700 Subject: [PATCH 6/6] Changeset --- .changeset/libargus-sys.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/libargus-sys.md diff --git a/.changeset/libargus-sys.md b/.changeset/libargus-sys.md new file mode 100644 index 000000000..564fab956 --- /dev/null +++ b/.changeset/libargus-sys.md @@ -0,0 +1,5 @@ +--- +libargus-sys: minor +--- + +Add a `libargus-sys` crate with bindings to the NVIDIA Jetson libargus camera API.