Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
318c351
sensing: publish SENSING sensors to LiveKit from the Jetson hardware …
gareth-morgan-nv Nov 5, 2026
d0d3cb8
sensing: add a mock backend that runs the LiveKit publisher without a…
gareth-morgan-nv Nov 5, 2026
78c549a
Revert "sensing: add a mock backend that runs the LiveKit publisher w…
gareth-morgan-nv Nov 5, 2026
23f688d
sensing: publish Unix time as the LiveKit frame's application timestamp
gareth-morgan-nv Nov 5, 2026
3733dc7
sensing: say the Unix stamp is processing time, and stop a parameter …
gareth-morgan-nv Nov 5, 2026
b1041cd
sensing: add a probe to measure the capture counter against the host …
gareth-morgan-nv Nov 5, 2026
570a68a
sensing: make the counter probe cheap when off, logged, and honest ab…
gareth-morgan-nv Nov 5, 2026
f84c3aa
sensing: document building and running camera_plugin_sensing on the Orin
gareth-morgan-nv Nov 5, 2026
105b838
sensing: stop calling the SIPL capture counter nanoseconds
gareth-morgan-nv Nov 5, 2026
edb32ba
sensing: record the Orin build flags that worked
gareth-morgan-nv Nov 5, 2026
7ae4aad
sensing: pair Jetson encoder units with captures by order
gareth-morgan-nv Nov 5, 2026
ab84e2a
sensing: clang-format the encoder pairing change
gareth-morgan-nv Nov 5, 2026
6267bb1
sensing: log the SIPL counter uncalibrated, and state the encoder tim…
gareth-morgan-nv Nov 5, 2026
90ac149
sensing: clang-format the counter probe
gareth-morgan-nv Nov 5, 2026
d7b2f22
egocap: stop checking in the agent-only LiveKit preview plan
gareth-morgan-nv Nov 5, 2026
096d754
sensing: publish the capture stamp, clear the room delegate, and buil…
gareth-morgan-nv Nov 6, 2026
2a6529f
sensing: add a camera setup guide
gareth-morgan-nv Nov 6, 2026
7ed72f9
sensing: add photos to the camera setup guide
gareth-morgan-nv Nov 6, 2026
629d7e0
sensing: drop the verification notes from the camera setup guide
gareth-morgan-nv Nov 6, 2026
ca70903
sensing: add Orin flashing and package install steps to the camera se…
gareth-morgan-nv Nov 9, 2026
cb776ee
sensing: add the capture boundary the sender will select a backend th…
gareth-morgan-nv Nov 5, 2026
6f310b1
sensing: run the SIPL capture through the CameraCapture interface
gareth-morgan-nv Nov 5, 2026
42ce447
sensing: choose the camera backend once at startup, with no silent fa…
gareth-morgan-nv Nov 5, 2026
7c17f29
sensing: state the frame release and DMA-BUF contract, drop the unuse…
gareth-morgan-nv Nov 5, 2026
c25a1ed
sensing: unit tests for backend selection, report a stopped encoder f…
gareth-morgan-nv Nov 5, 2026
0e0b3ff
sensing: report a failed encoder input dequeue instead of counting it…
gareth-morgan-nv Nov 5, 2026
3443403
sensing: scope the stop() promise to leased frames, and require avail…
gareth-morgan-nv Nov 5, 2026
c7cbf31
sensing: test that a matching but unavailable backend is never selected
gareth-morgan-nv Nov 5, 2026
af9e6bc
sensing: add the Argus backend plan
gareth-morgan-nv Nov 5, 2026
cc36741
sensing: add the Argus capture backend and the encoder's DMA-BUF inpu…
gareth-morgan-nv Nov 5, 2026
ed4b586
sensing: release a replaced Argus frame outside the output mutex; rec…
gareth-morgan-nv Nov 5, 2026
e72fc7f
sensing: link the Argus daemon client by default; keep dynamic backen…
gareth-morgan-nv Nov 5, 2026
42e1bdc
sensing: roll back started Argus sessions on a failed start; bound th…
gareth-morgan-nv Nov 5, 2026
ff639a8
sensing: remove two stray lines left after the Argus CMake block
gareth-morgan-nv Nov 5, 2026
3333c28
sensing: stop checking in the agent-only Argus plan
gareth-morgan-nv Nov 5, 2026
6032ea3
sensing: release the Argus EGL display on a failed construction; prob…
gareth-morgan-nv Nov 6, 2026
e4d9a7a
sensing: make the Argus-only build compile and link on JetPack 6.2
NVshawnhwang Nov 7, 2026
7d7ec51
sensing: address the Argus review: pool sizing, early scale check, de…
gareth-morgan-nv Nov 8, 2026
188d616
sensing: Argus acquisition thread per camera and bounded, ordered tea…
gareth-morgan-nv Nov 8, 2026
2683925
sensing: make Argus start transactional and teardown one bounded step
gareth-morgan-nv Nov 8, 2026
afb8127
sensing: one Argus teardown path; a session that will not stop ends t…
gareth-morgan-nv Nov 8, 2026
23d3ed4
sensing: give each Argus camera's buffer stream a single owner thread
gareth-morgan-nv Nov 8, 2026
5baec53
sensing: drain before the blocking stop, keep a refused buffer counte…
gareth-morgan-nv Nov 8, 2026
0e18b1b
sensing: stop captures at stop(), wait for idle only once every buffe…
gareth-morgan-nv Nov 8, 2026
8094c90
sensing: stopRepeat returns a capture-id range, not a Status
gareth-morgan-nv Nov 9, 2026
41d079d
sensing: answer the PC's clock-sync requests from the native sender
gareth-morgan-nv Nov 5, 2026
e8efeed
sensing: tests for the clock-sync wire format, with golden bytes from…
gareth-morgan-nv Nov 5, 2026
3ffe585
sensing: publish the sensor capture time on CLOCK_MONOTONIC, and prob…
gareth-morgan-nv Nov 5, 2026
383b6c5
egocap: age the clock mapping's uncertainty at read, and state what t…
gareth-morgan-nv Nov 5, 2026
46daecd
egocap, sensing: record and show latency from the frame-ready time by…
gareth-morgan-nv Nov 5, 2026
32e82a3
sensing: publish the encoder unit's stamp, not a FrameMeta field that…
gareth-morgan-nv Nov 6, 2026
85b7158
sensing: pace on the frame's stamped clock, not the poll time
gareth-morgan-nv Nov 8, 2026
2d63f19
video_stage: PC GPU video stage with a direct AVS1 receiver, NVDEC, e…
gareth-morgan-nv Nov 5, 2026
03c5138
video_stage: multi-stream worker, LiveKit publish, direct-link clock …
gareth-morgan-nv Nov 6, 2026
d625571
video_stage: clean session boundaries and a bounded handshake
gareth-morgan-nv Nov 6, 2026
ab6d4b1
sensing: install the producer as its own CMake component
gareth-morgan-nv Nov 6, 2026
1fa8252
video_stage: a mock Orin sender that behaves like the producer's dire…
gareth-morgan-nv Nov 6, 2026
bb7db37
sensing: say why the direct sender cannot reach the PC
gareth-morgan-nv Nov 6, 2026
3c2e323
video_stage: bound the direct link's latency on any network, and repo…
gareth-morgan-nv Nov 6, 2026
b01252a
video_stage: time the worker's own steps before and inside the LiveKi…
gareth-morgan-nv Nov 6, 2026
27f7dcd
video_stage: declare no reordering to NVDEC and measure the decoder's…
gareth-morgan-nv Nov 6, 2026
e868699
video_stage: unit tests for the H.264 SPS parser and the no-reorderin…
gareth-morgan-nv Nov 6, 2026
07f882b
video_stage: clang-format the SPS tests
gareth-morgan-nv Nov 6, 2026
5ef4f3a
video_stage: forward the Orin's access units untouched by default
gareth-morgan-nv Nov 6, 2026
b008047
Make DirectSender shutdown cancellable and fix the mock sender
gareth-morgan-nv Nov 6, 2026
328ca04
sensing: ask the Jetson encoder to write a VUI
gareth-morgan-nv Nov 6, 2026
85abe2c
sensing: remove a leftover conflict marker and the duplicated LiveKit…
gareth-morgan-nv Nov 6, 2026
f1a1f27
sensing: management channel, camera fault reporting and a mock producer
gareth-morgan-nv Nov 6, 2026
8aa74a3
sensing: diagnose more faults, watch for stalled streams, report dire…
gareth-morgan-nv Nov 6, 2026
5fc0864
sensing: the Orin producer streams only over its direct links; no Liv…
gareth-morgan-nv Nov 6, 2026
c4fb19f
sensing: tests for the shared wire headers and the NACK handling; upd…
gareth-morgan-nv Nov 6, 2026
2307a2f
video stage: keep publishing when the clock mapping is poor; sensing:…
gareth-morgan-nv Nov 6, 2026
e7851dc
video stage, sensing: address review findings on ownership, bounds an…
gareth-morgan-nv Nov 6, 2026
2dd974c
video stage: apply clang-format-14 to files it flagged
gareth-morgan-nv Nov 6, 2026
8b27888
sensing: match encoded units to captures by returned stamp, never by …
gareth-morgan-nv Nov 6, 2026
f18a97a
sensing: clang-format the video output's class comment
gareth-morgan-nv Nov 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ option(BUILD_PLUGIN_NOITOM_MOCAP "Build Noitom mocap plugin (downloads MocapApi
option(BUILD_PLUGIN_OGLO "Build OGLO tactile glove plugin (BLE, Linux only; fetches SimpleBLE + nlohmann/json)" OFF)
option(BUILD_PLUGIN_WUJI_GLOVE "Build Wuji glove plugin (requires the wuji_sdk C SDK)" OFF)
option(BUILD_PLUGIN_SHARPA_AVATAR "Build Sharpa AvatarExo Glove plugin (Linux; skips if the Avatar SDK is missing)" OFF)
option(BUILD_PLUGIN_SENSING "Build SENSING GMSL camera plugin (Jetson only; requires SIPL, NVENC and the LiveKit SDK)" OFF)
option(BUILD_VIDEO_STAGE "Build the PC GPU video stage (needs a CUDA toolkit; tests need a CUDA GPU)" OFF)
option(BUILD_EXAMPLES "Build examples" ON)
option(BUILD_EXAMPLE_TELEOP_ROS2 "Build only the teleop_ros2 ROS 2 reference integration (e.g. for Docker)" OFF)
option(BUILD_TESTING "Build unit tests" ON)
Expand Down
19 changes: 19 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ if(BUILD_VIZ)
add_subdirectory(viz)
endif()

# The LiveKit SDK and publisher library: the PC video stage publishes to LiveKit. The camera producer on the Orin does
# not use LiveKit; it sends over its direct links.
if(BUILD_VIDEO_STAGE)
add_subdirectory(livekit_publisher)
endif()

if(BUILD_VIDEO_STAGE)
add_subdirectory(video_stage)
elseif(BUILD_PLUGIN_SENSING)
# The SENSING plugin sends AVS1 records to the video stage and needs only its record codec and sender.
add_subdirectory(video_stage/wire)
add_subdirectory(video_stage/sender)
endif()

# The camera producer's management channel, also used by the mock producer.
if(BUILD_PLUGIN_SENSING OR BUILD_VIDEO_STAGE)
add_subdirectory(sensing_control)
endif()

if(BUILD_PLUGINS)
add_subdirectory(plugins)
endif()
95 changes: 95 additions & 0 deletions src/livekit_publisher/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# ==============================================================================
# LiveKit
# ==============================================================================
# Each selected sensor is published as a LiveKit camera track. The SDK is a prebuilt package (headers and shared libraries), pinned to a
# release and checked against its hash; point LIVEKIT_SDK_DIR at an unpacked copy to use another.
set(_livekit_local ON)
if(NOT LIVEKIT_SDK_DIR)
set(_livekit_local OFF)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
set(_livekit_arch "arm64")
set(_livekit_hash "e3a97f71943c0bf9fa4ebd70927ca2906b31dcd6c14b42312b1225b4b868e4ad")
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
set(_livekit_arch "x64")
set(_livekit_hash "5e0feb2e519c6ff131dd2d1c45149115057ae4e21a324fdb249e1789d5614e7b")
else()
message(FATAL_ERROR "No prebuilt LiveKit SDK for ${CMAKE_SYSTEM_PROCESSOR}; set LIVEKIT_SDK_DIR.")
endif()
include(FetchContent)
FetchContent_Declare(
livekit_sdk
URL https://github.com/livekit/client-sdk-cpp/releases/download/v1.12.4/livekit-sdk-ubuntu-24.04-${_livekit_arch}-1.12.4.tar.gz
URL_HASH SHA256=${_livekit_hash}
)
FetchContent_MakeAvailable(livekit_sdk)
set(LIVEKIT_SDK_DIR "${livekit_sdk_SOURCE_DIR}")
endif()
# The package ships no licence files. The pinned download supplies the SDK's own; a local SDK uses
# LICENSE/NOTICE from its directory, or LIVEKIT_SDK_LICENSE_FILE / LIVEKIT_SDK_NOTICE_FILE, and needs no network.
foreach(_notice LICENSE NOTICE)
set(_override "${LIVEKIT_SDK_${_notice}_FILE}")
if(_override)
set(_found "${_override}")
elseif(_livekit_local)
set(_found "")
foreach(_candidate "${LIVEKIT_SDK_DIR}/${_notice}" "${LIVEKIT_SDK_DIR}/../${_notice}")
if(NOT _found AND EXISTS "${_candidate}")
set(_found "${_candidate}")
endif()
endforeach()
if(NOT _found)
message(FATAL_ERROR
"LIVEKIT_SDK_DIR is set but ${_notice} was not found in it. Put the SDK's ${_notice} file there "
"or pass -DLIVEKIT_SDK_${_notice}_FILE=/path/to/${_notice}.")
endif()
else()
if(_notice STREQUAL "LICENSE")
set(_notice_hash "09e8a9bcec8067104652c168685ab0931e7868f9c8284b66f5ae6edae5f1130b")
else()
set(_notice_hash "6d0f7c5e0deb284362296c5da110dd353831da2e67ca77cb5abc051b951b0fac")
endif()
FetchContent_Declare(
livekit_sdk_${_notice}
URL https://raw.githubusercontent.com/livekit/client-sdk-cpp/v1.12.4/${_notice}
URL_HASH SHA256=${_notice_hash}
DOWNLOAD_NO_EXTRACT TRUE
)
FetchContent_MakeAvailable(livekit_sdk_${_notice})
# FetchContent publishes <lowercased name>_SOURCE_DIR.
string(TOLOWER "${_notice}" _notice_lower)
set(_found "${livekit_sdk_${_notice_lower}_SOURCE_DIR}/${_notice}")
endif()
set(SENSING_LIVEKIT_${_notice}_FILE "${_found}" CACHE INTERNAL "")
endforeach()
find_package(LiveKit CONFIG REQUIRED PATHS "${LIVEKIT_SDK_DIR}" NO_DEFAULT_PATH)
message(STATUS "LiveKit SDK=${LIVEKIT_SDK_DIR}")
# Cache, so tests/ (a sibling directory) can find the same SDK.
set(SENSING_LIVEKIT_SDK_DIR "${LIVEKIT_SDK_DIR}" CACHE INTERNAL "")



# The LiveKit SDK behind a small interface: join a room, publish a video track, submit H.264 access units.
# No LiveKit type appears in an installed header, and nothing here touches CUDA.
add_library(sensing_livekit STATIC
livekit_session.cpp
inc/sensing_livekit/livekit_session.hpp
inc/sensing_livekit/video_track_sink.hpp
)

target_include_directories(sensing_livekit
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/inc>
)

target_link_libraries(sensing_livekit
PUBLIC
log_bridge::log_bridge_core
video_stage::wire
PRIVATE
LiveKit::livekit
)

add_library(sensing::livekit ALIAS sensing_livekit)
78 changes: 78 additions & 0 deletions src/livekit_publisher/inc/sensing_livekit/livekit_session.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include "video_track_sink.hpp"

#include <cstdint>
#include <memory>
#include <string>

namespace plugins
{
namespace sensing
{

struct LiveKitSessionImpl;

/**
* @brief Keeps the LiveKit SDK initialised while it is held.
*
* The SDK is process-wide and must be shut down only when nothing in the process uses it, so every
* user holds one of these and the last release shuts it down.
*/
class LiveKitSdkReference
{
public:
LiveKitSdkReference();
~LiveKitSdkReference();
LiveKitSdkReference(const LiveKitSdkReference&) = delete;
LiveKitSdkReference& operator=(const LiveKitSdkReference&) = delete;
};

struct PublishSpec
{
/// Name subscribers see for the track.
std::string name;
uint32_t width = 0;
uint32_t height = 0;
/// Upper bound the upstream encoder may use; WebRTC adapts below it.
uint64_t max_bitrate_bps = 0;
double max_fps = 30.0;
};

/**
* @brief One participant in one LiveKit room.
*
* The room and the participant's identity come from the join token. A terminal loss of the room
* (the server went away, the participant was removed) sets failed(); the SDK's own bounded
* reconnection runs first and is not wrapped in another supervisor.
*/
class LiveKitSession
{
public:
/// @throws std::runtime_error if the room cannot be joined. The token is never logged.
static std::unique_ptr<LiveKitSession> connect(const std::string& url, const std::string& token);

/// Unpublish every track (destroy them first), then leave the room.
~LiveKitSession();

LiveKitSession(const LiveKitSession&) = delete;
LiveKitSession& operator=(const LiveKitSession&) = delete;

/// Publishes an H.264 camera track that carries pre-encoded access units; the SDK does not encode.
/// @throws std::runtime_error if publication fails.
std::unique_ptr<VideoTrackSink> publish_video(const PublishSpec& spec);

/// True once the room has been lost for good.
bool failed() const;
std::string failure() const;

private:
explicit LiveKitSession(std::shared_ptr<LiveKitSessionImpl> impl);
std::shared_ptr<LiveKitSessionImpl> m_impl;
};

} // namespace sensing
} // namespace plugins
57 changes: 57 additions & 0 deletions src/livekit_publisher/inc/sensing_livekit/video_track_sink.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include <video_stage/encoded_video_frame.hpp>

#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>

namespace plugins
{
namespace sensing
{

using video_stage::EncodedVideoFrame;

/// What the receiving side asked of the encoder since the last call.
struct EncoderFeedback
{
/// The next frame must be an IDR with its parameter sets.
bool keyframe_requested = false;
/// Latest bitrate target from congestion control, if one was requested.
std::optional<uint32_t> target_bitrate_bps;
};

/**
* @brief One published video track that carries frames already encoded by hardware.
*
* The SDK forwards each access unit without encoding it again. A single thread must drive one sink.
*/
class VideoTrackSink
{
public:
virtual ~VideoTrackSink() = default;

/// Size of the encoded pictures this track was published for.
virtual uint32_t width() const = 0;
virtual uint32_t height() const = 0;

/// Publishes one access unit; the bytes are copied before this returns.
/// @return false when the SDK did not accept the frame, which can break the reference chain:
/// the caller must follow with a keyframe.
/// @throws std::runtime_error when the frame cannot be published at all.
virtual bool submit(const EncodedVideoFrame& frame) = 0;

/// Keyframe and rate requests accumulated since the previous call.
virtual EncoderFeedback take_feedback() = 0;

/// One line naming the transport's view of the stream, or empty if unavailable.
virtual std::string describe() = 0;
};

} // namespace sensing
} // namespace plugins
Loading
Loading