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    /// Starts an egress using the unified v2 [`StartEgressRequest`](proto::StartEgressRequest),
321    /// which supersedes the per-source `start_*_egress` helpers. Calls the
322    /// `Egress.StartEgress` RPC and returns the created [`EgressInfo`](proto::EgressInfo).
323    pub async fn start_egress(
324        &self,
325        request: proto::StartEgressRequest,
326    ) -> ServiceResult<proto::EgressInfo> {
327        self.client
328            .request(
329                SVC,
330                "StartEgress",
331                request,
332                self.base
333                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
334            )
335            .await
336            .map_err(Into::into)
337    }
338
339    pub async fn update_layout(
340        &self,
341        egress_id: &str,
342        layout: &str,
343    ) -> ServiceResult<proto::EgressInfo> {
344        self.client
345            .request(
346                SVC,
347                "UpdateLayout",
348                proto::UpdateLayoutRequest {
349                    egress_id: egress_id.to_owned(),
350                    layout: layout.to_owned(),
351                },
352                self.base
353                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
354            )
355            .await
356            .map_err(Into::into)
357    }
358
359    pub async fn update_stream(
360        &self,
361        egress_id: &str,
362        add_output_urls: Vec<String>,
363        remove_output_urls: Vec<String>,
364    ) -> ServiceResult<proto::EgressInfo> {
365        self.client
366            .request(
367                SVC,
368                "UpdateStream",
369                proto::UpdateStreamRequest {
370                    egress_id: egress_id.to_owned(),
371                    add_output_urls,
372                    remove_output_urls,
373                },
374                self.base
375                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
376            )
377            .await
378            .map_err(Into::into)
379    }
380
381    pub async fn list_egress(
382        &self,
383        options: EgressListOptions,
384    ) -> ServiceResult<Vec<proto::EgressInfo>> {
385        let mut room_name = String::default();
386        let mut egress_id = String::default();
387
388        match options.filter {
389            EgressListFilter::Room(room) => room_name = room,
390            EgressListFilter::Egress(egress) => egress_id = egress,
391            _ => {}
392        }
393
394        let resp: proto::ListEgressResponse = self
395            .client
396            .request(
397                SVC,
398                "ListEgress",
399                proto::ListEgressRequest {
400                    room_name,
401                    egress_id,
402                    active: options.active,
403                    page_token: options.page_token,
404                },
405                self.base
406                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
407            )
408            .await?;
409
410        Ok(resp.items)
411    }
412
413    pub async fn stop_egress(&self, egress_id: &str) -> ServiceResult<proto::EgressInfo> {
414        self.client
415            .request(
416                SVC,
417                "StopEgress",
418                proto::StopEgressRequest { egress_id: egress_id.to_owned() },
419                self.base
420                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
421            )
422            .await
423            .map_err(Into::into)
424    }
425}
426
427fn get_outputs(
428    outputs: Vec<EgressOutput>,
429) -> (
430    Vec<proto::EncodedFileOutput>,
431    Vec<proto::StreamOutput>,
432    Vec<proto::SegmentedFileOutput>,
433    Vec<proto::ImageOutput>,
434) {
435    let mut file_outputs = Vec::new();
436    let mut stream_outputs = Vec::new();
437    let mut segment_outputs = Vec::new();
438    let mut image_outputs = Vec::new();
439
440    for output in outputs {
441        match output {
442            EgressOutput::File(f) => file_outputs.push(f),
443            EgressOutput::Stream(s) => stream_outputs.push(s),
444            EgressOutput::Segments(s) => segment_outputs.push(s),
445            EgressOutput::Image(i) => image_outputs.push(i),
446        }
447    }
448
449    (file_outputs, stream_outputs, segment_outputs, image_outputs)
450}
451
452pub mod encoding {
453    use super::*;
454
455    #[derive(Clone, Debug)]
456    pub struct EncodingOptions {
457        pub width: i32,
458        pub height: i32,
459        pub depth: i32,
460        pub framerate: i32,
461        pub audio_codec: proto::AudioCodec,
462        pub audio_bitrate: i32,
463        pub audio_frequency: i32,
464        pub video_codec: proto::VideoCodec,
465        pub video_bitrate: i32,
466        pub keyframe_interval: f64,
467        pub audio_quality: i32,
468        pub video_quality: i32,
469    }
470
471    impl From<EncodingOptions> for proto::EncodingOptions {
472        fn from(opts: EncodingOptions) -> Self {
473            Self {
474                width: opts.width,
475                height: opts.height,
476                depth: opts.depth,
477                framerate: opts.framerate,
478                audio_codec: opts.audio_codec as i32,
479                audio_bitrate: opts.audio_bitrate,
480                audio_frequency: opts.audio_frequency,
481                video_codec: opts.video_codec as i32,
482                video_bitrate: opts.video_bitrate,
483                key_frame_interval: opts.keyframe_interval,
484                audio_quality: opts.audio_quality,
485                video_quality: opts.video_quality,
486            }
487        }
488    }
489
490    impl EncodingOptions {
491        const fn new() -> Self {
492            Self {
493                width: 1920,
494                height: 1080,
495                depth: 24,
496                framerate: 30,
497                audio_codec: proto::AudioCodec::Opus,
498                audio_bitrate: 128,
499                audio_frequency: 44100,
500                video_codec: proto::VideoCodec::H264Main,
501                video_bitrate: 4500,
502                keyframe_interval: 0.0,
503                audio_quality: 0,
504                video_quality: 0,
505            }
506        }
507    }
508
509    impl Default for EncodingOptions {
510        fn default() -> Self {
511            Self::new()
512        }
513    }
514
515    pub const H264_720P_30: EncodingOptions =
516        EncodingOptions { width: 1280, height: 720, video_bitrate: 3000, ..EncodingOptions::new() };
517    pub const H264_720P_60: EncodingOptions =
518        EncodingOptions { width: 1280, height: 720, framerate: 60, ..EncodingOptions::new() };
519    pub const H264_1080P_30: EncodingOptions = EncodingOptions::new();
520    pub const H264_1080P_60: EncodingOptions =
521        EncodingOptions { framerate: 60, video_bitrate: 6000, ..EncodingOptions::new() };
522    pub const PORTRAIT_H264_720P_30: EncodingOptions =
523        EncodingOptions { width: 720, height: 1280, video_bitrate: 3000, ..EncodingOptions::new() };
524    pub const PORTRAIT_H264_720P_60: EncodingOptions =
525        EncodingOptions { width: 720, height: 1280, framerate: 60, ..EncodingOptions::new() };
526    pub const PORTRAIT_H264_1080P_30: EncodingOptions =
527        EncodingOptions { width: 1080, height: 1920, ..EncodingOptions::new() };
528    pub const PORTRAIT_H264_1080P_60: EncodingOptions = EncodingOptions {
529        width: 1080,
530        height: 1920,
531        framerate: 60,
532        video_bitrate: 6000,
533        ..EncodingOptions::new()
534    };
535}