Skip to main content

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