name: cae-streaming
description: >
Run Kit-CAE as a WebRTC-streamed application that remote clients can view and
control via data-channel messages. Provides a streaming-enabled .kit dep set,
the wire-protocol reference, the messaging-API reference, a small reusable
Python helper (OmniverseAPI), and a runnable reference handler
(load_scene + scenes registry) that demonstrates the request/response
pattern end-to-end. Triggers on requests involving "stream Kit-CAE", "WebRTC",
"remote viewer", "browser viewport", "application streaming", "data-channel
messages", or "remote control of Kit-CAE".
depends:
- cae-core
version: "2.1.0"
metadata:
author: "NVIDIA"
tags:
- kit-cae
- cae
- streaming
- webrtc
- remote
- data-channel
- omniverse
CAE Streaming - Remote-Controlled Kit-CAE
Stand up Kit-CAE as a streaming application that a remote client (browser, agent, dashboard, notebook) can both watch (live WebRTC viewport) and drive (request/response messages over the data channel).
The skill ships:
- A streaming-enabled
.kitdep set (omni.kit.livestream.messaging+ ahideUisetting) you can point any custom.kitat. scripts/launch_streaming.sh— wrapsrepo.sh launchwith sensible defaults.scripts/omniverse_api.py— a small@api.request/@api.signaldecorator framework that handles sync AND async handlers.scripts/serve.py— a runnable reference handler (load_scene+ scene registry) you can study or replace.- The wire protocol (
references/streaming-protocol.md) and the underlying Python API (references/messaging-api.md).
load_scene is one example. Real consumers will write their own handlers.
Purpose
Run Kit-CAE as a WebRTC-streamed application that remote clients can view and control via data-channel messages. Provides the streaming-enabled .kit dep set, the wire-protocol and messaging-API references, a reusable Python helper (OmniverseAPI), and a runnable reference handler (load_scene + scenes registry).
Prerequisites
cae-core (loaded automatically). A built Kit-CAE checkout that includes omni.kit.livestream.messaging. See ## Standing up the streaming server for the launch command and references/streaming-protocol.md for the wire format.
Instructions
- Stand up the streaming server — follow the launch command in
## Standing up the streaming server;scripts/launch_streaming.shwrapsrepo.sh launchwith the right defaults. - Run the reference handler — invoke
scripts/serve.pywith therun_scriptpattern (./repo.sh launch -n <kit> -- --exec scripts/serve.py) to host the bundledload_scenehandler; see## Bundled example: load_scene + scenes registry. - Connect a client — open the WebRTC viewer and connect over the data channel as described in
## Connecting a client. - Write your own handlers — apply the
@api.requestand@api.signalpatterns in## Writing handlersand the wire format inreferences/streaming-protocol.md. - Troubleshoot — consult
## Troubleshootingfor common failures (port collisions, async coroutine handling, scene-id mismatches).
Examples
Bundled scripts:
| Script | Purpose |
|--------|---------|
| scripts/launch_streaming.sh | Wrap repo.sh launch with sensible streaming defaults. |
| scripts/serve.py | Runnable reference handler — load_scene + scenes registry. |
| scripts/omniverse_api.py | @api.request / @api.signal decorator helper for sync and async handlers. |
End-to-end runnable example: scripts/serve.py paired with the scenes registry in scenes.yaml. See ## Writing handlers for the request/response pattern and references/messaging-api.md for the full helper API.
Limitations
Streaming uses WebRTC — clients need a browser or WebRTC client. LivestreamMessaging is per-app, not per-session, so multiple clients see the same scene. Time-varying playback inherits the same constraints as cae-visualization.
When to use
- A user / agent needs to interact with Kit-CAE from a process that isn't Kit (browser, REST service, RPC client, ML pipeline, dashboard).
- You need a long-lived Kit-CAE session that responds to commands rather than one-shot scripts that exit.
- You're building any client UI that wants a live Kit viewport plus programmatic control.
For headless capture (clean PNG/EXR/MP4 outputs, no streaming), stay with
cae-capture. This skill is the live + interactive path, not the offline
render path.
Architecture
Remote client (browser, agent, …) Kit-CAE host
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ AppStreamer (WebRTC client) │ video │ omni.kit.livestream.app │
│ - <video> / <audio> sinks │ ◄────── │ omni.kit.livestream.webrtc │
│ │ │ │
│ data-channel: │ JSON │ omni.kit.livestream.messaging│
│ - send ({type}_request) │ ◄────── │ scripts/serve.py: │
│ - recv ({type}_response) │ ──────► │ @api.request handlers │
│ - recv ({signal}) │ ◄────── │ @api.signal pushes │
└──────────────────────────────┘ └──────────────────────────────┘
Standing up the streaming server
Minimal launch:
cd <kit-cae-dir>
bash skills/cae-streaming/scripts/launch_streaming.sh
What it does:
- Launches a streaming Kit-CAE app. The launcher defaults to
omni.cae_vtk_streaming.kit(streaming + VTK extensions). Override viaCAE_STREAMING_KIT=<other.kit>for the slim non-VTK build (omni.cae_streaming.kit) or any other custom app. - Both shipping streaming apps depend on
kit_cae_streaming(the streaming base template), which listsomni.kit.livestream.messagingand sets[settings.app.window] hideUi = true. The data-channelobserveAPI and the chrome-free viewport are both wired in via that template — no special launch flags required. - Runs
scripts/serve.pyvia--exec. That script registers handlers and loops forever, awaiting messages from connected clients.
After the launch, look for these log lines:
[ovapi] registered request handler: load_scene_request
[ovapi] registered request handler: list_scenes_request
[cae-streaming] viewport-only: hideUi=True via carb.settings
[omni.kit.livestream.app.plugin] Started primary stream server on signal port 49100 …
Now any compatible client can connect.
Connecting a client
Any client speaking omni.kit.livestream.*'s WebRTC + data channel works.
Compatible clients include:
- A JavaScript browser app using
@nvidia/omniverse-webrtc-streaming-library. - A Python client over
aiortc. - A native Kit window pointed at the same signaling endpoint.
Whatever the language, the JSON wire format is the same — see
references/streaming-protocol.md for the envelopes, naming convention, and
correlation rules.
A minimal browser snippet (JS):
import { AppStreamer, StreamType } from "@nvidia/omniverse-webrtc-streaming-library";
await AppStreamer.connect({
streamSource: StreamType.DIRECT,
streamConfig: {
signalingServer: "kit-host", signalingPort: 49100,
videoElementId: "kit-video", audioElementId: "kit-audio",
onCustomEvent: (msg) => console.log("from kit:", msg),
},
});
AppStreamer.sendMessage(JSON.stringify({
event_type: "load_scene_request",
payload: { id: 1, scene_id: "static_mixer" },
}));
The wire protocol
Two message families:
- request / response — client asks Kit to do something; correlation by
idfield. Naming convention:<type>_request→<type>_response(underscore — see the colon-default warning inreferences/messaging-api.md). - signal — one-way push from Kit to client; no
id, no reply expected.
Full spec, including the JSON envelopes, the bundled-example operations, and
the error encoding: references/streaming-protocol.md.
Writing handlers
Use scripts/omniverse_api.py's @api.request / @api.signal decorators
(handles sync and async); drop down to omni.kit.livestream.messaging
directly via references/messaging-api.md when you need finer control.
Request/response with OmniverseAPI
from omniverse_api import OmniverseAPI, exclusive
from omni.cae.core.commands import execute_command
from omni.cae.usd_plugins_importers import import_to_stage
from omni.cae.testing import wait_for_update
api = OmniverseAPI()
@api.request
@exclusive # optional: drop concurrent calls if mutating shared state
async def load_scene(scene_id: str = "", **_):
if not scene_id:
return {"ok": False, "error": "scene_id required"}
await import_to_stage(f"/data/{scene_id}.cgns", f"/World/{scene_id}")
await wait_for_update()
await execute_command("CreateCaeVizFaces",
dataset_path=f"/World/{scene_id}/Base/Zone",
prim_path=f"/World/CAE/Faces_{scene_id}")
return {"ok": True, "scene_id": scene_id}
The decorator handles payload unpacking, async scheduling, response dispatch,
and exception → {"ok": False, "error": "..."} conversion (so Python
tracebacks never cross the wire).
Pushing signals
@api.signal
def progress(scene_id: str, percent: int):
return {"scene_id": scene_id, "percent": percent}
# Calling progress(scene_id="static_mixer", percent=42) pushes a
# `progress_signal` event with that payload to all connected clients.
# Or fire a payload directly without a wrapper function:
api.dispatch_signal("notification", {"level": "info", "message": "Stage ready"})
Two footguns to avoid
- Long-lived
serve.py. Do NOT use the cae-core "Script shutdown (MANDATORY)" template (app.post_quit()+os._exit(0)). That's for one-shot capture; a streaming listener must run until the process is signaled. - Document new request types in
references/streaming-protocol.mdso future readers inherit the contract.
Bundled example: load_scene + scenes registry
The skill ships with one runnable handler so the end-to-end flow works the day you check it out:
scripts/serve.pyregistersload_scene_request,list_scenes_request, andregister_scene_requesthandlers.- A scene registry maps
scene_id→{ path, format, default_viz, ... }.serve.pyresolves it in this order:scenes.json(machine-friendly, no extra deps) →scenes.yaml(requires PyYAML) → an embeddedDEFAULT_SCENESdict inserve.pyitself (always works).
All three layers are illustrative — copy and adapt freely. The framework itself does not require a registry; it exists because the bundled handler reads it.
Field reference (when you do use the bundled handler):
| Key | Meaning |
|---|---|
| path | Filesystem path. ${KIT_CAE_DIR} is expanded from the env var. |
| format | One of cgns, vtk, npz, ensight. Selects the importer. |
| prim_path | Where the data lands in the stage (e.g. /World/<name>). |
| dataset_path | Prim with the dataset (per-format; see cae-core/references/formats.md). |
| default_viz | Short label appended to the viz prim path. |
| default_viz_command | One of the CreateCaeViz* commands from cae-visualization. |
| color_field | Optional. Path to the field bound as the viz colors target. |
Verify dataset paths with
cae-core/scripts/inspect_cgns.py/inspect_vtk.pybefore declaring done — paths vary per file.
Ports, hosts, firewalls
- WebRTC signaling: 49100 (configurable via
--/exts/omni.kit.livestream.app/primaryStream/streamPort=). - Media negotiation occurs over ranges 47995-48012 and 49000-49007 by default.
- Open these ports between client and Kit host (or use a TURN server if you cannot punch through NAT).
- HTTPS / WSS: not required for local testing. For remote deployment, terminate TLS in front of the signaling endpoint.
Viewport-only streamed output
The streaming .kit template (templates/kit_cae/kit_cae_streaming.kit)
sets [settings.app.window] hideUi = true, which collapses Kit's editor
chrome (menubar, panels, toolbar, timeline) and leaves only the rendered
viewport. serve.py defensively re-applies the same setting at startup via
carb.settings.set("/app/window/hideUi", True) plus toggles for the in-viewport
gizmos (camera/light/HUD/grid/axis). Override at launch with
--/app/window/hideUi=false for local debugging.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Client stuck "Connecting…" | Port 49100 blocked; verify with ss -tlnp \| grep 49100. |
| Client connects, viewport stays black | Stream is up but no scene loaded yet. Either trigger a handler or load a stage manually. |
| Request returns no reply | serve.py not running, handler not registered, or wire-name mismatch (_request vs :request). |
| Handler returns unknown scene_id 'X' | Bundled-example case: client scene_id not in the active registry. |
| Kit log: omni.kit.livestream.messaging: extension not found | Make sure the streaming .kit lists it explicitly under [dependencies]. |
| Multiple clients see the same scene | Expected — LivestreamMessaging is per-app, not per-session. |
| Camera frames an un-tessellated mesh | Add a 30-60 frame app.next_update_async() settle loop before calling frame_prims. |
| Python traceback never reaches client | Wrap handler body in try/except and return {"ok": False, "error": ...}. The OmniverseAPI framework does this for you. |
| Object of type coroutine is not JSON serializable | You're using LivestreamMessaging.observe_and_dispatch directly with an async def handler. The decorator does NOT await coroutines. Use OmniverseAPI's @api.request decorator (handles both sync and async), or split the work into a sync receiver + manual dispatch_event from the async coroutine. |
Cross-references
cae-core/SKILL.md— preflight, Z-up, launch flags.cae-core/references/kit-cae-api.md—import_to_stage,execute_commandsignatures (used by handlers that touch the stage).cae-core/references/formats.md— per-format dataset prim paths.cae-visualization/SKILL.md— choosing visualization commands inside handlers.cae-capture/SKILL.md— for offline rendering, not live viewing.references/streaming-protocol.md— the wire spec your handlers must follow.references/messaging-api.md—omni.kit.livestream.messagingPython API (the lower layerOmniverseAPIwraps).scripts/omniverse_api.py— the@api.request/@api.signalframework.scripts/serve.py— runnable reference handler set.
Next.js App Router Expert
Development
A skill that turns Claude into a Next.js App Router expert.
README Generator
Development
Creates professional and comprehensive README.md files for your projects.
API Documentation Writer
Development
Generates comprehensive API documentation in OpenAPI/Swagger format.