1use 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 #[default]
24 DefaultMixing,
25 DualChannelAgent,
27 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 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 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 {
114 Self::build(
115 host,
116 ServiceBase::with_api_key(api_key, api_secret),
117 crate::http_client::Client::new(),
118 )
119 }
120
121 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 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 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 pub fn with_failover(mut self, enabled: bool) -> Self {
148 self.client = self.client.with_failover(enabled);
149 self
150 }
151
152 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, ..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, 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, ..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}