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