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 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}