Skip to main content

Overview

The YAML configuration file defines how the LiveKit Portal for ROS bridges your ROS graph to and from a LiveKit room. It sets which topics and services to expose, including image topics that stream as video. LiveKit Portal for ROS loads the file when the node starts. Specify the file path with the node's config_path ROS parameter. Changes to the file are applied when the node restarts.

The configuration file is optional. Without one, the LiveKit Portal for ROS forwards all topics bidirectionally, which is convenient for development but not recommended for production: declare only the routes your app needs.

The file declares routes. There are two kinds of routes:

  • Topic routes bridge ROS topics to LiveKit DataTracks. Image topics use LiveKit video tracks instead. The direction controls the flow: out publishes a local topic into the room, in subscribes to a remote topic and republishes it on the local graph, and bidirectional does both.
  • Service routes expose a local ROS service that forwards each call to a specific LiveKit participant. Service routes are always out.

Declare only the topics and services you need, and only in the direction you need them: every route adds traffic to the LiveKit connection, so a narrow configuration keeps bandwidth and latency down. For outbound topics, max_rate_hz throttles how often samples are forwarded, and encoding selects how each message is serialized on the DataTrack.

Example configuration

This example configures three topic routes and one service route:

  • Send and receive all topics in the /test/ namespace.
  • Send out a rate-limited /imu/data_raw topic as JSON Schema.
  • Receive /remote/cmd_vel commands.
  • Expose a service for robot-a to navigate to a pose.
ros_portal:
version: "0.0.1"
topics:
- topic: "/test/.*"
direction: "bidirectional"
- topic: "/imu/data_raw"
direction: "out"
max_rate_hz: 10
encoding: "jsonschema"
- topic: "/remote/cmd_vel"
direction: "in"
services:
- service: "/navigate_to_pose"
direction: "out"
participant: "robot-a"
msg_type: "nav2_msgs/srv/NavigateToPose"

Credentials

ROS Portal supports literal tokens, an endpoint token server, and a development token server. Select one of the following options:

Token sourceEnvironment variablesUse caseToken issuer
Literal tokenLIVEKIT_URL + LIVEKIT_TOKEN<br><br>LIVEKIT_API_KEY + LIVEKIT_API_SECRET (optional - LiveKit Cloud)Local LiveKit development, or direct control over token grants and lifetimeUser, via lk token create or another token-generation service
Endpoint token serverLIVEKIT_TOKEN_ENDPOINTProduction, server authenticates requests and generates tokensUser-developed backend
Development token serverLIVEKIT_TOKEN_SERVER_IDQuick prototypes and testing with LiveKit Cloud only; never use in productionLiveKit Cloud

The following rules apply to these environment variables:

  • ROS Portal fails to start if more than one of LIVEKIT_TOKEN, LIVEKIT_TOKEN_ENDPOINT, and LIVEKIT_TOKEN_SERVER_ID is set.
  • LIVEKIT_API_KEY and LIVEKIT_API_SECRET are used by LiveKit CLI to generate literal tokens against a LiveKit Cloud project. ROS Portal does not read either variable, only the resulting LIVEKIT_TOKEN.

Token grants

A token that you or your endpoint generates must have the following grants:

Grantlk token create flagDescription
Room join--joinLets the node connect to the room.
Room name--room <name>Sets the room to join.
Identity--identity <identity>Sets the participant identity of the node, for example ros-portal-robot.
canUpdateOwnMetadata--allow-update-metadataLets the node set the lk.robot participant attribute to "true".

The development token server adds these grants for you.

Literal token

Set LIVEKIT_URL and a participant JWT in LIVEKIT_TOKEN. To generate a token for a LiveKit Cloud project with the LiveKit CLI, set LIVEKIT_API_KEY and LIVEKIT_API_SECRET first. The node does not read these two variables.

export LIVEKIT_URL=wss://<project-subdomain>.livekit.cloud
export LIVEKIT_API_KEY=<api-key>
export LIVEKIT_API_SECRET=<api-secret>
export LIVEKIT_TOKEN="$(lk token create \
--join \
--room robo_room \
--identity ros-portal \
--allow-update-metadata \
--valid-for 24h \
--token-only \
--yes)"

