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