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::services::twirp_client::TwirpClient;
19use livekit_token::{get_env_keys, VideoGrants};
20
21#[derive(Clone, Copy, Debug, Default)]
22pub enum AudioMixing {
23    /// All users are mixed together.
24    #[default]
25    DefaultMixing,
26    /// Agent audio in the left channel, all other audio in the right channel.
27    DualChannelAgent,
28    /// Each new audio track alternates between left and right channels.
29    DualChannelAlternate,
30}
31
32impl From<AudioMixing> for proto::AudioMixing {
33    fn from(value: AudioMixing) -> Self {
34        match value {
35            AudioMixing::DefaultMixing => proto::AudioMixing::DefaultMixing,
36            AudioMixing::DualChannelAgent => proto::AudioMixing::DualChannelAgent,
37            AudioMixing::DualChannelAlternate => proto::AudioMixing::DualChannelAlternate,
38        }
39    }
40}
41
42#[derive(Default, Clone, Debug)]
43pub struct RoomCompositeOptions {
44    pub layout: String,
45    pub encoding: encoding::EncodingOptions,
46    pub audio_only: bool,
47    pub video_only: bool,
48    pub custom_base_url: String,
49    /// Only applies when audio_only is true (default: DefaultMixing)
50    pub audio_mixing: AudioMixing,
51}
52
53#[derive(Default, Clone, Debug)]
54pub struct WebOptions {
55    pub encoding: encoding::EncodingOptions,
56    pub audio_only: bool,
57    pub video_only: bool,
58    pub await_start_signal: bool,
59}
60
61#[derive(Default, Clone, Debug)]
62pub struct ParticipantEgressOptions {
63    pub screenshare: bool,
64    pub encoding: encoding::EncodingOptions,
65}
66
67#[derive(Default, Clone, Debug)]
68pub struct TrackCompositeOptions {
69    pub encoding: encoding::EncodingOptions,
70    pub audio_track_id: String,
71    pub video_track_id: String,
72}
73
74#[derive(Debug, Clone)]
75pub enum EgressOutput {
76    File(proto::EncodedFileOutput),
77    Stream(proto::StreamOutput),
78    Segments(proto::SegmentedFileOutput),
79    Image(proto::ImageOutput),
80}
81
82#[derive(Debug, Clone)]
83pub enum TrackEgressOutput {
84    File(Box<proto::DirectFileOutput>),
85    WebSocket(String),
86}
87
88#[derive(Debug, Clone, Default)]
89pub enum EgressListFilter {
90    #[default]
91    All,
92    Egress(String),
93    Room(String),
94}
95
96#[derive(Debug, Clone, Default)]
97pub struct EgressListOptions {
98    pub filter: EgressListFilter,
99    pub active: bool,
100    /// Pagination token, e.g. from a previous response's `next_page_token`.
101    pub page_token: Option<proto::TokenPagination>,
102}
103
104const SVC: &str = "Egress";
105
106#[derive(Debug)]
107pub struct EgressClient {
108    base: ServiceBase,
109    client: TwirpClient,
110}
111
112impl EgressClient {
113    /// Authenticates with an API key and secret, signing a short-lived token per request.
114    pub fn with_api_key(host: &str, api_key: &str, api_secret: &str) -> Self {
115        Self::build(
116            host,
117            ServiceBase::with_api_key(api_key, api_secret),
118            crate::http_client::Client::new(),
119        )
120    }
121
122    /// Authenticates with a pre-signed token, sent verbatim on every request.
123    pub fn with_token(host: &str, token: &str) -> Self {
124        Self::build(host, ServiceBase::with_token(token), crate::http_client::Client::new())
125    }
126
127    /// Builds the client from an already-constructed HTTP client so the unified
128    /// [`LiveKitApi`](super::LiveKitApi) can share one connection pool across services.
129    pub(crate) fn build(host: &str, base: ServiceBase, client: crate::http_client::Client) -> Self {
130        Self { base, client: TwirpClient::with_client(host, LIVEKIT_PACKAGE, None, client) }
131    }
132
133    #[cfg(test)]
134    pub(crate) fn with_default_headers(mut self, headers: http::HeaderMap) -> Self {
135        self.client = self.client.with_default_headers(headers);
136        self
137    }
138
139    /// Reads the API key and secret from the `LIVEKIT_API_KEY` and
140    /// `LIVEKIT_API_SECRET` environment variables.
141    pub fn new(host: &str) -> ServiceResult<Self> {
142        let (api_key, api_secret) = get_env_keys()?;
143        Ok(Self::with_api_key(host, &api_key, &api_secret))
144    }
145
146    /// Enables or disables region failover (enabled by default). Failover only
147    /// engages for LiveKit Cloud hosts.
148    pub fn with_failover(mut self, enabled: bool) -> Self {
149        self.client = self.client.with_failover(enabled);
150        self
151    }
152
153    /// Overrides the default per-request timeout (10s) for calls on this client.
154    pub fn with_request_timeout(mut self, timeout: std::time::Duration) -> Self {
155        self.client = self.client.with_request_timeout(timeout);
156        self
157    }
158
159    pub async fn start_room_composite_egress(
160        &self,
161        room: &str,
162        outputs: Vec<EgressOutput>,
163        options: RoomCompositeOptions,
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                "StartRoomCompositeEgress",
170                proto::RoomCompositeEgressRequest {
171                    room_name: room.to_string(),
172                    layout: options.layout,
173                    audio_only: options.audio_only,
174                    audio_mixing: Into::<proto::AudioMixing>::into(options.audio_mixing) as i32,
175                    video_only: options.video_only,
176                    options: Some(proto::room_composite_egress_request::Options::Advanced(
177                        options.encoding.into(),
178                    )),
179                    custom_base_url: options.custom_base_url,
180                    file_outputs,
181                    stream_outputs,
182                    segment_outputs,
183                    image_outputs,
184                    output: None, // Deprecated
185                    ..Default::default()
186                },
187                self.base
188                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
189            )
190            .await
191            .map_err(Into::into)
192    }
193
194    pub async fn start_web_egress(
195        &self,
196        url: &str,
197        outputs: Vec<EgressOutput>,
198        options: WebOptions,
199    ) -> ServiceResult<proto::EgressInfo> {
200        let (file_outputs, stream_outputs, segment_outputs, image_outputs) = get_outputs(outputs);
201        self.client
202            .request(
203                SVC,
204                "StartWebEgress",
205                proto::WebEgressRequest {
206                    url: url.to_string(),
207                    options: Some(proto::web_egress_request::Options::Advanced(
208                        options.encoding.into(),
209                    )),
210                    audio_only: options.audio_only,
211                    video_only: options.video_only,
212                    file_outputs,
213                    stream_outputs,
214                    segment_outputs,
215                    image_outputs,
216                    output: None, // Deprecated
217                    await_start_signal: options.await_start_signal,
218                    ..Default::default()
219                },
220                self.base
221                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
222            )
223            .await
224            .map_err(Into::into)
225    }
226
227    pub async fn start_participant_egress(
228        &self,
229        room: &str,
230        participant_identity: &str,
231        outputs: Vec<EgressOutput>,
232        options: ParticipantEgressOptions,
233    ) -> ServiceResult<proto::EgressInfo> {
234        let (file_outputs, stream_outputs, segment_outputs, image_outputs) = get_outputs(outputs);
235        self.client
236            .request(
237                SVC,
238                "StartParticipantEgress",
239                proto::ParticipantEgressRequest {
240                    room_name: room.to_string(),
241                    identity: participant_identity.to_string(),
242                    options: Some(proto::participant_egress_request::Options::Advanced(
243                        options.encoding.into(),
244                    )),
245                    screen_share: options.screenshare,
246                    file_outputs,
247                    stream_outputs,
248                    segment_outputs,
249                    image_outputs,
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 start_track_composite_egress(
260        &self,
261        room: &str,
262        outputs: Vec<EgressOutput>,
263        options: TrackCompositeOptions,
264    ) -> ServiceResult<proto::EgressInfo> {
265        let (file_outputs, stream_outputs, segment_outputs, image_outputs) = get_outputs(outputs);
266        self.client
267            .request(
268                SVC,
269                "StartTrackCompositeEgress",
270                proto::TrackCompositeEgressRequest {
271                    room_name: room.to_string(),
272                    options: Some(proto::track_composite_egress_request::Options::Advanced(
273                        options.encoding.into(),
274                    )),
275                    audio_track_id: options.audio_track_id,
276                    video_track_id: options.video_track_id,
277                    file_outputs,
278                    stream_outputs,
279                    segment_outputs,
280                    image_outputs,
281                    output: None, // Deprecated
282                    ..Default::default()
283                },
284                self.base
285                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
286            )
287            .await
288            .map_err(Into::into)
289    }
290
291    pub async fn start_track_egress(
292        &self,
293        room: &str,
294        output: TrackEgressOutput,
295        track_id: &str,
296    ) -> ServiceResult<proto::EgressInfo> {
297        self.client
298            .request(
299                SVC,
300                "StartTrackEgress",
301                proto::TrackEgressRequest {
302                    room_name: room.to_string(),
303                    output: match output {
304                        TrackEgressOutput::File(f) => {
305                            Some(proto::track_egress_request::Output::File(*f))
306                        }
307                        TrackEgressOutput::WebSocket(url) => {
308                            Some(proto::track_egress_request::Output::WebsocketUrl(url))
309                        }
310                    },
311                    track_id: track_id.to_string(),
312                    ..Default::default()
313                },
314                self.base
315                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
316            )
317            .await
318            .map_err(Into::into)
319    }
320
321    /// Starts an egress using the unified v2 [`StartEgressRequest`](proto::StartEgressRequest),
322    /// which supersedes the per-source `start_*_egress` helpers. Calls the
323    /// `Egress.StartEgress` RPC and returns the created [`EgressInfo`](proto::EgressInfo).
324    pub async fn start_egress(
325        &self,
326        request: proto::StartEgressRequest,
327    ) -> ServiceResult<proto::EgressInfo> {
328        self.client
329            .request(
330                SVC,
331                "StartEgress",
332                request,
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_layout(
341        &self,
342        egress_id: &str,
343        layout: &str,
344    ) -> ServiceResult<proto::EgressInfo> {
345        self.client
346            .request(
347                SVC,
348                "UpdateLayout",
349                proto::UpdateLayoutRequest {
350                    egress_id: egress_id.to_owned(),
351                    layout: layout.to_owned(),
352                },
353                self.base
354                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
355            )
356            .await
357            .map_err(Into::into)
358    }
359
360    pub async fn update_stream(
361        &self,
362        egress_id: &str,
363        add_output_urls: Vec<String>,
364        remove_output_urls: Vec<String>,
365    ) -> ServiceResult<proto::EgressInfo> {
366        self.client
367            .request(
368                SVC,
369                "UpdateStream",
370                proto::UpdateStreamRequest {
371                    egress_id: egress_id.to_owned(),
372                    add_output_urls,
373                    remove_output_urls,
374                },
375                self.base
376                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
377            )
378            .await
379            .map_err(Into::into)
380    }
381
382    pub async fn list_egress(
383        &self,
384        options: EgressListOptions,
385    ) -> ServiceResult<Vec<proto::EgressInfo>> {
386        let mut room_name = String::default();
387        let mut egress_id = String::default();
388
389        match options.filter {
390            EgressListFilter::Room(room) => room_name = room,
391            EgressListFilter::Egress(egress) => egress_id = egress,
392            _ => {}
393        }
394
395        let resp: proto::ListEgressResponse = self
396            .client
397            .request(
398                SVC,
399                "ListEgress",
400                proto::ListEgressRequest {
401                    room_name,
402                    egress_id,
403                    active: options.active,
404                    page_token: options.page_token,
405                },
406                self.base
407                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
408            )
409            .await?;
410
411        Ok(resp.items)
412    }
413
414    pub async fn stop_egress(&self, egress_id: &str) -> ServiceResult<proto::EgressInfo> {
415        self.client
416            .request(
417                SVC,
418                "StopEgress",
419                proto::StopEgressRequest { egress_id: egress_id.to_owned() },
420                self.base
421                    .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
422            )
423            .await
424            .map_err(Into::into)
425    }
426}
427
428fn get_outputs(
429    outputs: Vec<EgressOutput>,
430) -> (
431    Vec<proto::EncodedFileOutput>,
432    Vec<proto::StreamOutput>,
433    Vec<proto::SegmentedFileOutput>,
434    Vec<proto::ImageOutput>,
435) {
436    let mut file_outputs = Vec::new();
437    let mut stream_outputs = Vec::new();
438    let mut segment_outputs = Vec::new();
439    let mut image_outputs = Vec::new();
440
441    for output in outputs {
442        match output {
443            EgressOutput::File(f) => file_outputs.push(f),
444            EgressOutput::Stream(s) => stream_outputs.push(s),
445            EgressOutput::Segments(s) => segment_outputs.push(s),
446            EgressOutput::Image(i) => image_outputs.push(i),
447        }
448    }
449
450    (file_outputs, stream_outputs, segment_outputs, image_outputs)
451}
452
453pub mod encoding {
454    use super::*;
455
456    #[derive(Clone, Debug)]
457    pub struct EncodingOptions {
458        pub width: i32,
459        pub height: i32,
460        pub depth: i32,
461        pub framerate: i32,
462        pub audio_codec: proto::AudioCodec,
463        pub audio_bitrate: i32,
464        pub audio_frequency: i32,
465        pub video_codec: proto::VideoCodec,
466        pub video_bitrate: i32,
467        pub keyframe_interval: f64,
468        pub audio_quality: i32,
469        pub video_quality: i32,
470    }
471
472    impl From<EncodingOptions> for proto::EncodingOptions {
473        fn from(opts: EncodingOptions) -> Self {
474            Self {
475                width: opts.width,
476                height: opts.height,
477                depth: opts.depth,
478                framerate: opts.framerate,
479                audio_codec: opts.audio_codec as i32,
480                audio_bitrate: opts.audio_bitrate,
481                audio_frequency: opts.audio_frequency,
482                video_codec: opts.video_codec as i32,
483                video_bitrate: opts.video_bitrate,
484                key_frame_interval: opts.keyframe_interval,
485                audio_quality: opts.audio_quality,
486                video_quality: opts.video_quality,
487            }
488        }
489    }
490
491    impl EncodingOptions {
492        const fn new() -> Self {
493            Self {
494                width: 1920,
495                height: 1080,
496                depth: 24,
497                framerate: 30,
498                audio_codec: proto::AudioCodec::Opus,
499                audio_bitrate: 128,
500                audio_frequency: 44100,
501                video_codec: proto::VideoCodec::H264Main,
502                video_bitrate: 4500,
503                keyframe_interval: 0.0,
504                audio_quality: 0,
505                video_quality: 0,
506            }
507        }
508    }
509
510    impl Default for EncodingOptions {
511        fn default() -> Self {
512            Self::new()
513        }
514    }
515
516    pub const H264_720P_30: EncodingOptions =
517        EncodingOptions { width: 1280, height: 720, video_bitrate: 3000, ..EncodingOptions::new() };
518    pub const H264_720P_60: EncodingOptions =
519        EncodingOptions { width: 1280, height: 720, framerate: 60, ..EncodingOptions::new() };
520    pub const H264_1080P_30: EncodingOptions = EncodingOptions::new();
521    pub const H264_1080P_60: EncodingOptions =
522        EncodingOptions { framerate: 60, video_bitrate: 6000, ..EncodingOptions::new() };
523    pub const PORTRAIT_H264_720P_30: EncodingOptions =
524        EncodingOptions { width: 720, height: 1280, video_bitrate: 3000, ..EncodingOptions::new() };
525    pub const PORTRAIT_H264_720P_60: EncodingOptions =
526        EncodingOptions { width: 720, height: 1280, framerate: 60, ..EncodingOptions::new() };
527    pub const PORTRAIT_H264_1080P_30: EncodingOptions =
528        EncodingOptions { width: 1080, height: 1920, ..EncodingOptions::new() };
529    pub const PORTRAIT_H264_1080P_60: EncodingOptions = EncodingOptions {
530        width: 1080,
531        height: 1920,
532        framerate: 60,
533        video_bitrate: 6000,
534        ..EncodingOptions::new()
535    };
536}