Skip to main content

Participant attributes

Attach state such as robot status or operator permissions to a participant and keep it synchronized across the room.

Overview

Participant attributes are key-value state attached to a participant and synchronized to everyone in the room. A participant sets its own attributes, LiveKit propagates each change to the other participants, and late joiners receive the current state when they connect. Other participants read the values directly or react to a room event when they change.

Delivery is reliable, so every participant converges on the same state. Keys and values are strings, up to 64 KiB total per participant, set when the participant's token is generated or updated at runtime. Update attributes at a low frequency, no more than once every few seconds, since they carry coarse state rather than high-frequency data.

Use participant attributes for participant roles, coarse robot state such as idle, navigating, or charging, and permissions. For continuous, high-frequency data, use data tracks. For request-response interactions, use remote procedure calls.

Examples

The following examples show two common uses of participant attributes in a teleoperation app: publishing the robot's coarse state to the room, and gating which participants can take control.

Robot state

A teleoperation frontend needs the robot's high-level state (for example, idle, navigating, charging) to reflect it in the UI for operators and viewers.

The robot participant can update a custom operation_state attribute to reflect its current state:

// Updating attribute when state changes.
let mut new_attributes = room
.local_participant()
.attributes();
new_attributes.insert("operation_state".into(), "charging".into());
room.local_participant()
.set_attributes(new_attributes)
.await?;
# Updating attribute when state changes.
new_attributes = dict(room.local_participant.attributes)
new_attributes["operation_state"] = "charging"
await room.local_participant.set_attributes(new_attributes)
// Updating attribute when state changes.
if (auto lp = room->localParticipant().lock()) {
lp->setAttributes({{"operation_state", "charging"}});
} else {
std::cerr << "Failed to get local participant\n";
return;
}
// Updating attribute when state changes.
await room.localParticipant.setAttributes({
...room.localParticipant.attributes,
operation_state: 'charging',
});
// Updating attribute when state changes.
await room.localParticipant?.setAttributes({
...room.localParticipant?.attributes,
operation_state: 'charging',
});
Permission to update own metadata

The robot participant must have the canUpdateOwnMetadata permission in its access token or else the call to update its attributes fails. To learn more about token generation and available fields, see access tokens & grants.

From the frontend, access the attribute on the remote participant object for the robot:

let robot_identity = ParticipantIdentity("robot-1".to_string());
let robot = room
.remote_participants()
.get(&robot_identity)
.context("Robot has not joined the room")?;
let attributes = robot.attributes();
let state = attributes
.get("operation_state")
.context("Robot has not published its state")?;
println!("Initial robot state: {state}");
robot = room.remote_participants.get("robot-1")
if robot is None:
raise RuntimeError("Robot has not joined the room")
state = robot.attributes.get("operation_state")
if state is None:
raise RuntimeError("Robot has not published its state")
print(f"Initial robot state: {state}")
auto robot = room->remoteParticipant("robot-1").lock();
if (!robot) {
std::cerr << "Robot has not joined the room\n";
return;
}
const auto& attributes = robot->attributes();
auto state = attributes.find("operation_state");
if (state == attributes.end()) {
std::cerr << "Robot has not published its state\n";
return;
}
std::cout << "Initial robot state: " << state->second << "\n";
const robot = room.remoteParticipants.get('robot-1');
if (!robot) {
throw new Error('Robot has not joined the room');
}
const state = robot.attributes['operation_state'];
if (!state) {
throw new Error('Robot has not published its state');
}
console.log(`Initial robot state: ${state}`);
const robot = room.remoteParticipants.get('robot-1');
if (!robot) {
throw new Error('Robot has not joined the room');
}
const state = robot.attributes['operation_state'];
if (!state) {
throw new Error('Robot has not published its state');
}
console.log(`Initial robot state: ${state}`);

Detect subsequent changes via the participant attributes changed room event:

