use libwebrtc::prelude::*;
use livekit_protocol as proto;
use crate::prelude::*;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum VideoCodec {
VP8,
H264,
VP9,
AV1,
H265,
}
impl VideoCodec {
pub fn as_str(&self) -> &'static str {
match self {
VideoCodec::VP8 => "vp8",
VideoCodec::H264 => "h264",
VideoCodec::VP9 => "vp9",
VideoCodec::AV1 => "av1",
VideoCodec::H265 => "h265",
}
}
}
#[derive(Debug, Clone)]
pub struct VideoResolution {
pub width: u32,
pub height: u32,
pub frame_rate: f64,
pub aspect_ratio: f32,
}
#[derive(Debug, Clone)]
pub struct VideoEncoding {
pub max_bitrate: u64,
pub max_framerate: f64,
}
#[derive(Debug, Clone)]
pub struct VideoPreset {
pub encoding: VideoEncoding,
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone)]
pub struct AudioEncoding {
pub max_bitrate: u64,
}
#[derive(Debug, Clone)]
pub struct AudioPreset {
pub encoding: AudioEncoding,
}
impl AudioPreset {
pub const fn new(max_bitrate: u64) -> Self {
Self { encoding: AudioEncoding { max_bitrate } }
}
}
#[derive(Clone, Debug)]
pub struct TrackPublishOptions {
pub video_encoding: Option<VideoEncoding>,
pub audio_encoding: Option<AudioEncoding>,
pub video_codec: VideoCodec,
pub dtx: bool,
pub red: bool,
pub simulcast: bool,
pub source: TrackSource,
pub stream: String,
pub preconnect_buffer: bool,
}
impl Default for TrackPublishOptions {
fn default() -> Self {
Self {
video_encoding: None,
audio_encoding: None,
video_codec: VideoCodec::VP8,
dtx: true,
red: true,
simulcast: true,
source: TrackSource::Unknown,
stream: "".to_string(),
preconnect_buffer: false,
}
}
}
impl VideoPreset {
pub const fn new(width: u32, height: u32, max_bitrate: u64, max_framerate: f64) -> Self {
Self { width, height, encoding: VideoEncoding { max_bitrate, max_framerate } }
}
pub fn resolution(&self) -> VideoResolution {
VideoResolution {
width: self.width,
height: self.height,
frame_rate: self.encoding.max_framerate,
aspect_ratio: self.width as f32 / self.height as f32,
}
}
}
pub fn compute_video_encodings(
width: u32,
height: u32,
options: &TrackPublishOptions,
) -> Vec<RtpEncodingParameters> {
let screenshare = options.source == TrackSource::Screenshare;
let encoding = match options.video_encoding.clone() {
Some(encoding) => encoding,
None => compute_appropriate_encoding(screenshare, width, height, options.video_codec),
};
let initial_preset = VideoPreset {
width,
height,
encoding: VideoEncoding {
max_bitrate: encoding.max_bitrate,
max_framerate: encoding.max_framerate,
},
};
if !options.simulcast {
return into_rtp_encodings(width, height, &[initial_preset]);
}
let mut simulcast_presets = compute_default_simulcast_presets(screenshare, &initial_preset);
let mid_preset = simulcast_presets.pop();
let low_preset = simulcast_presets.pop();
let size = u32::max(width, height);
if size >= 960 && low_preset.is_some() {
#[allow(clippy::unnecessary_unwrap)]
return into_rtp_encodings(
width,
height,
&[low_preset.unwrap(), mid_preset.unwrap(), initial_preset],
);
} else if size >= 480 {
return into_rtp_encodings(width, height, &[mid_preset.unwrap(), initial_preset]);
}
into_rtp_encodings(width, height, &[initial_preset])
}
pub fn compute_appropriate_encoding(
is_screenshare: bool,
width: u32,
height: u32,
codec: VideoCodec,
) -> VideoEncoding {
let presets = compute_presets_for_resolution(is_screenshare, width, height);
let size = u32::max(width, height);
let mut encoding = presets.first().unwrap().encoding.clone();
for preset in presets {
encoding = preset.encoding.clone();
if preset.width > size {
break;
}
}
match codec {
VideoCodec::VP9 => encoding.max_bitrate = (encoding.max_bitrate as f32 * 0.85) as u64,
VideoCodec::AV1 => encoding.max_bitrate = (encoding.max_bitrate as f32 * 0.7) as u64,
_ => {}
}
encoding
}
pub fn compute_presets_for_resolution(
is_screenshare: bool,
width: u32,
height: u32,
) -> &'static [VideoPreset] {
if is_screenshare {
return screenshare::PRESETS;
}
let ar = landscape_aspect_ratio(width, height);
if f32::abs(ar - 16.0 / 9.0) < f32::abs(ar - 4.0 / 3.0) {
return video::PRESETS;
}
video43::PRESETS
}
pub fn compute_default_simulcast_presets(
is_screenshare: bool,
initial: &VideoPreset,
) -> Vec<VideoPreset> {
if is_screenshare {
return vec![screenshare::compute_default_simulcast_preset(initial)];
}
let ar = landscape_aspect_ratio(initial.width, initial.height);
if f32::abs(ar - 16.0 / 9.0) < f32::abs(ar - 4.0 / 3.0) {
return video::DEFAULT_SIMULCAST_PRESETS.to_owned();
}
video43::DEFAULT_SIMULCAST_PRESETS.to_owned()
}
pub fn landscape_aspect_ratio(width: u32, height: u32) -> f32 {
if width > height {
width as f32 / height as f32
} else {
height as f32 / width as f32
}
}
pub fn into_rtp_encodings(
initial_width: u32,
initial_height: u32,
presets: &[VideoPreset],
) -> Vec<RtpEncodingParameters> {
let mut encodings = Vec::with_capacity(presets.len());
let size = u32::min(initial_width, initial_height);
for (i, preset) in presets.iter().enumerate() {
encodings.push(RtpEncodingParameters {
rid: VIDEO_RIDS[i].to_string(),
scale_resolution_down_by: Some(f64::max(
1.0,
size as f64 / u32::min(preset.width, preset.height) as f64,
)),
max_bitrate: Some(preset.encoding.max_bitrate),
max_framerate: Some(preset.encoding.max_framerate),
..Default::default()
})
}
encodings.reverse();
encodings
}
pub fn video_quality_for_rid(rid: &str) -> Option<proto::VideoQuality> {
match rid {
"f" => Some(proto::VideoQuality::High),
"h" => Some(proto::VideoQuality::Medium),
"q" => Some(proto::VideoQuality::Low),
_ => None,
}
}
pub fn video_layers_from_encodings(
width: u32,
height: u32,
encodings: &[RtpEncodingParameters],
) -> Vec<proto::VideoLayer> {
if encodings.is_empty() {
return vec![proto::VideoLayer {
quality: proto::VideoQuality::High as i32,
width,
height,
bitrate: 0,
ssrc: 0,
..Default::default()
}];
}
let mut layers = Vec::with_capacity(encodings.len());
for encoding in encodings {
let scale = encoding.scale_resolution_down_by.unwrap_or(1.0);
let quality = video_quality_for_rid(&encoding.rid).unwrap_or(proto::VideoQuality::High);
layers.push(proto::VideoLayer {
quality: quality as i32,
width: (width as f64 / scale) as u32,
height: (height as f64 / scale) as u32,
bitrate: encoding.max_bitrate.unwrap_or(0) as u32,
ssrc: 0,
..Default::default()
});
}
layers
}
const VIDEO_RIDS: &[char] = &['q', 'h', 'f'];
pub mod audio {
use super::AudioPreset;
pub const TELEPHONE: AudioPreset = AudioPreset::new(12_000);
pub const SPEECH: AudioPreset = AudioPreset::new(24_000);
pub const MUSIC: AudioPreset = AudioPreset::new(48_000);
pub const MUSIC_STEREO: AudioPreset = AudioPreset::new(64_000);
pub const MUSIC_HIGH_QUALITY: AudioPreset = AudioPreset::new(96_000);
pub const MUSIC_HIGH_QUALITY_STEREO: AudioPreset = AudioPreset::new(128_000);
pub const PRESETS: &[AudioPreset] =
&[TELEPHONE, SPEECH, MUSIC, MUSIC_STEREO, MUSIC_HIGH_QUALITY, MUSIC_HIGH_QUALITY_STEREO];
}
pub mod video {
use super::VideoPreset;
pub const H90: VideoPreset = VideoPreset::new(160, 90, 90_000, 15.0);
pub const H180: VideoPreset = VideoPreset::new(320, 180, 160_000, 15.0);
pub const H216: VideoPreset = VideoPreset::new(384, 216, 180_000, 15.0);
pub const H360: VideoPreset = VideoPreset::new(640, 360, 450_000, 20.0);
pub const H540: VideoPreset = VideoPreset::new(960, 540, 800_000, 25.0);
pub const H720: VideoPreset = VideoPreset::new(1280, 720, 1_700_000, 30.0);
pub const H1080: VideoPreset = VideoPreset::new(1920, 1080, 3_000_000, 30.0);
pub const H1440: VideoPreset = VideoPreset::new(2560, 1440, 5_000_000, 30.0);
pub const H2160: VideoPreset = VideoPreset::new(3840, 2160, 8_000_000, 30.0);
pub const PRESETS: &[VideoPreset] = &[H90, H180, H216, H360, H540, H720, H1080, H1440, H2160];
pub const DEFAULT_SIMULCAST_PRESETS: &[VideoPreset] = &[H180, H360];
}
pub mod video43 {
use super::VideoPreset;
pub const H120: VideoPreset = VideoPreset::new(160, 120, 80_000, 15.0);
pub const H180: VideoPreset = VideoPreset::new(240, 180, 100_000, 15.0);
pub const H240: VideoPreset = VideoPreset::new(320, 240, 150_000, 15.0);
pub const H360: VideoPreset = VideoPreset::new(480, 360, 225_000, 20.0);
pub const H480: VideoPreset = VideoPreset::new(640, 480, 300_000, 20.0);
pub const H540: VideoPreset = VideoPreset::new(720, 540, 450_000, 25.0);
pub const H720: VideoPreset = VideoPreset::new(960, 720, 1_500_000, 30.0);
pub const H1080: VideoPreset = VideoPreset::new(1440, 1080, 2_500_000, 30.0);
pub const H1440: VideoPreset = VideoPreset::new(1920, 1440, 3_500_000, 30.0);
pub const PRESETS: &[VideoPreset] = &[H120, H180, H240, H360, H480, H540, H720, H1080, H1440];
pub const DEFAULT_SIMULCAST_PRESETS: &[VideoPreset] = &[H180, H360];
}
pub mod screenshare {
use super::VideoPreset;
pub const H360_FPS3: VideoPreset = VideoPreset::new(640, 360, 200_000, 3.0);
pub const H720_FPS5: VideoPreset = VideoPreset::new(1280, 720, 400_000, 5.0);
pub const H720_FPS15: VideoPreset = VideoPreset::new(1280, 720, 1_000_000, 15.0);
pub const H1080_FPS15: VideoPreset = VideoPreset::new(1920, 1080, 1_500_000, 15.0);
pub const H1080_FPS30: VideoPreset = VideoPreset::new(1920, 1080, 3_000_000, 30.0);
pub const PRESETS: &[VideoPreset] =
&[H360_FPS3, H720_FPS5, H720_FPS15, H1080_FPS15, H1080_FPS30];
pub fn compute_default_simulcast_preset(initial: &VideoPreset) -> VideoPreset {
const SCALE_DOWN_FACTOR: u32 = 2;
const FPS: f64 = 3.0;
VideoPreset::new(
initial.width / SCALE_DOWN_FACTOR,
initial.height / SCALE_DOWN_FACTOR,
u64::max(
150_000,
initial.encoding.max_bitrate
/ (SCALE_DOWN_FACTOR.pow(2) as u64
* (initial.encoding.max_framerate / FPS) as u64),
),
FPS,
)
}
}