Skip to main content

livekit_api/services/
room.rs

1// Copyright 2025 LiveKit, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use livekit_protocol as proto;
16use std::collections::HashMap;
17
18use super::{ServiceBase, ServiceResult, LIVEKIT_PACKAGE};
19use crate::services::twirp_client::TwirpClient;
20use livekit_token::{get_env_keys, VideoGrants};
21use rand::Rng;
22
23const SVC: &str = "RoomService";
24
25#[derive(Debug, Clone, Default)]
26pub struct CreateRoomOptions {
27    pub empty_timeout: u32,
28    pub departure_timeout: u32,
29    pub max_participants: u32,
30    pub node_id: String,
31    pub metadata: String,
32    pub egress: Option<proto::RoomEgress>, // TODO(theomonnom): Better API?
33}
34
35#[derive(Debug, Clone, Default)]
36pub struct UpdateParticipantOptions {
37    pub metadata: String,
38    pub attributes: HashMap<String, String>,
39    pub permission: Option<proto::ParticipantPermission>,
40    pub name: String, // No effect if left empty
41}
42
43#[derive(Debug, Clone, Default)]
44pub struct RemoveParticipantOptions {
45    /// Revoke all tokens issued to this participant before this Unix timestamp (ms).
46    pub revoke_token_ts: i64,
47}
48
49#[derive(Debug, Clone, Default)]
50pub struct SendDataOptions {
51    pub kind: proto::data_packet::Kind,
52    #[deprecated(note = "Use destination_identities instead")]
53    pub destination_sids: Vec<String>,
54    pub destination_identities: Vec<String>,
55    pub topic: Option<String>,
56}
57
58#[derive(Debug)]
59pub struct RoomClient {
60    base: ServiceBase,
61    client: TwirpClient,
62}
63
64impl RoomClient {
65    /// Authenticates with an API key and secret, signing a short-lived token per request.
66    pub fn with_api_key(host: &str, api_key: &str, api_secret: &str) -> Self {
67        Self::build(
68            host,
69            ServiceBase::with_api_key(api_key, api_secret),
70            crate::http_client::Client::new(),
71        )
72    }
73
74    /// Authenticates with a pre-signed token, sent verbatim on every request.
75    pub fn with_token(host: &str, token: &str) -> Self {
76        Self::build(host, ServiceBase::with_token(token), crate::http_client::Client::new())
77    }
78
79    /// Builds the client from an already-constructed HTTP client so the unified
80    /// [`LiveKitApi`](super::LiveKitApi) can share one connection pool across services.
81    pub(crate) fn build(host: &str, base: ServiceBase, client: crate::http_client::Client) -> Self {
82        Self { base, client: TwirpClient::with_client(host, LIVEKIT_PACKAGE, None, client) }
83    }
84
85    #[cfg(test)]
86    pub(crate) fn with_default_headers(mut self, headers: http::HeaderMap) -> Self {
87        self.client = self.client.with_default_headers(headers);
88        self
89    }
90
91    /// Reads the API key and secret from the `LIVEKIT_API_KEY` and
92    /// `LIVEKIT_API_SECRET` environment variables.
93    pub fn new(host: &str) -> ServiceResult<Self> {
94        let (api_key, api_secret) = get_env_keys()?;
95        Ok(Self::with_api_key(host, &api_key, &api_secret))
96    }
97
98    /// Enables or disables region failover (enabled by default). Failover only
99    /// engages for LiveKit Cloud hosts.
100    pub fn with_failover(mut self, enabled: bool) -> Self {
101        self.client = self.client.with_failover(enabled);
102        self
103    }
104
105    /// Overrides the default per-request timeout (10s) for calls on this client.
106    pub fn with_request_timeout(mut self, timeout: std::time::Duration) -> Self {
107        self.client = self.client.with_request_timeout(timeout);
108        self
109    }
110
111    pub async fn create_room(
112        &self,
113        name: &str,
114        options: CreateRoomOptions,
115    ) -> ServiceResult<proto::Room> {
116        self.create_room_request(proto::CreateRoomRequest {
117            name: name.to_owned(),
118            empty_timeout: options.empty_timeout,
119            departure_timeout: options.departure_timeout,
120            max_participants: options.max_participants,
121            node_id: options.node_id,
122            metadata: options.metadata,
123            egress: options.egress,
124            ..Default::default()
125        })
126        .await
127    }
128
129    /// Create a room with an explicit subscriber playout delay.
130    pub async fn create_room_with_playout_delay(
131        &self,
132        name: &str,
133        options: CreateRoomOptions,
134        min_playout_delay: u32,
135        max_playout_delay: u32,
136    ) -> ServiceResult<proto::Room> {
137        self.create_room_request(proto::CreateRoomRequest {
138            name: name.to_owned(),
139            empty_timeout: options.empty_timeout,
140            departure_timeout: options.departure_timeout,
141            max_participants: options.max_participants,
142            node_id: options.node_id,
143            metadata: options.metadata,
144            egress: options.egress,
145            min_playout_delay,
146            max_playout_delay,
147            ..Default::default()
148        })
149        .await
150    }
151
152    async fn create_room_request(
153        &self,
154        request: proto::CreateRoomRequest,
155    ) -> ServiceResult<proto::Room> {
156        self.client
157            .request(
158                SVC,
159                "CreateRoom",
160                request,
161                self.base
162                    .auth_header(VideoGrants { room_create: true, ..Default::default() }, None)?,
163            )
164            .await
165            .map_err(Into::into)
166    }
167
168    pub async fn list_rooms(&self, names: Vec<String>) -> ServiceResult<Vec<proto::Room>> {
169        let resp: proto::ListRoomsResponse = self
170            .client
171            .request(
172                SVC,
173                "ListRooms",
174                proto::ListRoomsRequest { names },
175                self.base
176                    .auth_header(VideoGrants { room_list: true, ..Default::default() }, None)?,
177            )
178            .await?;
179
180        Ok(resp.rooms)
181    }
182
183    pub async fn delete_room(&self, room: &str) -> ServiceResult<()> {
184        self.client
185            .request(
186                SVC,
187                "DeleteRoom",
188                proto::DeleteRoomRequest { room: room.to_owned() },
189                self.base
190                    .auth_header(VideoGrants { room_create: true, ..Default::default() }, None)?,
191            )
192            .await
193            .map_err(Into::into)
194    }
195
196    pub async fn update_room_metadata(
197        &self,
198        room: &str,
199        metadata: &str,
200    ) -> ServiceResult<proto::Room> {
201        self.client
202            .request(
203                SVC,
204                "UpdateRoomMetadata",
205                proto::UpdateRoomMetadataRequest {
206                    room: room.to_owned(),
207                    metadata: metadata.to_owned(),
208                },
209                self.base.auth_header(
210                    VideoGrants { room_admin: true, room: room.to_owned(), ..Default::default() },
211                    None,
212                )?,
213            )
214            .await
215            .map_err(Into::into)
216    }
217
218    pub async fn list_participants(
219        &self,
220        room: &str,
221    ) -> ServiceResult<Vec<proto::ParticipantInfo>> {
222        let resp: proto::ListParticipantsResponse = self
223            .client
224            .request(
225                SVC,
226                "ListParticipants",
227                proto::ListParticipantsRequest { room: room.to_owned() },
228                self.base.auth_header(
229                    VideoGrants { room_admin: true, room: room.to_owned(), ..Default::default() },
230                    None,
231                )?,
232            )
233            .await?;
234
235        Ok(resp.participants)
236    }
237
238    pub async fn get_participant(
239        &self,
240        room: &str,
241        identity: &str,
242    ) -> ServiceResult<proto::ParticipantInfo> {
243        self.client
244            .request(
245                SVC,
246                "GetParticipant",
247                proto::RoomParticipantIdentity {
248                    room: room.to_owned(),
249                    identity: identity.to_owned(),
250                    ..Default::default()
251                },
252                self.base.auth_header(
253                    VideoGrants { room_admin: true, room: room.to_owned(), ..Default::default() },
254                    None,
255                )?,
256            )
257            .await
258            .map_err(Into::into)
259    }
260
261    pub async fn remove_participant(&self, room: &str, identity: &str) -> ServiceResult<()> {
262        self.remove_participant_with_options(room, identity, RemoveParticipantOptions::default())
263            .await
264    }
265
266    pub async fn remove_participant_with_options(
267        &self,
268        room: &str,
269        identity: &str,
270        options: RemoveParticipantOptions,
271    ) -> ServiceResult<()> {
272        self.client
273            .request(
274                SVC,
275                "RemoveParticipant",
276                proto::RoomParticipantIdentity {
277                    room: room.to_owned(),
278                    identity: identity.to_owned(),
279                    revoke_token_ts: options.revoke_token_ts,
280                },
281                self.base.auth_header(
282                    VideoGrants { room_admin: true, room: room.to_owned(), ..Default::default() },
283                    None,
284                )?,
285            )
286            .await
287            .map_err(Into::into)
288    }
289
290    pub async fn forward_participant(
291        &self,
292        room: &str,
293        identity: &str,
294        destination_room: &str,
295    ) -> ServiceResult<()> {
296        self.client
297            .request(
298                SVC,
299                "ForwardParticipant",
300                proto::ForwardParticipantRequest {
301                    room: room.to_owned(),
302                    identity: identity.to_owned(),
303                    destination_room: destination_room.to_owned(),
304                },
305                self.base.auth_header(
306                    VideoGrants {
307                        room_admin: true,
308                        room: room.to_owned(),
309                        destination_room: destination_room.to_owned(),
310                        ..Default::default()
311                    },
312                    None,
313                )?,
314            )
315            .await
316            .map_err(Into::into)
317    }
318
319    pub async fn move_participant(
320        &self,
321        room: &str,
322        identity: &str,
323        destination_room: &str,
324    ) -> ServiceResult<()> {
325        self.client
326            .request(
327                SVC,
328                "MoveParticipant",
329                proto::MoveParticipantRequest {
330                    room: room.to_owned(),
331                    identity: identity.to_owned(),
332                    destination_room: destination_room.to_owned(),
333                },
334                self.base.auth_header(
335                    VideoGrants {
336                        room_admin: true,
337                        room: room.to_owned(),
338                        destination_room: destination_room.to_owned(),
339                        ..Default::default()
340                    },
341                    None,
342                )?,
343            )
344            .await
345            .map_err(Into::into)
346    }
347
348    pub async fn mute_published_track(
349        &self,
350        room: &str,
351        identity: &str,
352        track_sid: &str,
353        muted: bool,
354    ) -> ServiceResult<proto::TrackInfo> {
355        let resp: proto::MuteRoomTrackResponse = self
356            .client
357            .request(
358                SVC,
359                "MutePublishedTrack",
360                proto::MuteRoomTrackRequest {
361                    room: room.to_owned(),
362                    identity: identity.to_owned(),
363                    track_sid: track_sid.to_owned(),
364                    muted,
365                },
366                self.base.auth_header(
367                    VideoGrants { room_admin: true, room: room.to_owned(), ..Default::default() },
368                    None,
369                )?,
370            )
371            .await?;
372
373        Ok(resp.track.unwrap())
374    }
375
376    pub async fn update_participant(
377        &self,
378        room: &str,
379        identity: &str,
380        options: UpdateParticipantOptions,
381    ) -> ServiceResult<proto::ParticipantInfo> {
382        self.client
383            .request(
384                SVC,
385                "UpdateParticipant",
386                proto::UpdateParticipantRequest {
387                    room: room.to_owned(),
388                    identity: identity.to_owned(),
389                    permission: options.permission,
390                    metadata: options.metadata,
391                    attributes: options.attributes.to_owned(),
392                    name: options.name,
393                },
394                self.base.auth_header(
395                    VideoGrants { room_admin: true, room: room.to_owned(), ..Default::default() },
396                    None,
397                )?,
398            )
399            .await
400            .map_err(Into::into)
401    }
402
403    pub async fn update_subscriptions(
404        &self,
405        room: &str,
406        identity: &str,
407        track_sids: Vec<String>,
408        subscribe: bool,
409    ) -> ServiceResult<()> {
410        self.client
411            .request(
412                SVC,
413                "UpdateSubscriptions",
414                proto::UpdateSubscriptionsRequest {
415                    room: room.to_owned(),
416                    identity: identity.to_owned(),
417                    track_sids,
418                    subscribe,
419                    ..Default::default()
420                },
421                self.base.auth_header(
422                    VideoGrants { room_admin: true, room: room.to_owned(), ..Default::default() },
423                    None,
424                )?,
425            )
426            .await
427            .map_err(Into::into)
428    }
429
430    pub async fn send_data(
431        &self,
432        room: &str,
433        data: Vec<u8>,
434        options: SendDataOptions,
435    ) -> ServiceResult<()> {
436        let mut rng = rand::rng();
437        let nonce: Vec<u8> = (0..16).map(|_| rng.random::<u8>()).collect();
438        #[allow(deprecated)]
439        self.client
440            .request(
441                SVC,
442                "SendData",
443                proto::SendDataRequest {
444                    room: room.to_owned(),
445                    data,
446                    destination_sids: options.destination_sids,
447                    topic: options.topic,
448                    kind: options.kind as i32,
449                    destination_identities: options.destination_identities,
450                    nonce,
451                },
452                self.base.auth_header(
453                    VideoGrants { room_admin: true, room: room.to_owned(), ..Default::default() },
454                    None,
455                )?,
456            )
457            .await
458            .map_err(Into::into)
459    }
460}