Connecting clients#
Every interactive agent sample starts DeviceIOHub and then accepts one or more
platform clients.
This client reference is the canonical build and connection guide for the
clients under client-samples/. Refer to Quickstart for starting an
agent sample and Networking and firewall for firewall and TLS configuration.
Client matrix#
Client |
Directory |
Transport |
Build |
|---|---|---|---|
Web |
|
LiveKit from CDN |
None |
Web-XR |
|
Local LiveKit and CloudXR bundles |
|
Android |
|
LiveKit Android |
Android Studio or Gradle |
iOS/visionOS |
|
LiveKit Swift and CloudXRKit |
Xcode |
Native C++ |
|
LiveKit C++ |
CMake |
The clients share a StreamKit shape: one transport-neutral StreamSession
delegates to a StreamingBackend, and LiveKitBackend is the only layer that
imports a LiveKit SDK. Connection, microphone, camera, participant status, data,
and network metrics remain separate operations.
Graphical clients display LiveKit connection quality, round-trip time, and receive jitter; the C++ sample reports them through its callback. Backends sample existing WebRTC statistics about once per second. No hub message or separate telemetry service is involved.
Web#
The basic page in client-samples/web/ uses plain ES modules and loads LiveKit
from jsDelivr. It has no build step. Open https://<host>:8080, accept the
development certificate warning, leave Token URL blank to use /token, and
connect. The camera preview follows the published track’s aspect ratio.
Web-XR (xr-render-demo)#
The Web-XR page uses same-origin bundles under
client-samples/web-xr/vendor/. The xr-render orchestrator builds missing
bundles automatically when npm is available. To rebuild manually:
cd client-samples/web-xr-build
./build.sh
The idempotent script reads the CloudXR version from .sdk-version, reuses a
matching versioned tarball when present, otherwise reuses the matching
IsaacTeleop tarball at ~/hub/IsaacTeleop/deps/cloudxr/ when available, and
finally downloads the public NGC tarball. It installs npm dependencies, writes
both ESM bundles to ../web-xr/vendor/, and records the SDK version beside
them. The orchestrator automatically rebuilds missing or stale bundles.
To bump CloudXR, edit .sdk-version and re-run the script. To bump LiveKit,
edit package.json, remove node_modules, and re-run the script.
Android#
The Android app is a Jetpack Compose client with selectable Camera2 devices, microphone capture, participant status, arbitrary data, and network metrics.
Requirements and build#
Requirement |
Version |
|---|---|
Android Studio |
Koala 2024.1.1 or newer |
JDK |
17 |
Minimum Android |
API 24 |
Target Android |
API 34 |
Open client-samples/android/ in Android Studio, allow Gradle sync to finish,
select a device, and run. The checked-in wrapper also supports:
cd client-samples/android
./gradlew assembleDebug
Connect and permissions#
Enter the server host, port 8080, a unique identity, and either the printed
token or the default token URL. Tap Install hub certificate before the
first connection; Android opens the system certificate-install flow.
The app requests microphone permission when starting the microphone and camera
permission when opening a physical camera. INTERNET and
MODIFY_AUDIO_SETTINGS are install-time permissions. BLUETOOTH_CONNECT is a
runtime permission on Android 12 and newer; the current sample declares it but
does not prompt for it, so grant Nearby devices in system settings before
depending on Bluetooth audio routing.
The current Android backend maps every enabled AudioConfig mode to LiveKit’s
default microphone capture. It does not yet select distinct voice-processing,
software-processing, or raw DSP settings, so the sample UI does not expose a
DSP-mode picker. LiveKit plays remote agent audio automatically. Camera choices
come from Camera2 and include front, back, extra built-in lenses, and attached
USB cameras when the device exposes them.
The root Android build pins the Netty dependency pulled in by AGP’s test tooling
above known vulnerable 4.1 releases. After changing AGP or the version catalog,
run ./gradlew verifyNettyPin from client-samples/android/.
The app can run as a flat Android panel on Android XR, but that path is not validated. It does not use Jetpack XR or immersive Android XR APIs, and its Camera2 selector is not a passthrough-camera integration.
iOS/visionOS#
The Apple client is a checked-in SwiftUI project and local Swift package. It targets iOS 18 and visionOS 26, requires Xcode 26 with Swift 6.2, and resolves LiveKit Swift and CloudXRKit through Swift Package Manager.
Build#
Open client-samples/ios-visionos/StreamKitSample.xcworkspace, select the
StreamKitSample scheme and destination, choose a signing team and a bundle ID
owned by that team, then build and run. The workspace already includes the app
project and local StreamKit package; do not create a replacement Xcode project.
The checked-in bundle ID is com.nvidia.xr-ai-example. Changing it does not
change the StreamKitSample display name, but it does select a new
UserDefaults domain, so saved settings reset on the first launch after a
rename.
The simulators stream
StreamKit/Sources/StreamKit/Resources/SimulatorFeed.gif instead of a physical
camera. Replace that resource to customize the simulated feed.
On an iOS device the preview follows the live camera aspect ratio. The Vision
Pro main-camera track goes directly from ARKit to LiveKit and is not copied into
the 2D preview; the LIVE badge, rather than the placeholder preview, indicates
capture. Start the immersive space before camera capture on a Vision Pro device.
Vision Pro permissions#
Main passthrough camera access on a Vision Pro device requires both the
com.apple.developer.arkit.main-camera-access.allow entitlement and the team’s
Apple-issued Enterprise.license. Put the non-redistributable license at
client-samples/ios-visionos/App/Enterprise.license; the build phase copies it
into the application. A missing license leaves audio, data, simulator camera,
and the rest of the app usable, but disables device passthrough camera access.
Xcode automatic signing supports development builds. App Store and TestFlight distribution require a manually issued provisioning profile that grants the main-camera entitlement.
The native CloudXR path also declares
com.apple.developer.low-latency-streaming, which requires an Apple Developer
Program team. A team that cannot provision this entitlement can remove it from
client-samples/ios-visionos/App/StreamKitSample.entitlements; native CloudXR
remains available with higher latency. Simulators do not require the enterprise
camera license.
Connect and trust the certificate#
Enter the host and port 8080, then use a pasted token or the default /token
endpoint. SessionConfig contains only the participant identity; the token
encodes the room. Incoming data callbacks receive (topic, data), and
_agent.status is delivered separately through the status callback.
The LiveKit WebSocket requires system trust for the hub certificate. In the app, tap Install hub certificate, allow Safari to download the profile, install it under Settings → General → VPN & Device Management, then enable full trust under Settings → General → About → Certificate Trust Settings. Refer to Troubleshooting for certificate regeneration, SAN mismatch, 401, and media-interruption diagnostics.
When microphone or camera state appears stuck, filter Console.app by
category:MediaSession to inspect interruption and recovery events. CoreAudio
-50 and FigAudioSession -19224 messages also appear during successful
microphone starts and are not failure indicators by themselves.
If microphone startup fails with
io.livekit.swift-sdk Code=101 "Timed out" after about five seconds, the
LiveKit recording engine did not produce the first buffer. Current builds
pre-warm that engine before publishing. After leaving XR, an iOS microphone
activation sound can play while capture restarts; this is expected.
Native CloudXR#
Native Apple clients require the CloudXR native device profile. Start the demo from the repository root with:
NV_DEVICE_PROFILE=auto-native \
uv run --project agent-samples/xr-render-demo xr_render_demo
The environment variable overrides the auto-webrtc YAML default without a
checkout edit. LiveKit remains on port 8080 while CloudXRKit uses its native
transport. Connecting or stopping XR does not disconnect the LiveKit agent
session. Closing the immersive space or disconnecting from the hub stops the XR
session so no render component is orphaned.
In auto-native mode, CloudXR does not use the web client’s port 48322 WSS
proxy and needs no additional certificate installation. CloudXRKit’s native
transport provides its own encryption without a user-facing trust prompt. The
hub certificate on port 8080 remains required for the LiveKit channel.
Native C++#
The C++20 sample requires LiveKit C++ SDK v1.10.2 or newer.
cd client-samples/native
cmake -S . -B build -DLIVEKIT_SDK_ROOT=/path/to/livekit-cpp-sdk
cmake --build build
./build/bin/streamkit_sample --host 192.168.1.100 --port 8080 \
--secure --token <jwt>
Install the hub certificate in the native host’s trust store before connecting. Refer to Linux native certificate trust for the Ubuntu and Debian procedure. The command uses the supported TLS proxy on port 8080. Insecure port 7880 is a direct LiveKit debugging path; do not expose it outside the XR AI host or a trusted development network.
Without LIVEKIT_SDK_ROOT, it builds in stub mode and reports a connected state
without opening a network session. Build the standalone assertion-based tests
with -DSTREAMKIT_BUILD_TESTS=ON and run:
ctest --test-dir build --output-on-failure
Test |
Coverage |
|---|---|
|
|
|
Canonical, missing, truncated, and empty |
|
Move and span frame-injection overloads |
|
Audio parameter forwarding and virtual dispatch |
|
Mock-backed connection, media, data, status, and disconnection lifecycle |
|
State deduplication, disconnect races, stale metrics, and callback exceptions |
Run one test with CTest, invoke its binary directly, or rerun failures:
ctest --test-dir build -R streamkit_frame_sink_tests --output-on-failure
./build/StreamKit/Tests/StreamKitTests/streamkit_frame_sink_tests
ctest --test-dir build --rerun-failed --output-on-failure
The backend implements connection state, data, _agent.status, network
metrics, host-injected audio, and host-injected video. StartAudio() and
StartCamera() arm publication; the host opens its devices and pushes PCM or
frames through AudioSink and FrameSink. Camera facing and device ID are
therefore inert in this backend, while optional encoding settings apply before
the first frame creates the video track. Real-time producers must prefer the
std::vector<uint8_t>&& frame-injection overload; the span overload copies each
frame and can add about 1.4 MB of copying for a typical frame.
Token-URL HTTP fetching is not implemented; pass an inline token or subclass
LiveKitBackend::FetchToken. GetRoom() is the LiveKit-specific escape hatch
for remote audio rendering or AEC reference capture. Microphone processing
presets are not mapped because the C++ SDK exposes no corresponding source
controls.
Adding another platform#
DeviceIOHub uses standard LiveKit. A new client fetches a token, joins through
the hub’s port 8080, publishes microphone and optional camera tracks, plays the
remote audio track, and reads data topics such as agent.response. Keep the
transport-specific SDK behind a StreamKit backend and reuse the same status,
interruption, and participant-routing contracts.