use std::collections::VecDeque;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Instant;
use crate::client::Client;
use crate::metrics::SfuMetrics;
use crate::net::{IncomingDatagram, SfuProtocol};
use crate::propagate::Propagated;
mod drive;
mod lifecycle;
#[cfg(any(test, feature = "test-utils"))]
mod test_seams;
#[cfg(all(feature = "googcc-bwe", not(feature = "kalman-bwe")))]
compile_error!(
"googcc-bwe requires kalman-bwe at the Registry level: the `bandwidth` field on \
`Registry` that hosts GoogCC's per-subscriber ceiling is gated on `kalman-bwe` alone \
(see the `Registry` struct definition in this file). Enable both features, or drive \
`oxpulse_sfu_kit::BandwidthEstimator` directly without `Registry`."
);
#[derive(Debug)]
pub struct Registry {
pub(super) clients: Vec<Client>,
pub(super) to_propagate: VecDeque<Propagated>,
pub(super) metrics: Arc<SfuMetrics>,
#[cfg(feature = "active-speaker")]
pub(super) detector: dominant_speaker::ActiveSpeakerDetector,
#[cfg(feature = "active-speaker")]
detector_epoch: std::time::Instant,
#[cfg(feature = "kalman-bwe")]
pub(crate) bandwidth: crate::bwe::estimator::BandwidthEstimator,
#[cfg(all(feature = "kalman-bwe", feature = "googcc-bwe"))]
bwe_epoch: std::time::Instant,
}
impl Registry {
pub fn new(metrics: Arc<SfuMetrics>) -> Self {
Self {
clients: Vec::new(),
to_propagate: VecDeque::new(),
metrics,
#[cfg(feature = "active-speaker")]
detector: dominant_speaker::ActiveSpeakerDetector::new(),
#[cfg(feature = "active-speaker")]
detector_epoch: std::time::Instant::now(),
#[cfg(feature = "kalman-bwe")]
bandwidth: crate::bwe::estimator::BandwidthEstimator::new(),
#[cfg(all(feature = "kalman-bwe", feature = "googcc-bwe"))]
bwe_epoch: std::time::Instant::now(),
}
}
#[cfg(any(test, feature = "test-utils"))]
pub fn new_for_tests() -> Self {
Self::new(Arc::new(SfuMetrics::new_default()))
}
pub fn is_empty(&self) -> bool {
self.clients.is_empty()
}
pub fn len(&self) -> usize {
self.clients.len()
}
pub fn clients(&self) -> &[Client] {
&self.clients
}
pub fn insert(&mut self, mut client: Client) {
client.metrics = self.metrics.clone();
#[cfg(feature = "metrics-prometheus")]
{
client.video_frames_dropped = self.metrics.peer_drop_counter(*client.id);
}
for entry in self.clients.iter().flat_map(|c| c.tracks_in.iter()) {
client.handle_track_open(std::sync::Arc::downgrade(&entry.id));
}
#[cfg(feature = "active-speaker")]
{
let register = !client.is_relay();
client.in_speaker_detector = register;
if register {
let now_ms = self.now_ms();
self.detector.add_peer(*client.id, now_ms);
}
}
self.metrics.inc_client_connect();
self.metrics.inc_active_participants();
self.clients.push(client);
}
pub fn handle_incoming(
&mut self,
source: SocketAddr,
destination: SocketAddr,
payload: &[u8],
) -> bool {
let datagram = IncomingDatagram {
received_at: Instant::now(),
proto: SfuProtocol::Udp,
source,
destination,
contents: payload.to_vec(),
};
if let Some(client) = self.clients.iter_mut().find(|c| c.accepts(&datagram)) {
client.handle_input(datagram);
true
} else {
tracing::debug!(?source, "no client accepts udp datagram");
false
}
}
#[cfg(feature = "active-speaker")]
#[cfg_attr(docsrs, doc(cfg(feature = "active-speaker")))]
pub fn record_audio_level(&mut self, peer_id: u64, level_raw: u8, now: Instant) {
if !self
.clients
.iter()
.any(|c| *c.id == peer_id && c.in_speaker_detector)
{
return;
}
let now_ms = now
.saturating_duration_since(self.detector_epoch)
.as_millis() as u64;
self.detector.record_level(peer_id, level_raw, now_ms);
}
#[cfg(feature = "active-speaker")]
fn now_ms(&self) -> u64 {
self.detector_epoch.elapsed().as_millis() as u64
}
#[cfg(feature = "active-speaker")]
#[cfg_attr(docsrs, doc(cfg(feature = "active-speaker")))]
#[must_use]
pub fn peer_audio_scores(&self) -> Vec<(u64, f64, f64, f64)> {
self.detector.peer_scores()
}
}
#[cfg(feature = "kalman-bwe")]
#[cfg_attr(docsrs, doc(cfg(feature = "kalman-bwe")))]
impl Registry {
pub fn on_twcc_feedback(
&mut self,
subscriber: crate::propagate::ClientId,
feedback: &crate::bwe::feedback::TwccFeedback,
now: std::time::Instant,
) {
self.bandwidth.on_twcc_feedback(subscriber, feedback, now);
}
#[must_use]
pub fn bandwidth(&self) -> &crate::bwe::estimator::BandwidthEstimator {
&self.bandwidth
}
}
#[cfg(all(feature = "kalman-bwe", feature = "googcc-bwe"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "kalman-bwe", feature = "googcc-bwe"))))]
impl Registry {
pub fn enable_googcc_for_subscriber(&mut self, subscriber: crate::propagate::ClientId) {
self.bandwidth.enable_googcc_for_subscriber(subscriber);
}
#[must_use]
pub fn googcc_ceiling_for_subscriber_mut(
&mut self,
subscriber: crate::propagate::ClientId,
) -> Option<&mut crate::bwe::GoogCcEstimator> {
self.bandwidth.googcc_for_subscriber_mut(subscriber)
}
}
#[cfg(all(test, feature = "kalman-bwe"))]
mod bandwidth_accessor_tests {
use super::Registry;
use crate::propagate::ClientId;
#[test]
fn bandwidth_reads_native_estimate_fed_via_on_twcc_feedback_seam() {
let mut reg = Registry::new_for_tests();
let id = ClientId(1);
reg.bandwidth_mut_for_tests()
.record_native_estimate(id, 1_000_000.0);
assert!(
reg.bandwidth()
.estimate_bps(id, std::time::Instant::now())
.is_some(),
"bandwidth() must observe state written via the estimator seam"
);
}
#[test]
fn bandwidth_returns_none_for_unknown_subscriber() {
let reg = Registry::new_for_tests();
assert!(reg
.bandwidth()
.estimate_bps(ClientId(99), std::time::Instant::now())
.is_none());
}
}
#[cfg(all(test, feature = "kalman-bwe", feature = "googcc-bwe"))]
mod googcc_pass_through_tests {
use super::Registry;
use crate::propagate::ClientId;
#[test]
fn enable_googcc_for_subscriber_is_idempotent_and_reachable_via_ceiling_mut() {
let mut reg = Registry::new_for_tests();
let id = ClientId(7);
assert!(reg.googcc_ceiling_for_subscriber_mut(id).is_none());
reg.enable_googcc_for_subscriber(id);
assert!(reg.googcc_ceiling_for_subscriber_mut(id).is_some());
reg.googcc_ceiling_for_subscriber_mut(id)
.unwrap()
.on_receive(100.0, 95.0, 0.0);
let bps_before = reg
.googcc_ceiling_for_subscriber_mut(id)
.unwrap()
.current_bps();
reg.enable_googcc_for_subscriber(id);
let bps_after = reg
.googcc_ceiling_for_subscriber_mut(id)
.unwrap()
.current_bps();
assert_eq!(
bps_before, bps_after,
"re-enabling must not reset existing GoogCC state"
);
}
#[test]
fn googcc_ceiling_applies_through_bandwidth_accessor() {
let mut reg = Registry::new_for_tests();
let id = ClientId(8);
let now = std::time::Instant::now();
reg.bandwidth_mut_for_tests()
.force_high_estimate_for_tests(id, 5_000_000.0);
let bps_before = reg.bandwidth().estimate_bps(id, now).unwrap();
assert!(
bps_before > 1_000_000,
"expected a high pre-ceiling estimate"
);
reg.enable_googcc_for_subscriber(id);
let gcc = reg.googcc_ceiling_for_subscriber_mut(id).unwrap();
for i in 0..20 {
gcc.on_receive(i as f64 * 10.0, i as f64 * 10.0, 0.5);
}
let bps_after = reg.bandwidth().estimate_bps(id, now).unwrap();
assert!(
bps_after < bps_before,
"GoogCC ceiling fed via the Registry pass-through did not apply: \
before={bps_before}, after={bps_after}"
);
}
}