let robot_identity = ParticipantIdentity("robot-1".to_string());
while let Some(event) = room_events.recv().await {
let RoomEvent::ParticipantAttributesChanged {
participant,
changed_attributes,
} = event else { continue };
if let Participant::Remote(participant) = participant
&& participant.identity() == robot_identity
&& let Some(state) = changed_attributes.get("operation_state")
{
println!("Robot state changed to {state}");
}
}
@room.on("participant_attributes_changed")
def on_attributes_changed(
changed_attributes: dict[str, str],
participant: rtc.Participant,
):
if participant.identity != "robot-1":
return
state = changed_attributes.get("operation_state")
if state is not None:
print(f"Robot state changed to {state}")
class MyRoomDelegate : public livekit::RoomDelegate {
void onParticipantAttributesChanged(
livekit::Room&,
const livekit::ParticipantAttributesChangedEvent& event) override {
if (!event.participant || event.participant->identity() != "robot-1") {
return;
}
for (const auto& entry : event.changed_attributes) {
if (entry.key == "operation_state") {
std::cout << "Robot state changed to " << entry.value << "\n";
}
}
}
};
// The delegate must remain valid for the lifetime of the room.
MyRoomDelegate delegate;
room->setDelegate(&delegate);
room.on(
RoomEvent.ParticipantAttributesChanged,
(changed: Record<string, string>, participant: Participant) => {
if (participant.identity !== 'robot-1') {
return;
}
const state = changed['operation_state'];
if (state) {
console.log(`Robot state changed to ${state}`);
}
}
);
room.on(
RoomEvent.ParticipantAttributesChanged,
(changed: Record<string, string>, participant: Participant) => {
if (participant.identity !== 'robot-1') {
return;
}
const state = changed['operation_state'];
if (state) {
console.log(`Robot state changed to ${state}`);
}
}
);

Teleop permission

In a teleoperation app, you might want some participants to join as viewers that can see robot status and video streams, but can't take control.

Generate the participant's token with a custom can_teleop attribute indicating whether that participant is allowed to teleoperate the robot:

{
"exp": 1621657263,
"iss": "APIMmxiL8rquKztZEoZJV9Fb",
"sub": "someviewer@example.com",
"nbf": 1619065263,
"video": {
"room": "robot-1",
"roomJoin": true
},
"attributes": {
"can_teleop": "false"
}
}

Since your backend is responsible for token generation, your own authentication mechanism can set this flag based on the user's identity and role.

Use proper grants

When using participant attributes to define permissions, the participant must not have canUpdateOwnMetadata or roomAdmin in its video grant otherwise it could update its own attributes to claim arbitrary permissions.

The robot participant can decide whether or not to allow a particular participant to acquire control based on the value of the can_teleop attribute when they request to do so (for example, via an RPC call to the robot):

fn is_teleop_allowed(participant: &RemoteParticipant) -> Result<bool> {
let attributes = participant.attributes();
let can_teleop = attributes.get("can_teleop")
.context("Missing permission attribute")?;
match can_teleop.as_str() {
"true" => Ok(true),
"false" => Ok(false),
other => Err(anyhow!("Unsupported attribute value '{other}'")),
}
}
def is_teleop_allowed(participant: rtc.RemoteParticipant) -> bool:
can_teleop = participant.attributes.get("can_teleop")
if can_teleop is None:
raise RuntimeError("Missing permission attribute")
if can_teleop == "true":
return True
if can_teleop == "false":
return False
raise RuntimeError(
f"Unsupported attribute value '{can_teleop}'"
)
bool isTeleopAllowed(const livekit::RemoteParticipant& participant) {
const auto& attributes = participant.attributes();
auto can_teleop = attributes.find("can_teleop");
if (can_teleop == attributes.end()) {
throw std::runtime_error("Missing permission attribute");
}
if (can_teleop->second == "true") {
return true;
}
if (can_teleop->second == "false") {
return false;
}
throw std::runtime_error(
"Unsupported attribute value '" + can_teleop->second + "'");
}
function isTeleopAllowed(participant: RemoteParticipant): boolean {
const canTeleop = participant.attributes['can_teleop'];
switch (canTeleop) {
case 'true':
return true;
case 'false':
return false;
case undefined:
throw new Error('Missing permission attribute');
default:
throw new Error(
`Unsupported attribute value '${canTeleop}'`
);
}
}
function isTeleopAllowed(participant: RemoteParticipant): boolean {
const canTeleop = participant.attributes['can_teleop'];
switch (canTeleop) {
case 'true':
return true;
case 'false':
return false;
case undefined:
throw new Error('Missing permission attribute');
default:
throw new Error(
`Unsupported attribute value '${canTeleop}'`
);
}
}

Additional resources

State synchronization

Full state synchronization API, other state synchronization options, and examples.