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