A distributed robotics simulation platform that runs isolated Gazebo Sim instances with live camera streaming to a browser via WebRTC. Built on Gazebo Sim 11 (Kilimanjaro), simulations run headless on Kubernetes/OpenShift with optional GPU acceleration, while a shared media infrastructure handles video relay, NAT traversal, and a web-based viewer with real-time telemetry.
Robotics simulation is compute-heavy, latency-sensitive, and typically locked to a single workstation. rhork moves simulation to the cluster so that:
- Each simulation gets its own isolated environment with dedicated resources
- Camera feeds stream to any browser with sub-second latency via WebRTC
- Shared infrastructure (media server, TURN relay, viewer) is deployed once
- Sims scale independently - spin up a new one with a single Helm install
- Fuel model caches persist across restarts so large worlds don't re-download
Video capture and encoding are handled by the gz-camera-stream plugin, a world-level Gazebo system plugin that hooks into the render loop, encodes frames with libx264, and pushes H.264 to MediaMTX via RTSP or WHIP. Each simulation's streams are namespaced (e.g. pick-place/overhead_cam, nav-test/aisle_cam) so they coexist on the shared media server without collision.
| Component | Helm Chart | Count | Purpose |
|---|---|---|---|
| coturn | charts/coturn |
1 | TURN relay for WebRTC UDP traversal through the cluster boundary |
| MediaMTX | charts/mediamtx |
1 | Media server - ingests H.264 from Gazebo pods, serves WebRTC/HLS to browsers |
| Viewer | charts/viewer |
1 | nginx serving the web UI with runtime-injected endpoints |
| Gazebo Sim | charts/gz-sim |
N | Headless simulation with the camera streaming plugin |
Shared services (coturn, MediaMTX, viewer) are deployed once per namespace. Simulation instances are spun up as independent Helm releases.
The viewer is a single-file web UI that connects to both MediaMTX (for WebRTC video) and each Gazebo instance's WebSocket server (for gz-transport telemetry). It provides:
- WebRTC video playback via WHEP with automatic ICE/TURN negotiation
- Topic tree with type-based filtering and icons for all published gz-transport topics
- Live telemetry cards - specialized renderers for Pose, Odometry (attitude indicator + altitude sparkline), Clock, WorldStatistics, CameraInfo, Twist, and a generic key-value fallback
- Stream control - click a camera in the sidebar to start/stop encoding on demand
- Simulation selector - connect to any running sim for topic browsing and telemetry
Encoding is demand-driven: streams activate when a viewer requests them and stop when all viewers disconnect, so idle cameras use zero encoding resources.
A single container image supports CPU-only, NVIDIA, and AMD GPUs. GPU selection happens at the pod level through Helm values:
| Mode | gpu.vendor |
What happens |
|---|---|---|
| CPU-only | none |
Mesa llvmpipe software rendering. No GPU hardware required. |
| NVIDIA | nvidia |
Sets runtimeClassName: nvidia, requests nvidia.com/gpu: 1. Requires NVIDIA GPU Operator on the cluster. |
| AMD | amd |
Requests amd.com/gpu: 1, adds video group. Requires AMD GPU device plugin on the cluster. |
- Kubernetes or OpenShift cluster with Helm v3
ocorkubectlCLI authenticated- Container images built and pushed (see Building images)
- For GPU: NVIDIA GPU Operator or AMD device plugin installed
# Create namespace
oc new-project gz-sim
# TURN relay
helm install coturn ./charts/coturn
# Media server (set coturn.host to the node IP where coturn is running)
helm install mediamtx ./charts/mediamtx \
--set coturn.host=<node-ip>
# Web viewer (set the MediaMTX route URLs for your cluster)
helm install viewer ./charts/viewer \
--set mediamtx.base=https://mediamtx-gz-sim.apps.<cluster> \
--set mediamtx.api=https://mediamtx-api-gz-sim.apps.<cluster># Warehouse world (default)
helm install pick-place ./charts/gz-sim --set sim=pick-place
# Quadcopter demo with GPU
helm install nav-test ./charts/gz-sim \
--set sim=nav-test \
--set world=quadcopter_demo \
--set gpu.vendor=nvidia
# Custom world file
oc create configmap dock-world --from-file=my_world.sdf
helm install dock-sequence ./charts/gz-sim \
--set sim=dock-sequence \
--set customWorldConfigMap=dock-world \
--set world=my_world# Open the viewer
open https://viewer-gz-sim.apps.<cluster>
# Shell into a simulation pod
oc rsh deploy/pick-place-gazebohelm uninstall pick-place # Remove one sim (PVC is retained)
helm uninstall viewer mediamtx coturn # Remove shared services
oc delete pvc pick-place-fuel-cache # Remove retained PVC if desiredTURN server for WebRTC NAT traversal. Deployed with hostNetwork: true so it binds directly to the node's network stack, avoiding NodePort range-mapping issues for UDP relay ports. An init container discovers the node's external IP via the Kubernetes downward API and injects it into turnserver.conf.
Key values:
| Value | Default | Description |
|---|---|---|
credentials.username |
gzsim |
TURN authentication username |
credentials.password |
gzsimpass |
TURN authentication password |
ports.listening |
3478 |
TURN signaling port |
ports.minRelay / maxRelay |
49152 / 49252 |
UDP relay port range |
MediaMTX media server. Gazebo pods push H.264 streams here via RTSP or WHIP. Browsers connect via WHEP for WebRTC playback or fall back to HLS. Streams are namespaced by simulation name (e.g. pick-place/overhead_cam).
When coturn is configured, MediaMTX includes the TURN server in ICE candidates so WebRTC works through NAT/firewalls.
Key values:
| Value | Default | Description |
|---|---|---|
coturn.host |
"" |
Node IP where coturn is running (enables TURN relay) |
coturn.port |
3478 |
TURN server port |
Exposes two services: mediamtx-webrtc (port 8889) for WHEP/WHIP signaling and mediamtx-api (port 9997) for the stream listing API.
nginx serving the viewer web UI. Endpoint URLs are injected at container startup via environment variables so the same image works across clusters.
Key values:
| Value | Default | Description |
|---|---|---|
mediamtx.base |
"" |
Public MediaMTX URL for WHEP playback |
mediamtx.api |
"" |
Public MediaMTX API URL for stream discovery |
Gazebo simulation instance. The sim value is required and controls stream namespacing, route naming, and resource labeling.
Key values:
| Value | Default | Description |
|---|---|---|
sim |
"" |
Required. Simulation name for isolation and stream namespacing. |
world |
small_warehouse |
SDF world file name from /worlds/ |
gpu.vendor |
none |
GPU mode: none, nvidia, or amd |
gazebo.bitrate |
4000000 |
H.264 encoding bitrate (bps) |
gazebo.fps |
30 |
Encoding framerate |
persistence.enabled |
true |
Create a PVC for the Gazebo Fuel model cache |
persistence.keep |
true |
Retain PVC on helm uninstall |
persistence.size |
5Gi |
Fuel cache PVC size |
customWorldConfigMap |
"" |
ConfigMap containing a custom SDF world file |
Multi-stage build on UBI 10. Compiles the CameraStream plugin in the build stage, copies the shared library and world files into a minimal runtime image with Mesa drivers for all GPU modes.
podman build -t quay.io/<org>/gz-sim-streamer:latest -f Containerfile.gazebo .
podman push quay.io/<org>/gz-sim-streamer:latestnginx on UBI 10 minimal. Copies viewer.html as a template and runs envsubst at startup to inject MediaMTX endpoint URLs.
podman build -t quay.io/<org>/gz-viewer:latest -f Containerfile.viewer .
podman push quay.io/<org>/gz-viewer:latestAmazon small warehouse environment, ported from the AWS RoboMaker Small Warehouse World (MIT-0 license). Includes shelves, pallet jacks, boxes, buckets, and clutter arranged in a realistic warehouse layout. Three fixed surveillance cameras provide streaming coverage:
overhead_cam- ceiling-mounted, looking straight down at the warehouse flooraisle_cam- shelf-height view down a main aisle between shelving unitsentrance_cam- corner-mounted wide-angle view of the warehouse entrance
The original Classic Gazebo (SDF 1.6) world has been adapted for Gazebo Sim 11 with rhork system plugins, camera sensors, and updated model definitions.
X3 UAV quadcopter with velocity control, three cameras (front 1280x720, downward 640x480, tower overview 1280x720), ground objects, and a landing pad. Run fly_patrol.sh inside the pod for an autonomous rectangular patrol pattern.
helm install nav-test ./charts/gz-sim --set sim=nav-test --set world=quadcopter_demoMinimal test scene with a spinning arm, falling shapes, and a single static camera. Good for verifying the encoding pipeline without the overhead of a full world.
| Route | Target | Purpose |
|---|---|---|
mediamtx.apps.<cluster> |
mediamtx-webrtc:8889 | WHEP video signaling, HLS fallback |
mediamtx-api.apps.<cluster> |
mediamtx-api:9997 | Stream listing API |
viewer.apps.<cluster> |
gz-viewer:8080 | Web UI |
gz-<sim>.apps.<cluster> |
<sim>-gazebo:9002 |
Gazebo WebSocket (telemetry) |
WebSocket routes use the haproxy.router.openshift.io/timeout: 300s annotation for long-lived connections.
coturn uses hostNetwork and binds directly to node ports - no Route or Ingress needed.
All stream paths follow the pattern <sim>/<camera_name>. The STREAM_PREFIX environment variable (set by the Helm chart to the sim name) is prepended automatically by the CameraStream plugin. This means multiple simulations can use identical world files with identical camera names and their streams won't collide.
This is the initial deployment infrastructure. Planned work includes:
- Multi-robot coordination across simulation instances
- Shared world state for collaborative scenarios
- ROS 2 bridge integration for control pipeline testing
- Automated CI/CD for world file validation
- Cluster autoscaling policies for simulation workloads
- Recording and replay of simulation sessions
Apache License 2.0