Endpoint token server

For production, set LIVEKIT_TOKEN_ENDPOINT to a backend token endpoint. The endpoint returns the LiveKit server URL and a new participant JWT.

export LIVEKIT_TOKEN_ENDPOINT=https://example.com/livekit/token

Development token server

For development with LiveKit Cloud, enable a development token server in your project settings. Then set its ID:

export LIVEKIT_TOKEN_SERVER_ID=token-server-xxxxxx
Development only

Do not use the development token server in production. Any app can request a token from it with any permissions and no restrictions.

Top-level fields

Every field below is nested under a single top-level ros_portal key, as shown in the example above.

FieldTypeRequiredDefaultDescription
versionstringyesMust match ROS Portal's expected config schema version.
ros_threadsinteger0ROS executor thread count. The default 0 uses the number of available CPU cores. Ignored in composition, where the container owns the executor.
enable_all_ros_topic_statsbooleanfalseEnables ROS 2 topic statistics for every outbound topic subscription. When true, it overrides each topic's value.
serviceslist[]Service route declarations.
topicslist[]Topic route declarations.
video_sourceslist[]Capture sources published as LiveKit video tracks. See Video sources.

JSON schema

To validate a configuration file in your editor, add the optional $schema field at the top level of the file, next to ros_portal. An IDE YAML plugin uses the schema for validation and autocomplete. The node ignores this field.

$schema: https://raw.githubusercontent.com/livekit/ros-portal/main/src/ros_portal_config/schema/ros_portal_config.schema.json
ros_portal:
version: "0.0.1"

Topics

Each entry in the topics list declares a topic route with the following fields:

FieldTypeRequiredDefaultDescription
topicstringyesROS topic pattern. Must be non-empty. Treated as an ECMAScript regex for the DataTrack path. Matched as a literal name for max_rate_hz, latched, encoding, and inbound video tracks.
directionstringyesValid values are: in, out, or bidirectional.
preserve_idbooleanfalseInbound topics only. Prefix the republished ROS topic with the publishing participant's identity. Applies to data tracks and video tracks.
max_rate_hznumberOutbound topics only. Cap (in Hz) on the rate samples are forwarded to LiveKit. Samples that arrive within one period of the last forwarded one are dropped (like topic_tools throttle messages). Literal topic names only.
latchedbooleanfalseTreat the topic as latched. Literal topic names only.
enable_ros_topic_statsbooleanfalseOutbound topics only. Enables ROS 2 topic statistics for subscriptions that match this topic pattern.
encodingstringros2msgSelects how data is encoded on the DataTrack. Outbound topics only. Literal topic names only. Valid values are: ros2msg, ros2idl, or jsonschema.

Topic patterns are ECMAScript regular expressions matched against the full topic name. Only forwarding the streams you actually need (and only in the required direction) keeps unnecessary traffic off the LiveKit connection.

Topic statistics

Set enable_ros_topic_stats: true on an outbound or bidirectional topic to publish ROS 2 topic statistics for its subscription. The node publishes them on <topic>/statistics, for example /imu/statistics.

ros_portal:
version: "0.0.1"
topics:
- topic: "/imu"
direction: "out"
enable_ros_topic_stats: true

To enable statistics for every outbound subscription, set enable_all_ros_topic_stats: true at the top level. This setting overrides the value on each topic.

Topic statistics have the following limitations:

  • Inbound-only topics have ROS publishers, not subscriptions, so these settings do not apply to them.
  • The node never enables statistics on a topic whose name ends in /statistics. This prevents recursive statistics topics when you use a broad pattern such as .*.
  • Topic statistics require ROS 2 Jazzy or later. Humble does not support topic statistics for generic serialized subscriptions.

Video topics

The LiveKit Portal for ROS forwards image topics as LiveKit video tracks, not as DataTracks. The track name is the same as the topic name.

Outbound video

