Skip to main content

gosuto_livekit/room/
options.rs

1// Copyright 2025 LiveKit, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use gosuto_libwebrtc::prelude::*;
16use livekit_protocol as proto;
17
18use crate::prelude::*;
19
20#[derive(Debug, Copy, Clone, PartialEq, Eq)]
21pub enum VideoCodec {
22    VP8,
23    H264,
24    VP9,
25    AV1,
26    H265,
27}
28
29impl VideoCodec {
30    pub fn as_str(&self) -> &'static str {
31        match self {
32            VideoCodec::VP8 => "vp8",
33            VideoCodec::H264 => "h264",
34            VideoCodec::VP9 => "vp9",
35            VideoCodec::AV1 => "av1",
36            VideoCodec::H265 => "h265",
37        }
38    }
39}
40
41#[derive(Debug, Clone)]
42pub struct VideoResolution {
43    pub width: u32,
44    pub height: u32,
45    pub frame_rate: f64,
46    pub aspect_ratio: f32,
47}
48
49#[derive(Debug, Clone)]
50pub struct VideoEncoding {
51    pub max_bitrate: u64,
52    pub max_framerate: f64,
53}
54
55#[derive(Debug, Clone)]
56pub struct VideoPreset {
57    pub encoding: VideoEncoding,
58    pub width: u32,
59    pub height: u32,
60}
61
62#[derive(Debug, Clone)]
63pub struct AudioEncoding {
64    pub max_bitrate: u64,
65}
66
67#[derive(Debug, Clone)]
68pub struct AudioPreset {
69    pub encoding: AudioEncoding,
70}
71
72impl AudioPreset {
73    pub const fn new(max_bitrate: u64) -> Self {
74        Self { encoding: AudioEncoding { max_bitrate } }
75    }
76}
77
78#[derive(Clone, Debug)]
79pub struct TrackPublishOptions {
80    // If the encodings aren't set, LiveKit will compute the most appropriate ones
81    pub video_encoding: Option<VideoEncoding>,
82    pub audio_encoding: Option<AudioEncoding>,
83    pub video_codec: VideoCodec,
84    pub dtx: bool,
85    pub red: bool,
86    pub simulcast: bool,
87    // pub name: String,
88    pub source: TrackSource,
89    pub stream: String,
90    pub preconnect_buffer: bool,
91}
92
93impl Default for TrackPublishOptions {
94    fn default() -> Self {
95        Self {
96            video_encoding: None,
97            audio_encoding: None,
98            video_codec: VideoCodec::VP8,
99            dtx: true,
100            red: true,
101            simulcast: true,
102            source: TrackSource::Unknown,
103            stream: "".to_string(),
104            preconnect_buffer: false,
105        }
106    }
107}
108
109impl VideoPreset {
110    pub const fn new(width: u32, height: u32, max_bitrate: u64, max_framerate: f64) -> Self {
111        Self { width, height, encoding: VideoEncoding { max_bitrate, max_framerate } }
112    }
113
114    pub fn resolution(&self) -> VideoResolution {
115        VideoResolution {
116            width: self.width,
117            height: self.height,
118            frame_rate: self.encoding.max_framerate,
119            aspect_ratio: self.width as f32 / self.height as f32,
120        }
121    }
122}
123
124/// Compute appropriate RtpEncodingParameters from the video resolution.
125/// TrackPublishOptions helps to find the most appropriate encodings
126pub fn compute_video_encodings(
127    width: u32,
128    height: u32,
129    options: &TrackPublishOptions,
130) -> Vec<RtpEncodingParameters> {
131    let screenshare = options.source == TrackSource::Screenshare;
132    let encoding = match options.video_encoding.clone() {
133        Some(encoding) => encoding,
134        None => compute_appropriate_encoding(screenshare, width, height, options.video_codec),
135    };
136
137    let initial_preset = VideoPreset {
138        width,
139        height,
140        encoding: VideoEncoding {
141            max_bitrate: encoding.max_bitrate,
142            max_framerate: encoding.max_framerate,
143        },
144    };
145
146    if !options.simulcast {
147        return into_rtp_encodings(width, height, &[initial_preset]);
148    }
149
150    let mut simulcast_presets = compute_default_simulcast_presets(screenshare, &initial_preset);
151
152    let mid_preset = simulcast_presets.pop();
153    let low_preset = simulcast_presets.pop();
154
155    let size = u32::max(width, height);
156
157    if size >= 960 && low_preset.is_some() {
158        #[allow(clippy::unnecessary_unwrap)]
159        return into_rtp_encodings(
160            width,
161            height,
162            &[low_preset.unwrap(), mid_preset.unwrap(), initial_preset],
163        );
164    } else if size >= 480 {
165        return into_rtp_encodings(width, height, &[mid_preset.unwrap(), initial_preset]);
166    }
167
168    // Other layers not needed
169    into_rtp_encodings(width, height, &[initial_preset])
170}
171
172/// Return an appropriate VideoEncdoding for the specified resolution based on our presets
173pub fn compute_appropriate_encoding(
174    is_screenshare: bool,
175    width: u32,
176    height: u32,
177    codec: VideoCodec,
178) -> VideoEncoding {
179    let presets = compute_presets_for_resolution(is_screenshare, width, height);
180    let size = u32::max(width, height);
181
182    let mut encoding = presets.first().unwrap().encoding.clone();
183
184    for preset in presets {
185        encoding = preset.encoding.clone();
186        if preset.width > size {
187            break;
188        }
189    }
190
191    match codec {
192        VideoCodec::VP9 => encoding.max_bitrate = (encoding.max_bitrate as f32 * 0.85) as u64,
193        VideoCodec::AV1 => encoding.max_bitrate = (encoding.max_bitrate as f32 * 0.7) as u64,
194        _ => {}
195    }
196
197    encoding
198}
199
200pub fn compute_presets_for_resolution(
201    is_screenshare: bool,
202    width: u32,
203    height: u32,
204) -> &'static [VideoPreset] {
205    if is_screenshare {
206        return screenshare::PRESETS;
207    }
208
209    // Check how close width & height are from 16/9 or 4/3
210    let ar = landscape_aspect_ratio(width, height);
211    if f32::abs(ar - 16.0 / 9.0) < f32::abs(ar - 4.0 / 3.0) {
212        return video::PRESETS;
213    }
214
215    video43::PRESETS
216}
217
218/// Returns our most appropriate default presets
219pub fn compute_default_simulcast_presets(
220    is_screenshare: bool,
221    initial: &VideoPreset,
222) -> Vec<VideoPreset> {
223    if is_screenshare {
224        return vec![screenshare::compute_default_simulcast_preset(initial)];
225    }
226
227    let ar = landscape_aspect_ratio(initial.width, initial.height);
228    if f32::abs(ar - 16.0 / 9.0) < f32::abs(ar - 4.0 / 3.0) {
229        return video::DEFAULT_SIMULCAST_PRESETS.to_owned();
230    }
231
232    video43::DEFAULT_SIMULCAST_PRESETS.to_owned()
233}
234
235pub fn landscape_aspect_ratio(width: u32, height: u32) -> f32 {
236    if width > height {
237        width as f32 / height as f32
238    } else {
239        height as f32 / width as f32
240    }
241}
242
243/// Presets must be ordered
244pub fn into_rtp_encodings(
245    initial_width: u32,
246    initial_height: u32,
247    presets: &[VideoPreset],
248) -> Vec<RtpEncodingParameters> {
249    let mut encodings = Vec::with_capacity(presets.len());
250    let size = u32::min(initial_width, initial_height);
251    for (i, preset) in presets.iter().enumerate() {
252        encodings.push(RtpEncodingParameters {
253            rid: VIDEO_RIDS[i].to_string(),
254            scale_resolution_down_by: Some(f64::max(
255                1.0,
256                size as f64 / u32::min(preset.width, preset.height) as f64,
257            )),
258            max_bitrate: Some(preset.encoding.max_bitrate),
259            max_framerate: Some(preset.encoding.max_framerate),
260            ..Default::default()
261        })
262    }
263
264    encodings.reverse();
265    encodings
266}
267
268pub fn video_quality_for_rid(rid: &str) -> Option<proto::VideoQuality> {
269    match rid {
270        "f" => Some(proto::VideoQuality::High),
271        "h" => Some(proto::VideoQuality::Medium),
272        "q" => Some(proto::VideoQuality::Low),
273        _ => None,
274    }
275}
276
277pub fn video_layers_from_encodings(
278    width: u32,
279    height: u32,
280    encodings: &[RtpEncodingParameters],
281) -> Vec<proto::VideoLayer> {
282    if encodings.is_empty() {
283        return vec![proto::VideoLayer {
284            quality: proto::VideoQuality::High as i32,
285            width,
286            height,
287            bitrate: 0,
288            ssrc: 0,
289            ..Default::default()
290        }];
291    }
292
293    let mut layers = Vec::with_capacity(encodings.len());
294    for encoding in encodings {
295        let scale = encoding.scale_resolution_down_by.unwrap_or(1.0);
296        let quality = video_quality_for_rid(&encoding.rid).unwrap_or(proto::VideoQuality::High);
297
298        layers.push(proto::VideoLayer {
299            quality: quality as i32,
300            width: (width as f64 / scale) as u32,
301            height: (height as f64 / scale) as u32,
302            bitrate: encoding.max_bitrate.unwrap_or(0) as u32,
303            ssrc: 0,
304            ..Default::default()
305        });
306    }
307
308    layers
309}
310
311const VIDEO_RIDS: &[char] = &['q', 'h', 'f'];
312
313pub mod audio {
314    use super::AudioPreset;
315
316    pub const TELEPHONE: AudioPreset = AudioPreset::new(12_000);
317    pub const SPEECH: AudioPreset = AudioPreset::new(24_000);
318    pub const MUSIC: AudioPreset = AudioPreset::new(48_000);
319    pub const MUSIC_STEREO: AudioPreset = AudioPreset::new(64_000);
320    pub const MUSIC_HIGH_QUALITY: AudioPreset = AudioPreset::new(96_000);
321    pub const MUSIC_HIGH_QUALITY_STEREO: AudioPreset = AudioPreset::new(128_000);
322
323    pub const PRESETS: &[AudioPreset] =
324        &[TELEPHONE, SPEECH, MUSIC, MUSIC_STEREO, MUSIC_HIGH_QUALITY, MUSIC_HIGH_QUALITY_STEREO];
325}
326
327pub mod video {
328    use super::VideoPreset;
329
330    pub const H90: VideoPreset = VideoPreset::new(160, 90, 90_000, 15.0);
331    pub const H180: VideoPreset = VideoPreset::new(320, 180, 160_000, 15.0);
332    pub const H216: VideoPreset = VideoPreset::new(384, 216, 180_000, 15.0);
333    pub const H360: VideoPreset = VideoPreset::new(640, 360, 450_000, 20.0);
334    pub const H540: VideoPreset = VideoPreset::new(960, 540, 800_000, 25.0);
335    pub const H720: VideoPreset = VideoPreset::new(1280, 720, 1_700_000, 30.0);
336    pub const H1080: VideoPreset = VideoPreset::new(1920, 1080, 3_000_000, 30.0);
337    pub const H1440: VideoPreset = VideoPreset::new(2560, 1440, 5_000_000, 30.0);
338    pub const H2160: VideoPreset = VideoPreset::new(3840, 2160, 8_000_000, 30.0);
339
340    pub const PRESETS: &[VideoPreset] = &[H90, H180, H216, H360, H540, H720, H1080, H1440, H2160];
341    pub const DEFAULT_SIMULCAST_PRESETS: &[VideoPreset] = &[H180, H360];
342}
343
344pub mod video43 {
345    use super::VideoPreset;
346
347    pub const H120: VideoPreset = VideoPreset::new(160, 120, 80_000, 15.0);
348    pub const H180: VideoPreset = VideoPreset::new(240, 180, 100_000, 15.0);
349    pub const H240: VideoPreset = VideoPreset::new(320, 240, 150_000, 15.0);
350    pub const H360: VideoPreset = VideoPreset::new(480, 360, 225_000, 20.0);
351    pub const H480: VideoPreset = VideoPreset::new(640, 480, 300_000, 20.0);
352    pub const H540: VideoPreset = VideoPreset::new(720, 540, 450_000, 25.0);
353    pub const H720: VideoPreset = VideoPreset::new(960, 720, 1_500_000, 30.0);
354    pub const H1080: VideoPreset = VideoPreset::new(1440, 1080, 2_500_000, 30.0);
355    pub const H1440: VideoPreset = VideoPreset::new(1920, 1440, 3_500_000, 30.0);
356
357    pub const PRESETS: &[VideoPreset] = &[H120, H180, H240, H360, H480, H540, H720, H1080, H1440];
358    pub const DEFAULT_SIMULCAST_PRESETS: &[VideoPreset] = &[H180, H360];
359}
360
361pub mod screenshare {
362    /// The screenshare presets are optimized for quality.
363    /// When simulcasting, we prefer to reduce the FPS.
364    use super::VideoPreset;
365
366    pub const H360_FPS3: VideoPreset = VideoPreset::new(640, 360, 200_000, 3.0);
367    pub const H720_FPS5: VideoPreset = VideoPreset::new(1280, 720, 400_000, 5.0);
368    pub const H720_FPS15: VideoPreset = VideoPreset::new(1280, 720, 1_000_000, 15.0);
369    pub const H1080_FPS15: VideoPreset = VideoPreset::new(1920, 1080, 1_500_000, 15.0);
370    pub const H1080_FPS30: VideoPreset = VideoPreset::new(1920, 1080, 3_000_000, 30.0);
371
372    pub const PRESETS: &[VideoPreset] =
373        &[H360_FPS3, H720_FPS5, H720_FPS15, H1080_FPS15, H1080_FPS30];
374
375    /// Only one additional layer for screenshares. (Prioritize quality)
376    pub fn compute_default_simulcast_preset(initial: &VideoPreset) -> VideoPreset {
377        const SCALE_DOWN_FACTOR: u32 = 2;
378        const FPS: f64 = 3.0;
379
380        VideoPreset::new(
381            initial.width / SCALE_DOWN_FACTOR,
382            initial.height / SCALE_DOWN_FACTOR,
383            u64::max(
384                150_000,
385                initial.encoding.max_bitrate
386                    / (SCALE_DOWN_FACTOR.pow(2) as u64
387                        * (initial.encoding.max_framerate / FPS) as u64),
388            ),
389            FPS,
390        )
391    }
392}