Files
wz-phone/crates/wzp-video/src/controller.rs
Siavash Sameni 086d0a4845 T6.1.2: Wire AV1 into call engine (factory + step tables)
- New: factory.rs — create_video_encoder/decoder dispatch by CodecId with
  platform-aware HW→SW fallback. AV1 encoder: SvtAv1Encoder (universal SW).
  AV1 decoder: VideoToolboxAv1Decoder (macOS M3+) → MediaCodecAv1Decoder
  (Android) → Dav1dDecoder (all platforms fallback).
- controller.rs: codec-specific step tables (H.264/H.265/AV1). AV1 ~30%
  lower thresholds than H.264; H.265 ~20% lower. VideoQualityController
  gains codec field with with_codec()/set_codec()/codec() accessors.
- lib.rs: export factory fns and VideoToolboxAv1Decoder
- wzp-client/Cargo.toml: add wzp-video dependency
- 11 new tests (7 factory + 4 controller); 77→88 wzp-video tests; fmt +
  clippy clean; all workspace tests pass
2026-05-12 19:05:45 +04:00

753 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Video quality controller — maps bandwidth estimate + priority mode to
//! encoder target parameters (bitrate, fps, resolution).
//!
//! See `docs/PRD/PRD-video-quality-priority.md`.
use std::sync::Arc;
use std::sync::atomic::{AtomicU8, AtomicU32, Ordering::Relaxed};
use wzp_proto::BandwidthEstimator;
use wzp_proto::CodecId;
use wzp_proto::PriorityMode;
use crate::simulcast::LayerTarget;
/// Target parameters for the video encoder.
///
/// A `bitrate_kbps` of `0` means video is disabled (not enough bandwidth).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct VideoTarget {
/// Target bitrate in kilobits per second.
pub bitrate_kbps: u32,
/// Target frame rate.
pub fps: u8,
/// Frame width in pixels.
pub width: u16,
/// Frame height in pixels.
pub height: u16,
}
impl VideoTarget {
/// Disabled video — zero budget.
pub const DISABLED: Self = Self {
bitrate_kbps: 0,
fps: 0,
width: 0,
height: 0,
};
}
/// Step in the bitrate -> (resolution, fps) lookup table.
struct Step {
min_budget_kbps: u32,
width: u16,
height: u16,
fps: u8,
}
/// H.264 baseline step table. Each step is the minimum budget required to
/// sustain the corresponding resolution + frame rate.
///
/// Steps are ordered from highest to lowest budget. The first step whose
/// `min_budget_kbps` is <= the available video budget wins.
static STEP_TABLE_H264: &[Step] = &[
Step {
min_budget_kbps: 4000,
width: 1280,
height: 720,
fps: 30,
},
Step {
min_budget_kbps: 2000,
width: 640,
height: 480,
fps: 30,
},
Step {
min_budget_kbps: 1000,
width: 480,
height: 360,
fps: 30,
},
Step {
min_budget_kbps: 500,
width: 480,
height: 360,
fps: 15,
},
Step {
min_budget_kbps: 250,
width: 320,
height: 240,
fps: 15,
},
Step {
min_budget_kbps: 150,
width: 320,
height: 240,
fps: 10,
},
Step {
min_budget_kbps: 100,
width: 240,
height: 180,
fps: 10,
},
Step {
min_budget_kbps: 50,
width: 240,
height: 180,
fps: 5,
},
];
/// H.265 main step table. H.265 is ~20% more efficient than H.264,
/// so thresholds are ~80% of the H.264 values.
static STEP_TABLE_H265: &[Step] = &[
Step {
min_budget_kbps: 3200,
width: 1280,
height: 720,
fps: 30,
},
Step {
min_budget_kbps: 1600,
width: 640,
height: 480,
fps: 30,
},
Step {
min_budget_kbps: 800,
width: 480,
height: 360,
fps: 30,
},
Step {
min_budget_kbps: 400,
width: 480,
height: 360,
fps: 15,
},
Step {
min_budget_kbps: 200,
width: 320,
height: 240,
fps: 15,
},
Step {
min_budget_kbps: 120,
width: 320,
height: 240,
fps: 10,
},
Step {
min_budget_kbps: 80,
width: 240,
height: 180,
fps: 10,
},
Step {
min_budget_kbps: 40,
width: 240,
height: 180,
fps: 5,
},
];
/// AV1 main step table. AV1 is ~30% more efficient than H.264,
/// so thresholds are ~70% of the H.264 values.
static STEP_TABLE_AV1: &[Step] = &[
Step {
min_budget_kbps: 2800,
width: 1280,
height: 720,
fps: 30,
},
Step {
min_budget_kbps: 1400,
width: 640,
height: 480,
fps: 30,
},
Step {
min_budget_kbps: 700,
width: 480,
height: 360,
fps: 30,
},
Step {
min_budget_kbps: 350,
width: 480,
height: 360,
fps: 15,
},
Step {
min_budget_kbps: 175,
width: 320,
height: 240,
fps: 15,
},
Step {
min_budget_kbps: 105,
width: 320,
height: 240,
fps: 10,
},
Step {
min_budget_kbps: 70,
width: 240,
height: 180,
fps: 10,
},
Step {
min_budget_kbps: 35,
width: 240,
height: 180,
fps: 5,
},
];
/// Select the step table for the given video codec.
fn step_table_for_codec(codec: CodecId) -> &'static [Step] {
match codec {
CodecId::H264Baseline => STEP_TABLE_H264,
CodecId::H265Main => STEP_TABLE_H265,
CodecId::Av1Main => STEP_TABLE_AV1,
_ => STEP_TABLE_H264, // safe default for non-video codecs
}
}
/// Audio floor budgets per priority mode (kbps).
const AUDIO_FLOOR_KBPS: u32 = 24;
const AUDIO_FLOOR_SCREENCAST_KBPS: u32 = 16;
/// Proportion of total budget allocated to audio in `Balanced` mode.
const BALANCED_AUDIO_RATIO: f64 = 0.15;
/// Maximum bitrate change ratio per second (2x up or down).
const MAX_CHANGE_RATIO_PER_SEC: f64 = 2.0;
/// SD video floor (kbps). When ScreenShare video budget drops below this,
/// the controller recommends [`EncoderMode::SlideFallback`].
const SD_VIDEO_FLOOR_KBPS: u32 = 150;
/// Video quality controller.
///
/// Consumes a [`BandwidthEstimator`] and a [`PriorityMode`] and produces
/// [`VideoTarget`] recommendations for the encoder. The controller is
/// thread-safe: `mode`, `loss_pct`, and `rtt_ms` can be updated from any
/// thread while `tick()` runs on the encoder thread.
pub struct VideoQualityController {
bwe: Arc<BandwidthEstimator>,
mode: AtomicU8, // PriorityMode as u8
codec: AtomicU8, // CodecId as u8
loss_pct: AtomicU8,
rtt_ms: AtomicU32,
last_target: std::sync::Mutex<VideoTarget>,
last_tick_ms: AtomicU32,
}
impl VideoQualityController {
/// Create a new controller defaulting to H.264.
pub fn new(bwe: Arc<BandwidthEstimator>) -> Self {
Self::with_codec(bwe, CodecId::H264Baseline)
}
/// Create a new controller with an explicit video codec.
pub fn with_codec(bwe: Arc<BandwidthEstimator>, codec: CodecId) -> Self {
Self {
bwe,
mode: AtomicU8::new(PriorityMode::AudioFirst as u8),
codec: AtomicU8::new(codec as u8),
loss_pct: AtomicU8::new(0),
rtt_ms: AtomicU32::new(0),
last_target: std::sync::Mutex::new(VideoTarget::DISABLED),
last_tick_ms: AtomicU32::new(0),
}
}
/// Set the active video codec (mid-call codec switch).
pub fn set_codec(&self, codec: CodecId) {
self.codec.store(codec as u8, Relaxed);
}
/// Read the current video codec.
pub fn codec(&self) -> CodecId {
match self.codec.load(Relaxed) {
9 => CodecId::H264Baseline,
11 => CodecId::H265Main,
12 => CodecId::Av1Main,
_ => CodecId::H264Baseline,
}
}
/// Set the current priority mode (mid-call override).
pub fn set_mode(&self, mode: PriorityMode) {
self.mode.store(mode as u8, Relaxed);
}
/// Update network observables.
pub fn update_network(&self, loss_pct: u8, rtt_ms: u32) {
self.loss_pct.store(loss_pct, Relaxed);
self.rtt_ms.store(rtt_ms, Relaxed);
}
/// Read the current priority mode.
pub fn mode(&self) -> PriorityMode {
match self.mode.load(Relaxed) {
1 => PriorityMode::VideoFirst,
2 => PriorityMode::ScreenShare,
3 => PriorityMode::Balanced,
_ => PriorityMode::AudioFirst,
}
}
/// Recommend the encoder operating mode based on priority + budget.
///
/// Returns [`EncoderMode::SlideFallback`] when the current mode is
/// [`PriorityMode::ScreenShare`] and the video budget is below the
/// SD floor (150 kbps). Otherwise returns [`EncoderMode::Normal`].
pub fn encoder_mode(&self) -> crate::EncoderMode {
if self.mode() != PriorityMode::ScreenShare {
return crate::EncoderMode::Normal;
}
let (_audio, video) = self.allocate();
if video < SD_VIDEO_FLOOR_KBPS {
crate::EncoderMode::SlideFallback
} else {
crate::EncoderMode::Normal
}
}
/// Compute audio and video budgets from the current BWE and priority mode.
///
/// Returns `(audio_budget_kbps, video_budget_kbps)`.
pub fn allocate(&self) -> (u32, u32) {
let bwe_kbps = (self.bwe.target_send_bps() / 1000) as u32;
let mode = self.mode();
let table = step_table_for_codec(self.codec());
match mode {
PriorityMode::AudioFirst => {
let audio = AUDIO_FLOOR_KBPS.min(bwe_kbps);
let video = bwe_kbps.saturating_sub(audio);
(audio, video)
}
PriorityMode::VideoFirst => {
// Video floor: enough for the lowest step.
let video_floor = table.last().map(|s| s.min_budget_kbps).unwrap_or(50);
let video = video_floor.min(bwe_kbps);
let audio = bwe_kbps.saturating_sub(video);
(audio, video)
}
PriorityMode::ScreenShare => {
let audio = AUDIO_FLOOR_SCREENCAST_KBPS.min(bwe_kbps);
let video = bwe_kbps.saturating_sub(audio);
(audio, video)
}
PriorityMode::Balanced => {
let audio = ((bwe_kbps as f64) * BALANCED_AUDIO_RATIO) as u32;
let video = bwe_kbps.saturating_sub(audio);
(audio, video)
}
}
}
/// Map a video budget to a `(bitrate_kbps, width, height, fps)` target.
///
/// Uses the static step table. If budget is below the lowest step,
/// returns [`VideoTarget::DISABLED`].
fn derive_target(&self, video_budget_kbps: u32) -> VideoTarget {
let table = step_table_for_codec(self.codec());
for step in table {
if video_budget_kbps >= step.min_budget_kbps {
return VideoTarget {
bitrate_kbps: video_budget_kbps,
fps: step.fps,
width: step.width,
height: step.height,
};
}
}
VideoTarget::DISABLED
}
/// Smooth the target to avoid jumps larger than `MAX_CHANGE_RATIO_PER_SEC`
/// over one second.
///
/// `dt_ms` is the elapsed time since the last tick.
fn smooth(&self, raw: VideoTarget, dt_ms: u32) -> VideoTarget {
if raw.bitrate_kbps == 0 {
return raw;
}
let last = *self.last_target.lock().unwrap();
if last.bitrate_kbps == 0 {
return raw;
}
let dt_s = dt_ms as f64 / 1000.0;
let max_ratio = MAX_CHANGE_RATIO_PER_SEC.powf(dt_s);
let min_br = (last.bitrate_kbps as f64 / max_ratio) as u32;
let max_br = (last.bitrate_kbps as f64 * max_ratio) as u32;
let clamped_br = raw.bitrate_kbps.clamp(min_br, max_br);
VideoTarget {
bitrate_kbps: clamped_br,
..raw
}
}
/// Run one controller tick.
///
/// `now_ms` is a monotonic timestamp (e.g. `timestamp_ms` from the media
/// pipeline). Returns the current [`VideoTarget`] which the caller should
/// pass to the encoder.
pub fn tick(&self, now_ms: u32) -> VideoTarget {
let (_audio_budget, video_budget) = self.allocate();
let raw = self.derive_target(video_budget);
let prev = self.last_tick_ms.swap(now_ms, Relaxed);
let dt_ms = if prev == 0 {
1000
} else {
now_ms.saturating_sub(prev)
};
let smoothed = self.smooth(raw, dt_ms);
*self.last_target.lock().unwrap() = smoothed;
smoothed
}
/// Run one simulcast controller tick.
///
/// Returns a 3-element array of [`LayerTarget`] in order low → mid → high.
/// A layer is marked `active = true` when the current video budget can
/// sustain it (including all lower layers).
pub fn tick_simulcast(&self, now_ms: u32) -> [LayerTarget; 3] {
use crate::simulcast::SimulcastLayer;
let (_audio_budget, video_budget) = self.allocate();
let mut result = [
LayerTarget {
layer: SimulcastLayer::LOW,
active: false,
},
LayerTarget {
layer: SimulcastLayer::MID,
active: false,
},
LayerTarget {
layer: SimulcastLayer::HIGH,
active: false,
},
];
// Cumulative bitrate required to sustain layers up to index i.
let cumulative = [
SimulcastLayer::LOW.bitrate_kbps,
SimulcastLayer::LOW.bitrate_kbps + SimulcastLayer::MID.bitrate_kbps,
SimulcastLayer::total_bitrate_kbps(),
];
for (i, target) in result.iter_mut().enumerate() {
target.active = video_budget >= cumulative[i];
}
// Update internal smoothing state using the highest active layer's
// bitrate as the representative value.
let highest_active = result
.iter()
.rposition(|t| t.active)
.map(|i| cumulative[i])
.unwrap_or(0);
let raw = if highest_active > 0 {
self.derive_target(highest_active)
} else {
VideoTarget::DISABLED
};
let prev = self.last_tick_ms.swap(now_ms, Relaxed);
let dt_ms = if prev == 0 {
1000
} else {
now_ms.saturating_sub(prev)
};
let smoothed = self.smooth(raw, dt_ms);
*self.last_target.lock().unwrap() = smoothed;
result
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_bwe(bps: u64) -> Arc<BandwidthEstimator> {
let bwe = BandwidthEstimator::new((bps / 1000) as f64, 10.0, 100_000.0);
// Seed cwnd so target_send_bps() returns a non-zero value.
// cwnd_bps = cwnd_bytes * 8 / rtt_s. For 1s RTT: cwnd_bytes = bps / 8.
let cwnd_bytes = bps / 8;
bwe.update_from_path(cwnd_bytes, 0, 1000);
Arc::new(bwe)
}
#[test]
fn audio_first_reserves_floor() {
let bwe = dummy_bwe(100_000); // 100 kbps
let ctrl = VideoQualityController::new(bwe);
let (audio, video) = ctrl.allocate();
// BWE target is 90% of raw = 90 kbps.
assert_eq!(audio, 24, "audio floor is 24 kbps");
assert_eq!(video, 66, "video gets remainder after 90% BWE factor");
}
#[test]
fn audio_first_floor_not_below_bwe() {
let bwe = dummy_bwe(10_000); // 10 kbps
let ctrl = VideoQualityController::new(bwe);
let (audio, video) = ctrl.allocate();
// BWE target is 90% of raw = 9 kbps.
assert_eq!(audio, 9, "audio cannot exceed bwe");
assert_eq!(video, 0, "video gets nothing");
}
#[test]
fn screen_share_clamps_audio() {
let bwe = dummy_bwe(200_000); // 200 kbps
let ctrl = VideoQualityController::new(bwe);
ctrl.set_mode(PriorityMode::ScreenShare);
let (audio, video) = ctrl.allocate();
// BWE target is 90% of raw = 180 kbps.
assert_eq!(audio, 16, "screen-share audio clamped to 16 kbps");
assert_eq!(video, 164);
}
#[test]
fn balanced_split() {
let bwe = dummy_bwe(1_000_000); // 1 Mbps
let ctrl = VideoQualityController::new(bwe);
ctrl.set_mode(PriorityMode::Balanced);
let (audio, video) = ctrl.allocate();
// BWE target is 90% of raw = 900 kbps.
assert_eq!(audio, 135, "15% of 900 kbps = 135 kbps audio");
assert_eq!(video, 765);
}
#[test]
fn derive_target_disabled_below_floor() {
let bwe = dummy_bwe(1_000_000);
let ctrl = VideoQualityController::new(bwe);
let target = ctrl.derive_target(10); // below lowest step (50 kbps)
assert_eq!(target, VideoTarget::DISABLED);
}
#[test]
fn derive_target_lowest_step() {
let bwe = dummy_bwe(1_000_000);
let ctrl = VideoQualityController::new(bwe);
let target = ctrl.derive_target(50);
assert_eq!(target.width, 240);
assert_eq!(target.height, 180);
assert_eq!(target.fps, 5);
}
#[test]
fn derive_target_highest_step() {
let bwe = dummy_bwe(10_000_000); // 10 Mbps
let ctrl = VideoQualityController::new(bwe);
let target = ctrl.derive_target(5000);
assert_eq!(target.width, 1280);
assert_eq!(target.height, 720);
assert_eq!(target.fps, 30);
}
#[test]
fn smoothing_limits_jump() {
let bwe = dummy_bwe(10_000_000);
let ctrl = VideoQualityController::new(bwe);
// First tick establishes baseline at 720p.
let t0 = ctrl.tick(0);
assert!(t0.bitrate_kbps > 0);
// Simulate a BWE drop from 10 Mbps to 1 Mbps.
let bwe2 = dummy_bwe(1_000_000);
let ctrl2 = VideoQualityController::new(bwe2);
// Pre-seed last_target so smoothing has something to compare against.
*ctrl2.last_target.lock().unwrap() = VideoTarget {
bitrate_kbps: 4000,
..VideoTarget::DISABLED
};
ctrl2.last_tick_ms.store(0, Relaxed);
let t1 = ctrl2.tick(1000); // 1 s later
// Max change per second is 2x, so 4000 -> min 2000.
assert!(
t1.bitrate_kbps >= 2000,
"smoothing should prevent >2x drop in 1s"
);
// Raw budget after 1 Mbps drop is ~900 kbps; smoothing clamps to 2000.
assert!(
t1.bitrate_kbps < 4000,
"smoothing should also cap upward jumps"
);
}
#[test]
fn mode_roundtrip() {
let bwe = dummy_bwe(1_000_000);
let ctrl = VideoQualityController::new(bwe);
assert_eq!(ctrl.mode(), PriorityMode::AudioFirst);
ctrl.set_mode(PriorityMode::ScreenShare);
assert_eq!(ctrl.mode(), PriorityMode::ScreenShare);
}
#[test]
fn screenshare_above_floor_is_normal() {
// 1 Mbps → ~900 kbps after 90% factor. Video budget ~884 kbps > 150.
let bwe = dummy_bwe(1_000_000);
let ctrl = VideoQualityController::new(bwe);
ctrl.set_mode(PriorityMode::ScreenShare);
assert_eq!(ctrl.encoder_mode(), crate::EncoderMode::Normal);
}
#[test]
fn screenshare_below_floor_is_slide_fallback() {
// 100 kbps → ~90 kbps after 90% factor. Video budget ~74 kbps < 150.
let bwe = dummy_bwe(100_000);
let ctrl = VideoQualityController::new(bwe);
ctrl.set_mode(PriorityMode::ScreenShare);
assert_eq!(ctrl.encoder_mode(), crate::EncoderMode::SlideFallback);
}
#[test]
fn non_screenshare_never_slide_fallback() {
let bwe = dummy_bwe(50_000);
let ctrl = VideoQualityController::new(bwe);
ctrl.set_mode(PriorityMode::AudioFirst);
assert_eq!(ctrl.encoder_mode(), crate::EncoderMode::Normal);
ctrl.set_mode(PriorityMode::VideoFirst);
assert_eq!(ctrl.encoder_mode(), crate::EncoderMode::Normal);
ctrl.set_mode(PriorityMode::Balanced);
assert_eq!(ctrl.encoder_mode(), crate::EncoderMode::Normal);
}
#[test]
fn simulcast_all_layers_at_4mbps() {
// 4 Mbps → ~3600 kbps video budget after audio floor.
let bwe = dummy_bwe(4_000_000);
let ctrl = VideoQualityController::new(bwe);
let layers = ctrl.tick_simulcast(0);
assert!(layers[0].active, "low should be active");
assert!(layers[1].active, "mid should be active");
assert!(layers[2].active, "high should be active");
}
#[test]
fn simulcast_low_mid_only_at_1mbps() {
// 1 Mbps → ~900 kbps video budget. High needs 3250 total.
let bwe = dummy_bwe(1_000_000);
let ctrl = VideoQualityController::new(bwe);
let layers = ctrl.tick_simulcast(0);
assert!(layers[0].active, "low should be active");
assert!(layers[1].active, "mid should be active");
assert!(!layers[2].active, "high should be inactive");
}
#[test]
fn simulcast_low_only_at_200kbps() {
// 200 kbps → ~180 kbps video budget. Mid needs 750 total.
let bwe = dummy_bwe(200_000);
let ctrl = VideoQualityController::new(bwe);
let layers = ctrl.tick_simulcast(0);
assert!(layers[0].active, "low should be active");
assert!(!layers[1].active, "mid should be inactive");
assert!(!layers[2].active, "high should be inactive");
}
#[test]
fn simulcast_no_video_at_20kbps() {
// 20 kbps → ~18 kbps total. Below audio floor.
let bwe = dummy_bwe(20_000);
let ctrl = VideoQualityController::new(bwe);
let layers = ctrl.tick_simulcast(0);
assert!(!layers[0].active, "low should be inactive");
assert!(!layers[1].active, "mid should be inactive");
assert!(!layers[2].active, "high should be inactive");
}
#[test]
fn av1_step_table_lower_than_h264() {
// At 1500 kbps budget:
// - H.264: below 2000 kbps step → 480×360 @ 30fps
// - AV1: above 1400 kbps step → 640×480 @ 30fps
let bwe = dummy_bwe(2_000_000); // ~1800 kbps after 90% factor
let h264_ctrl = VideoQualityController::with_codec(bwe.clone(), CodecId::H264Baseline);
let av1_ctrl = VideoQualityController::with_codec(bwe.clone(), CodecId::Av1Main);
let h264_target = h264_ctrl.derive_target(1800);
let av1_target = av1_ctrl.derive_target(1800);
assert_eq!(h264_target.width, 480);
assert_eq!(
av1_target.width, 640,
"AV1 should sustain higher res at same budget"
);
}
#[test]
fn h265_step_table_between_h264_and_av1() {
let bwe = dummy_bwe(2_000_000);
let h264_ctrl = VideoQualityController::with_codec(bwe.clone(), CodecId::H264Baseline);
let h265_ctrl = VideoQualityController::with_codec(bwe.clone(), CodecId::H265Main);
let av1_ctrl = VideoQualityController::with_codec(bwe.clone(), CodecId::Av1Main);
let h264_target = h264_ctrl.derive_target(1800);
let h265_target = h265_ctrl.derive_target(1800);
let av1_target = av1_ctrl.derive_target(1800);
// H.265 should be better than H.264 but worse than AV1 at the same budget.
assert!(h265_target.width >= h264_target.width);
assert!(av1_target.width >= h265_target.width);
}
#[test]
fn codec_switch_changes_target() {
let bwe = dummy_bwe(2_000_000);
let ctrl = VideoQualityController::with_codec(bwe, CodecId::H264Baseline);
let h264_target = ctrl.derive_target(1800);
assert_eq!(h264_target.width, 480);
ctrl.set_codec(CodecId::Av1Main);
let av1_target = ctrl.derive_target(1800);
assert_eq!(av1_target.width, 640);
ctrl.set_codec(CodecId::H265Main);
let h265_target = ctrl.derive_target(1800);
assert_eq!(h265_target.width, 640);
}
#[test]
fn av1_video_first_floor_lower_than_h264() {
// VideoFirst mode reserves the video floor first.
// AV1 floor (35 kbps) < H.264 floor (50 kbps).
let bwe_h264 = dummy_bwe(100_000);
let h264_ctrl = VideoQualityController::with_codec(bwe_h264, CodecId::H264Baseline);
h264_ctrl.set_mode(PriorityMode::VideoFirst);
let (_audio_h264, video_h264) = h264_ctrl.allocate();
assert_eq!(video_h264, 50); // H.264 floor
let bwe_av1 = dummy_bwe(100_000);
let av1_ctrl = VideoQualityController::with_codec(bwe_av1, CodecId::Av1Main);
av1_ctrl.set_mode(PriorityMode::VideoFirst);
let (_audio_av1, video_av1) = av1_ctrl.allocate();
assert_eq!(video_av1, 35); // AV1 floor
}
}