An outbound topic of type sensor_msgs/Image or sensor_msgs/CompressedImage becomes a LiveKit video track:

  • sensor_msgs/Image supports the rgba8, rgb8, bgr8, bgra8, and mono8 encodings.
  • sensor_msgs/CompressedImage supports JPEG only. The format field must contain jpeg or jpg (case-insensitive). The node decodes each frame on the CPU before LiveKit encodes it as video.

The size of the first frame sets the track resolution. The track is not recreated if the resolution changes later. The node attaches the ROS header.stamp and header.frame_id to each video frame as LiveKit frame metadata.

ros_portal:
version: "0.0.1"
topics:
# Sends the LiveKit video track /camera/image_raw/compressed.
- topic: "/camera/image_raw/compressed"
direction: "out"

Inbound video

The node republishes a remote LiveKit video track as a sensor_msgs/CompressedImage topic with JPEG data. It forwards a video track only when the track name exactly matches the topic of an in or bidirectional entry. Video tracks never match by regex. For example, the entry /camera/.* forwards matching DataTracks but no video tracks.

On the receiving host, add an entry with the track name:

ros_portal:
version: "0.0.1"
topics:
# Receives the edge camera as JPEG.
- topic: "/camera/image_raw"
direction: "in"
LiveKit video track: /camera/image_raw (from participant "edge-robot")
ROS topic: /camera/image_raw
ROS type: sensor_msgs/msg/CompressedImage (format "jpeg")

With preserve_id: true, the ROS topic is /edge_robot/camera/image_raw. The node does not add or remove a /compressed suffix. Configure consumers for sensor_msgs/CompressedImage, even when the topic name has no /compressed suffix.

The receiving node copies header.stamp and header.frame_id from the frame metadata into the message header. If a frame has no metadata, for example because a browser sent it, the node uses fallback values. The stamp comes from the receiver's ROS clock when the frame arrives. The frame_id is the track name without the leading /. The inbound.video_stamp_fallbacks diagnostic counts these frames.

The receiving node handles each inbound video topic as follows:

  • LiveKit delivers decoded frames only. The receiving node encodes each frame again as JPEG at quality 90. This uses CPU on the receiving host and adds JPEG loss to the video codec loss.
  • If JPEG encoding is slower than the frame rate, the node drops the oldest buffered frame. Frames do not queue without limit.
  • While the ROS topic has no subscribers, the node stops encoding frames. Within 0.5 seconds, it also pauses the LiveKit track, so the server stops sending it. A rosbag2 recorder counts as a subscriber.
  • When a subscriber appears, the node resumes the track within 0.5 seconds. The first image arrives about one network round trip later, after the sender produces a key frame.
  • The publisher uses reliable QoS with depth 10. If the topic matches the best_effort_qos_topics ROS parameter, the publisher uses the sensor data QoS profile (best effort, depth 5).

The node unsubscribes from all remote audio tracks and from all video tracks that match no inbound topic. The LiveKit SDK decodes every subscribed track, so this saves bandwidth and CPU.

Services

Each entry in the services list declares a service route with the following fields:

FieldTypeRequiredDefaultDescription
servicestringyesROS service name. Must be non-empty.
directionstringyesValid value: out
participantstringyesLiveKit participant identity. Must be non-empty.
msg_typestringyesROS service type, such as std_srvs/srv/SetBool. Must be non-empty.

Setting direction: "out" creates a local ROS service server that forwards calls to the configured LiveKit participant using msg_type. Services support only out: a service route always exposes a local ROS server that proxies calls to a remote participant, so unlike topics, there's no inbound or bidirectional direction.

Video sources

Version requirement

Video sources require v0.3.0  or later of LiveKit Portal for ROS.

Each entry in video_sources publishes one LiveKit video track that doesn't depend on the ROS graph. The source can be a GStreamer pipeline, a built-in test pattern, or a built-in wall clock.

The ROS Portal creates, publishes, and starts each source after it connects to the room. A failure affects only that entry. The node and the other sources keep running, and the source's diagnostic status reports the error.

