1use 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 #[default]
25 DefaultMixing,
26 DualChannelAgent,
28 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 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 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 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 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 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 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 pub fn with_failover(mut self, enabled: bool) -> Self {
149 self.client = self.client.with_failover(enabled);
150 self
151 }
152
153 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, ..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, 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, ..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 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}