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