FieldTypeRequiredDefaultDescription
track_namestringyesNon-empty LiveKit video track name.
sourcemapyesSource configuration. Its type is gstreamer, pattern, or clock.
simulcastbooleanfalseEnables simulcast for pattern and clock sources. GStreamer sources ignore this field.
publish_optionsmapOptional max_bitrate_bps and max_framerate limits. Both values must be positive integers.

The publish_options limits set what the track advertises to LiveKit. They don't configure a GStreamer encoder. To control the encoder, set its properties in the pipeline or use rate_control.

GStreamer source

A GStreamer source publishes pre-encoded video. The pipeline must end in appsink name=lk_appsink or leave one encoded video source pad unlinked. The encoder in the pipeline sets the codec, so the track always publishes a single layer without simulcast.

FieldRequiredDescription
pipelineyesGStreamer launch description.
codech264, h265, vp8, vp9, or av1. Inferred from the negotiated caps when omitted.
resolutionPositive width and height. Inferred from the negotiated caps when omitted, and checked against the stream when set.
rate_controlConnects WebRTC bitrate targets to an encoder property.

If the node can't determine the codec or resolution, the source fails to start.

The optional rate_control block has three required fields: element names the encoder element, property names a writable integer bitrate property, and unit is bps or kbps to match that property. Without rate_control, the encoder keeps the fixed bitrate from the pipeline. The node still forwards keyframe requests to the pipeline.

ros_portal:
version: "0.0.1"
video_sources:
- track_name: "demo_camera"
source:
type: "gstreamer"
# videotestsrc pattern=smpte is the classic SMPTE 100% color bars with
# the television-snow block in the bottom right — an instantly
# recognizable reference for checking colors, scaling and tearing.
# clockoverlay stamps wall-clock date + time on every frame so uplink
# latency and frozen/stale video are obvious at a glance.
pipeline: >-
videotestsrc is-live=true do-timestamp=true pattern=smpte
! video/x-raw,width=640,height=480,framerate=30/1
! clockoverlay time-format="%Y-%m-%d %H:%M:%S" halignment=center
valignment=top shaded-background=true font-desc="Sans Bold 20"
! videoconvert ! video/x-raw,format=I420
! vp8enc name=lk_encoder deadline=1 cpu-used=8 keyframe-max-dist=30
lag-in-frames=0 target-bitrate=2500000
! video/x-vp8
! appsink name=lk_appsink sync=false max-buffers=2 drop=true
codec: "vp8"
resolution:
width: 640
height: 480
rate_control:
element: "lk_encoder"
property: "target-bitrate"
unit: "bps"
publish_options:
max_bitrate_bps: 2500000
max_framerate: 30

To read a camera with v4l2src in Docker, pass the device into the container, for example --device /dev/video0.

GStreamer plugins

The Docker images and the Debian package include the gstreamer1.0-plugins-base, -good, -bad, and -ugly packages, and gstreamer1.0-libav. The Docker images also include gstreamer1.0-x, which provides the clockoverlay, textoverlay, and timeoverlay elements. The Debian package doesn't depend on gstreamer1.0-x, so it doesn't install X11 libraries on a headless robot.

If a pipeline uses an element from a missing plugin set, the source fails at startup:

[ERROR] [ros_portal]: Video source 'front_camera' failed to start: invalid
request: GStreamer pipeline error: failed to create pipeline: no element
"clockoverlay"

To use the overlay elements with a Debian install, install the package:

sudo apt install gstreamer1.0-x

Test Pattern and Clock Sources

Use a pattern or clock source to stub out camera streams during development, debugging, or simulations.

Use a pattern source to replace camera hardware during debugging or demonstrations. Reuse the track_name of the source you're replacing, so subscribers don't need any configuration changes.

ros_portal:
version: "0.0.1"
video_sources:
- track_name: "front_camera"
source:
type: "pattern" # or "clock"

Additional resources

The following resource provides more information about configuration.

LiveKit Portal for ROS configuration guide

The complete configuration reference in the LiveKit Portal for ROS repository, covering every field, token sources, and full example configs.