Add native self-hosted instance connection to fluxer_desktop

Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 +
tools/ci) with a "Connect to a Different Server" menu item and popout
that lets the desktop app switch to any self-hosted Fluxer instance,
plus fixes for well-known discovery on single-domain self-hosted
deployments and a false-positive ERR_ABORTED on same-origin client
redirects during the switch. Defaults to chat.fluxr.chat and uses an
isolated userData directory from the official build.
This commit is contained in:
2026-07-01 18:22:43 -04:00
commit 682afacd30
1763 changed files with 613720 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use napi_derive::napi;
pub const ENGINE_BRIDGE_VERSION: u32 = 18;
const _: () = assert!(ENGINE_BRIDGE_VERSION > 0);
fn check_engine_bridge_version(version: u32) -> Result<(), String> {
if version == ENGINE_BRIDGE_VERSION {
return Ok(());
}
Err(format!(
"voice engine bridge version mismatch: host sent {version}, native addon expects {ENGINE_BRIDGE_VERSION}"
))
}
#[napi]
pub fn get_engine_bridge_version() -> u32 {
ENGINE_BRIDGE_VERSION
}
#[napi]
pub fn assert_engine_bridge_version(version: u32) -> napi::Result<()> {
check_engine_bridge_version(version).map_err(napi::Error::from_reason)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matching_version_passes() {
assert!(check_engine_bridge_version(ENGINE_BRIDGE_VERSION).is_ok());
assert!(assert_engine_bridge_version(ENGINE_BRIDGE_VERSION).is_ok());
}
#[test]
fn mismatched_version_fails_with_both_versions_in_message() {
let error = check_engine_bridge_version(ENGINE_BRIDGE_VERSION + 1).unwrap_err();
assert!(error.contains("voice engine bridge version mismatch"));
assert!(error.contains(&(ENGINE_BRIDGE_VERSION + 1).to_string()));
assert!(error.contains(&ENGINE_BRIDGE_VERSION.to_string()));
}
#[test]
fn zero_version_fails() {
assert!(check_engine_bridge_version(0).is_err());
}
#[test]
fn mismatch_surfaces_as_napi_error() {
let error = assert_engine_bridge_version(ENGINE_BRIDGE_VERSION - 1).unwrap_err();
assert!(
error
.reason
.contains("voice engine bridge version mismatch")
);
}
#[test]
fn exported_getter_reports_the_constant() {
assert_eq!(get_engine_bridge_version(), ENGINE_BRIDGE_VERSION);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,200 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#[cfg(test)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PublishConfig {
pub url: String,
pub token: String,
pub width: u32,
pub height: u32,
pub fps: u32,
pub codec: String,
}
#[cfg(test)]
impl PublishConfig {
pub fn validate(&self) -> Result<(), String> {
if self.url.trim().is_empty() {
return Err("livekit url is empty".into());
}
if !(self.url.starts_with("ws://") || self.url.starts_with("wss://")) {
return Err("livekit url must be ws:// or wss://".into());
}
if self.token.trim().is_empty() {
return Err("livekit token is empty".into());
}
if self.width < 2 || self.height < 2 {
return Err("capture dimensions too small".into());
}
if !self.width.is_multiple_of(2) || !self.height.is_multiple_of(2) {
return Err("capture dimensions must be even".into());
}
if self.width > 8192 || self.height > 8192 {
return Err("capture dimensions too large".into());
}
if self.fps == 0 {
return Err("capture fps must be positive".into());
}
if !self.codec.trim().is_empty() && canonical_codec_name(&self.codec).is_none() {
return Err("unsupported video codec".into());
}
Ok(())
}
}
pub const SUPPORTED_CODECS: &[&str] = &["vp8", "h264", "vp9", "av1", "h265"];
pub fn canonical_codec_name(name: &str) -> Option<&'static str> {
let lower = name.trim().to_ascii_lowercase();
if lower == "hevc" {
return Some("h265");
}
SUPPORTED_CODECS.iter().copied().find(|&c| c == lower)
}
#[cfg(test)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PublisherState {
Idle,
Connecting,
Publishing,
Closed,
Failed,
}
#[cfg(test)]
impl PublisherState {
pub fn accepts_frames(self) -> bool {
matches!(self, PublisherState::Publishing)
}
pub fn can_connect(self) -> bool {
matches!(
self,
PublisherState::Idle | PublisherState::Closed | PublisherState::Failed
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg() -> PublishConfig {
PublishConfig {
url: "wss://sfu.example/rtc".into(),
token: "jwt".into(),
width: 1920,
height: 1080,
fps: 30,
codec: String::new(),
}
}
#[test]
fn valid_config_passes() {
assert!(cfg().validate().is_ok());
}
#[test]
fn rejects_bad_url() {
let mut c = cfg();
c.url = "https://sfu".into();
assert!(c.validate().is_err());
c.url = String::new();
assert!(c.validate().is_err());
}
#[test]
fn rejects_empty_token() {
let mut c = cfg();
c.token = " ".into();
assert!(c.validate().is_err());
}
#[test]
fn rejects_odd_or_oob_dimensions() {
let mut c = cfg();
c.width = 1921;
assert!(c.validate().is_err());
c.width = 1920;
c.height = 0;
assert!(c.validate().is_err());
c.height = 16384;
assert!(c.validate().is_err());
}
#[test]
fn accepts_dimension_boundaries_and_rejects_zero_fps() {
let mut c = cfg();
c.width = 2;
c.height = 2;
c.fps = 1;
assert!(c.validate().is_ok());
c.width = 8192;
c.height = 8192;
assert!(c.validate().is_ok());
c.fps = 0;
assert_eq!(
c.validate(),
Err("capture fps must be positive".to_string())
);
}
#[test]
fn rejects_unknown_non_empty_codec_but_allows_empty_default() {
let mut c = cfg();
c.codec = String::new();
assert!(c.validate().is_ok());
c.codec = " ".into();
assert!(c.validate().is_ok());
c.codec = "h266".into();
assert_eq!(c.validate(), Err("unsupported video codec".to_string()));
}
#[test]
fn canonical_codec_name_accepts_all_five_case_insensitively() {
for (input, expected) in [
("vp8", "vp8"),
(" vp8 ", "vp8"),
("VP8", "vp8"),
("h264", "h264"),
("H264", "h264"),
("vp9", "vp9"),
("Vp9", "vp9"),
("av1", "av1"),
("AV1", "av1"),
("h265", "h265"),
("H265", "h265"),
("hevc", "h265"),
("HEVC", "h265"),
] {
assert_eq!(canonical_codec_name(input), Some(expected), "codec {input}");
}
assert_eq!(SUPPORTED_CODECS.len(), 5);
}
#[test]
fn canonical_codec_name_rejects_empty_and_unknown() {
assert_eq!(canonical_codec_name(""), None);
assert_eq!(canonical_codec_name("h266"), None);
assert_eq!(canonical_codec_name("rubbish"), None);
}
#[test]
fn state_gates_frames_and_connect() {
assert!(PublisherState::Publishing.accepts_frames());
assert!(!PublisherState::Connecting.accepts_frames());
assert!(!PublisherState::Idle.accepts_frames());
assert!(PublisherState::Idle.can_connect());
assert!(PublisherState::Closed.can_connect());
assert!(PublisherState::Failed.can_connect());
assert!(!PublisherState::Failed.accepts_frames());
assert!(!PublisherState::Publishing.can_connect());
assert!(!PublisherState::Connecting.can_connect());
}
}
@@ -0,0 +1,181 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::audio::{
DEEP_FILTER_NOISE_REDUCTION_LEVEL_MAX, DEEP_FILTER_NOISE_REDUCTION_LEVEL_MIN,
clamp_deep_filter_noise_reduction_level,
};
use df::tract::{DfParams, DfTract, RuntimeParams};
use ndarray::Array2;
pub const DEEP_FILTER_SAMPLE_RATE_HZ: u32 = 48_000;
pub const DEEP_FILTER_NUM_CHANNELS: u32 = 1;
pub const DEEP_FILTER_FRAME_SAMPLES: usize = 480;
const SAMPLE_SCALE_I16_TO_F32: f32 = 1.0 / 32_768.0;
const SAMPLE_SCALE_F32_TO_I16: f32 = 32_767.0;
const _: () = assert!(DEEP_FILTER_FRAME_SAMPLES == DEEP_FILTER_SAMPLE_RATE_HZ as usize / 100);
const _: () = assert!(DEEP_FILTER_NUM_CHANNELS == 1);
pub struct DeepFilterProcessor {
model: DfTract,
input: Array2<f32>,
output: Array2<f32>,
}
impl DeepFilterProcessor {
pub fn new(noise_reduction_level: f64) -> Result<DeepFilterProcessor, String> {
let level_db = clamp_deep_filter_noise_reduction_level(noise_reduction_level);
assert!(level_db >= DEEP_FILTER_NOISE_REDUCTION_LEVEL_MIN);
assert!(level_db <= DEEP_FILTER_NOISE_REDUCTION_LEVEL_MAX);
let params = RuntimeParams::default_with_ch(DEEP_FILTER_NUM_CHANNELS as usize)
.with_atten_lim(level_db as f32);
let model = DfTract::new(DfParams::default(), &params)
.map_err(|error| format!("deep filter model init: {error:#}"))?;
if model.sr != DEEP_FILTER_SAMPLE_RATE_HZ as usize {
return Err(format!(
"deep filter model sample rate {} != {DEEP_FILTER_SAMPLE_RATE_HZ}",
model.sr
));
}
if model.ch != DEEP_FILTER_NUM_CHANNELS as usize {
return Err(format!(
"deep filter model channels {} != {DEEP_FILTER_NUM_CHANNELS}",
model.ch
));
}
if model.hop_size != DEEP_FILTER_FRAME_SAMPLES {
return Err(format!(
"deep filter model hop {} != {DEEP_FILTER_FRAME_SAMPLES}",
model.hop_size
));
}
Ok(DeepFilterProcessor {
model,
input: Array2::zeros((1, DEEP_FILTER_FRAME_SAMPLES)),
output: Array2::zeros((1, DEEP_FILTER_FRAME_SAMPLES)),
})
}
pub fn process_frame(&mut self, samples: &mut [i16]) -> Result<(), String> {
assert_eq!(samples.len(), DEEP_FILTER_FRAME_SAMPLES);
assert_eq!(self.input.len(), DEEP_FILTER_FRAME_SAMPLES);
assert_eq!(self.output.len(), DEEP_FILTER_FRAME_SAMPLES);
for (target, sample) in self.input.iter_mut().zip(samples.iter()) {
*target = f32::from(*sample) * SAMPLE_SCALE_I16_TO_F32;
}
self.model
.process(self.input.view(), self.output.view_mut())
.map_err(|error| format!("deep filter process: {error:#}"))?;
for (sample, enhanced) in samples.iter_mut().zip(self.output.iter()) {
*sample = sample_f32_to_i16(*enhanced);
}
Ok(())
}
}
fn sample_f32_to_i16(sample: f32) -> i16 {
if !sample.is_finite() {
return 0;
}
let clamped = sample.clamp(-1.0, 1.0);
assert!(clamped >= -1.0);
assert!(clamped <= 1.0);
(clamped * SAMPLE_SCALE_F32_TO_I16) as i16
}
#[cfg(test)]
mod tests {
use super::*;
fn next_noise_sample(seed: &mut u32) -> i16 {
*seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
((*seed >> 16) as u16 as i16) / 4
}
fn noise_frame(seed: &mut u32) -> [i16; DEEP_FILTER_FRAME_SAMPLES] {
let mut frame = [0i16; DEEP_FILTER_FRAME_SAMPLES];
for sample in frame.iter_mut() {
*sample = next_noise_sample(seed);
}
frame
}
fn frame_rms(samples: &[i16]) -> f64 {
assert!(!samples.is_empty());
let sum_squares: f64 = samples
.iter()
.map(|sample| {
let normalized = f64::from(*sample) / 32_768.0;
normalized * normalized
})
.sum();
(sum_squares / samples.len() as f64).sqrt()
}
#[test]
fn sample_conversion_holds_the_contract_range() {
assert_eq!(sample_f32_to_i16(0.0), 0);
assert_eq!(sample_f32_to_i16(1.0), 32_767);
assert_eq!(sample_f32_to_i16(-1.0), -32_767);
assert_eq!(sample_f32_to_i16(2.0), 32_767);
assert_eq!(sample_f32_to_i16(-2.0), -32_767);
assert_eq!(sample_f32_to_i16(0.5), 16_383);
}
#[test]
fn sample_conversion_maps_non_finite_to_silence() {
assert_eq!(sample_f32_to_i16(f32::NAN), 0);
assert_eq!(sample_f32_to_i16(f32::INFINITY), 0);
assert_eq!(sample_f32_to_i16(f32::NEG_INFINITY), 0);
}
#[test]
fn zero_level_passes_audio_through() {
let mut processor = DeepFilterProcessor::new(0.0).expect("embedded model must initialize");
let mut seed = 0x2545_f491u32;
for _ in 0..5 {
let original = noise_frame(&mut seed);
let mut processed = original;
processor
.process_frame(&mut processed)
.expect("processing must succeed");
for (output, input) in processed.iter().zip(original.iter()) {
assert!((i32::from(*output) - i32::from(*input)).abs() <= 1);
}
}
}
#[test]
fn full_level_attenuates_steady_noise() {
let mut processor = DeepFilterProcessor::new(DEEP_FILTER_NOISE_REDUCTION_LEVEL_MAX)
.expect("embedded model must initialize");
let mut seed = 0x9e37_79b9u32;
let mut input_rms = 0.0;
let mut output_rms = 0.0;
for frame_index in 0..30 {
let mut frame = noise_frame(&mut seed);
let frame_input_rms = frame_rms(&frame);
processor
.process_frame(&mut frame)
.expect("processing must succeed");
if frame_index >= 20 {
input_rms += frame_input_rms;
output_rms += frame_rms(&frame);
}
}
assert!(input_rms > 0.0);
assert!(output_rms < input_rms * 0.5);
}
#[test]
fn frame_length_contract_is_enforced() {
let mut processor = DeepFilterProcessor::new(DEEP_FILTER_NOISE_REDUCTION_LEVEL_MAX)
.expect("embedded model must initialize");
let mut short_frame = [0i16; DEEP_FILTER_FRAME_SAMPLES - 1];
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = processor.process_frame(&mut short_frame);
}));
assert!(result.is_err());
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,643 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pub fn push_json_string(out: &mut String, value: &str) {
out.push('"');
for ch in value.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{08}' => out.push_str("\\b"),
'\u{0C}' => out.push_str("\\f"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
}
pub enum JsonValue {
Str(String),
Raw(String),
}
pub fn json_object(fields: &[(&str, JsonValue)]) -> String {
let mut out = String::from("{");
for (i, (key, value)) in fields.iter().enumerate() {
if i > 0 {
out.push(',');
}
push_json_string(&mut out, key);
out.push(':');
match value {
JsonValue::Str(s) => push_json_string(&mut out, s),
JsonValue::Raw(r) => out.push_str(r),
}
}
out.push('}');
out
}
pub fn json_string_array(items: &[String]) -> String {
let mut out = String::from("[");
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
push_json_string(&mut out, item);
}
out.push(']');
out
}
pub fn json_u8_array(items: &[u8]) -> String {
let mut out = String::from("[");
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&item.to_string());
}
out.push(']');
out
}
pub fn json_raw_array(items: &[String]) -> String {
let mut out = String::from("[");
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(item);
}
out.push(']');
out
}
pub fn json_string_map(items: &std::collections::HashMap<String, String>) -> String {
let mut entries: Vec<(&String, &String)> = items.iter().collect();
entries.sort_by(|left, right| left.0.cmp(right.0));
let mut out = String::from("{");
for (i, (key, value)) in entries.iter().enumerate() {
if i > 0 {
out.push(',');
}
push_json_string(&mut out, key);
out.push(':');
push_json_string(&mut out, value);
}
out.push('}');
out
}
#[cfg(feature = "publisher")]
mod live {
use super::{
JsonValue, json_object, json_raw_array, json_string_array, json_string_map, json_u8_array,
};
use livekit::participant::{
ConnectionQuality, LocalParticipant, Participant, RemoteParticipant,
};
use livekit::publication::{
LocalTrackPublication, RemoteTrackPublication, SubscriptionStatus, TrackPublication,
};
use livekit::track::{TrackKind, TrackSource};
use livekit::{DataPacketKind, RoomEvent};
pub fn track_kind_str(kind: TrackKind) -> &'static str {
match kind {
TrackKind::Audio => "audio",
TrackKind::Video => "video",
}
}
pub fn track_source_str(source: TrackSource) -> &'static str {
match source {
TrackSource::Unknown => "unknown",
TrackSource::Camera => "camera",
TrackSource::Microphone => "microphone",
TrackSource::Screenshare => "screen_share",
TrackSource::ScreenshareAudio => "screen_share_audio",
}
}
pub fn connection_quality_str(quality: ConnectionQuality) -> &'static str {
match quality {
ConnectionQuality::Excellent => "excellent",
ConnectionQuality::Good => "good",
ConnectionQuality::Poor => "poor",
ConnectionQuality::Lost => "lost",
}
}
fn s(value: impl Into<String>) -> JsonValue {
JsonValue::Str(value.into())
}
fn b(value: bool) -> JsonValue {
JsonValue::Raw(value.to_string())
}
fn subscription_status_str(status: SubscriptionStatus) -> &'static str {
match status {
SubscriptionStatus::Desired => "desired",
SubscriptionStatus::Subscribed => "subscribed",
SubscriptionStatus::Unsubscribed => "unsubscribed",
}
}
fn participant_snapshot(participant: &Participant) -> String {
json_object(&[
("sid", s(participant.sid().to_string())),
("identity", s(participant.identity().to_string())),
("name", s(participant.name())),
])
}
fn push_remote_participant_fields(
fields: &mut Vec<(&'static str, JsonValue)>,
participant: &RemoteParticipant,
) {
fields.push(("participantSid", s(participant.sid().to_string())));
fields.push(("identity", s(participant.identity().to_string())));
fields.push(("participantName", s(participant.name())));
}
fn push_local_participant_fields(
fields: &mut Vec<(&'static str, JsonValue)>,
participant: &LocalParticipant,
) {
fields.push(("participantSid", s(participant.sid().to_string())));
fields.push(("identity", s(participant.identity().to_string())));
fields.push(("participantName", s(participant.name())));
}
fn push_participant_fields(
fields: &mut Vec<(&'static str, JsonValue)>,
participant: &Participant,
) {
fields.push(("participantSid", s(participant.sid().to_string())));
fields.push(("identity", s(participant.identity().to_string())));
fields.push(("participantName", s(participant.name())));
}
fn push_remote_publication_fields(
fields: &mut Vec<(&'static str, JsonValue)>,
publication: &RemoteTrackPublication,
) {
fields.push(("trackSid", s(publication.sid().to_string())));
fields.push(("trackName", s(publication.name())));
fields.push(("kind", s(track_kind_str(publication.kind()))));
fields.push(("source", s(track_source_str(publication.source()))));
fields.push(("muted", b(publication.is_muted())));
fields.push(("subscribed", b(publication.is_subscribed())));
fields.push((
"subscriptionStatus",
s(subscription_status_str(publication.subscription_status())),
));
}
fn push_local_publication_fields(
fields: &mut Vec<(&'static str, JsonValue)>,
publication: &LocalTrackPublication,
) {
fields.push(("trackSid", s(publication.sid().to_string())));
fields.push(("trackName", s(publication.name())));
fields.push(("kind", s(track_kind_str(publication.kind()))));
fields.push(("source", s(track_source_str(publication.source()))));
fields.push(("muted", b(publication.is_muted())));
}
fn push_publication_fields(
fields: &mut Vec<(&'static str, JsonValue)>,
publication: &TrackPublication,
) {
fields.push(("trackSid", s(publication.sid().to_string())));
fields.push(("trackName", s(publication.name())));
fields.push(("kind", s(track_kind_str(publication.kind()))));
fields.push(("source", s(track_source_str(publication.source()))));
fields.push(("muted", b(publication.is_muted())));
}
fn remote_track_payload(
participant: &RemoteParticipant,
publication: &RemoteTrackPublication,
) -> String {
let mut fields = Vec::new();
push_remote_participant_fields(&mut fields, participant);
push_remote_publication_fields(&mut fields, publication);
json_object(&fields)
}
const CONNECTED_ROSTER_PARTICIPANTS_MAX: usize = 1024;
const CONNECTED_ROSTER_TRACKS_PER_PARTICIPANT_MAX: usize = 16;
fn connected_payload(
participants_with_tracks: &[(RemoteParticipant, Vec<RemoteTrackPublication>)],
) -> String {
let participant_count = participants_with_tracks
.len()
.min(CONNECTED_ROSTER_PARTICIPANTS_MAX);
let mut entries = Vec::with_capacity(participant_count);
for (participant, publications) in participants_with_tracks
.iter()
.take(CONNECTED_ROSTER_PARTICIPANTS_MAX)
{
let track_count = publications
.len()
.min(CONNECTED_ROSTER_TRACKS_PER_PARTICIPANT_MAX);
let mut tracks = Vec::with_capacity(track_count);
for publication in publications
.iter()
.take(CONNECTED_ROSTER_TRACKS_PER_PARTICIPANT_MAX)
{
tracks.push(remote_track_payload(participant, publication));
}
entries.push(json_object(&[
("sid", s(participant.sid().to_string())),
("identity", s(participant.identity().to_string())),
("name", s(participant.name())),
("tracks", JsonValue::Raw(json_raw_array(&tracks))),
]));
}
assert!(entries.len() <= CONNECTED_ROSTER_PARTICIPANTS_MAX);
json_object(&[("participants", JsonValue::Raw(json_raw_array(&entries)))])
}
fn local_track_payload(
participant: &LocalParticipant,
publication: &LocalTrackPublication,
) -> String {
let mut fields = Vec::new();
push_local_participant_fields(&mut fields, participant);
push_local_publication_fields(&mut fields, publication);
json_object(&fields)
}
pub fn map_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
map_participant_lifecycle_room_event(event)
.or_else(|| map_participant_profile_room_event(event))
.or_else(|| map_track_room_event(event))
.or_else(|| map_local_track_room_event(event))
.or_else(|| map_connection_room_event(event))
}
fn map_participant_lifecycle_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
match event {
RoomEvent::ParticipantConnected(p) | RoomEvent::ParticipantActive(p) => Some((
"participantJoined",
json_object(&[
("sid", s(p.sid().to_string())),
("identity", s(p.identity().to_string())),
("name", s(p.name())),
]),
)),
RoomEvent::ParticipantDisconnected(p) => Some((
"participantLeft",
json_object(&[
("sid", s(p.sid().to_string())),
("identity", s(p.identity().to_string())),
("name", s(p.name())),
]),
)),
RoomEvent::ParticipantNameChanged {
participant,
old_name,
name,
} => Some((
"participantNameChanged",
json_object(&[
("sid", s(participant.sid().to_string())),
("identity", s(participant.identity().to_string())),
("oldName", s(old_name.to_string())),
("name", s(name.to_string())),
]),
)),
_ => None,
}
}
fn map_participant_profile_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
match event {
RoomEvent::ParticipantMetadataChanged {
participant,
old_metadata,
metadata,
} => Some((
"participantMetadataChanged",
json_object(&[
("sid", s(participant.sid().to_string())),
("identity", s(participant.identity().to_string())),
("name", s(participant.name())),
("oldMetadata", s(old_metadata.to_string())),
("metadata", s(metadata.to_string())),
(
"attributes",
JsonValue::Raw(json_string_map(&participant.attributes())),
),
]),
)),
RoomEvent::ParticipantAttributesChanged {
participant,
changed_attributes,
} => Some((
"participantAttributesChanged",
json_object(&[
("sid", s(participant.sid().to_string())),
("identity", s(participant.identity().to_string())),
("name", s(participant.name())),
(
"attributes",
JsonValue::Raw(json_string_map(&participant.attributes())),
),
(
"changedAttributes",
JsonValue::Raw(json_string_map(changed_attributes)),
),
]),
)),
_ => None,
}
}
fn map_track_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
match event {
RoomEvent::TrackSubscribed {
publication,
participant,
..
} => Some((
"trackSubscribed",
remote_track_payload(participant, publication),
)),
RoomEvent::TrackUnsubscribed {
publication,
participant,
..
} => Some((
"trackUnsubscribed",
remote_track_payload(participant, publication),
)),
RoomEvent::TrackSubscriptionFailed {
participant,
error,
track_sid,
} => {
let mut fields = Vec::new();
push_remote_participant_fields(&mut fields, participant);
if let Some(publication) = participant.get_track_publication(track_sid) {
push_remote_publication_fields(&mut fields, &publication);
} else {
fields.push(("trackSid", s(track_sid.to_string())));
}
fields.push(("error", s(format!("{error}"))));
Some(("trackSubscriptionFailed", json_object(&fields)))
}
RoomEvent::TrackPublished {
publication,
participant,
} => Some((
"trackPublished",
remote_track_payload(participant, publication),
)),
RoomEvent::TrackUnpublished {
publication,
participant,
} => Some((
"trackUnpublished",
remote_track_payload(participant, publication),
)),
RoomEvent::TrackMuted {
participant,
publication,
} => {
let mut fields = Vec::new();
push_participant_fields(&mut fields, participant);
push_publication_fields(&mut fields, publication);
Some(("trackMuted", json_object(&fields)))
}
RoomEvent::TrackUnmuted {
participant,
publication,
} => {
let mut fields = Vec::new();
push_participant_fields(&mut fields, participant);
push_publication_fields(&mut fields, publication);
Some(("trackUnmuted", json_object(&fields)))
}
_ => None,
}
}
fn map_connection_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
match event {
RoomEvent::ActiveSpeakersChanged { speakers } => {
let sids: Vec<String> = speakers.iter().map(|p| p.sid().to_string()).collect();
let participants: Vec<String> = speakers.iter().map(participant_snapshot).collect();
Some((
"activeSpeakers",
json_object(&[
("sids", JsonValue::Raw(json_string_array(&sids))),
(
"participants",
JsonValue::Raw(json_raw_array(&participants)),
),
]),
))
}
RoomEvent::ConnectionQualityChanged {
quality,
participant,
} => Some((
"connectionQuality",
json_object(&[
("sid", s(participant.sid().to_string())),
("identity", s(participant.identity().to_string())),
("name", s(participant.name())),
("quality", s(connection_quality_str(*quality))),
]),
)),
RoomEvent::DataReceived {
payload,
topic,
kind,
participant,
} => Some((
"dataReceived",
data_received_payload(
payload.as_ref().as_slice(),
topic.as_deref(),
kind,
participant.as_ref(),
),
)),
RoomEvent::E2eeStateChanged { participant, state } => Some((
"e2eeState",
json_object(&[
("sid", s(participant.sid().to_string())),
("identity", s(participant.identity().to_string())),
("name", s(participant.name())),
("state", s(format!("{state:?}").to_lowercase())),
]),
)),
RoomEvent::ConnectionStateChanged(state) => Some((
"connectionState",
json_object(&[("state", s(format!("{state:?}").to_lowercase()))]),
)),
RoomEvent::Disconnected { reason } => Some((
"disconnected",
json_object(&[("reason", s(format!("{reason:?}").to_lowercase()))]),
)),
RoomEvent::Connected {
participants_with_tracks,
} => Some(("connected", connected_payload(participants_with_tracks))),
_ => None,
}
}
fn data_received_payload(
payload: &[u8],
topic: Option<&str>,
kind: &DataPacketKind,
participant: Option<&RemoteParticipant>,
) -> String {
let mut fields = vec![
("payloadBytes", JsonValue::Raw(json_u8_array(payload))),
(
"reliable",
JsonValue::Raw(matches!(kind, DataPacketKind::Reliable).to_string()),
),
(
"kind",
s(match kind {
DataPacketKind::Reliable => "reliable",
DataPacketKind::Lossy => "lossy",
}),
),
];
if let Some(topic) = topic {
fields.push(("topic", s(topic.to_string())));
}
if let Ok(payload_text) = std::str::from_utf8(payload) {
fields.push(("payloadText", s(payload_text.to_string())));
}
if let Some(participant) = participant {
push_remote_participant_fields(&mut fields, participant);
}
json_object(&fields)
}
fn map_local_track_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
match event {
RoomEvent::LocalTrackPublished {
publication,
participant,
..
} => Some((
"localTrackPublished",
local_track_payload(participant, publication),
)),
RoomEvent::LocalTrackUnpublished {
publication,
participant,
} => Some((
"localTrackUnpublished",
local_track_payload(participant, publication),
)),
RoomEvent::LocalTrackRepublished {
previous_sid,
publication,
participant,
..
} => {
let mut fields = Vec::new();
push_local_participant_fields(&mut fields, participant);
fields.push(("previousTrackSid", s(previous_sid.to_string())));
push_local_publication_fields(&mut fields, publication);
Some(("localTrackRepublished", json_object(&fields)))
}
_ => None,
}
}
}
#[cfg(feature = "publisher")]
pub use live::{map_room_event, track_kind_str, track_source_str};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escapes_quote_backslash_and_control_chars() {
let mut out = String::new();
push_json_string(&mut out, "a\"b\\c\nd\te");
assert_eq!(out, "\"a\\\"b\\\\c\\nd\\te\"");
let mut ctrl = String::new();
push_json_string(&mut ctrl, "\u{01}");
assert_eq!(ctrl, "\"\\u0001\"");
}
#[test]
fn leaves_plain_ascii_and_unicode_untouched() {
let mut out = String::new();
push_json_string(&mut out, "PA_abc123");
assert_eq!(out, "\"PA_abc123\"");
}
#[test]
fn json_object_preserves_order_and_mixes_str_and_raw() {
let json = json_object(&[
("sid", JsonValue::Str("PA_1".into())),
(
"sids",
JsonValue::Raw(json_string_array(&["PA_1".into(), "PA_2".into()])),
),
]);
assert_eq!(json, "{\"sid\":\"PA_1\",\"sids\":[\"PA_1\",\"PA_2\"]}");
}
#[cfg(feature = "publisher")]
#[test]
fn track_source_strings_match_livekit_js_sources() {
use livekit::track::TrackSource;
assert_eq!(track_source_str(TrackSource::Camera), "camera");
assert_eq!(track_source_str(TrackSource::Microphone), "microphone");
assert_eq!(track_source_str(TrackSource::Screenshare), "screen_share");
assert_eq!(
track_source_str(TrackSource::ScreenshareAudio),
"screen_share_audio"
);
}
#[test]
fn empty_object_and_array() {
assert_eq!(json_object(&[]), "{}");
assert_eq!(json_string_array(&[]), "[]");
assert_eq!(json_u8_array(&[]), "[]");
assert_eq!(json_raw_array(&[]), "[]");
}
#[test]
fn json_u8_array_serializes_bytes_as_numbers() {
assert_eq!(json_u8_array(&[0, 1, 127, 255]), "[0,1,127,255]");
}
#[test]
fn raw_array_preserves_prebuilt_json_objects() {
let items = vec![
json_object(&[("sid", JsonValue::Str("PA_1".into()))]),
json_object(&[("sid", JsonValue::Str("PA_2".into()))]),
];
assert_eq!(
json_raw_array(&items),
"[{\"sid\":\"PA_1\"},{\"sid\":\"PA_2\"}]"
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,415 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use futures_core::Stream;
use napi::tokio;
use napi::tokio::task::AbortHandle;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::future::poll_fn;
use std::pin::pin;
pub const INBOUND_FORWARDERS_MAX: usize = 512;
struct ForwarderEntry {
participant_sid: String,
handle: AbortHandle,
}
pub struct InboundForwarderRegistry {
entries: Mutex<HashMap<String, ForwarderEntry>>,
}
impl InboundForwarderRegistry {
pub fn new() -> Self {
Self {
entries: Mutex::new(HashMap::new()),
}
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.entries.lock().len()
}
#[cfg(test)]
pub fn contains(&self, track_sid: &str) -> bool {
self.entries.lock().contains_key(track_sid)
}
pub fn register(&self, track_sid: &str, participant_sid: &str, handle: AbortHandle) -> bool {
if track_sid.is_empty() {
handle.abort();
return false;
}
let mut entries = self.entries.lock();
if let Some(previous) = entries.remove(track_sid) {
previous.handle.abort();
}
if entries.len() >= INBOUND_FORWARDERS_MAX {
drop(entries);
handle.abort();
eprintln!(
"webrtc-sender: inbound forwarder registry at cap {INBOUND_FORWARDERS_MAX}; \
refusing forwarder for track {track_sid}"
);
return false;
}
entries.insert(
track_sid.to_string(),
ForwarderEntry {
participant_sid: participant_sid.to_string(),
handle,
},
);
assert!(entries.len() <= INBOUND_FORWARDERS_MAX);
true
}
pub fn cancel(&self, track_sid: &str) {
let removed = self.entries.lock().remove(track_sid);
if let Some(entry) = removed {
entry.handle.abort();
}
}
pub fn cancel_for_participant(&self, participant_sid: &str) {
let aborted: Vec<ForwarderEntry> = {
let mut entries = self.entries.lock();
let matching: Vec<String> = entries
.iter()
.filter(|(_, entry)| entry.participant_sid == participant_sid)
.map(|(track_sid, _)| track_sid.clone())
.collect();
matching
.into_iter()
.filter_map(|track_sid| entries.remove(&track_sid))
.collect()
};
for entry in aborted {
entry.handle.abort();
}
}
pub fn clear(&self) {
let drained: Vec<ForwarderEntry> = {
let mut entries = self.entries.lock();
entries.drain().map(|(_, entry)| entry).collect()
};
for entry in drained {
entry.handle.abort();
}
}
}
impl Default for InboundForwarderRegistry {
fn default() -> Self {
Self::new()
}
}
pub fn spawn_drain_forwarder<S, F>(stream: S, mut on_item: F) -> AbortHandle
where
S: Stream + Send + 'static,
S::Item: Send,
F: FnMut(S::Item) + Send + 'static,
{
let task = tokio::spawn(async move {
let mut stream = pin!(stream);
loop {
let item = poll_fn(|cx| stream.as_mut().poll_next(cx)).await;
match item {
Some(item) => on_item(item),
None => return,
}
}
});
task.abort_handle()
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::task::{Context, Poll};
use tokio::runtime::Builder;
use tokio::sync::Notify;
struct CloseSignal {
closed: AtomicBool,
notify: Notify,
}
impl CloseSignal {
fn new() -> Arc<Self> {
Arc::new(Self {
closed: AtomicBool::new(false),
notify: Notify::new(),
})
}
fn is_closed(&self) -> bool {
self.closed.load(Ordering::SeqCst)
}
fn fire(&self) {
self.closed.store(true, Ordering::SeqCst);
self.notify.notify_one();
}
async fn wait(&self) {
if self.is_closed() {
return;
}
self.notify.notified().await;
assert!(self.is_closed());
}
}
struct FakeVideoStream {
signal: Arc<CloseSignal>,
}
impl Stream for FakeVideoStream {
type Item = u64;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Pending
}
}
impl Drop for FakeVideoStream {
fn drop(&mut self) {
self.signal.fire();
}
}
struct CountedStream {
items: VecDeque<u64>,
count: Arc<AtomicUsize>,
done: Arc<Notify>,
}
impl Stream for CountedStream {
type Item = u64;
fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match self.items.pop_front() {
Some(item) => {
self.count.fetch_add(1, Ordering::SeqCst);
Poll::Ready(Some(item))
}
None => {
self.done.notify_one();
Poll::Ready(None)
}
}
}
}
fn runtime() -> tokio::runtime::Runtime {
Builder::new_multi_thread()
.worker_threads(1)
.build()
.expect("tokio runtime")
}
fn dispatch_track_subscribed(
registry: &InboundForwarderRegistry,
track_sid: &str,
participant_sid: &str,
) -> Arc<CloseSignal> {
let (signal, registered) = dispatch_with_outcome(registry, track_sid, participant_sid);
assert!(registered);
signal
}
fn dispatch_with_outcome(
registry: &InboundForwarderRegistry,
track_sid: &str,
participant_sid: &str,
) -> (Arc<CloseSignal>, bool) {
let signal = CloseSignal::new();
let stream = FakeVideoStream {
signal: signal.clone(),
};
let handle = spawn_drain_forwarder(stream, |_frame| {});
let registered = registry.register(track_sid, participant_sid, handle);
(signal, registered)
}
#[test]
fn teardown_symmetry_closes_the_stream() {
let runtime = runtime();
runtime.block_on(async {
let registry = InboundForwarderRegistry::new();
let signal = dispatch_track_subscribed(&registry, "TR_x", "PA_one");
assert_eq!(registry.len(), 1);
assert!(!signal.is_closed());
registry.cancel("TR_x");
assert_eq!(registry.len(), 0);
signal.wait().await;
assert!(signal.is_closed());
});
}
#[test]
fn repeated_subscribe_unsubscribe_never_accumulates() {
let runtime = runtime();
runtime.block_on(async {
let registry = InboundForwarderRegistry::new();
let cycles = 32usize;
let mut signals = Vec::with_capacity(cycles);
for _ in 0..cycles {
let signal = dispatch_track_subscribed(&registry, "TR_x", "PA_one");
assert_eq!(registry.len(), 1);
registry.cancel("TR_x");
assert_eq!(registry.len(), 0);
signal.wait().await;
signals.push(signal);
}
assert_eq!(registry.len(), 0);
let closed_count = signals.iter().filter(|signal| signal.is_closed()).count();
assert_eq!(closed_count, cycles);
});
}
#[test]
fn double_subscribe_keeps_one_live_forwarder() {
let runtime = runtime();
runtime.block_on(async {
let registry = InboundForwarderRegistry::new();
let first_signal = dispatch_track_subscribed(&registry, "TR_x", "PA_one");
let second_signal = dispatch_track_subscribed(&registry, "TR_x", "PA_one");
assert_eq!(registry.len(), 1);
first_signal.wait().await;
assert!(first_signal.is_closed());
assert!(!second_signal.is_closed());
registry.cancel("TR_x");
assert_eq!(registry.len(), 0);
second_signal.wait().await;
assert!(second_signal.is_closed());
});
}
#[test]
fn participant_disconnect_tears_down_all_forwarders() {
let runtime = runtime();
runtime.block_on(async {
let registry = InboundForwarderRegistry::new();
let video_signal = dispatch_track_subscribed(&registry, "TR_video", "PA_one");
let audio_signal = dispatch_track_subscribed(&registry, "TR_audio", "PA_one");
let other_signal = dispatch_track_subscribed(&registry, "TR_other", "PA_two");
assert_eq!(registry.len(), 3);
registry.cancel_for_participant("PA_one");
assert_eq!(registry.len(), 1);
assert!(registry.contains("TR_other"));
video_signal.wait().await;
audio_signal.wait().await;
assert!(video_signal.is_closed());
assert!(audio_signal.is_closed());
assert!(!other_signal.is_closed());
registry.clear();
assert_eq!(registry.len(), 0);
other_signal.wait().await;
assert!(other_signal.is_closed());
});
}
#[test]
fn clear_tears_down_every_forwarder() {
let runtime = runtime();
runtime.block_on(async {
let registry = InboundForwarderRegistry::new();
let mut signals = Vec::new();
for index in 0..8 {
let track_sid = format!("TR_{index}");
signals.push(dispatch_track_subscribed(&registry, &track_sid, "PA_one"));
}
assert_eq!(registry.len(), 8);
registry.clear();
assert_eq!(registry.len(), 0);
for signal in &signals {
signal.wait().await;
}
assert!(signals.iter().all(|signal| signal.is_closed()));
});
}
#[test]
fn drain_forwarder_invokes_callback_per_item() {
let runtime = runtime();
runtime.block_on(async {
let count = Arc::new(AtomicUsize::new(0));
let polled = Arc::new(AtomicUsize::new(0));
let polled_in_task = polled.clone();
let done = Arc::new(Notify::new());
let stream = CountedStream {
items: VecDeque::from(vec![1u64, 2, 3, 4]),
count: count.clone(),
done: done.clone(),
};
let _handle = spawn_drain_forwarder(stream, move |_item| {
polled_in_task.fetch_add(1, Ordering::SeqCst);
});
done.notified().await;
assert_eq!(count.load(Ordering::SeqCst), 4);
assert_eq!(polled.load(Ordering::SeqCst), 4);
});
}
#[test]
fn register_refuses_and_closes_forwarder_at_cap() {
let runtime = runtime();
runtime.block_on(async {
let registry = InboundForwarderRegistry::new();
for index in 0..INBOUND_FORWARDERS_MAX {
let track_sid = format!("TR_{index}");
let (_signal, registered) = dispatch_with_outcome(&registry, &track_sid, "PA_one");
assert!(registered);
}
assert_eq!(registry.len(), INBOUND_FORWARDERS_MAX);
let (overflow_signal, registered) =
dispatch_with_outcome(&registry, "TR_overflow", "PA_one");
assert!(!registered);
assert_eq!(registry.len(), INBOUND_FORWARDERS_MAX);
assert!(!registry.contains("TR_overflow"));
overflow_signal.wait().await;
assert!(overflow_signal.is_closed());
registry.clear();
assert_eq!(registry.len(), 0);
});
}
#[test]
fn register_refuses_and_closes_forwarder_for_empty_sid() {
let runtime = runtime();
runtime.block_on(async {
let registry = InboundForwarderRegistry::new();
let (signal, registered) = dispatch_with_outcome(&registry, "", "PA_one");
assert!(!registered);
assert_eq!(registry.len(), 0);
signal.wait().await;
assert!(signal.is_closed());
});
}
}
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![allow(unsafe_op_in_unsafe_fn)]
#![cfg_attr(
not(all(feature = "publisher", feature = "camera-native")),
allow(dead_code)
)]
mod audio;
mod bridge_version;
mod camera;
mod camera_background;
mod config;
mod deep_filter;
mod events;
mod hardware_encoder;
mod inbound_forwarder;
mod mask_refine;
mod native_camera;
mod person_segmentation;
mod send_control;
mod speaking;
mod stats;
mod texture_source;
mod yuv;
#[cfg(feature = "publisher")]
mod engine;
#[cfg(feature = "bench-internals")]
pub mod bench_internals {
pub use crate::audio::DEEP_FILTER_NOISE_REDUCTION_LEVEL_MAX;
pub use crate::deep_filter::{DEEP_FILTER_FRAME_SAMPLES, DeepFilterProcessor};
pub use crate::mask_refine::MaskRefiner;
pub struct BlurScratch(crate::camera_background::BlurScratch);
impl BlurScratch {
pub fn new(width: usize, height: usize) -> Self {
Self(crate::camera_background::BlurScratch::new(width, height))
}
}
pub fn blur_plane_masked(
plane: &mut [u8],
width: usize,
height: usize,
mask: &[u8],
radius_pass: usize,
scratch: &mut BlurScratch,
) {
let mask = crate::camera_background::plane_mask(mask, width, 1);
crate::camera_background::blur_plane_masked(
plane,
width,
height,
mask,
radius_pass,
&mut scratch.0,
);
}
pub fn composite_masked_plane(
plane: &mut [u8],
background: &[u8],
width: usize,
height: usize,
mask: &[u8],
) {
let mask = crate::camera_background::plane_mask(mask, width, 1);
crate::camera_background::composite_masked_plane(plane, background, width, height, mask);
}
}
@@ -0,0 +1,626 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const FRAME_EDGE_MAX: usize = 8192;
const MASK_REFINE_DOWNSAMPLE: usize = 4;
const GUIDED_FILTER_RADIUS_LOW: usize = 4;
const GUIDED_FILTER_EPSILON: f32 = 1e-4;
const TEMPORAL_COMBINE_RATIO: f32 = 0.7;
const TEMPORAL_UNCERTAINTY_C1: f32 = 5.68842;
const TEMPORAL_UNCERTAINTY_C2: f32 = -0.748699;
const TEMPORAL_UNCERTAINTY_C3: f32 = -57.8051;
const TEMPORAL_UNCERTAINTY_C4: f32 = 291.309;
const TEMPORAL_UNCERTAINTY_C5: f32 = -624.717;
const SHAPE_SMOOTHSTEP_EDGE_LOW: f32 = 0.55;
const SHAPE_SMOOTHSTEP_EDGE_HIGH: f32 = 0.85;
const LUT_LEN: usize = 256;
pub struct MaskRefiner {
width: usize,
height: usize,
low_width: usize,
low_height: usize,
previous_mask: Vec<u8>,
previous_mask_valid: bool,
temporal_weight_lut: [u16; LUT_LEN],
shape_lut: [u8; LUT_LEN],
column_fixed: Vec<u32>,
guide_low: Vec<f32>,
mask_low: Vec<f32>,
mean_guide: Vec<f32>,
mean_mask: Vec<f32>,
corr_guide_guide: Vec<f32>,
corr_guide_mask: Vec<f32>,
coeff_a: Vec<f32>,
coeff_b: Vec<f32>,
scratch: Vec<f32>,
}
impl MaskRefiner {
pub fn new(width: usize, height: usize) -> Self {
assert!(width >= 2);
assert!(height >= 2);
assert!(width <= FRAME_EDGE_MAX);
assert!(height <= FRAME_EDGE_MAX);
let low_width = (width / MASK_REFINE_DOWNSAMPLE).max(1);
let low_height = (height / MASK_REFINE_DOWNSAMPLE).max(1);
let low_len = low_width * low_height;
let mut column_fixed = vec![0u32; width];
for (x, slot) in column_fixed.iter_mut().enumerate() {
*slot = bilinear_fixed_coord(x, width, low_width);
}
Self {
width,
height,
low_width,
low_height,
previous_mask: vec![0; width * height],
previous_mask_valid: false,
temporal_weight_lut: temporal_weight_lut(),
shape_lut: shape_lut(),
column_fixed,
guide_low: vec![0.0; low_len],
mask_low: vec![0.0; low_len],
mean_guide: vec![0.0; low_len],
mean_mask: vec![0.0; low_len],
corr_guide_guide: vec![0.0; low_len],
corr_guide_mask: vec![0.0; low_len],
coeff_a: vec![0.0; low_len],
coeff_b: vec![0.0; low_len],
scratch: vec![0.0; low_len],
}
}
pub fn refine(&mut self, luma: &[u8], mask: &mut [u8]) {
assert!(luma.len() >= self.width * self.height);
assert!(mask.len() >= self.width * self.height);
self.blend_temporal(mask);
self.downsample(luma, mask);
self.close_mask_low();
self.solve_guided_coefficients();
self.apply_guided_coefficients(luma, mask);
}
fn blend_temporal(&mut self, mask: &mut [u8]) {
let len = self.width * self.height;
assert!(mask.len() >= len);
assert_eq!(self.previous_mask.len(), len);
if self.previous_mask_valid {
for (current, previous) in mask[..len].iter_mut().zip(self.previous_mask.iter()) {
let new_value = i32::from(*current);
let weight = i32::from(self.temporal_weight_lut[usize::from(*current)]);
let delta = (i32::from(*previous) - new_value) * weight;
*current = (new_value + ((delta + 128) >> 8)).clamp(0, 255) as u8;
}
}
self.previous_mask.copy_from_slice(&mask[..len]);
self.previous_mask_valid = true;
}
fn downsample(&mut self, luma: &[u8], mask: &[u8]) {
assert_eq!(self.guide_low.len(), self.low_width * self.low_height);
assert_eq!(self.mask_low.len(), self.guide_low.len());
for low_y in 0..self.low_height {
let y_start = low_y * MASK_REFINE_DOWNSAMPLE;
let y_end = (y_start + MASK_REFINE_DOWNSAMPLE).min(self.height);
for low_x in 0..self.low_width {
let x_start = low_x * MASK_REFINE_DOWNSAMPLE;
let x_end = (x_start + MASK_REFINE_DOWNSAMPLE).min(self.width);
let mut guide_sum: u32 = 0;
let mut mask_sum: u32 = 0;
for y in y_start..y_end {
let row = y * self.width;
for x in x_start..x_end {
guide_sum += u32::from(luma[row + x]);
mask_sum += u32::from(mask[row + x]);
}
}
let count = ((y_end - y_start) * (x_end - x_start)) as f32;
assert!(count >= 1.0);
let low_offset = low_y * self.low_width + low_x;
self.guide_low[low_offset] = guide_sum as f32 / (count * 255.0);
self.mask_low[low_offset] = mask_sum as f32 / (count * 255.0);
}
}
}
fn close_mask_low(&mut self) {
morph_pass_low(
&self.mask_low,
&mut self.scratch,
&mut self.coeff_a,
self.low_width,
self.low_height,
f32::max,
);
morph_pass_low(
&self.coeff_a,
&mut self.scratch,
&mut self.mask_low,
self.low_width,
self.low_height,
f32::min,
);
}
fn solve_guided_coefficients(&mut self) {
let len = self.low_width * self.low_height;
assert_eq!(self.coeff_a.len(), len);
assert_eq!(self.coeff_b.len(), len);
let radius = GUIDED_FILTER_RADIUS_LOW;
let w = self.low_width;
let h = self.low_height;
box_filter_low(
&self.guide_low,
&mut self.scratch,
&mut self.mean_guide,
w,
h,
radius,
);
box_filter_low(
&self.mask_low,
&mut self.scratch,
&mut self.mean_mask,
w,
h,
radius,
);
for i in 0..len {
self.coeff_a[i] = self.guide_low[i] * self.guide_low[i];
self.coeff_b[i] = self.guide_low[i] * self.mask_low[i];
}
box_filter_low(
&self.coeff_a,
&mut self.scratch,
&mut self.corr_guide_guide,
w,
h,
radius,
);
box_filter_low(
&self.coeff_b,
&mut self.scratch,
&mut self.corr_guide_mask,
w,
h,
radius,
);
for i in 0..len {
let variance = self.corr_guide_guide[i] - self.mean_guide[i] * self.mean_guide[i];
let covariance = self.corr_guide_mask[i] - self.mean_guide[i] * self.mean_mask[i];
let a = covariance / (variance.max(0.0) + GUIDED_FILTER_EPSILON);
self.coeff_a[i] = a;
self.coeff_b[i] = self.mean_mask[i] - a * self.mean_guide[i];
}
box_filter_low(
&self.coeff_a,
&mut self.scratch,
&mut self.mean_guide,
w,
h,
radius,
);
box_filter_low(
&self.coeff_b,
&mut self.scratch,
&mut self.mean_mask,
w,
h,
radius,
);
}
fn apply_guided_coefficients(&self, luma: &[u8], mask: &mut [u8]) {
assert!(luma.len() >= self.width * self.height);
assert!(mask.len() >= self.width * self.height);
let low_w = self.low_width;
for y in 0..self.height {
let row_fixed = bilinear_fixed_coord(y, self.height, self.low_height);
let sy = (row_fixed / 256) as usize;
let fy = (row_fixed % 256) as f32 / 256.0;
let sy_next = (sy + 1).min(self.low_height - 1);
let row = y * self.width;
for x in 0..self.width {
let col_fixed = self.column_fixed[x];
let sx = (col_fixed / 256) as usize;
let fx = (col_fixed % 256) as f32 / 256.0;
let sx_next = (sx + 1).min(low_w - 1);
let a = bilinear_sample(&self.mean_guide, low_w, sx, sx_next, sy, sy_next, fx, fy);
let b = bilinear_sample(&self.mean_mask, low_w, sx, sx_next, sy, sy_next, fx, fy);
let q = a * (f32::from(luma[row + x]) / 255.0) + b;
let shaped = (q * 255.0 + 0.5).clamp(0.0, 255.0) as usize;
mask[row + x] = self.shape_lut[shaped.min(LUT_LEN - 1)];
}
}
}
}
pub(crate) fn bilinear_fixed_coord(index: usize, full_len: usize, low_len: usize) -> u32 {
assert!(full_len >= 1);
assert!(low_len >= 1);
if full_len == 1 {
return 0;
}
(index * (low_len - 1) * 256 / (full_len - 1)) as u32
}
#[expect(clippy::too_many_arguments)]
pub(crate) fn bilinear_sample(
plane: &[f32],
width: usize,
sx: usize,
sx_next: usize,
sy: usize,
sy_next: usize,
fx: f32,
fy: f32,
) -> f32 {
assert!(sy * width + sx_next < plane.len());
assert!(sy_next * width + sx_next < plane.len());
let top = plane[sy * width + sx] * (1.0 - fx) + plane[sy * width + sx_next] * fx;
let bottom = plane[sy_next * width + sx] * (1.0 - fx) + plane[sy_next * width + sx_next] * fx;
top * (1.0 - fy) + bottom * fy
}
fn temporal_uncertainty(probability: f32) -> f32 {
assert!(probability >= 0.0);
assert!(probability <= 1.0);
let x = (probability - 0.5) * (probability - 0.5);
let polynomial = x
* (TEMPORAL_UNCERTAINTY_C1
+ x * (TEMPORAL_UNCERTAINTY_C2
+ x * (TEMPORAL_UNCERTAINTY_C3
+ x * (TEMPORAL_UNCERTAINTY_C4 + x * TEMPORAL_UNCERTAINTY_C5))));
1.0 - polynomial.min(1.0)
}
fn temporal_weight_lut() -> [u16; LUT_LEN] {
let mut lut = [0u16; LUT_LEN];
for (value, slot) in lut.iter_mut().enumerate() {
let probability = value as f32 / 255.0;
let weight = temporal_uncertainty(probability) * TEMPORAL_COMBINE_RATIO;
assert!(weight >= 0.0);
assert!(weight <= 1.0);
*slot = (weight * 256.0 + 0.5) as u16;
}
lut
}
fn shape_lut() -> [u8; LUT_LEN] {
let span = SHAPE_SMOOTHSTEP_EDGE_HIGH - SHAPE_SMOOTHSTEP_EDGE_LOW;
assert!(span > 0.0);
let mut lut = [0u8; LUT_LEN];
for (value, slot) in lut.iter_mut().enumerate() {
let probability = value as f32 / 255.0;
let t = ((probability - SHAPE_SMOOTHSTEP_EDGE_LOW) / span).clamp(0.0, 1.0);
let smooth = t * t * (3.0 - 2.0 * t);
*slot = (smooth * 255.0 + 0.5) as u8;
}
assert_eq!(lut[0], 0);
assert_eq!(lut[LUT_LEN - 1], 255);
lut
}
pub(crate) fn box_filter_low(
src: &[f32],
scratch: &mut [f32],
dst: &mut [f32],
width: usize,
height: usize,
radius: usize,
) {
assert!(width >= 1);
assert!(height >= 1);
assert!(src.len() >= width * height);
assert!(scratch.len() >= width * height);
assert!(dst.len() >= width * height);
box_filter_rows_low(src, scratch, width, height, radius);
box_filter_columns_low(scratch, dst, width, height, radius);
}
fn box_filter_rows_low(src: &[f32], dst: &mut [f32], width: usize, height: usize, radius: usize) {
assert!(width >= 1);
assert!(src.len() >= width * height);
for y in 0..height {
let row = y * width;
let mut start = 0usize;
let mut end = radius.min(width - 1);
let mut sum: f32 = src[row..=row + end].iter().sum();
for x in 0..width {
dst[row + x] = sum / ((end - start + 1) as f32);
let next_end = (x + 1 + radius).min(width - 1);
if next_end > end {
sum += src[row + next_end];
end = next_end;
}
let next_start = (x + 1).saturating_sub(radius);
if next_start > start {
sum -= src[row + start];
start = next_start;
}
}
}
}
fn box_filter_columns_low(
src: &[f32],
dst: &mut [f32],
width: usize,
height: usize,
radius: usize,
) {
assert!(width >= 1);
assert!(width <= FRAME_EDGE_MAX);
assert!(height >= 1);
let mut sums = [0.0f32; FRAME_EDGE_MAX];
let mut start = 0usize;
let mut end = radius.min(height - 1);
for y in 0..=end {
let row = y * width;
for x in 0..width {
sums[x] += src[row + x];
}
}
for y in 0..height {
let scale = 1.0 / ((end - start + 1) as f32);
let row = y * width;
for x in 0..width {
dst[row + x] = sums[x] * scale;
}
let next_end = (y + 1 + radius).min(height - 1);
if next_end > end {
let next_row = next_end * width;
for x in 0..width {
sums[x] += src[next_row + x];
}
end = next_end;
}
let next_start = (y + 1).saturating_sub(radius);
if next_start > start {
let previous_row = start * width;
for x in 0..width {
sums[x] -= src[previous_row + x];
}
start = next_start;
}
}
}
fn morph_pass_low(
src: &[f32],
scratch: &mut [f32],
dst: &mut [f32],
width: usize,
height: usize,
select: fn(f32, f32) -> f32,
) {
assert!(width >= 1);
assert!(height >= 1);
assert!(src.len() >= width * height);
assert!(scratch.len() >= width * height);
assert!(dst.len() >= width * height);
for y in 0..height {
let row = y * width;
for x in 0..width {
let left = src[row + x.saturating_sub(1)];
let right = src[row + (x + 1).min(width - 1)];
scratch[row + x] = select(select(left, src[row + x]), right);
}
}
for y in 0..height {
let above = y.saturating_sub(1) * width;
let below = (y + 1).min(height - 1) * width;
let row = y * width;
for x in 0..width {
dst[row + x] = select(
select(scratch[above + x], scratch[row + x]),
scratch[below + x],
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn gradient_luma(width: usize, height: usize) -> Vec<u8> {
let mut luma = vec![0u8; width * height];
for (index, value) in luma.iter_mut().enumerate() {
*value = ((index % width) * 255 / (width - 1).max(1)) as u8;
}
luma
}
fn left_half_mask(width: usize, height: usize) -> Vec<u8> {
let mut mask = vec![0u8; width * height];
for y in 0..height {
for x in 0..width / 2 {
mask[y * width + x] = 255;
}
}
mask
}
#[test]
fn temporal_weight_lut_smooths_uncertain_values_and_trusts_confident_ones() {
let lut = temporal_weight_lut();
assert!(lut[128] >= 170);
assert!(lut[128] <= 182);
assert!(lut[0] <= 8);
assert!(lut[255] <= 8);
assert!(lut[64] > lut[16]);
}
#[test]
fn temporal_blend_pulls_uncertain_pixels_toward_previous_mask() {
let mut refiner = MaskRefiner::new(8, 8);
let mut first = vec![128u8; 64];
refiner.blend_temporal(&mut first);
let mut second = vec![128u8; 64];
second[0] = 255;
second[1] = 130;
refiner.blend_temporal(&mut second);
assert_eq!(second[0], 255);
assert!(second[1] < 130);
assert_eq!(second[63], 128);
}
#[test]
fn temporal_blend_first_frame_passes_mask_through_unchanged() {
let mut refiner = MaskRefiner::new(8, 8);
let mut mask = vec![37u8; 64];
refiner.blend_temporal(&mut mask);
assert!(mask.iter().all(|value| *value == 37));
assert!(refiner.previous_mask_valid);
}
#[test]
fn shape_lut_is_monotonic_and_saturates_at_both_ends() {
let lut = shape_lut();
for value in 1..LUT_LEN {
assert!(lut[value] >= lut[value - 1]);
}
assert_eq!(lut[(255.0 * SHAPE_SMOOTHSTEP_EDGE_LOW) as usize - 4], 0);
assert_eq!(lut[(255.0 * SHAPE_SMOOTHSTEP_EDGE_HIGH) as usize + 4], 255);
}
#[test]
fn refine_keeps_solid_person_and_background_regions_saturated() {
let width = 64usize;
let height = 48usize;
let mut refiner = MaskRefiner::new(width, height);
let luma = {
let mut luma = vec![32u8; width * height];
for y in 0..height {
for x in width / 2..width {
luma[y * width + x] = 224;
}
}
luma
};
let mut mask = left_half_mask(width, height);
refiner.refine(&luma, &mut mask);
assert_eq!(mask[24 * width], 255);
assert_eq!(mask[24 * width + 4], 255);
assert_eq!(mask[24 * width + width - 1], 0);
assert_eq!(mask[24 * width + width - 5], 0);
}
#[test]
fn refine_snaps_mask_transition_to_the_luma_edge() {
let width = 64usize;
let height = 48usize;
let mut refiner = MaskRefiner::new(width, height);
let mut luma = vec![16u8; width * height];
for y in 0..height {
for x in 0..width / 2 {
luma[y * width + x] = 240;
}
}
let mut blurry_mask = vec![0u8; width * height];
for y in 0..height {
for x in 0..width {
let distance = (width as i32 / 2 - x as i32).clamp(-12, 12);
blurry_mask[y * width + x] = (127 + distance * 10).clamp(0, 255) as u8;
}
}
refiner.refine(&luma, &mut blurry_mask);
let row = 24 * width;
assert!(blurry_mask[row + width / 2 - 8] > 220);
assert!(blurry_mask[row + width / 2 + 8] < 35);
}
#[test]
fn refine_fills_small_holes_inside_the_person() {
let width = 64usize;
let height = 48usize;
let mut refiner = MaskRefiner::new(width, height);
let luma = vec![128u8; width * height];
let mut mask = vec![255u8; width * height];
mask[24 * width + 32] = 0;
refiner.refine(&luma, &mut mask);
assert!(mask[24 * width + 32] > 200);
assert_eq!(mask[0], 255);
}
#[test]
fn refine_handles_minimum_dimensions_without_panicking() {
let mut refiner = MaskRefiner::new(2, 2);
let luma = vec![128u8; 4];
let mut mask = vec![255u8; 4];
refiner.refine(&luma, &mut mask);
assert_eq!(mask.len(), 4);
assert!(mask.iter().all(|value| *value == 255));
}
#[test]
fn box_filter_low_preserves_constant_planes_exactly() {
let width = 9usize;
let height = 7usize;
let src = vec![0.625f32; width * height];
let mut scratch = vec![0.0f32; width * height];
let mut dst = vec![0.0f32; width * height];
box_filter_low(&src, &mut scratch, &mut dst, width, height, 4);
for value in dst {
assert!((value - 0.625).abs() < 1e-6);
}
}
#[test]
fn morph_close_removes_single_pixel_pits_and_keeps_plateaus() {
let width = 8usize;
let height = 8usize;
let mut src = vec![1.0f32; width * height];
src[3 * width + 3] = 0.0;
let mut scratch = vec![0.0f32; width * height];
let mut maxed = vec![0.0f32; width * height];
let mut closed = vec![0.0f32; width * height];
morph_pass_low(&src, &mut scratch, &mut maxed, width, height, f32::max);
morph_pass_low(&maxed, &mut scratch, &mut closed, width, height, f32::min);
assert!(closed[3 * width + 3] > 0.99);
assert!(closed[0] > 0.99);
}
#[test]
fn refine_converges_to_stable_mask_over_repeated_identical_frames() {
let width = 32usize;
let height = 24usize;
let mut refiner = MaskRefiner::new(width, height);
let luma = gradient_luma(width, height);
let raw = left_half_mask(width, height);
let mut previous_output = vec![0u8; width * height];
for iteration in 0..8 {
let mut mask = raw.clone();
refiner.refine(&luma, &mut mask);
if iteration == 7 {
let drift: i32 = mask
.iter()
.zip(previous_output.iter())
.map(|(a, b)| (i32::from(*a) - i32::from(*b)).abs())
.sum();
assert!(drift <= (width * height) as i32);
}
previous_output.copy_from_slice(&mask);
}
assert_eq!(previous_output[12 * width], 255);
}
}
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#![allow(dead_code)]
#[cfg(feature = "publisher")]
use livekit::webrtc::video_frame::{VideoBuffer, VideoFrame, VideoRotation, native::NativeBuffer};
#[cfg(feature = "publisher")]
use livekit::webrtc::video_source::native::NativeVideoSource;
use napi_derive::napi;
pub const NATIVE_CAMERA_FRAME_QUEUE_CAPACITY: usize = 3;
const MIN_NATIVE_CAMERA_EDGE: u32 = 2;
const MAX_NATIVE_CAMERA_EDGE: u32 = 8192;
const TRANSPORT_CV_PIXEL_BUFFER: &str = "cvPixelBuffer";
const TRANSPORT_D3D11_TEXTURE: &str = "d3d11Texture";
const TRANSPORT_DMABUF: &str = "dmabuf";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NativeCameraTransport {
CvPixelBuffer,
D3d11Texture,
Dmabuf,
}
impl NativeCameraTransport {
pub fn as_str(self) -> &'static str {
match self {
Self::CvPixelBuffer => TRANSPORT_CV_PIXEL_BUFFER,
Self::D3d11Texture => TRANSPORT_D3D11_TEXTURE,
Self::Dmabuf => TRANSPORT_DMABUF,
}
}
}
pub fn required_transports() -> &'static [NativeCameraTransport] {
const TRANSPORTS: &[NativeCameraTransport] = &[
NativeCameraTransport::CvPixelBuffer,
NativeCameraTransport::D3d11Texture,
NativeCameraTransport::Dmabuf,
];
assert_eq!(TRANSPORTS.len(), 3);
TRANSPORTS
}
pub fn required_transport_names() -> [&'static str; 3] {
let transports = required_transports();
[
transports[0].as_str(),
transports[1].as_str(),
transports[2].as_str(),
]
}
pub fn platform_native_backgrounds_available() -> bool {
platform_unavailable_reason().is_none()
}
pub fn camera_backgrounds_available() -> bool {
cfg!(feature = "camera-native")
}
#[napi]
pub fn has_native_camera_backgrounds() -> bool {
camera_backgrounds_available()
}
pub fn platform_unavailable_reason() -> Option<&'static str> {
Some(match std::env::consts::OS {
"macos" => "macOS AVFoundation CVPixelBuffer camera backend is not compiled",
"windows" => "Windows Media Foundation D3D11 camera backend is not compiled",
"linux" => "Linux PipeWire/V4L2 dmabuf camera backend is not compiled",
_ => "native platform-buffer camera backend is unsupported on this platform",
})
}
pub fn validate_native_frame_dimensions(width: u32, height: u32) -> bool {
if width < MIN_NATIVE_CAMERA_EDGE {
return false;
}
if height < MIN_NATIVE_CAMERA_EDGE {
return false;
}
if !width.is_multiple_of(2) {
return false;
}
if !height.is_multiple_of(2) {
return false;
}
width <= MAX_NATIVE_CAMERA_EDGE && height <= MAX_NATIVE_CAMERA_EDGE
}
pub fn unavailable_error() -> String {
let reason = platform_unavailable_reason().unwrap_or("native camera backend unavailable");
format!(
"native camera backgrounds require platform camera buffers ({}, {}, {}): {reason}",
TRANSPORT_CV_PIXEL_BUFFER, TRANSPORT_D3D11_TEXTURE, TRANSPORT_DMABUF
)
}
#[cfg(feature = "publisher")]
pub fn publish_native_buffer(
source: &NativeVideoSource,
buffer: NativeBuffer,
timestamp_us: i64,
) -> bool {
assert!(timestamp_us >= 0);
let width = buffer.width();
let height = buffer.height();
if !validate_native_frame_dimensions(width, height) {
return false;
}
source.capture_frame(&VideoFrame {
rotation: VideoRotation::VideoRotation0,
timestamp_us,
frame_metadata: None,
buffer,
});
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn required_transports_are_stable() {
assert_eq!(
required_transport_names(),
["cvPixelBuffer", "d3d11Texture", "dmabuf"]
);
}
#[test]
fn queue_capacity_stays_bounded_for_realtime_capture() {
assert_eq!(NATIVE_CAMERA_FRAME_QUEUE_CAPACITY, 3);
assert!(NATIVE_CAMERA_FRAME_QUEUE_CAPACITY < 8);
}
#[test]
fn camera_background_capability_tracks_native_camera_feature() {
assert_eq!(
camera_backgrounds_available(),
cfg!(feature = "camera-native")
);
}
#[test]
fn native_frame_dimensions_require_even_reasonable_sizes() {
assert!(validate_native_frame_dimensions(1280, 720));
assert!(!validate_native_frame_dimensions(0, 720));
assert!(!validate_native_frame_dimensions(1280, 1));
assert!(!validate_native_frame_dimensions(1279, 720));
assert!(!validate_native_frame_dimensions(1280, 721));
assert!(!validate_native_frame_dimensions(16_384, 720));
}
#[test]
fn unavailable_error_names_all_required_native_transports() {
let error = unavailable_error();
assert!(error.contains("cvPixelBuffer"));
assert!(error.contains("d3d11Texture"));
assert!(error.contains("dmabuf"));
}
}
@@ -0,0 +1,681 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pub const PERSON_MASK_BACKGROUND: u8 = 0;
pub const PERSON_MASK_PERSON: u8 = 255;
pub const SEGMENTATION_FRAME_BUDGET_MS: u64 = 12;
pub const SEGMENTATION_SLOW_FRAMES_CONSECUTIVE_MAX: u32 = 30;
pub trait PersonMaskSource {
fn mask_into(&mut self, frame: &crate::yuv::I420, mask: &mut [u8]) -> bool;
}
#[derive(Debug, Default)]
pub struct SegmentationQualityGovernor {
consecutive_slow_frames: u32,
downgraded: bool,
}
impl SegmentationQualityGovernor {
pub fn new() -> Self {
Self::default()
}
pub fn record_frame_duration_ms(&mut self, duration_ms: u64) -> bool {
assert!(self.consecutive_slow_frames < SEGMENTATION_SLOW_FRAMES_CONSECUTIVE_MAX);
if self.downgraded {
return false;
}
if duration_ms <= SEGMENTATION_FRAME_BUDGET_MS {
self.consecutive_slow_frames = 0;
return false;
}
self.consecutive_slow_frames += 1;
assert!(self.consecutive_slow_frames <= SEGMENTATION_SLOW_FRAMES_CONSECUTIVE_MAX);
if self.consecutive_slow_frames < SEGMENTATION_SLOW_FRAMES_CONSECUTIVE_MAX {
return false;
}
self.consecutive_slow_frames = 0;
self.downgraded = true;
true
}
}
pub fn create_person_mask_source(width: u32, height: u32) -> Option<Box<dyn PersonMaskSource>> {
assert!(width >= 2);
assert!(height >= 2);
#[cfg(target_os = "macos")]
{
if let Some(source) = vision::VisionPersonMaskSource::new(width, height) {
return Some(Box::new(source) as Box<dyn PersonMaskSource>);
}
}
selfie::SelfieMaskSource::new(width, height)
.map(|source| Box::new(source) as Box<dyn PersonMaskSource>)
}
pub fn resize_mask_bilinear(
src: &[u8],
src_width: usize,
src_height: usize,
src_stride: usize,
dst: &mut [u8],
dst_width: usize,
dst_height: usize,
) {
assert!(src_width >= 1);
assert!(src_height >= 1);
assert!(src_stride >= src_width);
assert!(dst_width >= 1);
assert!(dst_height >= 1);
assert!(src.len() >= src_stride * (src_height - 1) + src_width);
assert!(dst.len() >= dst_width * dst_height);
for y in 0..dst_height {
let sy_fixed = if dst_height == 1 {
0
} else {
y * (src_height - 1) * 256 / (dst_height - 1)
};
let sy = sy_fixed / 256;
let fy = (sy_fixed % 256) as u32;
let sy_next = (sy + 1).min(src_height - 1);
for x in 0..dst_width {
let sx_fixed = if dst_width == 1 {
0
} else {
x * (src_width - 1) * 256 / (dst_width - 1)
};
let sx = sx_fixed / 256;
let fx = (sx_fixed % 256) as u32;
let sx_next = (sx + 1).min(src_width - 1);
let top = u32::from(src[sy * src_stride + sx]) * (256 - fx)
+ u32::from(src[sy * src_stride + sx_next]) * fx;
let bottom = u32::from(src[sy_next * src_stride + sx]) * (256 - fx)
+ u32::from(src[sy_next * src_stride + sx_next]) * fx;
dst[y * dst_width + x] = ((top * (256 - fy) + bottom * fy) >> 16) as u8;
}
}
}
mod selfie {
use super::PersonMaskSource;
use std::sync::{Arc, OnceLock};
use tract_onnx::prelude::*;
const MODEL_BYTES: &[u8] = include_bytes!("../models/selfie_segmenter_landscape.onnx");
const MODEL_INPUT_WIDTH: usize = 256;
const MODEL_INPUT_HEIGHT: usize = 144;
const MODEL_INPUT_CHANNELS: usize = 3;
const MODEL_INPUT_LEN: usize = MODEL_INPUT_WIDTH * MODEL_INPUT_HEIGHT * MODEL_INPUT_CHANNELS;
const MODEL_CHROMA_WIDTH: usize = MODEL_INPUT_WIDTH / 2;
const MODEL_CHROMA_HEIGHT: usize = MODEL_INPUT_HEIGHT / 2;
const INFERENCE_FRAME_INTERVAL_FULL: u32 = 1;
const INFERENCE_FRAME_INTERVAL_DOWNGRADED: u32 = 2;
type SelfiePlan = TypedRunnableModel;
fn shared_plan() -> Option<Arc<SelfiePlan>> {
static PLAN: OnceLock<Option<Arc<SelfiePlan>>> = OnceLock::new();
PLAN.get_or_init(|| match load_plan() {
Ok(plan) => Some(plan),
Err(error) => {
eprintln!(
"webrtc-sender: selfie segmentation model failed to load; camera \
background effects fall back to the portrait ellipse: {error}"
);
None
}
})
.clone()
}
fn load_plan() -> TractResult<Arc<SelfiePlan>> {
let mut reader = std::io::Cursor::new(MODEL_BYTES);
tract_onnx::onnx()
.model_for_read(&mut reader)?
.with_input_fact(
0,
f32::fact([
1,
MODEL_INPUT_HEIGHT,
MODEL_INPUT_WIDTH,
MODEL_INPUT_CHANNELS,
])
.into(),
)?
.into_optimized()?
.into_runnable()
}
pub struct SelfieMaskSource {
width: u32,
height: u32,
plan: Arc<SelfiePlan>,
luma_low: Vec<u8>,
chroma_u_low: Vec<u8>,
chroma_v_low: Vec<u8>,
input_rgb: Vec<f32>,
raw_mask_low: Vec<u8>,
raw_mask_valid: bool,
frame_counter: u32,
inference_interval: u32,
inference_error_logged: bool,
governor: super::SegmentationQualityGovernor,
}
impl SelfieMaskSource {
pub fn new(width: u32, height: u32) -> Option<Self> {
assert!(width >= 2);
assert!(height >= 2);
let plan = shared_plan()?;
Some(Self {
width,
height,
plan,
luma_low: vec![0; MODEL_INPUT_WIDTH * MODEL_INPUT_HEIGHT],
chroma_u_low: vec![128; MODEL_CHROMA_WIDTH * MODEL_CHROMA_HEIGHT],
chroma_v_low: vec![128; MODEL_CHROMA_WIDTH * MODEL_CHROMA_HEIGHT],
input_rgb: vec![0.0; MODEL_INPUT_LEN],
raw_mask_low: vec![0; MODEL_INPUT_WIDTH * MODEL_INPUT_HEIGHT],
raw_mask_valid: false,
frame_counter: 0,
inference_interval: INFERENCE_FRAME_INTERVAL_FULL,
inference_error_logged: false,
governor: super::SegmentationQualityGovernor::new(),
})
}
fn fill_model_input(&mut self, frame: &crate::yuv::I420) {
assert_eq!(frame.width, self.width);
assert_eq!(frame.height, self.height);
let width = self.width as usize;
let height = self.height as usize;
super::resize_mask_bilinear(
&frame.y,
width,
height,
width,
&mut self.luma_low,
MODEL_INPUT_WIDTH,
MODEL_INPUT_HEIGHT,
);
super::resize_mask_bilinear(
&frame.u,
width / 2,
height / 2,
width / 2,
&mut self.chroma_u_low,
MODEL_CHROMA_WIDTH,
MODEL_CHROMA_HEIGHT,
);
super::resize_mask_bilinear(
&frame.v,
width / 2,
height / 2,
width / 2,
&mut self.chroma_v_low,
MODEL_CHROMA_WIDTH,
MODEL_CHROMA_HEIGHT,
);
for y in 0..MODEL_INPUT_HEIGHT {
let row = y * MODEL_INPUT_WIDTH;
let chroma_row = (y / 2) * MODEL_CHROMA_WIDTH;
for x in 0..MODEL_INPUT_WIDTH {
let luma = i32::from(self.luma_low[row + x]) - 16;
let cb = i32::from(self.chroma_u_low[chroma_row + x / 2]) - 128;
let cr = i32::from(self.chroma_v_low[chroma_row + x / 2]) - 128;
let r = ((298 * luma + 409 * cr + 128) >> 8).clamp(0, 255);
let g = ((298 * luma - 100 * cb - 208 * cr + 128) >> 8).clamp(0, 255);
let b = ((298 * luma + 516 * cb + 128) >> 8).clamp(0, 255);
let offset = (row + x) * MODEL_INPUT_CHANNELS;
self.input_rgb[offset] = r as f32 / 255.0;
self.input_rgb[offset + 1] = g as f32 / 255.0;
self.input_rgb[offset + 2] = b as f32 / 255.0;
}
}
}
fn run_inference(&mut self, frame: &crate::yuv::I420) -> bool {
self.fill_model_input(frame);
let produced = self.run_model();
if !produced && !self.inference_error_logged {
self.inference_error_logged = true;
eprintln!(
"webrtc-sender: selfie segmentation inference failed; reusing the \
previous person mask"
);
}
produced
}
fn run_model(&mut self) -> bool {
assert_eq!(self.input_rgb.len(), MODEL_INPUT_LEN);
let Ok(tensor) = Tensor::from_shape(
&[
1,
MODEL_INPUT_HEIGHT,
MODEL_INPUT_WIDTH,
MODEL_INPUT_CHANNELS,
],
&self.input_rgb,
) else {
return false;
};
let Ok(result) = self.plan.run(tvec!(tensor.into())) else {
return false;
};
let Some(output) = result.first() else {
return false;
};
let Ok(alphas) = output.to_plain_array_view::<f32>() else {
return false;
};
if alphas.len() != self.raw_mask_low.len() {
return false;
}
for (slot, alpha) in self.raw_mask_low.iter_mut().zip(alphas.iter()) {
*slot = (alpha * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
}
true
}
fn record_inference_duration(&mut self, duration_ms: u64) {
if self.governor.record_frame_duration_ms(duration_ms) {
self.inference_interval = INFERENCE_FRAME_INTERVAL_DOWNGRADED;
eprintln!(
"webrtc-sender: selfie segmentation exceeded the {}ms frame budget for {} \
consecutive frames; downgrading to inference every {} frames",
super::SEGMENTATION_FRAME_BUDGET_MS,
super::SEGMENTATION_SLOW_FRAMES_CONSECUTIVE_MAX,
INFERENCE_FRAME_INTERVAL_DOWNGRADED
);
}
}
}
impl PersonMaskSource for SelfieMaskSource {
fn mask_into(&mut self, frame: &crate::yuv::I420, mask: &mut [u8]) -> bool {
let width = self.width as usize;
let height = self.height as usize;
assert!(mask.len() >= width * height);
if frame.width != self.width || frame.height != self.height {
return false;
}
assert!(self.inference_interval >= 1);
let due = self.frame_counter.is_multiple_of(self.inference_interval);
self.frame_counter = self.frame_counter.wrapping_add(1);
if due || !self.raw_mask_valid {
let started = std::time::Instant::now();
if self.run_inference(frame) {
self.raw_mask_valid = true;
let duration_ms = started.elapsed().as_millis() as u64;
self.record_inference_duration(duration_ms);
}
}
if !self.raw_mask_valid {
return false;
}
super::resize_mask_bilinear(
&self.raw_mask_low,
MODEL_INPUT_WIDTH,
MODEL_INPUT_HEIGHT,
MODEL_INPUT_WIDTH,
mask,
width,
height,
);
true
}
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_frame(width: u32, height: u32) -> crate::yuv::I420 {
let mut frame = crate::yuv::I420::new(width, height).unwrap();
for (index, value) in frame.y.iter_mut().enumerate() {
*value = ((index * 31 + 17) % 220) as u8 + 16;
}
frame.u.fill(128);
frame.v.fill(128);
frame
}
#[test]
fn selfie_source_produces_full_range_mask_for_synthetic_frames() {
let mut source = SelfieMaskSource::new(128, 96).expect("bundled model loads");
let frame = synthetic_frame(128, 96);
let mut mask = vec![0u8; 128 * 96];
assert!(source.mask_into(&frame, &mut mask));
assert_eq!(mask.len(), 128 * 96);
assert!(source.raw_mask_valid);
}
#[test]
fn selfie_source_rejects_mismatched_frame_dimensions() {
let mut source = SelfieMaskSource::new(128, 96).expect("bundled model loads");
let frame = synthetic_frame(64, 48);
let mut mask = vec![0u8; 128 * 96];
assert!(!source.mask_into(&frame, &mut mask));
}
#[test]
fn selfie_source_reuses_cached_mask_between_inference_frames() {
let mut source = SelfieMaskSource::new(64, 48).expect("bundled model loads");
source.inference_interval = INFERENCE_FRAME_INTERVAL_DOWNGRADED;
let frame = synthetic_frame(64, 48);
let mut first = vec![0u8; 64 * 48];
let mut second = vec![0u8; 64 * 48];
assert!(source.mask_into(&frame, &mut first));
assert!(source.mask_into(&frame, &mut second));
assert_eq!(first, second);
}
}
}
#[cfg(target_os = "macos")]
mod vision {
use super::PersonMaskSource;
use core::ptr::NonNull;
use objc2::rc::Retained;
use objc2_core_foundation::CFRetained;
use objc2_core_video::{
CVPixelBuffer, CVPixelBufferGetBaseAddress, CVPixelBufferGetBaseAddressOfPlane,
CVPixelBufferGetBytesPerRow, CVPixelBufferGetBytesPerRowOfPlane, CVPixelBufferGetHeight,
CVPixelBufferGetPixelFormatType, CVPixelBufferGetWidth, CVPixelBufferLockBaseAddress,
CVPixelBufferLockFlags, CVPixelBufferUnlockBaseAddress,
};
use objc2_vision::{
VNGeneratePersonSegmentationRequest, VNGeneratePersonSegmentationRequestQualityLevel,
VNRequest, VNSequenceRequestHandler,
};
const PIXEL_FORMAT_NV12_FULL_RANGE: u32 = u32::from_be_bytes(*b"420f");
const PIXEL_FORMAT_ONE_COMPONENT_8: u32 = u32::from_be_bytes(*b"L008");
const MASK_PIXELS_MAX: usize = 8192 * 8192;
pub struct VisionPersonMaskSource {
width: u32,
height: u32,
pixel_buffer: CFRetained<CVPixelBuffer>,
request: Retained<VNGeneratePersonSegmentationRequest>,
requests: Retained<objc2_foundation::NSArray<VNRequest>>,
handler: Retained<VNSequenceRequestHandler>,
quality_governor: super::SegmentationQualityGovernor,
}
impl VisionPersonMaskSource {
pub fn new(width: u32, height: u32) -> Option<Self> {
assert!(width >= 2);
assert!(height >= 2);
assert!(width.is_multiple_of(2));
assert!(height.is_multiple_of(2));
let mut pixel_buffer_out: *mut CVPixelBuffer = core::ptr::null_mut();
let status = unsafe {
objc2_core_video::CVPixelBufferCreate(
None,
width as usize,
height as usize,
PIXEL_FORMAT_NV12_FULL_RANGE,
None,
NonNull::new(&mut pixel_buffer_out)?,
)
};
if status != 0 {
return None;
}
let pixel_buffer = unsafe { CFRetained::from_raw(NonNull::new(pixel_buffer_out)?) };
let request = unsafe { VNGeneratePersonSegmentationRequest::new() };
unsafe {
request.setQualityLevel(VNGeneratePersonSegmentationRequestQualityLevel::Balanced);
request.setOutputPixelFormat(PIXEL_FORMAT_ONE_COMPONENT_8);
}
let request_as_base: Retained<VNRequest> =
Retained::into_super(Retained::into_super(Retained::into_super(request.clone())));
let requests = objc2_foundation::NSArray::from_retained_slice(&[request_as_base]);
assert_eq!(requests.len(), 1);
let handler = unsafe { VNSequenceRequestHandler::new() };
Some(Self {
width,
height,
pixel_buffer,
request,
requests,
handler,
quality_governor: super::SegmentationQualityGovernor::new(),
})
}
fn downgrade_to_fast_quality(&self) {
unsafe {
self.request
.setQualityLevel(VNGeneratePersonSegmentationRequestQualityLevel::Fast);
}
eprintln!(
"webrtc-sender: person segmentation exceeded the {}ms frame budget for {} \
consecutive frames; downgrading Vision quality from balanced to fast",
super::SEGMENTATION_FRAME_BUDGET_MS,
super::SEGMENTATION_SLOW_FRAMES_CONSECUTIVE_MAX
);
}
fn fill_pixel_buffer(&self, frame: &crate::yuv::I420) -> bool {
assert_eq!(frame.width, self.width);
assert_eq!(frame.height, self.height);
let width = self.width as usize;
let height = self.height as usize;
let lock_flags = CVPixelBufferLockFlags(0);
let lock_status =
unsafe { CVPixelBufferLockBaseAddress(&self.pixel_buffer, lock_flags) };
if lock_status != 0 {
return false;
}
let y_base = CVPixelBufferGetBaseAddressOfPlane(&self.pixel_buffer, 0);
let y_stride = CVPixelBufferGetBytesPerRowOfPlane(&self.pixel_buffer, 0);
let uv_base = CVPixelBufferGetBaseAddressOfPlane(&self.pixel_buffer, 1);
let uv_stride = CVPixelBufferGetBytesPerRowOfPlane(&self.pixel_buffer, 1);
if y_base.is_null() || uv_base.is_null() || y_stride < width || uv_stride < width {
let _ = unsafe { CVPixelBufferUnlockBaseAddress(&self.pixel_buffer, lock_flags) };
return false;
}
let chroma_width = width / 2;
let chroma_height = height / 2;
unsafe {
let y_base = y_base as *mut u8;
for row in 0..height {
let src = &frame.y[row * width..row * width + width];
core::ptr::copy_nonoverlapping(src.as_ptr(), y_base.add(row * y_stride), width);
}
let uv_base = uv_base as *mut u8;
for row in 0..chroma_height {
let dst_row = uv_base.add(row * uv_stride);
for col in 0..chroma_width {
let chroma_index = row * chroma_width + col;
dst_row.add(col * 2).write(frame.u[chroma_index]);
dst_row.add(col * 2 + 1).write(frame.v[chroma_index]);
}
}
}
let unlock_status =
unsafe { CVPixelBufferUnlockBaseAddress(&self.pixel_buffer, lock_flags) };
unlock_status == 0
}
fn copy_observation_mask(
mask_buffer: &CVPixelBuffer,
mask: &mut [u8],
width: usize,
height: usize,
) -> bool {
if CVPixelBufferGetPixelFormatType(mask_buffer) != PIXEL_FORMAT_ONE_COMPONENT_8 {
return false;
}
let lock_flags = CVPixelBufferLockFlags::ReadOnly;
if unsafe { CVPixelBufferLockBaseAddress(mask_buffer, lock_flags) } != 0 {
return false;
}
let src_width = CVPixelBufferGetWidth(mask_buffer);
let src_height = CVPixelBufferGetHeight(mask_buffer);
let src_stride = CVPixelBufferGetBytesPerRow(mask_buffer);
let base = CVPixelBufferGetBaseAddress(mask_buffer);
let valid = !base.is_null()
&& src_width >= 1
&& src_height >= 1
&& src_stride >= src_width
&& src_width * src_height <= MASK_PIXELS_MAX;
if valid {
let src = unsafe {
core::slice::from_raw_parts(
base as *const u8,
src_stride * (src_height - 1) + src_width,
)
};
super::resize_mask_bilinear(
src, src_width, src_height, src_stride, mask, width, height,
);
}
let _ = unsafe { CVPixelBufferUnlockBaseAddress(mask_buffer, lock_flags) };
valid
}
}
impl PersonMaskSource for VisionPersonMaskSource {
fn mask_into(&mut self, frame: &crate::yuv::I420, mask: &mut [u8]) -> bool {
let started = std::time::Instant::now();
let produced = self.mask_into_timed(frame, mask);
let duration_ms = started.elapsed().as_millis() as u64;
if self.quality_governor.record_frame_duration_ms(duration_ms) {
self.downgrade_to_fast_quality();
}
produced
}
}
impl VisionPersonMaskSource {
fn mask_into_timed(&mut self, frame: &crate::yuv::I420, mask: &mut [u8]) -> bool {
let width = self.width as usize;
let height = self.height as usize;
assert!(mask.len() >= width * height);
if frame.width != self.width || frame.height != self.height {
return false;
}
if !self.fill_pixel_buffer(frame) {
return false;
}
let performed = unsafe {
self.handler
.performRequests_onCVPixelBuffer_error(&self.requests, &self.pixel_buffer)
};
if performed.is_err() {
return false;
}
let Some(results) = (unsafe { self.request.results() }) else {
return false;
};
let Some(observation) = results.firstObject() else {
return false;
};
let mask_buffer = unsafe { observation.pixelBuffer() };
Self::copy_observation_mask(&mask_buffer, mask, width, height)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resize_identity_returns_same_values() {
let src = vec![0u8, 64, 128, 255];
let mut dst = vec![0u8; 4];
resize_mask_bilinear(&src, 2, 2, 2, &mut dst, 2, 2);
assert_eq!(dst, src);
}
#[test]
fn resize_upscales_with_interpolated_midpoints() {
let src = vec![0u8, 255, 0, 255];
let mut dst = vec![0u8; 9];
resize_mask_bilinear(&src, 2, 2, 2, &mut dst, 3, 3);
assert_eq!(dst[0], 0);
assert_eq!(dst[2], 255);
assert!(dst[1] > 100);
assert!(dst[1] < 156);
}
#[test]
fn resize_honours_source_stride_padding() {
let src = vec![10u8, 20, 99, 99, 30, 40, 99, 99];
let mut dst = vec![0u8; 4];
resize_mask_bilinear(&src, 2, 2, 4, &mut dst, 2, 2);
assert_eq!(dst, vec![10, 20, 30, 40]);
}
#[test]
fn resize_collapses_to_single_pixel_average_free() {
let src = vec![200u8; 16];
let mut dst = vec![0u8; 1];
resize_mask_bilinear(&src, 4, 4, 4, &mut dst, 1, 1);
assert_eq!(dst, vec![200]);
}
#[test]
fn mask_constants_span_full_alpha_range() {
assert_eq!(PERSON_MASK_BACKGROUND, 0);
assert_eq!(PERSON_MASK_PERSON, 255);
}
#[test]
fn segmentation_governor_downgrades_after_consecutive_slow_frames() {
let mut governor = SegmentationQualityGovernor::new();
for _ in 1..SEGMENTATION_SLOW_FRAMES_CONSECUTIVE_MAX {
assert!(!governor.record_frame_duration_ms(SEGMENTATION_FRAME_BUDGET_MS + 1));
}
assert!(governor.record_frame_duration_ms(SEGMENTATION_FRAME_BUDGET_MS + 1));
}
#[test]
fn segmentation_governor_resets_count_after_a_frame_within_budget() {
let mut governor = SegmentationQualityGovernor::new();
for _ in 1..SEGMENTATION_SLOW_FRAMES_CONSECUTIVE_MAX {
assert!(!governor.record_frame_duration_ms(SEGMENTATION_FRAME_BUDGET_MS + 1));
}
assert!(!governor.record_frame_duration_ms(SEGMENTATION_FRAME_BUDGET_MS));
for _ in 1..SEGMENTATION_SLOW_FRAMES_CONSECUTIVE_MAX {
assert!(!governor.record_frame_duration_ms(SEGMENTATION_FRAME_BUDGET_MS + 1));
}
assert!(governor.record_frame_duration_ms(SEGMENTATION_FRAME_BUDGET_MS + 1));
}
#[test]
fn segmentation_governor_downgrades_only_once() {
let mut governor = SegmentationQualityGovernor::new();
for _ in 0..SEGMENTATION_SLOW_FRAMES_CONSECUTIVE_MAX - 1 {
assert!(!governor.record_frame_duration_ms(SEGMENTATION_FRAME_BUDGET_MS + 1));
}
assert!(governor.record_frame_duration_ms(SEGMENTATION_FRAME_BUDGET_MS + 1));
for _ in 0..SEGMENTATION_SLOW_FRAMES_CONSECUTIVE_MAX * 2 {
assert!(!governor.record_frame_duration_ms(SEGMENTATION_FRAME_BUDGET_MS + 100));
}
}
}
@@ -0,0 +1,974 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use parking_lot::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
pub const DEFAULT_AUDIO_BUFFER_TARGET_MS: u32 = 200;
pub const DEFAULT_AUDIO_BUFFER_MAX_MS: u32 = 750;
pub const DEFAULT_MIN_VIDEO_FPS: f64 = 15.0;
const PRESSURE_WINDOW_MS: u64 = 5_000;
const RECOVERY_WINDOW_COUNT: u32 = 12;
const AUDIO_REBUFFER_GAP_MS: u64 = 120;
const AUDIO_STABLE_GAP_MS: u64 = 60;
const AUDIO_BUFFER_STEP_MS: u32 = 100;
#[derive(Clone, Debug, PartialEq)]
pub struct SendHealthSnapshot {
pub outgoing_video_queue_depth: u64,
pub outgoing_video_queue_capacity: u64,
pub outgoing_video_max_queue_depth: u64,
pub outgoing_video_frames_produced: u64,
pub outgoing_video_frames_accepted: u64,
pub outgoing_video_frames_dropped: u64,
pub outgoing_video_frames_coalesced: u64,
pub outgoing_video_frames_captured: u64,
pub outgoing_video_capture_failures: u64,
pub outgoing_video_effective_fps: f64,
pub outgoing_video_target_fps: f64,
pub outgoing_video_pacing_target_fps: f64,
pub outgoing_video_max_queue_age_ms: u64,
pub outgoing_video_max_push_latency_ms: u64,
pub outgoing_video_pacing_mode: String,
pub outgoing_video_bus_active: bool,
pub outgoing_audio_buffer_target_ms: u32,
pub outgoing_audio_buffer_max_ms: u32,
pub outgoing_audio_underruns: u64,
pub outgoing_audio_rebuffers: u64,
pub outgoing_audio_max_frame_gap_ms: u64,
pub adaptive_send_tier: String,
pub adaptive_send_reason: String,
}
impl SendHealthSnapshot {
pub fn idle(audio: &AdaptiveAudioStats) -> Self {
Self {
outgoing_video_queue_depth: 0,
outgoing_video_queue_capacity: 0,
outgoing_video_max_queue_depth: 0,
outgoing_video_frames_produced: 0,
outgoing_video_frames_accepted: 0,
outgoing_video_frames_dropped: 0,
outgoing_video_frames_coalesced: 0,
outgoing_video_frames_captured: 0,
outgoing_video_capture_failures: 0,
outgoing_video_effective_fps: 0.0,
outgoing_video_target_fps: 0.0,
outgoing_video_pacing_target_fps: 0.0,
outgoing_video_max_queue_age_ms: 0,
outgoing_video_max_push_latency_ms: 0,
outgoing_video_pacing_mode: "idle".to_string(),
outgoing_video_bus_active: false,
outgoing_audio_buffer_target_ms: audio.target_buffer_ms(),
outgoing_audio_buffer_max_ms: audio.max_buffer_ms(),
outgoing_audio_underruns: audio.underruns.load(Ordering::Relaxed),
outgoing_audio_rebuffers: audio.rebuffers.load(Ordering::Relaxed),
outgoing_audio_max_frame_gap_ms: audio.max_frame_gap_ms.load(Ordering::Relaxed),
adaptive_send_tier: "idle".to_string(),
adaptive_send_reason: "notPublishing".to_string(),
}
}
}
pub struct AdaptiveVideoController {
requested_fps: f64,
min_fps: f64,
adaptive: bool,
state: Mutex<AdaptiveVideoState>,
}
struct AdaptiveVideoState {
current_fps: f64,
tier: String,
reason: String,
window_started_ms: u64,
window_produced: u64,
window_coalesced: u64,
window_dropped: u64,
window_max_queue_age_ms: u64,
window_max_push_latency_ms: u64,
window_egress_fps_sum: f64,
window_egress_fps_samples: u32,
stable_windows: u32,
}
impl AdaptiveVideoController {
pub fn new(requested_fps: f64, min_fps: f64, adaptive: bool, now_ms: u64) -> Self {
let requested_fps = sanitize_fps(requested_fps, 30.0);
let min_fps = sanitize_fps(min_fps, DEFAULT_MIN_VIDEO_FPS).min(requested_fps);
Self {
requested_fps,
min_fps,
adaptive,
state: Mutex::new(AdaptiveVideoState {
current_fps: requested_fps,
tier: "full".to_string(),
reason: "stable".to_string(),
window_started_ms: now_ms,
window_produced: 0,
window_coalesced: 0,
window_dropped: 0,
window_max_queue_age_ms: 0,
window_max_push_latency_ms: 0,
window_egress_fps_sum: 0.0,
window_egress_fps_samples: 0,
stable_windows: 0,
}),
}
}
pub fn current_fps(&self) -> f64 {
self.state.lock().current_fps
}
pub fn tier_and_reason(&self) -> (String, String) {
let state = self.state.lock();
(state.tier.clone(), state.reason.clone())
}
pub fn record_enqueue(&self, now_ms: u64, coalesced: bool) {
let mut state = self.state.lock();
self.rotate_window(&mut state, now_ms);
state.window_produced += 1;
if coalesced {
state.window_coalesced += 1;
}
}
#[cfg(test)]
pub fn record_drop(&self, now_ms: u64) {
let mut state = self.state.lock();
self.rotate_window(&mut state, now_ms);
state.window_dropped += 1;
}
pub fn record_capture(&self, now_ms: u64, queue_age_ms: u64, push_latency_ms: u64) {
let mut state = self.state.lock();
self.rotate_window(&mut state, now_ms);
state.window_max_queue_age_ms = state.window_max_queue_age_ms.max(queue_age_ms);
state.window_max_push_latency_ms = state.window_max_push_latency_ms.max(push_latency_ms);
}
pub fn record_egress_fps(&self, now_ms: u64, fps: f64) {
if !fps.is_finite() || fps < 0.0 {
return;
}
let mut state = self.state.lock();
self.rotate_window(&mut state, now_ms);
state.window_egress_fps_sum += fps;
state.window_egress_fps_samples += 1;
}
fn rotate_window(&self, state: &mut AdaptiveVideoState, now_ms: u64) {
if now_ms.saturating_sub(state.window_started_ms) < PRESSURE_WINDOW_MS {
return;
}
self.apply_window(state);
state.window_started_ms = now_ms;
state.window_produced = 0;
state.window_coalesced = 0;
state.window_dropped = 0;
state.window_max_queue_age_ms = 0;
state.window_max_push_latency_ms = 0;
state.window_egress_fps_sum = 0.0;
state.window_egress_fps_samples = 0;
}
fn apply_window(&self, state: &mut AdaptiveVideoState) {
if !self.adaptive {
state.current_fps = self.requested_fps;
state.tier = "full".to_string();
state.reason = "adaptiveDisabled".to_string();
return;
}
let frame_interval_ms = (1000.0 / state.current_fps.max(1.0)).ceil() as u64;
let latency_pressure = state.window_max_queue_age_ms > frame_interval_ms * 2
|| state.window_max_push_latency_ms > frame_interval_ms * 2;
let drop_ratio = state.window_dropped as f64 / state.window_produced.max(1) as f64;
let encoder_drop_pressure = state.window_produced >= 10 && drop_ratio > 0.05;
let average_egress_fps = if state.window_egress_fps_samples == 0 {
None
} else {
Some(state.window_egress_fps_sum / state.window_egress_fps_samples as f64)
};
let egress_pressure = average_egress_fps.is_some_and(|fps| {
state.window_egress_fps_samples >= 2
&& state.window_produced >= 10
&& fps < state.current_fps * 0.75
});
let pressure = latency_pressure || encoder_drop_pressure || egress_pressure;
if pressure {
let next = if state.current_fps > 30.0 {
30.0
} else if state.current_fps > self.min_fps {
self.min_fps
} else {
state.current_fps
};
if next < state.current_fps {
state.current_fps = next;
state.tier = tier_for_fps(self.requested_fps, state.current_fps);
}
state.reason = if latency_pressure {
"sendLatencyPressure".to_string()
} else if encoder_drop_pressure {
"encoderDropPressure".to_string()
} else {
"encoderEgressPressure".to_string()
};
state.stable_windows = 0;
return;
}
state.stable_windows += 1;
if state.current_fps >= self.requested_fps {
state.reason = "stable".to_string();
}
if state.stable_windows >= RECOVERY_WINDOW_COUNT && state.current_fps < self.requested_fps {
state.current_fps = (state.current_fps * 2.0).min(self.requested_fps);
state.tier = tier_for_fps(self.requested_fps, state.current_fps);
state.stable_windows = 0;
if state.current_fps >= self.requested_fps {
state.reason = "stable".to_string();
}
}
}
}
pub struct AdaptiveVideoStats {
produced: AtomicU64,
accepted: AtomicU64,
dropped: AtomicU64,
coalesced: AtomicU64,
captured: AtomicU64,
capture_failures: AtomicU64,
queue_depth: AtomicU64,
max_queue_depth: AtomicU64,
max_queue_age_ms: AtomicU64,
max_push_latency_ms: AtomicU64,
first_capture_ms: AtomicU64,
last_capture_ms: AtomicU64,
controller: AdaptiveVideoController,
}
#[derive(Clone, Debug)]
pub struct VideoTelemetryExtras {
pub pacing_mode: String,
pub pacing_target_fps: f64,
pub queue_capacity: u64,
pub bus_active: bool,
}
impl Default for VideoTelemetryExtras {
fn default() -> Self {
Self {
pacing_mode: "unknown".to_string(),
pacing_target_fps: 0.0,
queue_capacity: 0,
bus_active: false,
}
}
}
impl AdaptiveVideoStats {
pub fn new(requested_fps: f64, min_fps: f64, adaptive: bool, now_ms: u64) -> Self {
Self {
produced: AtomicU64::new(0),
accepted: AtomicU64::new(0),
dropped: AtomicU64::new(0),
coalesced: AtomicU64::new(0),
captured: AtomicU64::new(0),
capture_failures: AtomicU64::new(0),
queue_depth: AtomicU64::new(0),
max_queue_depth: AtomicU64::new(0),
max_queue_age_ms: AtomicU64::new(0),
max_push_latency_ms: AtomicU64::new(0),
first_capture_ms: AtomicU64::new(0),
last_capture_ms: AtomicU64::new(0),
controller: AdaptiveVideoController::new(requested_fps, min_fps, adaptive, now_ms),
}
}
#[cfg(test)]
pub fn record_enqueue(&self, now_ms: u64, replaced_pending: bool) {
self.record_enqueue_with_depth(now_ms, replaced_pending, 1);
}
pub fn record_enqueue_with_depth(&self, now_ms: u64, replaced_pending: bool, queue_depth: u64) {
self.produced.fetch_add(1, Ordering::Relaxed);
self.accepted.fetch_add(1, Ordering::Relaxed);
self.queue_depth.store(queue_depth, Ordering::Relaxed);
update_max(&self.max_queue_depth, queue_depth);
if replaced_pending {
self.coalesced.fetch_add(1, Ordering::Relaxed);
}
self.controller.record_enqueue(now_ms, replaced_pending);
}
#[cfg(test)]
pub fn record_drop(&self, now_ms: u64) {
self.dropped.fetch_add(1, Ordering::Relaxed);
self.controller.record_drop(now_ms);
}
pub fn record_reject(&self) {
self.dropped.fetch_add(1, Ordering::Relaxed);
}
pub fn record_capture(&self, now_ms: u64, queue_age_ms: u64, push_latency_ms: u64) {
update_min_nonzero(&self.first_capture_ms, now_ms);
update_max(&self.last_capture_ms, now_ms);
self.captured.fetch_add(1, Ordering::Relaxed);
self.queue_depth.store(0, Ordering::Relaxed);
update_max(&self.max_queue_age_ms, queue_age_ms);
update_max(&self.max_push_latency_ms, push_latency_ms);
self.controller
.record_capture(now_ms, queue_age_ms, push_latency_ms);
}
pub fn record_egress_fps(&self, now_ms: u64, fps: f64) {
self.controller.record_egress_fps(now_ms, fps);
}
pub fn record_capture_failure(&self) {
self.capture_failures.fetch_add(1, Ordering::Relaxed);
self.queue_depth.store(0, Ordering::Relaxed);
}
pub fn record_queue_cleared(&self) {
self.queue_depth.store(0, Ordering::Relaxed);
}
pub fn current_fps(&self) -> f64 {
self.controller.current_fps()
}
fn effective_fps(&self) -> f64 {
let first = self.first_capture_ms.load(Ordering::Relaxed);
let last = self.last_capture_ms.load(Ordering::Relaxed);
let captured = self.captured.load(Ordering::Relaxed);
if first == 0 || last <= first || captured <= 1 {
return 0.0;
}
let elapsed_s = (last - first) as f64 / 1000.0;
((captured - 1) as f64 / elapsed_s * 100.0).round() / 100.0
}
pub fn snapshot(
&self,
audio: &AdaptiveAudioStats,
extras: VideoTelemetryExtras,
) -> SendHealthSnapshot {
let (tier, reason) = self.controller.tier_and_reason();
SendHealthSnapshot {
outgoing_video_queue_depth: self.queue_depth.load(Ordering::Relaxed),
outgoing_video_queue_capacity: extras.queue_capacity,
outgoing_video_max_queue_depth: self.max_queue_depth.load(Ordering::Relaxed),
outgoing_video_frames_produced: self.produced.load(Ordering::Relaxed),
outgoing_video_frames_accepted: self.accepted.load(Ordering::Relaxed),
outgoing_video_frames_dropped: self.dropped.load(Ordering::Relaxed),
outgoing_video_frames_coalesced: self.coalesced.load(Ordering::Relaxed),
outgoing_video_frames_captured: self.captured.load(Ordering::Relaxed),
outgoing_video_capture_failures: self.capture_failures.load(Ordering::Relaxed),
outgoing_video_effective_fps: self.effective_fps(),
outgoing_video_target_fps: (self.current_fps() * 100.0).round() / 100.0,
outgoing_video_pacing_target_fps: (extras.pacing_target_fps * 100.0).round() / 100.0,
outgoing_video_max_queue_age_ms: self.max_queue_age_ms.load(Ordering::Relaxed),
outgoing_video_max_push_latency_ms: self.max_push_latency_ms.load(Ordering::Relaxed),
outgoing_video_pacing_mode: extras.pacing_mode,
outgoing_video_bus_active: extras.bus_active,
outgoing_audio_buffer_target_ms: audio.target_buffer_ms(),
outgoing_audio_buffer_max_ms: audio.max_buffer_ms(),
outgoing_audio_underruns: audio.underruns.load(Ordering::Relaxed),
outgoing_audio_rebuffers: audio.rebuffers.load(Ordering::Relaxed),
outgoing_audio_max_frame_gap_ms: audio.max_frame_gap_ms.load(Ordering::Relaxed),
adaptive_send_tier: tier,
adaptive_send_reason: reason,
}
}
}
pub struct AdaptiveAudioStats {
max_buffer_ms: AtomicU64,
target_buffer_ms: AtomicU64,
underruns: AtomicU64,
rebuffers: AtomicU64,
max_frame_gap_ms: AtomicU64,
last_push_ms: AtomicU64,
stable_started_ms: AtomicU64,
}
impl AdaptiveAudioStats {
pub fn new(max_buffer_ms: u32, now_ms: u64) -> Self {
let max_buffer_ms = clamp_audio_buffer_ms(max_buffer_ms);
Self {
max_buffer_ms: AtomicU64::new(max_buffer_ms as u64),
target_buffer_ms: AtomicU64::new(
DEFAULT_AUDIO_BUFFER_TARGET_MS.min(max_buffer_ms) as u64
),
underruns: AtomicU64::new(0),
rebuffers: AtomicU64::new(0),
max_frame_gap_ms: AtomicU64::new(0),
last_push_ms: AtomicU64::new(0),
stable_started_ms: AtomicU64::new(now_ms),
}
}
pub fn reset(&self, max_buffer_ms: u32, now_ms: u64) {
let max_buffer_ms = clamp_audio_buffer_ms(max_buffer_ms);
self.max_buffer_ms
.store(max_buffer_ms as u64, Ordering::Relaxed);
self.target_buffer_ms.store(
DEFAULT_AUDIO_BUFFER_TARGET_MS.min(max_buffer_ms) as u64,
Ordering::Relaxed,
);
self.underruns.store(0, Ordering::Relaxed);
self.rebuffers.store(0, Ordering::Relaxed);
self.max_frame_gap_ms.store(0, Ordering::Relaxed);
self.last_push_ms.store(0, Ordering::Relaxed);
self.stable_started_ms.store(now_ms, Ordering::Relaxed);
}
pub fn record_push(&self, now_ms: u64) {
let Some(previous) = advance_monotonic(&self.last_push_ms, now_ms) else {
return;
};
if previous == 0 {
self.stable_started_ms.store(now_ms, Ordering::Relaxed);
return;
}
let gap = now_ms - previous;
update_max(&self.max_frame_gap_ms, gap);
if gap > AUDIO_REBUFFER_GAP_MS {
self.rebuffers.fetch_add(1, Ordering::Relaxed);
if gap > self.target_buffer_ms() as u64 {
self.underruns.fetch_add(1, Ordering::Relaxed);
}
let next = (self.target_buffer_ms() + AUDIO_BUFFER_STEP_MS).min(self.max_buffer_ms());
self.target_buffer_ms.store(next as u64, Ordering::Relaxed);
self.stable_started_ms.store(now_ms, Ordering::Relaxed);
return;
}
if gap <= AUDIO_STABLE_GAP_MS {
let stable_started = self.stable_started_ms.load(Ordering::Relaxed);
if now_ms.saturating_sub(stable_started) >= 30_000 {
let current = self.target_buffer_ms();
let next = current
.saturating_sub(AUDIO_BUFFER_STEP_MS)
.max(DEFAULT_AUDIO_BUFFER_TARGET_MS.min(self.max_buffer_ms()));
self.target_buffer_ms.store(next as u64, Ordering::Relaxed);
self.stable_started_ms.store(now_ms, Ordering::Relaxed);
}
} else {
self.stable_started_ms.store(now_ms, Ordering::Relaxed);
}
}
pub fn max_buffer_ms(&self) -> u32 {
self.max_buffer_ms.load(Ordering::Relaxed) as u32
}
pub fn target_buffer_ms(&self) -> u32 {
self.target_buffer_ms.load(Ordering::Relaxed) as u32
}
}
pub fn clamp_audio_buffer_ms(value: u32) -> u32 {
let clamped = value.clamp(DEFAULT_AUDIO_BUFFER_TARGET_MS, DEFAULT_AUDIO_BUFFER_MAX_MS);
clamped - (clamped % 10)
}
fn sanitize_fps(value: f64, fallback: f64) -> f64 {
if value.is_finite() && value > 0.0 {
value
} else {
fallback
}
}
fn tier_for_fps(requested_fps: f64, current_fps: f64) -> String {
if current_fps >= requested_fps {
"full".to_string()
} else if current_fps >= 30.0 {
"fps30".to_string()
} else {
"fps15".to_string()
}
}
fn update_max(slot: &AtomicU64, value: u64) {
let mut current = slot.load(Ordering::Relaxed);
while value > current {
match slot.compare_exchange(current, value, Ordering::Relaxed, Ordering::Relaxed) {
Ok(_) => break,
Err(next) => current = next,
}
}
}
fn update_min_nonzero(slot: &AtomicU64, value: u64) {
if value == 0 {
return;
}
let mut current = slot.load(Ordering::Relaxed);
loop {
if current != 0 && current <= value {
return;
}
match slot.compare_exchange(current, value, Ordering::Relaxed, Ordering::Relaxed) {
Ok(_) => return,
Err(next) => current = next,
}
}
}
fn advance_monotonic(slot: &AtomicU64, value: u64) -> Option<u64> {
let mut current = slot.load(Ordering::Relaxed);
loop {
if current != 0 && value < current {
return None;
}
if value == current {
return Some(current);
}
match slot.compare_exchange(current, value, Ordering::Relaxed, Ordering::Relaxed) {
Ok(_) => return Some(current),
Err(next) => current = next,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn video_controller_ignores_pure_coalescing_jitter() {
let controller = AdaptiveVideoController::new(60.0, 15.0, true, 0);
for _ in 0..200 {
controller.record_enqueue(1_000, true);
}
controller.record_enqueue(5_001, false);
assert_eq!(controller.current_fps(), 60.0);
assert_eq!(controller.tier_and_reason().0, "full");
assert_eq!(controller.tier_and_reason().1, "stable");
}
#[test]
fn video_controller_ignores_sustained_coalescing_jitter_across_windows() {
let controller = AdaptiveVideoController::new(60.0, 15.0, true, 0);
for _ in 0..200 {
controller.record_enqueue(1_000, true);
}
controller.record_enqueue(5_001, false);
assert_eq!(controller.current_fps(), 60.0);
for _ in 0..200 {
controller.record_enqueue(6_000, true);
}
controller.record_enqueue(10_002, false);
assert_eq!(controller.current_fps(), 60.0);
assert_eq!(controller.tier_and_reason().1, "stable");
}
#[test]
fn video_controller_degrades_on_encoder_drop_pressure() {
let controller = AdaptiveVideoController::new(60.0, 15.0, true, 0);
for _ in 0..100 {
controller.record_enqueue(1_000, false);
}
for _ in 0..20 {
controller.record_drop(1_000);
}
controller.record_enqueue(5_001, false);
assert_eq!(controller.current_fps(), 30.0);
assert_eq!(controller.tier_and_reason().1, "encoderDropPressure");
}
#[test]
fn video_controller_continues_degrading_under_sustained_latency_pressure() {
let controller = AdaptiveVideoController::new(60.0, 15.0, true, 0);
controller.record_capture(1_000, 80, 1);
controller.record_capture(5_001, 1, 1);
assert_eq!(controller.current_fps(), 30.0);
controller.record_capture(6_000, 80, 1);
controller.record_capture(10_002, 1, 1);
assert_eq!(controller.current_fps(), 15.0);
assert_eq!(controller.tier_and_reason().0, "fps15");
assert_eq!(controller.tier_and_reason().1, "sendLatencyPressure");
}
#[test]
fn video_controller_degrades_on_latency_pressure_without_coalescing() {
let controller = AdaptiveVideoController::new(60.0, 15.0, true, 0);
controller.record_capture(1_000, 80, 5);
controller.record_capture(5_001, 1, 1);
assert_eq!(controller.current_fps(), 30.0);
assert_eq!(controller.tier_and_reason().1, "sendLatencyPressure");
}
#[test]
fn video_controller_degrades_on_encoder_egress_pressure() {
let controller = AdaptiveVideoController::new(30.0, 15.0, true, 0);
for _ in 0..60 {
controller.record_enqueue(1_000, false);
}
controller.record_egress_fps(1_000, 7.0);
controller.record_egress_fps(2_000, 8.0);
controller.record_egress_fps(4_000, 7.0);
controller.record_egress_fps(5_001, 7.0);
assert_eq!(controller.current_fps(), 15.0);
assert_eq!(controller.tier_and_reason().0, "fps15");
assert_eq!(controller.tier_and_reason().1, "encoderEgressPressure");
}
#[test]
fn video_controller_preserves_pressure_reason_while_degraded_but_stable() {
let controller = AdaptiveVideoController::new(30.0, 15.0, true, 0);
for _ in 0..60 {
controller.record_enqueue(1_000, false);
}
controller.record_egress_fps(1_000, 7.0);
controller.record_egress_fps(2_000, 8.0);
controller.record_egress_fps(4_000, 7.0);
controller.record_egress_fps(5_001, 7.0);
assert_eq!(controller.current_fps(), 15.0);
assert_eq!(controller.tier_and_reason().1, "encoderEgressPressure");
controller.record_capture(10_002, 1, 1);
assert_eq!(controller.current_fps(), 15.0);
assert_eq!(controller.tier_and_reason().0, "fps15");
assert_eq!(controller.tier_and_reason().1, "encoderEgressPressure");
for index in 2..=12 {
controller.record_capture(10_002 + index * 5_001, 1, 1);
}
assert_eq!(controller.current_fps(), 30.0);
assert_eq!(
controller.tier_and_reason(),
("full".to_string(), "stable".to_string())
);
}
#[test]
fn video_controller_ignores_single_encoder_egress_sample() {
let controller = AdaptiveVideoController::new(30.0, 15.0, true, 0);
for _ in 0..60 {
controller.record_enqueue(1_000, false);
}
controller.record_egress_fps(1_000, 7.0);
controller.record_egress_fps(5_001, 7.0);
assert_eq!(controller.current_fps(), 30.0);
assert_eq!(controller.tier_and_reason().1, "stable");
}
#[test]
fn video_controller_keeps_requested_fps_when_adaptive_send_is_disabled() {
let controller = AdaptiveVideoController::new(60.0, 15.0, false, 0);
for _ in 0..100 {
controller.record_enqueue(1_000, false);
}
controller.record_capture(5_001, 200, 200);
assert_eq!(controller.current_fps(), 60.0);
assert_eq!(
controller.tier_and_reason(),
("full".to_string(), "adaptiveDisabled".to_string())
);
}
#[test]
fn video_controller_recovers_after_stable_windows() {
let controller = AdaptiveVideoController::new(60.0, 15.0, true, 0);
controller.record_capture(1_000, 80, 1);
controller.record_capture(5_001, 1, 1);
assert_eq!(controller.current_fps(), 30.0);
for index in 1..=12 {
controller.record_capture(5_001 + index * 5_001, 1, 1);
}
assert_eq!(controller.current_fps(), 60.0);
}
#[test]
fn video_controller_recovers_from_minimum_in_two_stable_steps() {
let controller = AdaptiveVideoController::new(60.0, 15.0, true, 0);
controller.record_capture(1_000, 80, 1);
controller.record_capture(5_001, 1, 1);
assert_eq!(controller.current_fps(), 30.0);
controller.record_capture(6_000, 70, 1);
controller.record_capture(10_002, 1, 1);
assert_eq!(controller.current_fps(), 15.0);
for index in 1..=12 {
controller.record_capture(10_002 + index * 5_001, 1, 1);
}
assert_eq!(controller.current_fps(), 30.0);
for index in 13..=24 {
controller.record_capture(10_002 + index * 5_001, 1, 1);
}
assert_eq!(controller.current_fps(), 60.0);
}
#[test]
fn video_controller_requires_a_full_stable_recovery_window() {
let controller = AdaptiveVideoController::new(60.0, 15.0, true, 0);
controller.record_capture(1_000, 80, 1);
controller.record_capture(5_001, 1, 1);
assert_eq!(controller.current_fps(), 30.0);
for index in 1..12 {
controller.record_capture(5_001 + index * 5_001, 1, 1);
}
assert_eq!(controller.current_fps(), 30.0);
controller.record_capture(5_001 + 12 * 5_001, 1, 1);
assert_eq!(controller.current_fps(), 60.0);
}
#[test]
fn video_stats_snapshot_counts_coalescing_drops_failures_and_effective_fps() {
let audio = AdaptiveAudioStats::new(750, 0);
let stats = AdaptiveVideoStats::new(60.0, 15.0, true, 0);
stats.record_enqueue(1_000, false);
stats.record_enqueue(1_010, true);
stats.record_drop(1_011);
stats.record_capture(1_020, 20, 24);
stats.record_enqueue(2_000, false);
stats.record_capture(2_020, 20, 26);
stats.record_capture_failure();
let snapshot = stats.snapshot(&audio, VideoTelemetryExtras::default());
assert_eq!(snapshot.outgoing_video_frames_produced, 3);
assert_eq!(snapshot.outgoing_video_frames_accepted, 3);
assert_eq!(snapshot.outgoing_video_frames_dropped, 1);
assert_eq!(snapshot.outgoing_video_frames_coalesced, 1);
assert_eq!(snapshot.outgoing_video_frames_captured, 2);
assert_eq!(snapshot.outgoing_video_capture_failures, 1);
assert_eq!(snapshot.outgoing_video_effective_fps, 1.0);
assert_eq!(snapshot.outgoing_video_max_queue_age_ms, 20);
assert_eq!(snapshot.outgoing_video_max_push_latency_ms, 26);
assert_eq!(snapshot.outgoing_video_queue_depth, 0);
}
#[test]
fn video_stats_effective_fps_uses_capture_time_bounds_for_out_of_order_records() {
let audio = AdaptiveAudioStats::new(750, 0);
let stats = AdaptiveVideoStats::new(60.0, 15.0, true, 0);
stats.record_capture(2_000, 5, 6);
stats.record_capture(1_000, 7, 8);
stats.record_capture(3_000, 9, 10);
stats.record_capture(2_500, 11, 12);
let snapshot = stats.snapshot(&audio, VideoTelemetryExtras::default());
assert_eq!(snapshot.outgoing_video_frames_captured, 4);
assert_eq!(snapshot.outgoing_video_effective_fps, 1.5);
assert_eq!(snapshot.outgoing_video_max_queue_age_ms, 11);
assert_eq!(snapshot.outgoing_video_max_push_latency_ms, 12);
}
#[test]
fn video_stats_handles_concurrent_recording_without_lost_counts() {
let audio = AdaptiveAudioStats::new(750, 0);
let stats = Arc::new(AdaptiveVideoStats::new(60.0, 15.0, true, 0));
let mut workers = Vec::new();
for worker in 0..8 {
let stats = stats.clone();
workers.push(std::thread::spawn(move || {
for index in 0..250 {
let now_ms = 1_000 + worker * 1_000 + index;
stats.record_enqueue(now_ms, index % 3 == 0);
if index % 5 == 0 {
stats.record_drop(now_ms);
}
if index % 7 == 0 {
stats.record_capture(now_ms + 1, 1, 2);
}
}
}));
}
for worker in workers {
worker.join().expect("worker should not panic");
}
let snapshot = stats.snapshot(&audio, VideoTelemetryExtras::default());
assert_eq!(snapshot.outgoing_video_frames_produced, 2_000);
assert_eq!(snapshot.outgoing_video_frames_accepted, 2_000);
assert_eq!(snapshot.outgoing_video_frames_coalesced, 672);
assert_eq!(snapshot.outgoing_video_frames_dropped, 400);
assert_eq!(snapshot.outgoing_video_frames_captured, 288);
assert_eq!(snapshot.outgoing_video_max_queue_age_ms, 1);
assert_eq!(snapshot.outgoing_video_max_push_latency_ms, 2);
}
#[test]
fn idle_snapshot_reflects_audio_pressure_without_video_state() {
let audio = AdaptiveAudioStats::new(750, 0);
audio.record_push(1_000);
audio.record_push(1_300);
let snapshot = SendHealthSnapshot::idle(&audio);
assert_eq!(snapshot.outgoing_video_frames_produced, 0);
assert_eq!(snapshot.outgoing_audio_buffer_target_ms, 300);
assert_eq!(snapshot.outgoing_audio_buffer_max_ms, 750);
assert_eq!(snapshot.outgoing_audio_rebuffers, 1);
assert_eq!(snapshot.outgoing_audio_underruns, 1);
assert_eq!(snapshot.outgoing_audio_max_frame_gap_ms, 300);
assert_eq!(snapshot.adaptive_send_tier, "idle");
assert_eq!(snapshot.adaptive_send_reason, "notPublishing");
}
#[test]
fn video_controller_stress_keeps_target_inside_configured_bounds() {
let controller = AdaptiveVideoController::new(144.0, 24.0, true, 0);
for window in 0..240 {
let base = window * 5_001;
match window % 4 {
0 => controller.record_capture(base + 1_000, 200, 1),
1 => {
for _ in 0..30 {
controller.record_enqueue(base + 1_000, true);
}
controller.record_capture(base + 1_500, 1, 1);
}
2 => {
for _ in 0..60 {
controller.record_enqueue(base + 1_000, false);
}
controller.record_egress_fps(base + 1_500, 30.0);
controller.record_egress_fps(base + 2_500, 30.0);
}
_ => controller.record_capture(base + 1_000, 1, 1),
}
controller.record_capture(base + 5_001, 1, 1);
let fps = controller.current_fps();
assert!(
(24.0..=144.0).contains(&fps),
"fps target escaped configured bounds: {fps}"
);
}
}
#[test]
fn audio_buffer_expands_on_gaps_and_shrinks_after_stability() {
let audio = AdaptiveAudioStats::new(750, 0);
audio.record_push(10);
audio.record_push(200);
assert_eq!(audio.target_buffer_ms(), 300);
assert_eq!(audio.rebuffers.load(Ordering::Relaxed), 1);
for now_ms in (220..=30_240).step_by(20) {
audio.record_push(now_ms);
}
assert_eq!(audio.target_buffer_ms(), 200);
}
#[test]
fn audio_buffer_ignores_clock_regression_without_false_rebuffer() {
let audio = AdaptiveAudioStats::new(750, 0);
audio.record_push(1_000);
audio.record_push(1_020);
audio.record_push(900);
audio.record_push(1_040);
assert_eq!(audio.target_buffer_ms(), 200);
assert_eq!(audio.rebuffers.load(Ordering::Relaxed), 0);
assert_eq!(audio.underruns.load(Ordering::Relaxed), 0);
assert_eq!(audio.max_frame_gap_ms.load(Ordering::Relaxed), 20);
}
#[test]
fn audio_buffer_ignores_concurrent_stale_pushes_without_moving_last_push_backwards() {
let audio = Arc::new(AdaptiveAudioStats::new(750, 0));
audio.record_push(10_000);
let mut workers = Vec::new();
for worker in 0..8 {
let audio = audio.clone();
workers.push(std::thread::spawn(move || {
for index in 0..100 {
audio.record_push(1_000 + worker * 100 + index);
}
}));
}
for worker in workers {
worker.join().expect("worker should not panic");
}
audio.record_push(10_020);
assert_eq!(audio.target_buffer_ms(), 200);
assert_eq!(audio.rebuffers.load(Ordering::Relaxed), 0);
assert_eq!(audio.underruns.load(Ordering::Relaxed), 0);
assert_eq!(audio.max_frame_gap_ms.load(Ordering::Relaxed), 20);
}
#[test]
fn audio_buffer_growth_caps_at_configured_max_and_reset_clears_pressure() {
let audio = AdaptiveAudioStats::new(350, 0);
audio.record_push(10);
for index in 1..=10 {
audio.record_push(10 + index * 500);
}
assert_eq!(audio.target_buffer_ms(), 350);
assert_eq!(audio.rebuffers.load(Ordering::Relaxed), 10);
assert_eq!(audio.underruns.load(Ordering::Relaxed), 10);
audio.reset(250, 10_000);
assert_eq!(audio.target_buffer_ms(), 200);
assert_eq!(audio.max_buffer_ms(), 250);
assert_eq!(audio.rebuffers.load(Ordering::Relaxed), 0);
assert_eq!(audio.underruns.load(Ordering::Relaxed), 0);
assert_eq!(audio.max_frame_gap_ms.load(Ordering::Relaxed), 0);
}
#[test]
fn audio_buffer_stress_stays_within_realtime_bounds_under_jitter() {
let audio = AdaptiveAudioStats::new(620, 0);
let mut now_ms = 10;
audio.record_push(now_ms);
for index in 0..5_000 {
now_ms += match index % 11 {
0 => 180,
1 | 2 => 80,
_ => 20,
};
audio.record_push(now_ms);
assert!(
(DEFAULT_AUDIO_BUFFER_TARGET_MS..=620).contains(&audio.target_buffer_ms()),
"audio target escaped configured bounds"
);
}
assert_eq!(audio.max_buffer_ms(), 620);
assert!(audio.rebuffers.load(Ordering::Relaxed) > 0);
assert!(audio.max_frame_gap_ms.load(Ordering::Relaxed) >= 180);
}
#[test]
fn audio_buffer_max_is_clamped_to_real_time_bounds() {
assert_eq!(clamp_audio_buffer_ms(50), 200);
assert_eq!(clamp_audio_buffer_ms(777), 750);
assert_eq!(clamp_audio_buffer_ms(333), 330);
}
}
@@ -0,0 +1,275 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::sync::atomic::{AtomicU64, Ordering};
pub const SPEAKING_ATTACK_MS: u64 = 30;
pub const SPEAKING_RELEASE_MS_LOCAL: u64 = 180;
pub const SPEAKING_RELEASE_MS_REMOTE: u64 = 220;
pub const SPEAKING_HEARTBEAT_INTERVAL_MS: u64 = 1_000;
pub const SPEAKING_FRAME_TIMEOUT_MS: u64 = 250;
pub const SPEAKING_THRESHOLD_RMS_LOCAL_DEFAULT: f64 = 0.008;
pub const SPEAKING_THRESHOLD_RMS_REMOTE_DEFAULT: f64 = 0.006;
pub const SPEAKING_THRESHOLD_RMS_MIN: f64 = 0.000_1;
pub const SPEAKING_THRESHOLD_RMS_MAX: f64 = 0.5;
pub const SPEAKING_FRAME_SAMPLES_MAX: usize = 1 << 20;
const _: () = assert!(SPEAKING_ATTACK_MS < SPEAKING_RELEASE_MS_LOCAL);
const _: () = assert!(SPEAKING_ATTACK_MS < SPEAKING_RELEASE_MS_REMOTE);
const _: () = assert!(SPEAKING_RELEASE_MS_REMOTE < SPEAKING_HEARTBEAT_INTERVAL_MS);
const _: () = assert!(SPEAKING_FRAME_TIMEOUT_MS < SPEAKING_HEARTBEAT_INTERVAL_MS);
pub fn clamp_speaking_threshold_rms(threshold_rms: f64) -> f64 {
if !threshold_rms.is_finite() {
return SPEAKING_THRESHOLD_RMS_MIN;
}
threshold_rms.clamp(SPEAKING_THRESHOLD_RMS_MIN, SPEAKING_THRESHOLD_RMS_MAX)
}
pub struct SpeakingThresholds {
local_rms_bits: AtomicU64,
remote_rms_bits: AtomicU64,
}
impl SpeakingThresholds {
pub fn new() -> Self {
Self {
local_rms_bits: AtomicU64::new(SPEAKING_THRESHOLD_RMS_LOCAL_DEFAULT.to_bits()),
remote_rms_bits: AtomicU64::new(SPEAKING_THRESHOLD_RMS_REMOTE_DEFAULT.to_bits()),
}
}
pub fn set(&self, local_rms: f64, remote_rms: f64) {
let local = clamp_speaking_threshold_rms(local_rms);
let remote = clamp_speaking_threshold_rms(remote_rms);
assert!(local >= SPEAKING_THRESHOLD_RMS_MIN);
assert!(remote >= SPEAKING_THRESHOLD_RMS_MIN);
self.local_rms_bits
.store(local.to_bits(), Ordering::Release);
self.remote_rms_bits
.store(remote.to_bits(), Ordering::Release);
}
pub fn local_rms(&self) -> f64 {
let value = f64::from_bits(self.local_rms_bits.load(Ordering::Acquire));
assert!(value.is_finite());
value
}
pub fn remote_rms(&self) -> f64 {
let value = f64::from_bits(self.remote_rms_bits.load(Ordering::Acquire));
assert!(value.is_finite());
value
}
}
impl Default for SpeakingThresholds {
fn default() -> Self {
Self::new()
}
}
pub fn frame_rms_i16(samples: &[i16]) -> f64 {
assert!(!samples.is_empty());
assert!(samples.len() <= SPEAKING_FRAME_SAMPLES_MAX);
let mut sum_squares: f64 = 0.0;
for sample in samples {
let normalized = f64::from(*sample) / 32_768.0;
sum_squares += normalized * normalized;
}
let rms = (sum_squares / samples.len() as f64).sqrt();
assert!(rms.is_finite());
assert!(rms >= 0.0);
rms.min(1.0)
}
pub struct SpeakingGate {
attack_ms: u64,
release_ms: u64,
speaking: bool,
above_since_ms: Option<u64>,
below_since_ms: Option<u64>,
last_now_ms: u64,
}
impl SpeakingGate {
pub fn new(attack_ms: u64, release_ms: u64) -> Self {
assert!(attack_ms < release_ms);
assert!(release_ms <= SPEAKING_HEARTBEAT_INTERVAL_MS);
Self {
attack_ms,
release_ms,
speaking: false,
above_since_ms: None,
below_since_ms: None,
last_now_ms: 0,
}
}
pub fn speaking(&self) -> bool {
self.speaking
}
pub fn update(&mut self, rms: f64, threshold_rms: f64, now_ms: u64) -> Option<bool> {
assert!(rms.is_finite());
assert!(rms >= 0.0);
assert!(threshold_rms >= SPEAKING_THRESHOLD_RMS_MIN);
assert!(threshold_rms <= SPEAKING_THRESHOLD_RMS_MAX);
assert!(now_ms >= self.last_now_ms);
self.last_now_ms = now_ms;
if rms >= threshold_rms {
self.below_since_ms = None;
let above_since_ms = *self.above_since_ms.get_or_insert(now_ms);
if self.speaking {
return None;
}
if now_ms - above_since_ms < self.attack_ms {
return None;
}
self.speaking = true;
return Some(true);
}
self.above_since_ms = None;
let below_since_ms = *self.below_since_ms.get_or_insert(now_ms);
if !self.speaking {
return None;
}
if now_ms - below_since_ms < self.release_ms {
return None;
}
self.speaking = false;
Some(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
const THRESHOLD: f64 = 0.01;
fn gate() -> SpeakingGate {
SpeakingGate::new(SPEAKING_ATTACK_MS, SPEAKING_RELEASE_MS_LOCAL)
}
#[test]
fn stays_quiet_below_threshold() {
let mut gate = gate();
for tick in 0..100u64 {
assert_eq!(gate.update(0.001, THRESHOLD, tick * 10), None);
}
assert!(!gate.speaking());
}
#[test]
fn attack_requires_sustained_signal() {
let mut gate = gate();
assert_eq!(gate.update(0.5, THRESHOLD, 0), None);
assert_eq!(gate.update(0.5, THRESHOLD, 10), None);
assert_eq!(gate.update(0.5, THRESHOLD, 20), None);
assert_eq!(gate.update(0.5, THRESHOLD, 30), Some(true));
assert!(gate.speaking());
}
#[test]
fn single_frame_blip_does_not_trigger() {
let mut gate = gate();
assert_eq!(gate.update(0.5, THRESHOLD, 0), None);
assert_eq!(gate.update(0.001, THRESHOLD, 10), None);
assert_eq!(gate.update(0.5, THRESHOLD, 20), None);
assert_eq!(gate.update(0.001, THRESHOLD, 30), None);
assert!(!gate.speaking());
}
#[test]
fn release_bridges_inter_word_gaps() {
let mut gate = gate();
for tick in 0..=3u64 {
gate.update(0.5, THRESHOLD, tick * 10);
}
assert!(gate.speaking());
for tick in 4..=20u64 {
assert_eq!(gate.update(0.001, THRESHOLD, tick * 10), None);
}
assert!(gate.speaking());
assert_eq!(gate.update(0.5, THRESHOLD, 210), None);
assert!(gate.speaking());
}
#[test]
fn release_fires_after_sustained_silence() {
let mut gate = gate();
for tick in 0..=3u64 {
gate.update(0.5, THRESHOLD, tick * 10);
}
assert!(gate.speaking());
assert_eq!(gate.update(0.001, THRESHOLD, 40), None);
assert_eq!(gate.update(0.001, THRESHOLD, 219), None);
assert_eq!(gate.update(0.001, THRESHOLD, 220), Some(false));
assert!(!gate.speaking());
}
#[test]
fn retrigger_after_release_needs_full_attack() {
let mut gate = gate();
for tick in 0..=3u64 {
gate.update(0.5, THRESHOLD, tick * 10);
}
gate.update(0.001, THRESHOLD, 40);
assert_eq!(gate.update(0.001, THRESHOLD, 220), Some(false));
assert_eq!(gate.update(0.5, THRESHOLD, 230), None);
assert_eq!(gate.update(0.5, THRESHOLD, 260), Some(true));
}
#[test]
fn frame_rms_of_silence_is_zero() {
let samples = [0i16; 480];
assert_eq!(frame_rms_i16(&samples), 0.0);
}
#[test]
fn frame_rms_of_full_scale_square_wave_is_one() {
let mut samples = [i16::MIN; 480];
for (index, sample) in samples.iter_mut().enumerate() {
if index % 2 == 0 {
*sample = i16::MAX;
}
}
let rms = frame_rms_i16(&samples);
assert!(rms > 0.999);
assert!(rms <= 1.0);
}
#[test]
fn frame_rms_scales_with_amplitude() {
let loud = [8_192i16; 480];
let quiet = [1_024i16; 480];
assert!(frame_rms_i16(&loud) > frame_rms_i16(&quiet));
assert!((frame_rms_i16(&loud) - 0.25).abs() < 0.001);
}
#[test]
fn thresholds_default_and_clamp() {
let thresholds = SpeakingThresholds::new();
assert_eq!(thresholds.local_rms(), SPEAKING_THRESHOLD_RMS_LOCAL_DEFAULT);
assert_eq!(
thresholds.remote_rms(),
SPEAKING_THRESHOLD_RMS_REMOTE_DEFAULT
);
thresholds.set(-1.0, f64::NAN);
assert_eq!(thresholds.local_rms(), SPEAKING_THRESHOLD_RMS_MIN);
assert_eq!(thresholds.remote_rms(), SPEAKING_THRESHOLD_RMS_MIN);
thresholds.set(9.0, 0.02);
assert_eq!(thresholds.local_rms(), SPEAKING_THRESHOLD_RMS_MAX);
assert_eq!(thresholds.remote_rms(), 0.02);
}
#[test]
fn gate_update_is_monotonic_in_time() {
let mut gate = gate();
gate.update(0.5, THRESHOLD, 100);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
gate.update(0.5, THRESHOLD, 50);
}));
assert!(result.is_err());
}
}
@@ -0,0 +1,365 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::send_control::SendHealthSnapshot;
use fluxer_desktop_native::voice::stats as core_stats;
pub type ByteRateSample = core_stats::ByteRateSample;
pub type OutboundEntry = core_stats::OutboundStatsEntry;
pub type InboundEntry = core_stats::InboundStatsEntry;
#[derive(Clone, Debug, PartialEq, Default)]
pub struct ConnectionStats {
pub rtt_ms: Option<f64>,
pub outbound: Vec<OutboundEntry>,
pub inbound: Vec<InboundEntry>,
pub send: Option<SendHealthSnapshot>,
}
pub fn bitrate_kbps(prev: Option<ByteRateSample>, cur: ByteRateSample) -> f64 {
core_stats::bitrate_kbps(prev, cur)
}
#[cfg(test)]
pub fn sanitize_kbps(kbps: f64) -> f64 {
core_stats::sanitize_kbps(kbps)
}
pub fn jitter_seconds_to_ms(jitter_s: f64) -> Option<f64> {
core_stats::jitter_seconds_to_ms(jitter_s)
}
pub fn rtt_seconds_to_ms(rtt_s: f64) -> Option<f64> {
core_stats::rtt_seconds_to_ms(rtt_s)
}
pub fn sanitize_audio_level(level: f64) -> Option<f64> {
core_stats::sanitize_audio_level(level)
}
pub fn stats_to_json(stats: &ConnectionStats) -> String {
core_stats::stats_to_json(&core_stats::ConnectionStats {
rtt_ms: stats.rtt_ms,
outbound: stats.outbound.clone(),
inbound: stats.inbound.clone(),
send: stats.send.as_ref().map(send_health_to_core),
})
}
fn send_health_to_core(send: &SendHealthSnapshot) -> core_stats::SendHealthStats {
core_stats::SendHealthStats {
outgoing_video_queue_depth: send.outgoing_video_queue_depth,
outgoing_video_queue_capacity: send.outgoing_video_queue_capacity,
outgoing_video_max_queue_depth: send.outgoing_video_max_queue_depth,
outgoing_video_frames_produced: send.outgoing_video_frames_produced,
outgoing_video_frames_accepted: send.outgoing_video_frames_accepted,
outgoing_video_frames_dropped: send.outgoing_video_frames_dropped,
outgoing_video_frames_coalesced: send.outgoing_video_frames_coalesced,
outgoing_video_frames_captured: send.outgoing_video_frames_captured,
outgoing_video_capture_failures: send.outgoing_video_capture_failures,
outgoing_video_effective_fps: send.outgoing_video_effective_fps,
outgoing_video_target_fps: send.outgoing_video_target_fps,
outgoing_video_pacing_target_fps: send.outgoing_video_pacing_target_fps,
outgoing_video_max_queue_age_ms: send.outgoing_video_max_queue_age_ms,
outgoing_video_max_push_latency_ms: send.outgoing_video_max_push_latency_ms,
outgoing_video_pacing_mode: send.outgoing_video_pacing_mode.clone(),
outgoing_video_bus_active: send.outgoing_video_bus_active,
outgoing_audio_buffer_target_ms: send.outgoing_audio_buffer_target_ms,
outgoing_audio_buffer_max_ms: send.outgoing_audio_buffer_max_ms,
outgoing_audio_underruns: send.outgoing_audio_underruns,
outgoing_audio_rebuffers: send.outgoing_audio_rebuffers,
outgoing_audio_max_frame_gap_ms: send.outgoing_audio_max_frame_gap_ms,
adaptive_send_tier: send.adaptive_send_tier.clone(),
adaptive_send_reason: send.adaptive_send_reason.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bitrate_first_sample_is_zero() {
let cur = ByteRateSample {
bytes: 1000,
timestamp_us: 1_000_000,
};
assert_eq!(bitrate_kbps(None, cur), 0.0);
}
#[test]
fn bitrate_computes_kbps_from_byte_delta() {
let prev = ByteRateSample {
bytes: 0,
timestamp_us: 0,
};
let cur = ByteRateSample {
bytes: 12_500,
timestamp_us: 1_000_000,
};
assert_eq!(bitrate_kbps(Some(prev), cur), 100.0);
}
#[test]
fn bitrate_half_second_doubles_rate() {
let prev = ByteRateSample {
bytes: 1000,
timestamp_us: 1_000_000,
};
let cur = ByteRateSample {
bytes: 13_500,
timestamp_us: 1_500_000,
};
assert_eq!(bitrate_kbps(Some(prev), cur), 200.0);
}
#[test]
fn bitrate_rejects_backwards_time_and_bytes() {
let prev = ByteRateSample {
bytes: 5000,
timestamp_us: 2_000_000,
};
assert_eq!(
bitrate_kbps(
Some(prev),
ByteRateSample {
bytes: 6000,
timestamp_us: 1_000_000
}
),
0.0
);
assert_eq!(
bitrate_kbps(
Some(prev),
ByteRateSample {
bytes: 6000,
timestamp_us: 2_000_000
}
),
0.0
);
assert_eq!(
bitrate_kbps(
Some(prev),
ByteRateSample {
bytes: 100,
timestamp_us: 3_000_000
}
),
0.0
);
}
#[test]
fn sanitize_kbps_drops_nan_inf_negative_and_rounds() {
assert_eq!(sanitize_kbps(f64::NAN), 0.0);
assert_eq!(sanitize_kbps(f64::INFINITY), 0.0);
assert_eq!(sanitize_kbps(-5.0), 0.0);
assert_eq!(sanitize_kbps(123.456), 123.5);
assert_eq!(sanitize_kbps(100.0), 100.0);
}
#[test]
fn unit_conversions_seconds_to_ms() {
assert_eq!(jitter_seconds_to_ms(0.012), Some(12.0));
assert_eq!(jitter_seconds_to_ms(-1.0), None);
assert_eq!(jitter_seconds_to_ms(f64::NAN), None);
assert_eq!(rtt_seconds_to_ms(0.045), Some(45.0));
assert_eq!(rtt_seconds_to_ms(0.0), None);
assert_eq!(rtt_seconds_to_ms(f64::INFINITY), None);
}
#[test]
fn sanitize_audio_level_clamps() {
assert_eq!(sanitize_audio_level(0.5), Some(0.5));
assert_eq!(sanitize_audio_level(2.0), Some(1.0));
assert_eq!(sanitize_audio_level(-0.1), Some(0.0));
assert_eq!(sanitize_audio_level(f64::NAN), None);
}
#[test]
fn empty_stats_serialise_to_null_rtt_and_empty_arrays() {
let stats = ConnectionStats::default();
assert_eq!(
stats_to_json(&stats),
"{\"rttMs\":null,\"outbound\":[],\"inbound\":[],\"send\":null}"
);
}
#[test]
fn full_stats_serialise_to_exact_contract_shape() {
let stats = ConnectionStats {
rtt_ms: Some(42.0),
outbound: vec![
OutboundEntry {
track_sid: "TR_mic1".into(),
source: "microphone".into(),
kind: "audio".into(),
codec: Some("audio/opus".into()),
bitrate_kbps: 32.0,
packets_lost: 0,
fps: None,
audio_level: Some(0.62),
..Default::default()
},
OutboundEntry {
track_sid: "TR_screen1".into(),
source: "screen_share".into(),
kind: "video".into(),
codec: Some("video/H265".into()),
bitrate_kbps: 2500.5,
packets_lost: 3,
packets_sent: 9000,
fps: Some(30.0),
audio_level: None,
width: Some(2176),
height: Some(1200),
source_width: Some(2176),
source_height: Some(1200),
target_bitrate_kbps: Some(50_000.0),
configured_fps: Some(60.0),
target_fps: Some(30.0),
effective_fps: Some(29.8),
frames_produced: Some(120),
frames_accepted: Some(118),
frames_dropped: Some(1),
frames_coalesced: Some(2),
frames_captured: Some(117),
capture_failures: Some(0),
max_queue_age_ms: Some(18),
max_push_latency_ms: Some(12),
adaptive_send_tier: Some("fps30".into()),
adaptive_send_reason: Some("encoderEgressPressure".into()),
},
],
inbound: vec![InboundEntry {
participant_sid: "PA_remote1".into(),
participant_identity: Some("user_2_connection_2".into()),
track_sid: "TR_remoteAudio".into(),
source: Some("microphone".into()),
kind: "audio".into(),
codec: Some("audio/opus".into()),
bitrate_kbps: 28.0,
packets_lost: 1,
packets_received: 990,
jitter_ms: Some(5.0),
audio_level: Some(0.75),
fps: None,
width: None,
height: None,
source_width: None,
source_height: None,
}],
send: None,
};
let json = stats_to_json(&stats);
assert_eq!(
json,
"{\"rttMs\":42,\"outbound\":[\
{\"trackSid\":\"TR_mic1\",\"source\":\"microphone\",\"kind\":\"audio\",\"bitrateKbps\":32,\"packetsLost\":0,\"packetsSent\":0,\"audioLevel\":0.62,\"codec\":\"audio/opus\"},\
{\"trackSid\":\"TR_screen1\",\"source\":\"screen_share\",\"kind\":\"video\",\"bitrateKbps\":2500.5,\"packetsLost\":3,\"packetsSent\":9000,\"fps\":30,\"width\":2176,\"height\":1200,\"sourceWidth\":2176,\"sourceHeight\":1200,\"targetBitrateKbps\":50000,\"configuredFps\":60,\"targetFps\":30,\"effectiveFps\":29.8,\"framesProduced\":120,\"framesAccepted\":118,\"framesDropped\":1,\"framesCoalesced\":2,\"framesCaptured\":117,\"captureFailures\":0,\"maxQueueAgeMs\":18,\"maxPushLatencyMs\":12,\"adaptiveSendTier\":\"fps30\",\"adaptiveSendReason\":\"encoderEgressPressure\",\"codec\":\"video/H265\"}\
],\"inbound\":[\
{\"participantSid\":\"PA_remote1\",\"trackSid\":\"TR_remoteAudio\",\"kind\":\"audio\",\"bitrateKbps\":28,\"packetsLost\":1,\"packetsReceived\":990,\"participantIdentity\":\"user_2_connection_2\",\"source\":\"microphone\",\"jitterMs\":5,\"audioLevel\":0.75,\"codec\":\"audio/opus\"}\
],\"send\":null}"
);
let _ = json;
}
#[test]
fn send_health_serialises_to_exact_contract_shape() {
let stats = ConnectionStats {
rtt_ms: None,
outbound: vec![],
inbound: vec![],
send: Some(SendHealthSnapshot {
outgoing_video_queue_depth: 1,
outgoing_video_queue_capacity: 8,
outgoing_video_max_queue_depth: 4,
outgoing_video_frames_produced: 2,
outgoing_video_frames_accepted: 3,
outgoing_video_frames_dropped: 4,
outgoing_video_frames_coalesced: 5,
outgoing_video_frames_captured: 6,
outgoing_video_capture_failures: 7,
outgoing_video_effective_fps: 59.94,
outgoing_video_target_fps: 30.0,
outgoing_video_pacing_target_fps: 60.0,
outgoing_video_max_queue_age_ms: 8,
outgoing_video_max_push_latency_ms: 9,
outgoing_video_pacing_mode: "source".to_string(),
outgoing_video_bus_active: true,
outgoing_audio_buffer_target_ms: 300,
outgoing_audio_buffer_max_ms: 750,
outgoing_audio_underruns: 10,
outgoing_audio_rebuffers: 11,
outgoing_audio_max_frame_gap_ms: 120,
adaptive_send_tier: "fps30".to_string(),
adaptive_send_reason: "sendLatencyPressure".to_string(),
}),
};
assert_eq!(
stats_to_json(&stats),
"{\"rttMs\":null,\"outbound\":[],\"inbound\":[],\"send\":{\
\"outgoingVideoQueueDepth\":1,\
\"outgoingVideoQueueCapacity\":8,\
\"outgoingVideoMaxQueueDepth\":4,\
\"outgoingVideoFramesProduced\":2,\
\"outgoingVideoFramesAccepted\":3,\
\"outgoingVideoFramesDropped\":4,\
\"outgoingVideoFramesCoalesced\":5,\
\"outgoingVideoFramesCaptured\":6,\
\"outgoingVideoCaptureFailures\":7,\
\"outgoingVideoEffectiveFps\":59.94,\
\"outgoingVideoTargetFps\":30,\
\"outgoingVideoPacingTargetFps\":60,\
\"outgoingVideoMaxQueueAgeMs\":8,\
\"outgoingVideoMaxPushLatencyMs\":9,\
\"outgoingVideoPacingMode\":\"source\",\
\"outgoingVideoBusActive\":true,\
\"outgoingAudioBufferTargetMs\":300,\
\"outgoingAudioBufferMaxMs\":750,\
\"outgoingAudioUnderruns\":10,\
\"outgoingAudioRebuffers\":11,\
\"outgoingAudioMaxFrameGapMs\":120,\
\"adaptiveSendTier\":\"fps30\",\
\"adaptiveSendReason\":\"sendLatencyPressure\"\
}}"
);
}
#[test]
fn video_inbound_omits_audio_level_audio_omits_fps() {
let stats = ConnectionStats {
rtt_ms: None,
outbound: vec![],
inbound: vec![InboundEntry {
participant_sid: "PA_x".into(),
participant_identity: None,
track_sid: "TR_v".into(),
source: Some("screen_share".into()),
kind: "video".into(),
codec: None,
bitrate_kbps: 1000.0,
packets_lost: 0,
packets_received: 5000,
jitter_ms: None,
audio_level: None,
fps: Some(29.94),
width: Some(3840),
height: Some(2160),
source_width: Some(3840),
source_height: Some(2160),
}],
send: None,
};
let json = stats_to_json(&stats);
assert!(!json.contains("audioLevel"));
assert!(!json.contains("jitterMs"));
assert!(json.contains("\"source\":\"screen_share\""));
assert!(json.contains("\"fps\":29.9"));
assert!(json.contains("\"width\":3840"));
assert!(json.contains("\"height\":2160"));
assert!(json.contains("\"rttMs\":null"));
}
}
@@ -0,0 +1,747 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#[cfg(any(target_os = "windows", test))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TextureFrameDesc {
pub handle: u64,
pub width: u32,
pub height: u32,
pub dxgi_format: u32,
pub timestamp_us: i64,
}
#[cfg(any(target_os = "linux", test))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DmabufFrameDesc {
pub plane_count: u8,
pub width: u32,
pub height: u32,
pub drm_format: u32,
pub modifier: u64,
pub strides: [u32; 4],
pub offsets: [u32; 4],
pub device_uuid: [u8; 16],
pub timestamp_us: i64,
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TextureEncodeError {
NoTexture,
InvalidDimensions,
UnsupportedFormat,
InvalidPlanes,
UnsupportedCodec,
NoHardwareEncoder,
SdkNativeTextureUnsupported,
}
impl TextureEncodeError {
#[allow(dead_code)]
pub fn as_str(self) -> &'static str {
match self {
TextureEncodeError::NoTexture => "noTexture",
TextureEncodeError::InvalidDimensions => "invalidDimensions",
TextureEncodeError::UnsupportedFormat => "unsupportedFormat",
TextureEncodeError::InvalidPlanes => "invalidPlanes",
TextureEncodeError::UnsupportedCodec => "unsupportedCodec",
TextureEncodeError::NoHardwareEncoder => "noHardwareEncoder",
TextureEncodeError::SdkNativeTextureUnsupported => "sdkNativeTextureUnsupported",
}
}
}
#[cfg(any(target_os = "windows", test))]
const DXGI_FORMAT_R8G8B8A8_UNORM: u32 = 28;
#[cfg(any(target_os = "windows", test))]
const DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: u32 = 29;
#[cfg(any(target_os = "windows", test))]
const DXGI_FORMAT_B8G8R8A8_UNORM: u32 = 87;
#[cfg(any(target_os = "windows", test))]
const DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: u32 = 91;
#[cfg(any(target_os = "windows", test))]
const DXGI_FORMAT_NV12: u32 = 103;
#[cfg(any(target_os = "windows", test))]
pub fn dxgi_format_supported(dxgi_format: u32) -> bool {
matches!(
dxgi_format,
DXGI_FORMAT_R8G8B8A8_UNORM
| DXGI_FORMAT_R8G8B8A8_UNORM_SRGB
| DXGI_FORMAT_B8G8R8A8_UNORM
| DXGI_FORMAT_B8G8R8A8_UNORM_SRGB
| DXGI_FORMAT_NV12
)
}
#[cfg(any(target_os = "linux", test))]
const fn fourcc(bytes: [u8; 4]) -> u32 {
u32::from_le_bytes(bytes)
}
#[cfg(any(target_os = "linux", test))]
const DRM_FORMAT_XRGB8888: u32 = fourcc(*b"XR24");
#[cfg(any(target_os = "linux", test))]
const DRM_FORMAT_ARGB8888: u32 = fourcc(*b"AR24");
#[cfg(any(target_os = "linux", test))]
const DRM_FORMAT_XBGR8888: u32 = fourcc(*b"XB24");
#[cfg(any(target_os = "linux", test))]
const DRM_FORMAT_ABGR8888: u32 = fourcc(*b"AB24");
#[cfg(any(target_os = "linux", test))]
const DRM_FORMAT_XRGB2101010: u32 = fourcc(*b"XR30");
#[cfg(any(target_os = "linux", test))]
const DRM_FORMAT_ARGB2101010: u32 = fourcc(*b"AR30");
#[cfg(any(target_os = "linux", test))]
const DRM_FORMAT_XBGR2101010: u32 = fourcc(*b"XB30");
#[cfg(any(target_os = "linux", test))]
const DRM_FORMAT_ABGR2101010: u32 = fourcc(*b"AB30");
#[cfg(any(target_os = "linux", test))]
const DRM_FORMAT_NV12: u32 = fourcc(*b"NV12");
#[cfg(any(target_os = "linux", test))]
pub fn drm_format_supported(drm_format: u32) -> bool {
matches!(
drm_format,
DRM_FORMAT_XRGB8888
| DRM_FORMAT_ARGB8888
| DRM_FORMAT_XBGR8888
| DRM_FORMAT_ABGR8888
| DRM_FORMAT_XRGB2101010
| DRM_FORMAT_ARGB2101010
| DRM_FORMAT_XBGR2101010
| DRM_FORMAT_ABGR2101010
| DRM_FORMAT_NV12
)
}
#[cfg(any(target_os = "linux", target_os = "windows", test))]
const MAX_TEXTURE_EDGE: u32 = 8192;
#[cfg(any(target_os = "linux", target_os = "windows", test))]
fn validate_dimensions(width: u32, height: u32) -> Result<(), TextureEncodeError> {
if width < 2
|| height < 2
|| !width.is_multiple_of(2)
|| !height.is_multiple_of(2)
|| width > MAX_TEXTURE_EDGE
|| height > MAX_TEXTURE_EDGE
{
return Err(TextureEncodeError::InvalidDimensions);
}
Ok(())
}
#[cfg(any(target_os = "windows", test))]
pub fn validate_texture_desc(desc: &TextureFrameDesc) -> Result<(), TextureEncodeError> {
if desc.handle == 0 {
return Err(TextureEncodeError::NoTexture);
}
validate_dimensions(desc.width, desc.height)?;
if !dxgi_format_supported(desc.dxgi_format) {
return Err(TextureEncodeError::UnsupportedFormat);
}
Ok(())
}
#[cfg(any(target_os = "linux", test))]
pub fn validate_dmabuf_desc(desc: &DmabufFrameDesc) -> Result<(), TextureEncodeError> {
validate_dimensions(desc.width, desc.height)?;
if !drm_format_supported(desc.drm_format) {
return Err(TextureEncodeError::UnsupportedFormat);
}
let plane_count = desc.plane_count as usize;
if !(1..=4).contains(&plane_count) {
return Err(TextureEncodeError::InvalidPlanes);
}
for plane in 0..plane_count {
if desc.strides[plane] == 0 {
return Err(TextureEncodeError::InvalidPlanes);
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
#[cfg(test)]
pub fn dmabuf_desc_from_parts(
fds: &[i32],
plane_count: u32,
width: u32,
height: u32,
drm_format: u32,
modifier: u64,
strides: &[u32],
offsets: &[u32],
device_uuid: &[u8],
timestamp_us: f64,
) -> Option<(DmabufFrameDesc, [i32; 4])> {
let plane_count_u8 = u8::try_from(plane_count).ok()?;
let planes = plane_count as usize;
if !(1..=4).contains(&planes)
|| fds.len() < planes
|| strides.len() < planes
|| offsets.len() < planes
|| device_uuid.len() != 16
{
return None;
}
if fds.iter().take(planes).any(|fd| *fd < 0) {
return None;
}
let mut fd_array = [-1; 4];
let mut stride_array = [0; 4];
let mut offset_array = [0; 4];
fd_array[..planes].copy_from_slice(&fds[..planes]);
stride_array[..planes].copy_from_slice(&strides[..planes]);
offset_array[..planes].copy_from_slice(&offsets[..planes]);
let mut uuid = [0u8; 16];
uuid.copy_from_slice(device_uuid);
Some((
DmabufFrameDesc {
plane_count: plane_count_u8,
width,
height,
drm_format,
modifier,
strides: stride_array,
offsets: offset_array,
device_uuid: uuid,
timestamp_us: timestamp_us as i64,
},
fd_array,
))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TextureCapability {
pub available: bool,
pub reason: TextureEncodeError,
}
impl TextureCapability {
pub fn unavailable(reason: TextureEncodeError) -> Self {
Self {
available: false,
reason,
}
}
pub fn for_screen_codec(codec: &str, has_hardware_encoder: bool) -> Self {
if !codec_allows_native_gpu(codec) {
return Self::unavailable(TextureEncodeError::UnsupportedCodec);
}
if !has_hardware_encoder {
return Self {
available: false,
reason: TextureEncodeError::NoHardwareEncoder,
};
}
Self {
available: true,
reason: TextureEncodeError::NoTexture,
}
}
}
pub(crate) fn codec_allows_native_gpu(codec: &str) -> bool {
matches!(
codec.trim().to_ascii_lowercase().as_str(),
"h264" | "h265" | "hevc"
)
}
#[cfg(any(target_os = "windows", test))]
fn sdk_accepts_d3d11_texture_buffers() -> bool {
true
}
#[cfg(target_os = "linux")]
fn sdk_accepts_dmabuf_texture_buffers() -> bool {
cfg!(target_os = "linux")
}
#[cfg(any(target_os = "windows", test))]
pub fn should_attempt_texture_encode(
capability: &TextureCapability,
desc: &TextureFrameDesc,
) -> Result<(), TextureEncodeError> {
if !capability.available {
return Err(capability.reason);
}
if !sdk_accepts_d3d11_texture_buffers() {
return Err(TextureEncodeError::SdkNativeTextureUnsupported);
}
validate_texture_desc(desc)
}
#[cfg(target_os = "linux")]
pub fn should_attempt_dmabuf_encode(
capability: &TextureCapability,
desc: &DmabufFrameDesc,
) -> Result<(), TextureEncodeError> {
if !capability.available {
return Err(capability.reason);
}
if !sdk_accepts_dmabuf_texture_buffers() {
return Err(TextureEncodeError::SdkNativeTextureUnsupported);
}
validate_dmabuf_desc(desc)
}
#[cfg(test)]
pub fn should_attempt_texture_encode_for_tests(
capability: &TextureCapability,
desc: &TextureFrameDesc,
sdk_accepts_d3d11: bool,
) -> Result<(), TextureEncodeError> {
if !capability.available {
return Err(capability.reason);
}
if !sdk_accepts_d3d11 {
return Err(TextureEncodeError::SdkNativeTextureUnsupported);
}
validate_texture_desc(desc)
}
#[cfg(test)]
pub fn should_attempt_dmabuf_encode_for_tests(
capability: &TextureCapability,
desc: &DmabufFrameDesc,
sdk_accepts_dmabuf: bool,
) -> Result<(), TextureEncodeError> {
if !capability.available {
return Err(capability.reason);
}
if !sdk_accepts_dmabuf {
return Err(TextureEncodeError::SdkNativeTextureUnsupported);
}
validate_dmabuf_desc(desc)
}
#[cfg(all(feature = "publisher", any(target_os = "linux", target_os = "windows")))]
pub mod bridge {
#[cfg(target_os = "linux")]
use super::{DmabufFrameDesc, should_attempt_dmabuf_encode};
use super::{TextureCapability, TextureEncodeError};
#[cfg(target_os = "windows")]
use super::{TextureFrameDesc, should_attempt_texture_encode};
use livekit::webrtc::video_frame::{VideoFrame, VideoRotation, native::NativeBuffer};
use livekit::webrtc::video_source::native::NativeVideoSource;
#[cfg(target_os = "windows")]
pub fn try_publish_texture(
source: &NativeVideoSource,
capability: &TextureCapability,
desc: &TextureFrameDesc,
) -> Result<(), TextureEncodeError> {
should_attempt_texture_encode(capability, desc)?;
let Some(buffer) = NativeBuffer::from_fluxer_d3d11_texture(
desc.handle,
desc.width,
desc.height,
desc.dxgi_format,
) else {
return Err(TextureEncodeError::SdkNativeTextureUnsupported);
};
publish_native_buffer(source, buffer, desc.timestamp_us);
Ok(())
}
#[cfg(target_os = "linux")]
pub fn try_publish_dmabuf(
source: &NativeVideoSource,
capability: &TextureCapability,
desc: &DmabufFrameDesc,
fds: [i32; 4],
) -> Result<(), TextureEncodeError> {
should_attempt_dmabuf_encode(capability, desc)?;
let uuid_hi = u64::from_be_bytes(desc.device_uuid[0..8].try_into().unwrap_or([0; 8]));
let uuid_lo = u64::from_be_bytes(desc.device_uuid[8..16].try_into().unwrap_or([0; 8]));
let Some(buffer) = NativeBuffer::from_fluxer_dmabuf_texture(
fds,
desc.plane_count as u32,
desc.width,
desc.height,
desc.drm_format,
desc.modifier,
desc.strides,
desc.offsets,
uuid_hi,
uuid_lo,
) else {
return Err(TextureEncodeError::SdkNativeTextureUnsupported);
};
publish_native_buffer(source, buffer, desc.timestamp_us);
Ok(())
}
fn publish_native_buffer(source: &NativeVideoSource, buffer: NativeBuffer, timestamp_us: i64) {
source.capture_frame(&VideoFrame {
rotation: VideoRotation::VideoRotation0,
timestamp_us,
frame_metadata: None,
buffer,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
fn good_desc() -> TextureFrameDesc {
TextureFrameDesc {
handle: 0xDEAD_BEEF,
width: 1920,
height: 1080,
dxgi_format: DXGI_FORMAT_B8G8R8A8_UNORM,
timestamp_us: 123_456,
}
}
#[test]
fn dxgi_format_gate_accepts_8bit_rgba_bgra_only() {
assert!(dxgi_format_supported(DXGI_FORMAT_B8G8R8A8_UNORM));
assert!(dxgi_format_supported(DXGI_FORMAT_B8G8R8A8_UNORM_SRGB));
assert!(dxgi_format_supported(DXGI_FORMAT_R8G8B8A8_UNORM));
assert!(dxgi_format_supported(DXGI_FORMAT_R8G8B8A8_UNORM_SRGB));
assert!(dxgi_format_supported(DXGI_FORMAT_NV12));
assert!(!dxgi_format_supported(24));
assert!(!dxgi_format_supported(10));
assert!(!dxgi_format_supported(0));
}
#[test]
fn drm_format_gate_accepts_obs_vkcapture_texture_formats() {
assert!(drm_format_supported(DRM_FORMAT_ARGB8888));
assert!(drm_format_supported(DRM_FORMAT_ABGR8888));
assert!(drm_format_supported(DRM_FORMAT_ARGB2101010));
assert!(drm_format_supported(DRM_FORMAT_ABGR2101010));
assert!(drm_format_supported(DRM_FORMAT_NV12));
assert!(
!drm_format_supported(fourcc(*b"AB4H")),
"16-bit float DMA-BUF import is not encodable by the native NVENC bridge yet"
);
assert!(!drm_format_supported(0));
}
#[test]
fn validate_rejects_zero_handle_as_no_texture() {
let mut d = good_desc();
d.handle = 0;
assert_eq!(
validate_texture_desc(&d),
Err(TextureEncodeError::NoTexture)
);
}
#[test]
fn validate_rejects_odd_zero_and_oversized_dims() {
for (w, h) in [
(1921, 1080),
(1920, 1081),
(0, 1080),
(1920, 0),
(8194, 1080),
(1920, 8194),
] {
let mut d = good_desc();
d.width = w;
d.height = h;
assert_eq!(
validate_texture_desc(&d),
Err(TextureEncodeError::InvalidDimensions),
"dims {w}x{h} should be rejected"
);
}
}
#[test]
fn validate_rejects_unsupported_format() {
let mut d = good_desc();
d.dxgi_format = 24;
assert_eq!(
validate_texture_desc(&d),
Err(TextureEncodeError::UnsupportedFormat)
);
}
#[test]
fn validate_accepts_a_clean_bgra_texture() {
assert_eq!(validate_texture_desc(&good_desc()), Ok(()));
}
fn good_dmabuf_desc() -> DmabufFrameDesc {
DmabufFrameDesc {
plane_count: 1,
width: 1920,
height: 1080,
drm_format: DRM_FORMAT_ARGB8888,
modifier: 0,
strides: [1920 * 4, 0, 0, 0],
offsets: [0, 0, 0, 0],
device_uuid: [1; 16],
timestamp_us: 123_456,
}
}
#[test]
fn validate_dmabuf_accepts_supported_formats_with_optional_uuid() {
assert_eq!(validate_dmabuf_desc(&good_dmabuf_desc()), Ok(()));
let mut desc = good_dmabuf_desc();
desc.drm_format = DRM_FORMAT_NV12;
desc.strides[0] = 1920;
assert_eq!(validate_dmabuf_desc(&desc), Ok(()));
desc = good_dmabuf_desc();
desc.device_uuid = [0; 16];
assert_eq!(validate_dmabuf_desc(&desc), Ok(()));
}
#[test]
fn validate_dmabuf_rejects_invalid_planes() {
let mut desc = good_dmabuf_desc();
desc.plane_count = 0;
assert_eq!(
validate_dmabuf_desc(&desc),
Err(TextureEncodeError::InvalidPlanes)
);
desc = good_dmabuf_desc();
desc.strides[0] = 0;
assert_eq!(
validate_dmabuf_desc(&desc),
Err(TextureEncodeError::InvalidPlanes)
);
}
#[test]
fn dmabuf_desc_from_parts_rejects_negative_fds() {
assert!(
dmabuf_desc_from_parts(
&[-1],
1,
1920,
1080,
DRM_FORMAT_ARGB8888,
0,
&[1920 * 4],
&[0],
&[1; 16],
123.0,
)
.is_none()
);
}
#[test]
fn dmabuf_desc_from_parts_populates_all_plane_arrays() {
let uuid = [7u8; 16];
let (desc, fds) = dmabuf_desc_from_parts(
&[10, 11, 12, 99],
3,
1920,
1080,
DRM_FORMAT_NV12,
0xABCD,
&[1920, 960, 960, 777],
&[0, 2_073_600, 3_110_400, 999],
&uuid,
123_456.75,
)
.expect("valid multi-plane descriptor");
assert_eq!(desc.plane_count, 3);
assert_eq!(desc.width, 1920);
assert_eq!(desc.height, 1080);
assert_eq!(desc.drm_format, DRM_FORMAT_NV12);
assert_eq!(desc.modifier, 0xABCD);
assert_eq!(desc.strides, [1920, 960, 960, 0]);
assert_eq!(desc.offsets, [0, 2_073_600, 3_110_400, 0]);
assert_eq!(desc.device_uuid, uuid);
assert_eq!(desc.timestamp_us, 123_456);
assert_eq!(fds, [10, 11, 12, -1]);
}
#[test]
fn dmabuf_desc_from_parts_rejects_incomplete_native_inputs() {
let uuid = [1u8; 16];
for (fds, strides, offsets, uuid_bytes, label) in [
(&[4][..], &[128][..], &[0][..], &uuid[..], "too few fds"),
(
&[4, 5][..],
&[128][..],
&[0, 64][..],
&uuid[..],
"too few strides",
),
(
&[4, 5][..],
&[128, 128][..],
&[0][..],
&uuid[..],
"too few offsets",
),
(
&[4, 5][..],
&[128, 128][..],
&[0, 64][..],
&[1u8; 15][..],
"bad uuid",
),
] {
assert!(
dmabuf_desc_from_parts(
fds,
2,
128,
128,
DRM_FORMAT_ARGB8888,
0,
strides,
offsets,
uuid_bytes,
0.0,
)
.is_none(),
"{label} should be rejected"
);
}
assert!(
dmabuf_desc_from_parts(
&[4, 5, 6, 7, 8],
5,
128,
128,
DRM_FORMAT_ARGB8888,
0,
&[128; 5],
&[0; 5],
&uuid,
0.0,
)
.is_none()
);
}
#[test]
fn validate_precedence_handle_before_dims_before_format() {
let d = TextureFrameDesc {
handle: 0,
width: 1921,
height: 0,
dxgi_format: 999,
timestamp_us: 0,
};
assert_eq!(
validate_texture_desc(&d),
Err(TextureEncodeError::NoTexture)
);
}
#[test]
fn probe_is_available_only_for_explicit_hardware_codecs() {
for codec in ["h264", "H264", "h265", "hevc", "HEVC"] {
let cap = TextureCapability::for_screen_codec(codec, true);
assert!(cap.available, "{codec} should allow native GPU buffers");
assert_eq!(cap.reason, TextureEncodeError::NoTexture);
}
for codec in ["", "vp8", "vp9", "av1", "rubbish"] {
let cap = TextureCapability::for_screen_codec(codec, true);
assert!(
!cap.available,
"{codec} should not allow native GPU buffers"
);
assert_eq!(cap.reason, TextureEncodeError::UnsupportedCodec);
}
let cap = TextureCapability::for_screen_codec("h264", false);
assert!(!cap.available);
assert_eq!(cap.reason, TextureEncodeError::NoHardwareEncoder);
}
#[test]
fn should_attempt_falls_back_when_capability_unavailable() {
let cap = TextureCapability {
available: false,
reason: TextureEncodeError::SdkNativeTextureUnsupported,
};
assert_eq!(
should_attempt_texture_encode(&cap, &good_desc()),
Err(TextureEncodeError::SdkNativeTextureUnsupported)
);
}
#[test]
fn should_attempt_validates_frame_when_capability_available() {
let open = TextureCapability {
available: true,
reason: TextureEncodeError::NoTexture,
};
assert_eq!(
should_attempt_texture_encode_for_tests(&open, &good_desc(), true),
Ok(())
);
assert_eq!(should_attempt_texture_encode(&open, &good_desc()), Ok(()));
let mut bad = good_desc();
bad.handle = 0;
assert_eq!(
should_attempt_texture_encode_for_tests(&open, &bad, true),
Err(TextureEncodeError::NoTexture)
);
bad = good_desc();
bad.dxgi_format = 24;
assert_eq!(
should_attempt_texture_encode_for_tests(&open, &bad, true),
Err(TextureEncodeError::UnsupportedFormat)
);
}
#[test]
fn should_attempt_dmabuf_is_sdk_gated_after_validation_capability() {
let open = TextureCapability {
available: true,
reason: TextureEncodeError::NoTexture,
};
assert_eq!(
should_attempt_dmabuf_encode_for_tests(&open, &good_dmabuf_desc(), true),
Ok(())
);
assert_eq!(
should_attempt_dmabuf_encode_for_tests(&open, &good_dmabuf_desc(), false),
Err(TextureEncodeError::SdkNativeTextureUnsupported)
);
let mut invalid = good_dmabuf_desc();
invalid.plane_count = 5;
assert_eq!(
should_attempt_dmabuf_encode_for_tests(&open, &invalid, true),
Err(TextureEncodeError::InvalidPlanes)
);
}
#[test]
fn error_strings_are_stable() {
assert_eq!(TextureEncodeError::NoTexture.as_str(), "noTexture");
assert_eq!(
TextureEncodeError::InvalidDimensions.as_str(),
"invalidDimensions"
);
assert_eq!(
TextureEncodeError::UnsupportedFormat.as_str(),
"unsupportedFormat"
);
assert_eq!(TextureEncodeError::InvalidPlanes.as_str(), "invalidPlanes");
assert_eq!(
TextureEncodeError::UnsupportedCodec.as_str(),
"unsupportedCodec"
);
assert_eq!(
TextureEncodeError::NoHardwareEncoder.as_str(),
"noHardwareEncoder"
);
assert_eq!(
TextureEncodeError::SdkNativeTextureUnsupported.as_str(),
"sdkNativeTextureUnsupported"
);
}
}
@@ -0,0 +1,732 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct I420 {
pub width: u32,
pub height: u32,
pub y: Vec<u8>,
pub u: Vec<u8>,
pub v: Vec<u8>,
}
impl I420 {
pub fn new(width: u32, height: u32) -> Option<Self> {
if !dims_ok(width, height) {
return None;
}
let w = width as usize;
let h = height as usize;
Some(Self {
width,
height,
y: vec![0u8; w * h],
u: vec![0u8; (w / 2) * (h / 2)],
v: vec![0u8; (w / 2) * (h / 2)],
})
}
fn has_layout(&self, width: u32, height: u32) -> bool {
if self.width != width || self.height != height {
return false;
}
let w = width as usize;
let h = height as usize;
self.y.len() == w * h && self.u.len() == (w / 2) * (h / 2) && self.v.len() == self.u.len()
}
}
pub fn tight_i420_byte_len(width: u32, height: u32) -> Option<usize> {
if !dims_ok(width, height) {
return None;
}
let w = width as usize;
let h = height as usize;
let y_len = w.checked_mul(h)?;
let chroma_len = (w / 2).checked_mul(h / 2)?;
y_len.checked_add(chroma_len.checked_mul(2)?)
}
pub fn copy_tight_i420_into(src: &[u8], width: u32, height: u32, dst: &mut I420) -> bool {
if !dst.has_layout(width, height) {
return false;
}
let Some(total_len) = tight_i420_byte_len(width, height) else {
return false;
};
if src.len() != total_len {
return false;
}
let y_len = (width as usize) * (height as usize);
let chroma_len = y_len / 4;
dst.y.copy_from_slice(&src[..y_len]);
dst.u.copy_from_slice(&src[y_len..y_len + chroma_len]);
dst.v.copy_from_slice(&src[y_len + chroma_len..]);
true
}
fn dims_ok(width: u32, height: u32) -> bool {
width >= 2 && height >= 2 && width.is_multiple_of(2) && height.is_multiple_of(2)
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn nv12_to_i420(
src: &[u8],
width: u32,
height: u32,
stride_y: u32,
stride_uv: u32,
) -> Option<I420> {
let mut dst = I420::new(width, height)?;
if !nv12_to_i420_into(src, width, height, stride_y, stride_uv, &mut dst) {
return None;
}
Some(dst)
}
pub fn nv12_to_i420_into(
src: &[u8],
width: u32,
height: u32,
stride_y: u32,
stride_uv: u32,
dst: &mut I420,
) -> bool {
if !dims_ok(width, height) {
return false;
}
if !dst.has_layout(width, height) {
return false;
}
let w = width as usize;
let h = height as usize;
let cw = w / 2;
let ch = h / 2;
let sy = stride_y.max(width) as usize;
let suv = stride_uv.max(width) as usize;
let Some(uv_offset) = sy.checked_mul(h) else {
return false;
};
let Some(uv_len) = suv.checked_mul(ch) else {
return false;
};
let Some(needed) = uv_offset.checked_add(uv_len) else {
return false;
};
if src.len() < needed {
return false;
}
for row in 0..h {
let s = row * sy;
dst.y[row * w..row * w + w].copy_from_slice(&src[s..s + w]);
}
for row in 0..ch {
let base = uv_offset + row * suv;
for x in 0..cw {
dst.u[row * cw + x] = src[base + 2 * x];
dst.v[row * cw + x] = src[base + 2 * x + 1];
}
}
true
}
#[allow(clippy::too_many_arguments)]
pub fn copy_nv12_planes(
src: &[u8],
width: u32,
height: u32,
stride_y: u32,
stride_uv: u32,
dst_y: &mut [u8],
dst_uv: &mut [u8],
dst_stride_y: u32,
dst_stride_uv: u32,
) -> bool {
if !dims_ok(width, height) {
return false;
}
let w = width as usize;
let h = height as usize;
let ch = h / 2;
let sy = stride_y.max(width) as usize;
let suv = stride_uv.max(width) as usize;
let dsy = dst_stride_y as usize;
let dsuv = dst_stride_uv as usize;
if dsy < w || dsuv < w {
return false;
}
let Some(uv_offset) = sy.checked_mul(h) else {
return false;
};
let Some(uv_len) = suv.checked_mul(ch) else {
return false;
};
let Some(needed) = uv_offset.checked_add(uv_len) else {
return false;
};
if src.len() < needed || dst_y.len() < dsy * h || dst_uv.len() < dsuv * ch {
return false;
}
for row in 0..h {
let s = row * sy;
let d = row * dsy;
dst_y[d..d + w].copy_from_slice(&src[s..s + w]);
}
for row in 0..ch {
let s = uv_offset + row * suv;
let d = row * dsuv;
dst_uv[d..d + w].copy_from_slice(&src[s..s + w]);
}
true
}
#[cfg(test)]
pub fn yuyv_to_i420(src: &[u8], width: u32, height: u32, stride: u32) -> Option<I420> {
let mut dst = I420::new(width, height)?;
if !yuyv_to_i420_into(src, width, height, stride, &mut dst) {
return None;
}
Some(dst)
}
pub fn yuyv_to_i420_into(src: &[u8], width: u32, height: u32, stride: u32, dst: &mut I420) -> bool {
if !dims_ok(width, height) {
return false;
}
if !dst.has_layout(width, height) {
return false;
}
let w = width as usize;
let h = height as usize;
let cw = w / 2;
let ch = h / 2;
let stride = stride.max(width * 2) as usize;
if src.len() < stride * h {
return false;
}
for row in 0..h {
let row_base = row * stride;
for pair in 0..cw {
let src_offset = row_base + pair * 4;
let dst_offset = row * w + pair * 2;
dst.y[dst_offset] = src[src_offset];
dst.y[dst_offset + 1] = src[src_offset + 2];
}
}
for cy in 0..ch {
for cx in 0..cw {
let top = (cy * 2) * stride + cx * 4;
let bottom = (cy * 2 + 1) * stride + cx * 4;
dst.u[cy * cw + cx] =
((u16::from(src[top + 1]) + u16::from(src[bottom + 1])) / 2) as u8;
dst.v[cy * cw + cx] =
((u16::from(src[top + 3]) + u16::from(src[bottom + 3])) / 2) as u8;
}
}
true
}
fn clamp_u8(value: i32) -> u8 {
value.clamp(0, 255) as u8
}
fn rgb_to_y(r: i32, g: i32, b: i32) -> u8 {
clamp_u8(((66 * r + 129 * g + 25 * b + 128) >> 8) + 16)
}
fn rgb_to_u(r: i32, g: i32, b: i32) -> u8 {
clamp_u8(((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128)
}
fn rgb_to_v(r: i32, g: i32, b: i32) -> u8 {
clamp_u8(((112 * r - 94 * g - 18 * b + 128) >> 8) + 128)
}
#[cfg(test)]
pub fn bgra_to_i420(src: &[u8], width: u32, height: u32, stride: u32) -> Option<I420> {
let mut dst = I420::new(width, height)?;
if !bgra_to_i420_planes(
src,
width,
height,
stride,
&mut dst.y,
&mut dst.u,
&mut dst.v,
width,
width / 2,
width / 2,
) {
return None;
}
Some(dst)
}
#[allow(clippy::too_many_arguments)]
pub fn bgra_to_i420_planes(
src: &[u8],
width: u32,
height: u32,
stride: u32,
dst_y: &mut [u8],
dst_u: &mut [u8],
dst_v: &mut [u8],
dst_stride_y: u32,
dst_stride_u: u32,
dst_stride_v: u32,
) -> bool {
if !dims_ok(width, height) {
return false;
}
let w = width as usize;
let h = height as usize;
let cw = w / 2;
let ch = h / 2;
let stride = stride.max(width * 4) as usize;
if src.len() < stride * h {
return false;
}
let dsy = dst_stride_y as usize;
let dsu = dst_stride_u as usize;
let dsv = dst_stride_v as usize;
if dsy < w || dsu < cw || dsv < cw {
return false;
}
if dst_y.len() < dsy * h || dst_u.len() < dsu * ch || dst_v.len() < dsv * ch {
return false;
}
let px = |row: usize, col: usize| -> (i32, i32, i32) {
let o = row * stride + col * 4;
let b = src[o] as i32;
let g = src[o + 1] as i32;
let r = src[o + 2] as i32;
(r, g, b)
};
for row in 0..h {
for col in 0..w {
let (r, g, b) = px(row, col);
dst_y[row * dsy + col] = rgb_to_y(r, g, b);
}
}
for cy in 0..ch {
for cx in 0..cw {
let mut rs = 0;
let mut gs = 0;
let mut bs = 0;
for dy in 0..2 {
for dx in 0..2 {
let (r, g, b) = px(cy * 2 + dy, cx * 2 + dx);
rs += r;
gs += g;
bs += b;
}
}
let (r, g, b) = (rs / 4, gs / 4, bs / 4);
dst_u[cy * dsu + cx] = rgb_to_u(r, g, b);
dst_v[cy * dsv + cx] = rgb_to_v(r, g, b);
}
}
true
}
#[cfg_attr(not(feature = "camera-native"), allow(dead_code))]
pub fn rgb_to_i420(src: &[u8], width: u32, height: u32) -> Option<I420> {
let mut dst = I420::new(width, height)?;
if !rgb_to_i420_into(src, width, height, &mut dst) {
return None;
}
Some(dst)
}
pub fn rgb_to_i420_into(src: &[u8], width: u32, height: u32, dst: &mut I420) -> bool {
if !dims_ok(width, height) {
return false;
}
if !dst.has_layout(width, height) {
return false;
}
let w = width as usize;
let h = height as usize;
let cw = w / 2;
let ch = h / 2;
let stride = w * 3;
if src.len() < stride * h {
return false;
}
let px = |row: usize, col: usize| -> (i32, i32, i32) {
let o = row * stride + col * 3;
let r = src[o] as i32;
let g = src[o + 1] as i32;
let b = src[o + 2] as i32;
(r, g, b)
};
for row in 0..h {
for col in 0..w {
let (r, g, b) = px(row, col);
dst.y[row * w + col] = rgb_to_y(r, g, b);
}
}
for cy in 0..ch {
for cx in 0..cw {
let mut rs = 0;
let mut gs = 0;
let mut bs = 0;
for dy in 0..2 {
for dx in 0..2 {
let (r, g, b) = px(cy * 2 + dy, cx * 2 + dx);
rs += r;
gs += g;
bs += b;
}
}
let (r, g, b) = (rs / 4, gs / 4, bs / 4);
dst.u[cy * cw + cx] = rgb_to_u(r, g, b);
dst.v[cy * cw + cx] = rgb_to_v(r, g, b);
}
}
true
}
#[cfg_attr(not(feature = "camera-native"), allow(dead_code))]
pub fn bgr_to_i420_into(src: &[u8], width: u32, height: u32, dst: &mut I420) -> bool {
if !dims_ok(width, height) {
return false;
}
if !dst.has_layout(width, height) {
return false;
}
let w = width as usize;
let h = height as usize;
let cw = w / 2;
let ch = h / 2;
let stride = w * 3;
if src.len() < stride * h {
return false;
}
let px = |row: usize, col: usize| -> (i32, i32, i32) {
let o = row * stride + col * 3;
let b = src[o] as i32;
let g = src[o + 1] as i32;
let r = src[o + 2] as i32;
(r, g, b)
};
for row in 0..h {
for col in 0..w {
let (r, g, b) = px(row, col);
dst.y[row * w + col] = rgb_to_y(r, g, b);
}
}
for cy in 0..ch {
for cx in 0..cw {
let mut rs = 0;
let mut gs = 0;
let mut bs = 0;
for dy in 0..2 {
for dx in 0..2 {
let (r, g, b) = px(cy * 2 + dy, cx * 2 + dx);
rs += r;
gs += g;
bs += b;
}
}
let (r, g, b) = (rs / 4, gs / 4, bs / 4);
dst.u[cy * cw + cx] = rgb_to_u(r, g, b);
dst.v[cy * cw + cx] = rgb_to_v(r, g, b);
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_odd_or_tiny_dimensions() {
assert!(nv12_to_i420(&[0u8; 64], 3, 2, 3, 3).is_none());
assert!(nv12_to_i420(&[0u8; 64], 2, 1, 2, 2).is_none());
assert!(bgra_to_i420(&[0u8; 256], 2, 3, 8).is_none());
assert!(bgra_to_i420(&[0u8; 256], 0, 2, 0).is_none());
}
#[test]
fn nv12_short_buffer_is_rejected() {
assert!(nv12_to_i420(&[0u8; 5], 2, 2, 2, 2).is_none());
assert!(nv12_to_i420(&[0u8; 6], 2, 2, 2, 2).is_some());
}
#[test]
fn nv12_packed_2x2_deinterleaves() {
let src = [1u8, 2, 3, 4, 10, 20];
let out = nv12_to_i420(&src, 2, 2, 2, 2).unwrap();
assert_eq!(out.y, vec![1, 2, 3, 4]);
assert_eq!(out.u, vec![10]);
assert_eq!(out.v, vec![20]);
assert_eq!((out.width / 2, out.height / 2), (1, 1));
}
#[test]
fn yuyv_2x2_deinterleaves_and_vertically_averages_chroma() {
let src = [1u8, 10, 2, 20, 3, 30, 4, 40];
let out = yuyv_to_i420(&src, 2, 2, 4).unwrap();
assert_eq!(out.y, vec![1, 2, 3, 4]);
assert_eq!(out.u, vec![20]);
assert_eq!(out.v, vec![30]);
}
#[test]
fn yuyv_respects_row_padding() {
let src = [1u8, 10, 2, 20, 99, 99, 3, 30, 4, 40, 88, 88];
let out = yuyv_to_i420(&src, 2, 2, 6).unwrap();
assert_eq!(out.y, vec![1, 2, 3, 4]);
assert_eq!(out.u, vec![20]);
assert_eq!(out.v, vec![30]);
}
#[test]
fn nv12_4x4_deinterleaves_two_chroma_columns() {
let mut src = Vec::new();
src.extend(0u8..16);
src.extend([100, 101, 102, 103, 104, 105, 106, 107]);
let out = nv12_to_i420(&src, 4, 4, 4, 4).unwrap();
assert_eq!(out.y, (0u8..16).collect::<Vec<_>>());
assert_eq!(out.u, vec![100, 102, 104, 106]);
assert_eq!(out.v, vec![101, 103, 105, 107]);
}
#[test]
fn nv12_respects_row_padding() {
let src = [1u8, 2, 0xFF, 0xFF, 3, 4, 0xFF, 0xFF, 10, 20, 0xFF, 0xFF];
let out = nv12_to_i420(&src, 2, 2, 4, 4).unwrap();
assert_eq!(out.y, vec![1, 2, 3, 4]);
assert_eq!(out.u, vec![10]);
assert_eq!(out.v, vec![20]);
}
#[test]
fn copy_nv12_planes_preserves_nv12_layout() {
let src = [1u8, 2, 3, 4, 10, 20];
let mut y = [0u8; 4];
let mut uv = [0u8; 2];
assert!(copy_nv12_planes(&src, 2, 2, 2, 2, &mut y, &mut uv, 2, 2));
assert_eq!(y, [1, 2, 3, 4]);
assert_eq!(uv, [10, 20]);
}
#[test]
fn copy_nv12_planes_respects_destination_stride() {
let src = [1u8, 2, 0xFF, 0xFF, 3, 4, 0xFF, 0xFF, 10, 20, 0xFF, 0xFF];
let mut y = [0u8; 8];
let mut uv = [0u8; 4];
assert!(copy_nv12_planes(&src, 2, 2, 4, 4, &mut y, &mut uv, 4, 4));
assert_eq!(y, [1, 2, 0, 0, 3, 4, 0, 0]);
assert_eq!(uv, [10, 20, 0, 0]);
}
#[test]
fn copy_nv12_planes_rejects_short_buffers() {
let src = [0u8; 6];
let mut y = [0u8; 3];
let mut uv = [0u8; 2];
assert!(!copy_nv12_planes(&src, 2, 2, 2, 2, &mut y, &mut uv, 2, 2));
let mut y = [0u8; 4];
assert!(!copy_nv12_planes(
&src[..5],
2,
2,
2,
2,
&mut y,
&mut uv,
2,
2
));
}
fn solid_bgra(width: u32, height: u32, b: u8, g: u8, r: u8) -> Vec<u8> {
let mut v = Vec::with_capacity((width * height * 4) as usize);
for _ in 0..(width * height) {
v.extend([b, g, r, 255]);
}
v
}
fn near(a: u8, b: u8, tol: i32) -> bool {
(a as i32 - b as i32).abs() <= tol
}
#[test]
fn bgra_black_white_grey_levels() {
let black = bgra_to_i420(&solid_bgra(2, 2, 0, 0, 0), 2, 2, 8).unwrap();
assert!(near(black.y[0], 16, 1), "black Y={}", black.y[0]);
assert!(near(black.u[0], 128, 1) && near(black.v[0], 128, 1));
let white = bgra_to_i420(&solid_bgra(2, 2, 255, 255, 255), 2, 2, 8).unwrap();
assert!(near(white.y[0], 235, 2), "white Y={}", white.y[0]);
assert!(near(white.u[0], 128, 2) && near(white.v[0], 128, 2));
}
#[test]
fn bgra_primaries_have_expected_chroma_signs() {
let red = bgra_to_i420(&solid_bgra(2, 2, 0, 0, 255), 2, 2, 8).unwrap();
assert!(red.v[0] > 200, "red V={}", red.v[0]);
let blue = bgra_to_i420(&solid_bgra(2, 2, 255, 0, 0), 2, 2, 8).unwrap();
assert!(blue.u[0] > 200, "blue U={}", blue.u[0]);
let green = bgra_to_i420(&solid_bgra(2, 2, 0, 255, 0), 2, 2, 8).unwrap();
assert!(
green.u[0] < 60 && green.v[0] < 60,
"green U={} V={}",
green.u[0],
green.v[0]
);
}
#[test]
fn bgra_plane_sizes() {
let out = bgra_to_i420(&solid_bgra(8, 6, 10, 20, 30), 8, 6, 32).unwrap();
assert_eq!(out.y.len(), 8 * 6);
assert_eq!(out.u.len(), 4 * 3);
assert_eq!(out.v.len(), 4 * 3);
}
#[test]
fn bgra_golden_2x2_solid_colour_exact_bytes() {
let src = solid_bgra(2, 2, 32, 64, 128);
let out = bgra_to_i420(&src, 2, 2, 8).unwrap();
assert_eq!(out.width, 2);
assert_eq!(out.height, 2);
assert_eq!(out.y, vec![84, 84, 84, 84]);
assert_eq!(out.u, vec![105]);
assert_eq!(out.v, vec![158]);
}
#[test]
fn bgra_golden_strided_2x2_skips_row_padding() {
let mut src = vec![0xFFu8; 16 * 2];
for px in 0..2 {
let o = px * 4;
src[o..o + 4].copy_from_slice(&[0, 0, 0, 255]);
}
for px in 0..2 {
let o = 16 + px * 4;
src[o..o + 4].copy_from_slice(&[255, 255, 255, 255]);
}
let out = bgra_to_i420(&src, 2, 2, 16).unwrap();
assert!(
near(out.y[0], 16, 1) && near(out.y[1], 16, 1),
"row0 Y={:?}",
&out.y[0..2]
);
assert!(
near(out.y[2], 235, 2) && near(out.y[3], 235, 2),
"row1 Y={:?}",
&out.y[2..4]
);
assert!(
near(out.u[0], 128, 2) && near(out.v[0], 128, 2),
"U={} V={}",
out.u[0],
out.v[0]
);
}
#[test]
fn bgra_to_i420_planes_matches_tight_conversion_with_destination_padding() {
let src = solid_bgra(4, 2, 32, 64, 128);
let tight = bgra_to_i420(&src, 4, 2, 16).unwrap();
let mut y = [0u8; 10];
let mut u = [0u8; 4];
let mut v = [0u8; 4];
assert!(bgra_to_i420_planes(
&src, 4, 2, 16, &mut y, &mut u, &mut v, 5, 2, 2
));
assert_eq!(&y[0..4], &tight.y[0..4]);
assert_eq!(&y[5..9], &tight.y[4..8]);
assert_eq!(&u[0..2], &tight.u[0..2]);
assert_eq!(&v[0..2], &tight.v[0..2]);
}
#[test]
fn copy_tight_i420_into_reuses_existing_plane_storage() {
let mut dst = I420::new(4, 2).unwrap();
let ptrs = (dst.y.as_ptr(), dst.u.as_ptr(), dst.v.as_ptr());
let src: Vec<u8> = (0u8..12).collect();
assert_eq!(tight_i420_byte_len(4, 2), Some(12));
assert!(copy_tight_i420_into(&src, 4, 2, &mut dst));
assert_eq!(dst.y.as_ptr(), ptrs.0);
assert_eq!(dst.u.as_ptr(), ptrs.1);
assert_eq!(dst.v.as_ptr(), ptrs.2);
assert_eq!(dst.y, vec![0, 1, 2, 3, 4, 5, 6, 7]);
assert_eq!(dst.u, vec![8, 9]);
assert_eq!(dst.v, vec![10, 11]);
}
fn solid_rgb(width: u32, height: u32, r: u8, g: u8, b: u8) -> Vec<u8> {
let mut v = Vec::with_capacity((width * height * 3) as usize);
for _ in 0..(width * height) {
v.extend([r, g, b]);
}
v
}
#[test]
fn rgb_rejects_odd_dims_and_short_buffer() {
assert!(rgb_to_i420(&[0u8; 64], 3, 2).is_none());
assert!(rgb_to_i420(&[0u8; 64], 2, 1).is_none());
assert!(rgb_to_i420(&[0u8; 11], 2, 2).is_none());
assert!(rgb_to_i420(&[0u8; 12], 2, 2).is_some());
}
#[test]
fn rgb_golden_2x2_solid_colour_matches_bgra_path() {
let out = rgb_to_i420(&solid_rgb(2, 2, 128, 64, 32), 2, 2).unwrap();
assert_eq!(out.width, 2);
assert_eq!(out.height, 2);
assert_eq!(out.y, vec![84, 84, 84, 84]);
assert_eq!(out.u, vec![105]);
assert_eq!(out.v, vec![158]);
}
#[test]
fn rgb_plane_sizes_and_levels() {
let black = rgb_to_i420(&solid_rgb(2, 2, 0, 0, 0), 2, 2).unwrap();
assert!(near(black.y[0], 16, 1));
assert!(near(black.u[0], 128, 1) && near(black.v[0], 128, 1));
let white = rgb_to_i420(&solid_rgb(4, 4, 255, 255, 255), 4, 4).unwrap();
assert_eq!(white.y.len(), 16);
assert_eq!(white.u.len(), 4);
assert_eq!(white.v.len(), 4);
assert!(near(white.y[0], 235, 2));
}
#[test]
fn nv12_golden_4x2_packed_to_i420() {
let mut src = Vec::new();
src.extend(0u8..8);
src.extend([40, 41, 42, 43]);
let out = nv12_to_i420(&src, 4, 2, 4, 4).unwrap();
assert_eq!(out.width, 4);
assert_eq!(out.height, 2);
assert_eq!(out.y, (0u8..8).collect::<Vec<_>>());
assert_eq!(out.u, vec![40, 42]);
assert_eq!(out.v, vec![41, 43]);
}
}