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',});
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":returnstate = 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.
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 Trueif can_teleop == "false":return Falseraise 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.