livekit_api/services/
egress.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;
16
17use super::{ServiceBase, ServiceResult, LIVEKIT_PACKAGE};
18use crate::{access_token::VideoGrants, get_env_keys, services::twirp_client::TwirpClient};
19
20#[derive(Default, Clone, Debug)]
21pub struct RoomCompositeOptions {
22    pub layout: String,
23    pub encoding: encoding::EncodingOptions,
24    pub audio_only: bool,
25    pub video_only: bool,
26    pub custom_base_url: String,
27}
28
29#[derive(Default, Clone, Debug)]
30pub struct WebOptions {
31    pub encoding: encoding::EncodingOptions,
32    pub audio_only: bool,
33    pub video_only: bool,
34    pub await_start_signal: bool,
35}
36
37#[derive(Default, Clone, Debug)]
38pub struct ParticipantEgressOptions {
39    pub screenshare: bool,
40    pub encoding: encoding::EncodingOptions,
41}
42
43#[derive(Default, Clone, Debug)]
44pub struct TrackCompositeOptions {
45    pub encoding: encoding::EncodingOptions,
46    pub audio_track_id: String,
47    pub video_track_id: String,
48}
49
50#[derive(Debug, Clone)]
51pub enum EgressOutput {
52    File(proto::EncodedFileOutput),
53    Stream(proto::StreamOutput),
54    Segments(proto::SegmentedFileOutput),
55    Image(proto::ImageOutput),
56}
57
58#[derive(Debug, Clone)]
59pub enum TrackEgressOutput {
60    File(Box<proto::DirectFileOutput>),
61    WebSocket(String),
62}
63
64#[derive(Debug, Clone)]
65pub enum EgressListFilter {
66    All,
67    Egress(String),
68    Room(String),
69}
70
71#[derive(Debug, Clone)]
72pub struct EgressListOptions {
73    pub filter: EgressListFilter,
74    pub active: bool,
75}
76
77const SVC: &str = "Egress";
78
79#[derive(Debug)]
80pub struct EgressClient {
81    base: ServiceBase,
82    client: TwirpClient,
83}
84
85impl EgressClient {
86    pub fn with_api_key(host: &str, api_key: &str, api_secret: &str) -> Self {
87        Self {
88            base: ServiceBase::with_api_key(api_key, api_secret),
89            client: TwirpClient::new(host, LIVEKIT_PACKAGE, None),
90        }
91    }
92
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    pub async fn start_room_composite_egress(
99        &self,
100        room: &str,
101        outputs: Vec<EgressOutput>,
102        options: RoomCompositeOptions,
103    ) -> ServiceResult<proto::EgressInfo> {
104        let (file_outputs, stream_outputs, segment_outputs, image_outputs) = get_outputs(outputs);
105        self.client
106            .request(
107                SVC,
108                "StartRoomCompositeEgress",
109                proto::RoomCompositeEgressRequest {
110                    room_name: room.to_string(),
111                    layout: options.layout,
112                    audio_only: options.audio_only,
113                    video_only: options.video_only,
114                    options: Some(proto::room_composite_egress_request::Options::Advanced(
115                        options.encoding.into(),
116                    )),
117                    custom_base_url: options.custom_base_url,
118                    file_outputs,
119                    stream_outputs,
120                    segment_outputs,
121                    image_outputs,
122                    output: None, // Deprecated
123                    ..Default::default()
124                },
125                self.base
126                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
127            )
128            .await
129            .map_err(Into::into)
130    }
131
132    pub async fn start_web_egress(
133        &self,
134        url: &str,
135        outputs: Vec<EgressOutput>,
136        options: WebOptions,
137    ) -> ServiceResult<proto::EgressInfo> {
138        let (file_outputs, stream_outputs, segment_outputs, image_outputs) = get_outputs(outputs);
139        self.client
140            .request(
141                SVC,
142                "StartWebEgress",
143                proto::WebEgressRequest {
144                    url: url.to_string(),
145                    options: Some(proto::web_egress_request::Options::Advanced(
146                        options.encoding.into(),
147                    )),
148                    audio_only: options.audio_only,
149                    video_only: options.video_only,
150                    file_outputs,
151                    stream_outputs,
152                    segment_outputs,
153                    image_outputs,
154                    output: None, // Deprecated
155                    await_start_signal: options.await_start_signal,
156                    ..Default::default()
157                },
158                self.base
159                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
160            )
161            .await
162            .map_err(Into::into)
163    }
164
165    pub async fn start_participant_egress(
166        &self,
167        room: &str,
168        participant_identity: &str,
169        outputs: Vec<EgressOutput>,
170        options: ParticipantEgressOptions,
171    ) -> ServiceResult<proto::EgressInfo> {
172        let (file_outputs, stream_outputs, segment_outputs, image_outputs) = get_outputs(outputs);
173        self.client
174            .request(
175                SVC,
176                "StartParticipantEgress",
177                proto::ParticipantEgressRequest {
178                    room_name: room.to_string(),
179                    identity: participant_identity.to_string(),
180                    options: Some(proto::participant_egress_request::Options::Advanced(
181                        options.encoding.into(),
182                    )),
183                    screen_share: options.screenshare,
184                    file_outputs,
185                    stream_outputs,
186                    segment_outputs,
187                    image_outputs,
188                    ..Default::default()
189                },
190                self.base
191                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
192            )
193            .await
194            .map_err(Into::into)
195    }
196
197    pub async fn start_track_composite_egress(
198        &self,
199        room: &str,
200        outputs: Vec<EgressOutput>,
201        options: TrackCompositeOptions,
202    ) -> ServiceResult<proto::EgressInfo> {
203        let (file_outputs, stream_outputs, segment_outputs, image_outputs) = get_outputs(outputs);
204        self.client
205            .request(
206                SVC,
207                "StartTrackCompositeEgress",
208                proto::TrackCompositeEgressRequest {
209                    room_name: room.to_string(),
210                    options: Some(proto::track_composite_egress_request::Options::Advanced(
211                        options.encoding.into(),
212                    )),
213                    audio_track_id: options.audio_track_id,
214                    video_track_id: options.video_track_id,
215                    file_outputs,
216                    stream_outputs,
217                    segment_outputs,
218                    image_outputs,
219                    output: None, // Deprecated
220                    ..Default::default()
221                },
222                self.base
223                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
224            )
225            .await
226            .map_err(Into::into)
227    }
228
229    pub async fn start_track_egress(
230        &self,
231        room: &str,
232        output: TrackEgressOutput,
233        track_id: &str,
234    ) -> ServiceResult<proto::EgressInfo> {
235        self.client
236            .request(
237                SVC,
238                "StartTrackEgress",
239                proto::TrackEgressRequest {
240                    room_name: room.to_string(),
241                    output: match output {
242                        TrackEgressOutput::File(f) => {
243                            Some(proto::track_egress_request::Output::File(*f))
244                        }
245                        TrackEgressOutput::WebSocket(url) => {
246                            Some(proto::track_egress_request::Output::WebsocketUrl(url))
247                        }
248                    },
249                    track_id: track_id.to_string(),
250                    ..Default::default()
251                },
252                self.base
253                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
254            )
255            .await
256            .map_err(Into::into)
257    }
258
259    pub async fn update_layout(
260        &self,
261        egress_id: &str,
262        layout: &str,
263    ) -> ServiceResult<proto::EgressInfo> {
264        self.client
265            .request(
266                SVC,
267                "UpdateLayout",
268                proto::UpdateLayoutRequest {
269                    egress_id: egress_id.to_owned(),
270                    layout: layout.to_owned(),
271                },
272                self.base
273                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
274            )
275            .await
276            .map_err(Into::into)
277    }
278
279    pub async fn update_stream(
280        &self,
281        egress_id: &str,
282        add_output_urls: Vec<String>,
283        remove_output_urls: Vec<String>,
284    ) -> ServiceResult<proto::EgressInfo> {
285        self.client
286            .request(
287                SVC,
288                "UpdateStream",
289                proto::UpdateStreamRequest {
290                    egress_id: egress_id.to_owned(),
291                    add_output_urls,
292                    remove_output_urls,
293                },
294                self.base
295                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
296            )
297            .await
298            .map_err(Into::into)
299    }
300
301    pub async fn list_egress(
302        &self,
303        options: EgressListOptions,
304    ) -> ServiceResult<Vec<proto::EgressInfo>> {
305        let mut room_name = String::default();
306        let mut egress_id = String::default();
307
308        match options.filter {
309            EgressListFilter::Room(room) => room_name = room,
310            EgressListFilter::Egress(egress) => egress_id = egress,
311            _ => {}
312        }
313
314        let resp: proto::ListEgressResponse = self
315            .client
316            .request(
317                SVC,
318                "ListEgress",
319                proto::ListEgressRequest { room_name, egress_id, active: options.active },
320                self.base
321                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
322            )
323            .await?;
324
325        Ok(resp.items)
326    }
327
328    pub async fn stop_egress(&self, egress_id: &str) -> ServiceResult<proto::EgressInfo> {
329        self.client
330            .request(
331                SVC,
332                "StopEgress",
333                proto::StopEgressRequest { egress_id: egress_id.to_owned() },
334                self.base
335                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
336            )
337            .await
338            .map_err(Into::into)
339    }
340}
341
342fn get_outputs(
343    outputs: Vec<EgressOutput>,
344) -> (
345    Vec<proto::EncodedFileOutput>,
346    Vec<proto::StreamOutput>,
347    Vec<proto::SegmentedFileOutput>,
348    Vec<proto::ImageOutput>,
349) {
350    let mut file_outputs = Vec::new();
351    let mut stream_outputs = Vec::new();
352    let mut segment_outputs = Vec::new();
353    let mut image_outputs = Vec::new();
354
355    for output in outputs {
356        match output {
357            EgressOutput::File(f) => file_outputs.push(f),
358            EgressOutput::Stream(s) => stream_outputs.push(s),
359            EgressOutput::Segments(s) => segment_outputs.push(s),
360            EgressOutput::Image(i) => image_outputs.push(i),
361        }
362    }
363
364    (file_outputs, stream_outputs, segment_outputs, image_outputs)
365}
366
367pub mod encoding {
368    use super::*;
369
370    #[derive(Clone, Debug)]
371    pub struct EncodingOptions {
372        pub width: i32,
373        pub height: i32,
374        pub depth: i32,
375        pub framerate: i32,
376        pub audio_codec: proto::AudioCodec,
377        pub audio_bitrate: i32,
378        pub audio_frequency: i32,
379        pub video_codec: proto::VideoCodec,
380        pub video_bitrate: i32,
381        pub keyframe_interval: f64,
382        pub audio_quality: i32,
383        pub video_quality: i32,
384    }
385
386    impl From<EncodingOptions> for proto::EncodingOptions {
387        fn from(opts: EncodingOptions) -> Self {
388            Self {
389                width: opts.width,
390                height: opts.height,
391                depth: opts.depth,
392                framerate: opts.framerate,
393                audio_codec: opts.audio_codec as i32,
394                audio_bitrate: opts.audio_bitrate,
395                audio_frequency: opts.audio_frequency,
396                video_codec: opts.video_codec as i32,
397                video_bitrate: opts.video_bitrate,
398                key_frame_interval: opts.keyframe_interval,
399                audio_quality: opts.audio_quality,
400                video_quality: opts.video_quality,
401            }
402        }
403    }
404
405    impl EncodingOptions {
406        const fn new() -> Self {
407            Self {
408                width: 1920,
409                height: 1080,
410                depth: 24,
411                framerate: 30,
412                audio_codec: proto::AudioCodec::Opus,
413                audio_bitrate: 128,
414                audio_frequency: 44100,
415                video_codec: proto::VideoCodec::H264Main,
416                video_bitrate: 4500,
417                keyframe_interval: 0.0,
418                audio_quality: 0,
419                video_quality: 0,
420            }
421        }
422    }
423
424    impl Default for EncodingOptions {
425        fn default() -> Self {
426            Self::new()
427        }
428    }
429
430    pub const H264_720P_30: EncodingOptions =
431        EncodingOptions { width: 1280, height: 720, video_bitrate: 3000, ..EncodingOptions::new() };
432    pub const H264_720P_60: EncodingOptions =
433        EncodingOptions { width: 1280, height: 720, framerate: 60, ..EncodingOptions::new() };
434    pub const H264_1080P_30: EncodingOptions = EncodingOptions::new();
435    pub const H264_1080P_60: EncodingOptions =
436        EncodingOptions { framerate: 60, video_bitrate: 6000, ..EncodingOptions::new() };
437    pub const PORTRAIT_H264_720P_30: EncodingOptions =
438        EncodingOptions { width: 720, height: 1280, video_bitrate: 3000, ..EncodingOptions::new() };
439    pub const PORTRAIT_H264_720P_60: EncodingOptions =
440        EncodingOptions { width: 720, height: 1280, framerate: 60, ..EncodingOptions::new() };
441    pub const PORTRAIT_H264_1080P_30: EncodingOptions =
442        EncodingOptions { width: 1080, height: 1920, ..EncodingOptions::new() };
443    pub const PORTRAIT_H264_1080P_60: EncodingOptions = EncodingOptions {
444        width: 1080,
445        height: 1920,
446        framerate: 60,
447        video_bitrate: 6000,
448        ..EncodingOptions::new()
449    };
450}