#![allow(clippy::cast_lossless)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::similar_names)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::struct_excessive_bools)]
#![forbid(unsafe_code)]
use crate::frame::FrameType;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AllocationStrategy {
Uniform,
ComplexityBased,
MultiPass,
Hierarchical,
Constrained,
Adaptive,
}
impl Default for AllocationStrategy {
fn default() -> Self {
Self::ComplexityBased
}
}
#[derive(Clone, Debug)]
pub struct BitrateAllocator {
strategy: AllocationStrategy,
target_bitrate: u64,
framerate: f64,
gop_length: u32,
i_p_ratio: f32,
p_b_ratio: f32,
complexity_weight: f32,
complexity_history: Vec<f32>,
bit_usage_history: Vec<u64>,
max_history: usize,
vbv_buffer_size: Option<u64>,
bit_reservoir: i64,
max_reservoir: i64,
current_gop: u32,
frames_in_gop: u32,
bits_used_in_gop: u64,
gop_target_bits: u64,
}
impl BitrateAllocator {
#[must_use]
pub fn new(target_bitrate: u64, framerate: f64, gop_length: u32) -> Self {
let max_reservoir = (target_bitrate as i64) * 2;
Self {
strategy: AllocationStrategy::default(),
target_bitrate,
framerate,
gop_length,
i_p_ratio: 3.0,
p_b_ratio: 2.0,
complexity_weight: 1.0,
complexity_history: Vec::new(),
bit_usage_history: Vec::new(),
max_history: 100,
vbv_buffer_size: None,
bit_reservoir: 0,
max_reservoir,
current_gop: 0,
frames_in_gop: 0,
bits_used_in_gop: 0,
gop_target_bits: 0,
}
}
pub fn set_strategy(&mut self, strategy: AllocationStrategy) {
self.strategy = strategy;
}
pub fn set_i_p_ratio(&mut self, ratio: f32) {
self.i_p_ratio = ratio.max(1.0);
}
pub fn set_p_b_ratio(&mut self, ratio: f32) {
self.p_b_ratio = ratio.max(1.0);
}
pub fn set_complexity_weight(&mut self, weight: f32) {
self.complexity_weight = weight.clamp(0.0, 2.0);
}
pub fn set_vbv_buffer_size(&mut self, size: u64) {
self.vbv_buffer_size = Some(size);
if self.strategy == AllocationStrategy::Uniform {
self.strategy = AllocationStrategy::Constrained;
}
}
#[must_use]
pub fn allocate_frame_bits(
&self,
frame_type: FrameType,
complexity: f32,
frame_in_gop: u32,
) -> AllocationResult {
let base_bits = self.calculate_base_allocation();
let allocated_bits = match self.strategy {
AllocationStrategy::Uniform => self.allocate_uniform(base_bits, frame_type),
AllocationStrategy::ComplexityBased => {
self.allocate_complexity_based(base_bits, frame_type, complexity)
}
AllocationStrategy::MultiPass => {
self.allocate_multipass(base_bits, frame_type, complexity)
}
AllocationStrategy::Hierarchical => {
self.allocate_hierarchical(base_bits, frame_type, frame_in_gop)
}
AllocationStrategy::Constrained => {
self.allocate_constrained(base_bits, frame_type, complexity)
}
AllocationStrategy::Adaptive => {
self.allocate_adaptive(base_bits, frame_type, complexity)
}
};
let mut max_bits = allocated_bits * 4;
if let Some(vbv_size) = self.vbv_buffer_size {
max_bits = max_bits.min(vbv_size);
}
AllocationResult {
target_bits: allocated_bits,
min_bits: allocated_bits / 4,
max_bits,
frame_type,
complexity_factor: complexity / self.average_complexity(),
reservoir_adjustment: self.calculate_reservoir_adjustment(allocated_bits),
}
}
fn calculate_base_allocation(&self) -> u64 {
if self.framerate <= 0.0 {
return 0;
}
(self.target_bitrate as f64 / self.framerate) as u64
}
fn allocate_uniform(&self, base_bits: u64, frame_type: FrameType) -> u64 {
match frame_type {
FrameType::Key => (base_bits as f32 * self.i_p_ratio) as u64,
FrameType::Inter => base_bits,
FrameType::BiDir => (base_bits as f32 / self.p_b_ratio) as u64,
FrameType::Switch => (base_bits as f32 * 1.5) as u64,
}
}
fn allocate_complexity_based(
&self,
base_bits: u64,
frame_type: FrameType,
complexity: f32,
) -> u64 {
let type_bits = self.allocate_uniform(base_bits, frame_type);
let avg_complexity = self.average_complexity();
if avg_complexity <= 0.0 {
return type_bits;
}
let complexity_ratio = complexity / avg_complexity;
let complexity_multiplier = 1.0 + (complexity_ratio - 1.0) * self.complexity_weight;
(type_bits as f32 * complexity_multiplier.clamp(0.5, 2.0)) as u64
}
fn allocate_multipass(&self, base_bits: u64, frame_type: FrameType, complexity: f32) -> u64 {
if self.complexity_history.is_empty() {
return self.allocate_complexity_based(base_bits, frame_type, complexity);
}
let total_complexity: f32 = self.complexity_history.iter().sum();
if total_complexity <= 0.0 {
return self.allocate_complexity_based(base_bits, frame_type, complexity);
}
let gop_bits = base_bits * self.gop_length as u64;
let frame_proportion = complexity / total_complexity;
let mut allocated = (gop_bits as f32 * frame_proportion) as u64;
allocated = match frame_type {
FrameType::Key => (allocated as f32 * self.i_p_ratio) as u64,
FrameType::BiDir => (allocated as f32 / self.p_b_ratio) as u64,
_ => allocated,
};
allocated.max(base_bits / 4)
}
fn allocate_hierarchical(
&self,
base_bits: u64,
frame_type: FrameType,
frame_in_gop: u32,
) -> u64 {
let type_bits = self.allocate_uniform(base_bits, frame_type);
match frame_type {
FrameType::BiDir => {
let pyramid_level = self.calculate_pyramid_level(frame_in_gop);
let level_multiplier = 1.0 + (pyramid_level as f32 * 0.2);
(type_bits as f32 * level_multiplier) as u64
}
_ => type_bits,
}
}
fn calculate_pyramid_level(&self, frame_in_gop: u32) -> u32 {
let mut level = 0;
let mut pos = frame_in_gop;
while pos % 2 == 0 && pos > 0 {
level += 1;
pos /= 2;
}
level
}
fn allocate_constrained(&self, base_bits: u64, frame_type: FrameType, complexity: f32) -> u64 {
let mut allocated = self.allocate_complexity_based(base_bits, frame_type, complexity);
if let Some(vbv_size) = self.vbv_buffer_size {
let buffer_level = self.estimate_buffer_level();
let fullness_ratio = buffer_level as f32 / vbv_size as f32;
if fullness_ratio > 0.8 {
let reduction = (fullness_ratio - 0.8) * 5.0; allocated = (allocated as f32 * (1.0 - reduction).max(0.5)) as u64;
}
allocated = allocated.min(vbv_size);
}
allocated
}
fn allocate_adaptive(&self, base_bits: u64, frame_type: FrameType, complexity: f32) -> u64 {
let mut allocated = self.allocate_complexity_based(base_bits, frame_type, complexity);
if !self.bit_usage_history.is_empty() {
let recent_usage: u64 = self.bit_usage_history.iter().rev().take(10).sum();
let recent_target = base_bits * 10.min(self.bit_usage_history.len()) as u64;
if recent_target > 0 {
let usage_ratio = recent_usage as f32 / recent_target as f32;
if usage_ratio > 1.2 {
allocated = (allocated as f32 * 0.9) as u64;
} else if usage_ratio < 0.8 {
allocated = (allocated as f32 * 1.1) as u64;
}
}
}
let reservoir_adjustment = self.calculate_reservoir_adjustment(allocated);
((allocated as i64) + reservoir_adjustment).max(base_bits as i64 / 4) as u64
}
fn calculate_reservoir_adjustment(&self, target: u64) -> i64 {
if self.bit_reservoir == 0 {
return 0;
}
let reservoir_ratio = self.bit_reservoir as f32 / self.max_reservoir as f32;
let adjustment = (target as f32 * reservoir_ratio * 0.1) as i64;
adjustment.clamp(-(target as i64 / 4), target as i64 / 4)
}
fn estimate_buffer_level(&self) -> u64 {
if let Some(vbv_size) = self.vbv_buffer_size {
let bits_per_frame = self.calculate_base_allocation();
let recent_frames = 10.min(self.bit_usage_history.len());
if recent_frames == 0 {
return vbv_size / 2; }
let recent_bits: u64 = self
.bit_usage_history
.iter()
.rev()
.take(recent_frames)
.sum();
let target_bits = bits_per_frame * recent_frames as u64;
if recent_bits < target_bits {
let saved = target_bits - recent_bits;
(vbv_size / 2 + saved).min(vbv_size)
} else {
let overage = recent_bits - target_bits;
(vbv_size / 2).saturating_sub(overage)
}
} else {
0
}
}
fn average_complexity(&self) -> f32 {
if self.complexity_history.is_empty() {
return 1.0;
}
let sum: f32 = self.complexity_history.iter().sum();
(sum / self.complexity_history.len() as f32).max(0.01)
}
pub fn update(&mut self, complexity: f32, bits_used: u64) {
self.complexity_history.push(complexity);
if self.complexity_history.len() > self.max_history {
self.complexity_history.remove(0);
}
self.bit_usage_history.push(bits_used);
if self.bit_usage_history.len() > self.max_history {
self.bit_usage_history.remove(0);
}
let target_per_frame = self.calculate_base_allocation();
self.bit_reservoir += target_per_frame as i64 - bits_used as i64;
self.bit_reservoir = self
.bit_reservoir
.clamp(-self.max_reservoir, self.max_reservoir);
self.frames_in_gop += 1;
self.bits_used_in_gop += bits_used;
}
pub fn start_new_gop(&mut self) {
self.current_gop += 1;
self.frames_in_gop = 0;
self.bits_used_in_gop = 0;
self.gop_target_bits = self.calculate_base_allocation() * self.gop_length as u64;
}
#[must_use]
pub fn gop_status(&self) -> GopAllocationStatus {
let remaining_frames = self.gop_length.saturating_sub(self.frames_in_gop);
let remaining_bits = self.gop_target_bits.saturating_sub(self.bits_used_in_gop);
GopAllocationStatus {
gop_index: self.current_gop,
frames_encoded: self.frames_in_gop,
frames_remaining: remaining_frames,
bits_used: self.bits_used_in_gop,
bits_remaining: remaining_bits,
target_bits: self.gop_target_bits,
on_target: self.is_gop_on_target(),
}
}
fn is_gop_on_target(&self) -> bool {
if self.frames_in_gop == 0 {
return true;
}
let expected_bits = (self.gop_target_bits as f32
* (self.frames_in_gop as f32 / self.gop_length as f32))
as u64;
let accuracy = self.bits_used_in_gop as f32 / expected_bits as f32;
accuracy > 0.8 && accuracy < 1.2
}
pub fn reset(&mut self) {
self.complexity_history.clear();
self.bit_usage_history.clear();
self.bit_reservoir = 0;
self.current_gop = 0;
self.frames_in_gop = 0;
self.bits_used_in_gop = 0;
self.gop_target_bits = 0;
}
}
#[derive(Clone, Debug)]
pub struct AllocationResult {
pub target_bits: u64,
pub min_bits: u64,
pub max_bits: u64,
pub frame_type: FrameType,
pub complexity_factor: f32,
pub reservoir_adjustment: i64,
}
impl AllocationResult {
#[must_use]
pub fn is_within_range(&self, actual_bits: u64) -> bool {
actual_bits >= self.min_bits && actual_bits <= self.max_bits
}
#[must_use]
pub fn accuracy(&self, actual_bits: u64) -> f32 {
if self.target_bits == 0 {
return 1.0;
}
actual_bits as f32 / self.target_bits as f32
}
}
#[derive(Clone, Debug)]
pub struct GopAllocationStatus {
pub gop_index: u32,
pub frames_encoded: u32,
pub frames_remaining: u32,
pub bits_used: u64,
pub bits_remaining: u64,
pub target_bits: u64,
pub on_target: bool,
}
impl GopAllocationStatus {
#[must_use]
pub fn average_bits_per_frame(&self) -> u64 {
if self.frames_encoded == 0 {
return 0;
}
self.bits_used / self.frames_encoded as u64
}
#[must_use]
pub fn recommended_bits_per_frame(&self) -> u64 {
if self.frames_remaining == 0 {
return 0;
}
self.bits_remaining / self.frames_remaining as u64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_allocator_creation() {
let allocator = BitrateAllocator::new(5_000_000, 30.0, 250);
assert_eq!(allocator.target_bitrate, 5_000_000);
assert_eq!(allocator.gop_length, 250);
}
#[test]
fn test_base_allocation() {
let allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
let base = allocator.calculate_base_allocation();
assert_eq!(base, 100_000); }
#[test]
fn test_uniform_allocation() {
let allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
let base = 100_000;
let i_bits = allocator.allocate_uniform(base, FrameType::Key);
let p_bits = allocator.allocate_uniform(base, FrameType::Inter);
let b_bits = allocator.allocate_uniform(base, FrameType::BiDir);
assert!(i_bits > p_bits); assert!(p_bits > b_bits); }
#[test]
fn test_complexity_based_allocation() {
let mut allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
allocator.update(1.0, 100_000);
allocator.update(2.0, 150_000);
allocator.update(1.5, 125_000);
let result = allocator.allocate_frame_bits(FrameType::Inter, 2.0, 0);
let low_complexity_result = allocator.allocate_frame_bits(FrameType::Inter, 0.5, 0);
assert!(result.target_bits > low_complexity_result.target_bits);
}
#[test]
fn test_hierarchical_allocation() {
let allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
let base = 100_000;
let level0 = allocator.allocate_hierarchical(base, FrameType::BiDir, 2);
let level1 = allocator.allocate_hierarchical(base, FrameType::BiDir, 4);
assert!(level1 >= level0);
}
#[test]
fn test_reservoir_management() {
let mut allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
allocator.update(1.0, 80_000); assert!(allocator.bit_reservoir > 0);
allocator.update(1.0, 120_000);
assert!(allocator.bit_reservoir < 20_000);
}
#[test]
fn test_vbv_constraint() {
let mut allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
allocator.set_vbv_buffer_size(1_000_000);
allocator.set_strategy(AllocationStrategy::Constrained);
let result = allocator.allocate_frame_bits(FrameType::Key, 5.0, 0);
assert!(result.max_bits <= 1_000_000);
}
#[test]
fn test_adaptive_allocation() {
let mut allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
allocator.set_strategy(AllocationStrategy::Adaptive);
for _ in 0..10 {
allocator.update(1.0, 130_000); }
let result = allocator.allocate_frame_bits(FrameType::Inter, 1.0, 0);
assert!(result.target_bits < 100_000);
}
#[test]
fn test_gop_tracking() {
let mut allocator = BitrateAllocator::new(3_000_000, 30.0, 10);
allocator.start_new_gop();
for i in 0..5 {
allocator.update(1.0, 100_000);
let status = allocator.gop_status();
assert_eq!(status.frames_encoded, i + 1);
assert_eq!(status.frames_remaining, 10 - (i + 1));
}
let status = allocator.gop_status();
assert_eq!(status.frames_encoded, 5);
assert_eq!(status.bits_used, 500_000);
}
#[test]
fn test_allocation_result() {
let result = AllocationResult {
target_bits: 100_000,
min_bits: 25_000,
max_bits: 400_000,
frame_type: FrameType::Inter,
complexity_factor: 1.5,
reservoir_adjustment: 0,
};
assert!(result.is_within_range(100_000));
assert!(result.is_within_range(50_000));
assert!(!result.is_within_range(500_000));
assert!(!result.is_within_range(10_000));
let accuracy = result.accuracy(110_000);
assert!((accuracy - 1.1).abs() < 0.01);
}
#[test]
fn test_gop_status() {
let status = GopAllocationStatus {
gop_index: 1,
frames_encoded: 5,
frames_remaining: 5,
bits_used: 500_000,
bits_remaining: 500_000,
target_bits: 1_000_000,
on_target: true,
};
assert_eq!(status.average_bits_per_frame(), 100_000);
assert_eq!(status.recommended_bits_per_frame(), 100_000);
}
#[test]
fn test_strategy_switching() {
let mut allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
allocator.set_strategy(AllocationStrategy::Uniform);
let uniform_result = allocator.allocate_frame_bits(FrameType::Inter, 2.0, 0);
allocator.set_strategy(AllocationStrategy::ComplexityBased);
allocator.update(1.0, 100_000);
let complexity_result = allocator.allocate_frame_bits(FrameType::Inter, 2.0, 0);
assert_ne!(uniform_result.target_bits, complexity_result.target_bits);
}
#[test]
fn test_reset() {
let mut allocator = BitrateAllocator::new(3_000_000, 30.0, 250);
allocator.update(1.0, 100_000);
allocator.update(2.0, 150_000);
assert!(!allocator.complexity_history.is_empty());
allocator.reset();
assert!(allocator.complexity_history.is_empty());
assert!(allocator.bit_usage_history.is_empty());
assert_eq!(allocator.bit_reservoir, 0);
}
}