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    /// Authenticates with an API key and secret, signing a short-lived token per request.
113    pub fn with_api_key(host: &str, api_key: &str, api_secret: &str) -> Self {
114        Self::build(
115            host,
116            ServiceBase::with_api_key(api_key, api_secret),
117            crate::http_client::Client::new(),
118        )
119    }
120
121    /// Authenticates with a pre-signed token, sent verbatim on every request.
122    pub fn with_token(host: &str, token: &str) -> Self {
123        Self::build(host, ServiceBase::with_token(token), crate::http_client::Client::new())
124    }
125
126    /// Builds the client from an already-constructed HTTP client so the unified
127    /// [`LiveKitApi`](super::LiveKitApi) can share one connection pool across services.
128    pub(crate) fn build(host: &str, base: ServiceBase, client: crate::http_client::Client) -> Self {
129        Self { base, client: TwirpClient::with_client(host, LIVEKIT_PACKAGE, None, client) }
130    }
131
132    #[cfg(test)]
133    pub(crate) fn with_default_headers(mut self, headers: http::HeaderMap) -> Self {
134        self.client = self.client.with_default_headers(headers);
135        self
136    }
137
138    /// Reads the API key and secret from the `LIVEKIT_API_KEY` and
139    /// `LIVEKIT_API_SECRET` environment variables.
140    pub fn new(host: &str) -> ServiceResult<Self> {
141        let (api_key, api_secret) = get_env_keys()?;
142        Ok(Self::with_api_key(host, &api_key, &api_secret))
143    }
144
145    /// Enables or disables region failover (enabled by default). Failover only
146    /// engages for LiveKit Cloud hosts.
147    pub fn with_failover(mut self, enabled: bool) -> Self {
148        self.client = self.client.with_failover(enabled);
149        self
150    }
151
152    /// Overrides the default per-request timeout (10s) for calls on this client.
153    pub fn with_request_timeout(mut self, timeout: std::time::Duration) -> Self {
154        self.client = self.client.with_request_timeout(timeout);
155        self
156    }
157
158    pub async fn start_room_composite_egress(
159        &self,
160        room: &str,
161        outputs: Vec<EgressOutput>,
162        options: RoomCompositeOptions,
163    ) -> ServiceResult<proto::EgressInfo> {
164        let (file_outputs, stream_outputs, segment_outputs, image_outputs) = get_outputs(outputs);
165        self.client
166            .request(
167                SVC,
168                "StartRoomCompositeEgress",
169                proto::RoomCompositeEgressRequest {
170                    room_name: room.to_string(),
171                    layout: options.layout,
172                    audio_only: options.audio_only,
173                    audio_mixing: Into::<proto::AudioMixing>::into(options.audio_mixing) as i32,
174                    video_only: options.video_only,
175                    options: Some(proto::room_composite_egress_request::Options::Advanced(
176                        options.encoding.into(),
177                    )),
178                    custom_base_url: options.custom_base_url,
179                    file_outputs,
180                    stream_outputs,
181                    segment_outputs,
182                    image_outputs,
183                    output: None, // Deprecated
184                    ..Default::default()
185                },
186                self.base
187                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
188            )
189            .await
190            .map_err(Into::into)
191    }
192
193    pub async fn start_web_egress(
194        &self,
195        url: &str,
196        outputs: Vec<EgressOutput>,
197        options: WebOptions,
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                "StartWebEgress",
204                proto::WebEgressRequest {
205                    url: url.to_string(),
206                    options: Some(proto::web_egress_request::Options::Advanced(
207                        options.encoding.into(),
208                    )),
209                    audio_only: options.audio_only,
210                    video_only: options.video_only,
211                    file_outputs,
212                    stream_outputs,
213                    segment_outputs,
214                    image_outputs,
215                    output: None, // Deprecated
216                    await_start_signal: options.await_start_signal,
217                    ..Default::default()
218                },
219                self.base
220                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
221            )
222            .await
223            .map_err(Into::into)
224    }
225
226    pub async fn start_participant_egress(
227        &self,
228        room: &str,
229        participant_identity: &str,
230        outputs: Vec<EgressOutput>,
231        options: ParticipantEgressOptions,
232    ) -> ServiceResult<proto::EgressInfo> {
233        let (file_outputs, stream_outputs, segment_outputs, image_outputs) = get_outputs(outputs);
234        self.client
235            .request(
236                SVC,
237                "StartParticipantEgress",
238                proto::ParticipantEgressRequest {
239                    room_name: room.to_string(),
240                    identity: participant_identity.to_string(),
241                    options: Some(proto::participant_egress_request::Options::Advanced(
242                        options.encoding.into(),
243                    )),
244                    screen_share: options.screenshare,
245                    file_outputs,
246                    stream_outputs,
247                    segment_outputs,
248                    image_outputs,
249                    ..Default::default()
250                },
251                self.base
252                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
253            )
254            .await
255            .map_err(Into::into)
256    }
257
258    pub async fn start_track_composite_egress(
259        &self,
260        room: &str,
261        outputs: Vec<EgressOutput>,
262        options: TrackCompositeOptions,
263    ) -> ServiceResult<proto::EgressInfo> {
264        let (file_outputs, stream_outputs, segment_outputs, image_outputs) = get_outputs(outputs);
265        self.client
266            .request(
267                SVC,
268                "StartTrackCompositeEgress",
269                proto::TrackCompositeEgressRequest {
270                    room_name: room.to_string(),
271                    options: Some(proto::track_composite_egress_request::Options::Advanced(
272                        options.encoding.into(),
273                    )),
274                    audio_track_id: options.audio_track_id,
275                    video_track_id: options.video_track_id,
276                    file_outputs,
277                    stream_outputs,
278                    segment_outputs,
279                    image_outputs,
280                    output: None, // Deprecated
281                    ..Default::default()
282                },
283                self.base
284                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
285            )
286            .await
287            .map_err(Into::into)
288    }
289
290    pub async fn start_track_egress(
291        &self,
292        room: &str,
293        output: TrackEgressOutput,
294        track_id: &str,
295    ) -> ServiceResult<proto::EgressInfo> {
296        self.client
297            .request(
298                SVC,
299                "StartTrackEgress",
300                proto::TrackEgressRequest {
301                    room_name: room.to_string(),
302                    output: match output {
303                        TrackEgressOutput::File(f) => {
304                            Some(proto::track_egress_request::Output::File(*f))
305                        }
306                        TrackEgressOutput::WebSocket(url) => {
307                            Some(proto::track_egress_request::Output::WebsocketUrl(url))
308                        }
309                    },
310                    track_id: track_id.to_string(),
311                    ..Default::default()
312                },
313                self.base
314                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
315            )
316            .await
317            .map_err(Into::into)
318    }
319
320    pub async fn update_layout(
321        &self,
322        egress_id: &str,
323        layout: &str,
324    ) -> ServiceResult<proto::EgressInfo> {
325        self.client
326            .request(
327                SVC,
328                "UpdateLayout",
329                proto::UpdateLayoutRequest {
330                    egress_id: egress_id.to_owned(),
331                    layout: layout.to_owned(),
332                },
333                self.base
334                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
335            )
336            .await
337            .map_err(Into::into)
338    }
339
340    pub async fn update_stream(
341        &self,
342        egress_id: &str,
343        add_output_urls: Vec<String>,
344        remove_output_urls: Vec<String>,
345    ) -> ServiceResult<proto::EgressInfo> {
346        self.client
347            .request(
348                SVC,
349                "UpdateStream",
350                proto::UpdateStreamRequest {
351                    egress_id: egress_id.to_owned(),
352                    add_output_urls,
353                    remove_output_urls,
354                },
355                self.base
356                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
357            )
358            .await
359            .map_err(Into::into)
360    }
361
362    pub async fn list_egress(
363        &self,
364        options: EgressListOptions,
365    ) -> ServiceResult<Vec<proto::EgressInfo>> {
366        let mut room_name = String::default();
367        let mut egress_id = String::default();
368
369        match options.filter {
370            EgressListFilter::Room(room) => room_name = room,
371            EgressListFilter::Egress(egress) => egress_id = egress,
372            _ => {}
373        }
374
375        let resp: proto::ListEgressResponse = self
376            .client
377            .request(
378                SVC,
379                "ListEgress",
380                proto::ListEgressRequest {
381                    room_name,
382                    egress_id,
383                    active: options.active,
384                    page_token: options.page_token,
385                },
386                self.base
387                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
388            )
389            .await?;
390
391        Ok(resp.items)
392    }
393
394    pub async fn stop_egress(&self, egress_id: &str) -> ServiceResult<proto::EgressInfo> {
395        self.client
396            .request(
397                SVC,
398                "StopEgress",
399                proto::StopEgressRequest { egress_id: egress_id.to_owned() },
400                self.base
401                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
402            )
403            .await
404            .map_err(Into::into)
405    }
406}
407
408fn get_outputs(
409    outputs: Vec<EgressOutput>,
410) -> (
411    Vec<proto::EncodedFileOutput>,
412    Vec<proto::StreamOutput>,
413    Vec<proto::SegmentedFileOutput>,
414    Vec<proto::ImageOutput>,
415) {
416    let mut file_outputs = Vec::new();
417    let mut stream_outputs = Vec::new();
418    let mut segment_outputs = Vec::new();
419    let mut image_outputs = Vec::new();
420
421    for output in outputs {
422        match output {
423            EgressOutput::File(f) => file_outputs.push(f),
424            EgressOutput::Stream(s) => stream_outputs.push(s),
425            EgressOutput::Segments(s) => segment_outputs.push(s),
426            EgressOutput::Image(i) => image_outputs.push(i),
427        }
428    }
429
430    (file_outputs, stream_outputs, segment_outputs, image_outputs)
431}
432
433pub mod encoding {
434    use super::*;
435
436    #[derive(Clone, Debug)]
437    pub struct EncodingOptions {
438        pub width: i32,
439        pub height: i32,
440        pub depth: i32,
441        pub framerate: i32,
442        pub audio_codec: proto::AudioCodec,
443        pub audio_bitrate: i32,
444        pub audio_frequency: i32,
445        pub video_codec: proto::VideoCodec,
446        pub video_bitrate: i32,
447        pub keyframe_interval: f64,
448        pub audio_quality: i32,
449        pub video_quality: i32,
450    }
451
452    impl From<EncodingOptions> for proto::EncodingOptions {
453        fn from(opts: EncodingOptions) -> Self {
454            Self {
455                width: opts.width,
456                height: opts.height,
457                depth: opts.depth,
458                framerate: opts.framerate,
459                audio_codec: opts.audio_codec as i32,
460                audio_bitrate: opts.audio_bitrate,
461                audio_frequency: opts.audio_frequency,
462                video_codec: opts.video_codec as i32,
463                video_bitrate: opts.video_bitrate,
464                key_frame_interval: opts.keyframe_interval,
465                audio_quality: opts.audio_quality,
466                video_quality: opts.video_quality,
467            }
468        }
469    }
470
471    impl EncodingOptions {
472        const fn new() -> Self {
473            Self {
474                width: 1920,
475                height: 1080,
476                depth: 24,
477                framerate: 30,
478                audio_codec: proto::AudioCodec::Opus,
479                audio_bitrate: 128,
480                audio_frequency: 44100,
481                video_codec: proto::VideoCodec::H264Main,
482                video_bitrate: 4500,
483                keyframe_interval: 0.0,
484                audio_quality: 0,
485                video_quality: 0,
486            }
487        }
488    }
489
490    impl Default for EncodingOptions {
491        fn default() -> Self {
492            Self::new()
493        }
494    }
495
496    pub const H264_720P_30: EncodingOptions =
497        EncodingOptions { width: 1280, height: 720, video_bitrate: 3000, ..EncodingOptions::new() };
498    pub const H264_720P_60: EncodingOptions =
499        EncodingOptions { width: 1280, height: 720, framerate: 60, ..EncodingOptions::new() };
500    pub const H264_1080P_30: EncodingOptions = EncodingOptions::new();
501    pub const H264_1080P_60: EncodingOptions =
502        EncodingOptions { framerate: 60, video_bitrate: 6000, ..EncodingOptions::new() };
503    pub const PORTRAIT_H264_720P_30: EncodingOptions =
504        EncodingOptions { width: 720, height: 1280, video_bitrate: 3000, ..EncodingOptions::new() };
505    pub const PORTRAIT_H264_720P_60: EncodingOptions =
506        EncodingOptions { width: 720, height: 1280, framerate: 60, ..EncodingOptions::new() };
507    pub const PORTRAIT_H264_1080P_30: EncodingOptions =
508        EncodingOptions { width: 1080, height: 1920, ..EncodingOptions::new() };
509    pub const PORTRAIT_H264_1080P_60: EncodingOptions = EncodingOptions {
510        width: 1080,
511        height: 1920,
512        framerate: 60,
513        video_bitrate: 6000,
514        ..EncodingOptions::new()
515    };
516}