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)]
88pub enum EgressListFilter {
89 All,
90 Egress(String),
91 Room(String),
92}
93
94#[derive(Debug, Clone)]
95pub struct EgressListOptions {
96 pub filter: EgressListFilter,
97 pub active: bool,
98}
99
100const SVC: &str = "Egress";
101
102#[derive(Debug)]
103pub struct EgressClient {
104 base: ServiceBase,
105 client: TwirpClient,
106}
107
108impl EgressClient {
109 pub fn with_api_key(host: &str, api_key: &str, api_secret: &str) -> Self {
110 Self {
111 base: ServiceBase::with_api_key(api_key, api_secret),
112 client: TwirpClient::new(host, LIVEKIT_PACKAGE, None),
113 }
114 }
115
116 pub fn new(host: &str) -> ServiceResult<Self> {
117 let (api_key, api_secret) = get_env_keys()?;
118 Ok(Self::with_api_key(host, &api_key, &api_secret))
119 }
120
121 pub async fn start_room_composite_egress(
122 &self,
123 room: &str,
124 outputs: Vec<EgressOutput>,
125 options: RoomCompositeOptions,
126 ) -> ServiceResult<proto::EgressInfo> {
127 let (file_outputs, stream_outputs, segment_outputs, image_outputs) = get_outputs(outputs);
128 self.client
129 .request(
130 SVC,
131 "StartRoomCompositeEgress",
132 proto::RoomCompositeEgressRequest {
133 room_name: room.to_string(),
134 layout: options.layout,
135 audio_only: options.audio_only,
136 audio_mixing: Into::<proto::AudioMixing>::into(options.audio_mixing) as i32,
137 video_only: options.video_only,
138 options: Some(proto::room_composite_egress_request::Options::Advanced(
139 options.encoding.into(),
140 )),
141 custom_base_url: options.custom_base_url,
142 file_outputs,
143 stream_outputs,
144 segment_outputs,
145 image_outputs,
146 output: None, ..Default::default()
148 },
149 self.base
150 .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
151 )
152 .await
153 .map_err(Into::into)
154 }
155
156 pub async fn start_web_egress(
157 &self,
158 url: &str,
159 outputs: Vec<EgressOutput>,
160 options: WebOptions,
161 ) -> ServiceResult<proto::EgressInfo> {
162 let (file_outputs, stream_outputs, segment_outputs, image_outputs) = get_outputs(outputs);
163 self.client
164 .request(
165 SVC,
166 "StartWebEgress",
167 proto::WebEgressRequest {
168 url: url.to_string(),
169 options: Some(proto::web_egress_request::Options::Advanced(
170 options.encoding.into(),
171 )),
172 audio_only: options.audio_only,
173 video_only: options.video_only,
174 file_outputs,
175 stream_outputs,
176 segment_outputs,
177 image_outputs,
178 output: None, await_start_signal: options.await_start_signal,
180 ..Default::default()
181 },
182 self.base
183 .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
184 )
185 .await
186 .map_err(Into::into)
187 }
188
189 pub async fn start_participant_egress(
190 &self,
191 room: &str,
192 participant_identity: &str,
193 outputs: Vec<EgressOutput>,
194 options: ParticipantEgressOptions,
195 ) -> ServiceResult<proto::EgressInfo> {
196 let (file_outputs, stream_outputs, segment_outputs, image_outputs) = get_outputs(outputs);
197 self.client
198 .request(
199 SVC,
200 "StartParticipantEgress",
201 proto::ParticipantEgressRequest {
202 room_name: room.to_string(),
203 identity: participant_identity.to_string(),
204 options: Some(proto::participant_egress_request::Options::Advanced(
205 options.encoding.into(),
206 )),
207 screen_share: options.screenshare,
208 file_outputs,
209 stream_outputs,
210 segment_outputs,
211 image_outputs,
212 ..Default::default()
213 },
214 self.base
215 .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
216 )
217 .await
218 .map_err(Into::into)
219 }
220
221 pub async fn start_track_composite_egress(
222 &self,
223 room: &str,
224 outputs: Vec<EgressOutput>,
225 options: TrackCompositeOptions,
226 ) -> ServiceResult<proto::EgressInfo> {
227 let (file_outputs, stream_outputs, segment_outputs, image_outputs) = get_outputs(outputs);
228 self.client
229 .request(
230 SVC,
231 "StartTrackCompositeEgress",
232 proto::TrackCompositeEgressRequest {
233 room_name: room.to_string(),
234 options: Some(proto::track_composite_egress_request::Options::Advanced(
235 options.encoding.into(),
236 )),
237 audio_track_id: options.audio_track_id,
238 video_track_id: options.video_track_id,
239 file_outputs,
240 stream_outputs,
241 segment_outputs,
242 image_outputs,
243 output: None, ..Default::default()
245 },
246 self.base
247 .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
248 )
249 .await
250 .map_err(Into::into)
251 }
252
253 pub async fn start_track_egress(
254 &self,
255 room: &str,
256 output: TrackEgressOutput,
257 track_id: &str,
258 ) -> ServiceResult<proto::EgressInfo> {
259 self.client
260 .request(
261 SVC,
262 "StartTrackEgress",
263 proto::TrackEgressRequest {
264 room_name: room.to_string(),
265 output: match output {
266 TrackEgressOutput::File(f) => {
267 Some(proto::track_egress_request::Output::File(*f))
268 }
269 TrackEgressOutput::WebSocket(url) => {
270 Some(proto::track_egress_request::Output::WebsocketUrl(url))
271 }
272 },
273 track_id: track_id.to_string(),
274 ..Default::default()
275 },
276 self.base
277 .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
278 )
279 .await
280 .map_err(Into::into)
281 }
282
283 pub async fn update_layout(
284 &self,
285 egress_id: &str,
286 layout: &str,
287 ) -> ServiceResult<proto::EgressInfo> {
288 self.client
289 .request(
290 SVC,
291 "UpdateLayout",
292 proto::UpdateLayoutRequest {
293 egress_id: egress_id.to_owned(),
294 layout: layout.to_owned(),
295 },
296 self.base
297 .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
298 )
299 .await
300 .map_err(Into::into)
301 }
302
303 pub async fn update_stream(
304 &self,
305 egress_id: &str,
306 add_output_urls: Vec<String>,
307 remove_output_urls: Vec<String>,
308 ) -> ServiceResult<proto::EgressInfo> {
309 self.client
310 .request(
311 SVC,
312 "UpdateStream",
313 proto::UpdateStreamRequest {
314 egress_id: egress_id.to_owned(),
315 add_output_urls,
316 remove_output_urls,
317 },
318 self.base
319 .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
320 )
321 .await
322 .map_err(Into::into)
323 }
324
325 pub async fn list_egress(
326 &self,
327 options: EgressListOptions,
328 ) -> ServiceResult<Vec<proto::EgressInfo>> {
329 let mut room_name = String::default();
330 let mut egress_id = String::default();
331
332 match options.filter {
333 EgressListFilter::Room(room) => room_name = room,
334 EgressListFilter::Egress(egress) => egress_id = egress,
335 _ => {}
336 }
337
338 let resp: proto::ListEgressResponse = self
339 .client
340 .request(
341 SVC,
342 "ListEgress",
343 proto::ListEgressRequest { room_name, egress_id, active: options.active },
344 self.base
345 .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
346 )
347 .await?;
348
349 Ok(resp.items)
350 }
351
352 pub async fn stop_egress(&self, egress_id: &str) -> ServiceResult<proto::EgressInfo> {
353 self.client
354 .request(
355 SVC,
356 "StopEgress",
357 proto::StopEgressRequest { egress_id: egress_id.to_owned() },
358 self.base
359 .auth_header(VideoGrants { room_record: true, ..Default::default() }, None)?,
360 )
361 .await
362 .map_err(Into::into)
363 }
364}
365
366fn get_outputs(
367 outputs: Vec<EgressOutput>,
368) -> (
369 Vec<proto::EncodedFileOutput>,
370 Vec<proto::StreamOutput>,
371 Vec<proto::SegmentedFileOutput>,
372 Vec<proto::ImageOutput>,
373) {
374 let mut file_outputs = Vec::new();
375 let mut stream_outputs = Vec::new();
376 let mut segment_outputs = Vec::new();
377 let mut image_outputs = Vec::new();
378
379 for output in outputs {
380 match output {
381 EgressOutput::File(f) => file_outputs.push(f),
382 EgressOutput::Stream(s) => stream_outputs.push(s),
383 EgressOutput::Segments(s) => segment_outputs.push(s),
384 EgressOutput::Image(i) => image_outputs.push(i),
385 }
386 }
387
388 (file_outputs, stream_outputs, segment_outputs, image_outputs)
389}
390
391pub mod encoding {
392 use super::*;
393
394 #[derive(Clone, Debug)]
395 pub struct EncodingOptions {
396 pub width: i32,
397 pub height: i32,
398 pub depth: i32,
399 pub framerate: i32,
400 pub audio_codec: proto::AudioCodec,
401 pub audio_bitrate: i32,
402 pub audio_frequency: i32,
403 pub video_codec: proto::VideoCodec,
404 pub video_bitrate: i32,
405 pub keyframe_interval: f64,
406 pub audio_quality: i32,
407 pub video_quality: i32,
408 }
409
410 impl From<EncodingOptions> for proto::EncodingOptions {
411 fn from(opts: EncodingOptions) -> Self {
412 Self {
413 width: opts.width,
414 height: opts.height,
415 depth: opts.depth,
416 framerate: opts.framerate,
417 audio_codec: opts.audio_codec as i32,
418 audio_bitrate: opts.audio_bitrate,
419 audio_frequency: opts.audio_frequency,
420 video_codec: opts.video_codec as i32,
421 video_bitrate: opts.video_bitrate,
422 key_frame_interval: opts.keyframe_interval,
423 audio_quality: opts.audio_quality,
424 video_quality: opts.video_quality,
425 }
426 }
427 }
428
429 impl EncodingOptions {
430 const fn new() -> Self {
431 Self {
432 width: 1920,
433 height: 1080,
434 depth: 24,
435 framerate: 30,
436 audio_codec: proto::AudioCodec::Opus,
437 audio_bitrate: 128,
438 audio_frequency: 44100,
439 video_codec: proto::VideoCodec::H264Main,
440 video_bitrate: 4500,
441 keyframe_interval: 0.0,
442 audio_quality: 0,
443 video_quality: 0,
444 }
445 }
446 }
447
448 impl Default for EncodingOptions {
449 fn default() -> Self {
450 Self::new()
451 }
452 }
453
454 pub const H264_720P_30: EncodingOptions =
455 EncodingOptions { width: 1280, height: 720, video_bitrate: 3000, ..EncodingOptions::new() };
456 pub const H264_720P_60: EncodingOptions =
457 EncodingOptions { width: 1280, height: 720, framerate: 60, ..EncodingOptions::new() };
458 pub const H264_1080P_30: EncodingOptions = EncodingOptions::new();
459 pub const H264_1080P_60: EncodingOptions =
460 EncodingOptions { framerate: 60, video_bitrate: 6000, ..EncodingOptions::new() };
461 pub const PORTRAIT_H264_720P_30: EncodingOptions =
462 EncodingOptions { width: 720, height: 1280, video_bitrate: 3000, ..EncodingOptions::new() };
463 pub const PORTRAIT_H264_720P_60: EncodingOptions =
464 EncodingOptions { width: 720, height: 1280, framerate: 60, ..EncodingOptions::new() };
465 pub const PORTRAIT_H264_1080P_30: EncodingOptions =
466 EncodingOptions { width: 1080, height: 1920, ..EncodingOptions::new() };
467 pub const PORTRAIT_H264_1080P_60: EncodingOptions = EncodingOptions {
468 width: 1080,
469 height: 1920,
470 framerate: 60,
471 video_bitrate: 6000,
472 ..EncodingOptions::new()
473 };
474}