use anyhow::Result;
use bytes::Bytes;
use log::{debug, error};
use std::time::Instant;
use tokio::sync::mpsc;
use tokio::time::{sleep, Duration};
use crate::silence::SilenceTemplate;
use crate::stream::StreamProcessor;
use crate::timing::StreamClock;
#[derive(Clone, Copy)]
pub struct BufferConfig {
pub buffered_seconds: f64,
pub max_chunk_size: usize,
}
#[derive(Clone, Copy, Debug)]
pub enum VorbisBitrateMode {
CBR(u32), VBRQuality(u8), }
#[derive(Clone, Copy, Debug)]
pub struct VorbisConfig {
pub sample_rate: u32,
pub bitrate: VorbisBitrateMode,
}
impl VorbisConfig {
pub fn silence_key(&self) -> String {
match self.bitrate {
VorbisBitrateMode::CBR(kbps) => format!("{}_{}", self.sample_rate, kbps),
VorbisBitrateMode::VBRQuality(q) => format!("{}_q{}", self.sample_rate, q),
}
}
}
struct StreamSession {
stream_processor: StreamProcessor,
is_silence: bool,
silence_template: Option<Bytes>,
}
impl StreamSession {
fn new_real(serial: u32, base_granule: u64) -> Self {
Self {
stream_processor: StreamProcessor::with_serial(serial, base_granule),
is_silence: false,
silence_template: None,
}
}
fn new_silence(serial: u32, base_granule: u64, template: Bytes) -> Self {
Self {
stream_processor: StreamProcessor::with_silence(serial, base_granule),
is_silence: true,
silence_template: Some(template),
}
}
async fn run(
&mut self,
input_rx: &mut mpsc::Receiver<Bytes>,
output_tx: &mpsc::Sender<Bytes>,
clock: &StreamClock,
buffered_seconds: f64,
granule_position_ref: &mut u64,
) -> Result<()> {
if self.is_silence {
if let Some(ref template) = self.silence_template {
let out = self.stream_processor.process(template)?;
if !out.is_empty() {
self.maybe_sleep(clock, buffered_seconds, *granule_position_ref)
.await;
if output_tx.send(out).await.is_err() {
return Ok(());
}
*granule_position_ref = self.stream_processor.get_granule_position();
}
}
let final_out = self.stream_processor.finalise()?;
if !final_out.is_empty() {
self.maybe_sleep(clock, buffered_seconds, *granule_position_ref)
.await;
let _ = output_tx.send(final_out).await;
}
*granule_position_ref = self.stream_processor.get_granule_position();
return Ok(());
}
let mut last_action_time = Instant::now();
let timeout = Duration::from_millis(500);
loop {
let lead = clock.lead_secs(*granule_position_ref);
if lead >= buffered_seconds {
sleep(Duration::from_millis(20)).await;
}
match input_rx.try_recv() {
Ok(data) => {
last_action_time = Instant::now();
let out = self.stream_processor.process(&data)?;
if !out.is_empty() {
self.maybe_sleep(clock, buffered_seconds, *granule_position_ref)
.await;
if output_tx.send(out).await.is_err() {
break;
}
}
*granule_position_ref = self.stream_processor.get_granule_position();
}
Err(mpsc::error::TryRecvError::Empty) => {
if !self.stream_processor.has_produced_output()
&& last_action_time.elapsed() > timeout
{
debug!("No valid output produced after timeout; breaking to restart with silence");
break;
}
sleep(Duration::from_millis(10)).await;
}
Err(mpsc::error::TryRecvError::Disconnected) => break,
}
if self.stream_processor.is_finished() {
break;
}
}
let final_out = self.stream_processor.finalise()?;
if !final_out.is_empty() {
self.maybe_sleep(clock, buffered_seconds, *granule_position_ref)
.await;
let _ = output_tx.send(final_out).await;
}
*granule_position_ref = self.stream_processor.get_granule_position();
Ok(())
}
async fn maybe_sleep(&self, clock: &StreamClock, buffered_seconds: f64, current_granule: u64) {
let lead = clock.lead_secs(current_granule);
if lead >= buffered_seconds {
while clock.lead_secs(current_granule) >= buffered_seconds {
sleep(Duration::from_millis(20)).await;
}
}
}
}
pub struct OggMux {
buffer_config: BufferConfig,
vorbis_config: VorbisConfig,
silence: SilenceTemplate,
initial_serial: u32,
}
impl OggMux {
pub fn new() -> Self {
let vorbis_config = VorbisConfig {
sample_rate: 44100,
bitrate: VorbisBitrateMode::CBR(320),
};
let silence = Self::load_default_silence(&vorbis_config);
Self {
buffer_config: BufferConfig {
buffered_seconds: 10.0,
max_chunk_size: 65536,
},
vorbis_config,
silence,
initial_serial: 0xfeed_0000,
}
}
fn load_default_silence(config: &VorbisConfig) -> SilenceTemplate {
match config.silence_key().as_str() {
"44100_192" => {
SilenceTemplate::new_embedded(include_bytes!("../resources/silence_44100_192.ogg"))
}
"44100_128" => {
SilenceTemplate::new_embedded(include_bytes!("../resources/silence_44100_128.ogg"))
}
"44100_320" => {
SilenceTemplate::new_embedded(include_bytes!("../resources/silence_44100_320.ogg"))
}
"48000_q6" => {
SilenceTemplate::new_embedded(include_bytes!("../resources/silence_48000_q6.ogg"))
}
_ => SilenceTemplate::new_embedded(include_bytes!("../resources/silence_default.ogg")),
}
}
pub fn with_buffer_config(mut self, config: BufferConfig) -> Self {
self.buffer_config = config;
self
}
pub fn with_vorbis_config(mut self, config: VorbisConfig) -> Self {
self.vorbis_config = config;
self.silence = Self::load_default_silence(&self.vorbis_config);
self
}
pub fn with_silence_template(mut self, silence: SilenceTemplate) -> Self {
self.silence = silence;
self
}
fn next_serial(&mut self) -> u32 {
let s = self.initial_serial;
self.initial_serial = self.initial_serial.wrapping_add(1);
s
}
pub fn spawn(mut self) -> (mpsc::Sender<Bytes>, mpsc::Receiver<Bytes>) {
let (input_tx, mut input_rx) = mpsc::channel::<Bytes>(self.buffer_config.max_chunk_size);
let (output_tx, output_rx) = mpsc::channel::<Bytes>(self.buffer_config.max_chunk_size);
let clock = StreamClock::new(self.vorbis_config.sample_rate);
let buffered_seconds = self.buffer_config.buffered_seconds;
let mut global_granule_position: u64 = 0;
tokio::spawn(async move {
debug!("OggMux main loop started");
loop {
if output_tx.is_closed() {
debug!("Output channel closed; exiting mux loop");
break;
}
tokio::select! {
maybe_input = input_rx.recv() => {
match maybe_input {
Some(first_chunk) => {
let serial = self.next_serial();
debug!("Starting REAL stream, serial=0x{:x}", serial);
let mut session = StreamSession::new_real(serial, global_granule_position);
match session.stream_processor.process(&first_chunk) {
Ok(out) => {
if !out.is_empty() && !output_tx.is_closed() {
session.maybe_sleep(&clock, buffered_seconds, global_granule_position).await;
let _ = output_tx.send(out).await;
}
global_granule_position = session.stream_processor.get_granule_position();
}
Err(e) => {
error!("Error processing first chunk: {:?}", e);
continue;
}
}
if let Err(e) = session.run(
&mut input_rx,
&output_tx,
&clock,
buffered_seconds,
&mut global_granule_position
).await {
error!("Session run error: {:?}", e);
}
}
None => {
debug!("Input channel closed; exiting mux loop");
break;
}
}
}
_ = tokio::time::sleep(Duration::from_millis(100)) => {
let serial = self.next_serial();
debug!("Inserting SILENCE stream, serial=0x{:x}", serial);
let silence_data = self.silence.raw_bytes().to_vec().into();
let mut session = StreamSession::new_silence(
serial,
global_granule_position,
silence_data,
);
if let Err(e) = session.run(
&mut input_rx,
&output_tx,
&clock,
buffered_seconds,
&mut global_granule_position
).await {
error!("Silence session error: {:?}", e);
}
}
}
}
debug!("OggMux main loop ended");
});
(input_tx, output_rx)
}
}
impl Default for OggMux {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vorbis_config_silence_key_cbr() {
let cfg = VorbisConfig {
sample_rate: 44100,
bitrate: VorbisBitrateMode::CBR(192),
};
assert_eq!(cfg.silence_key(), "44100_192");
}
#[test]
fn test_vorbis_config_silence_key_vbr() {
let cfg = VorbisConfig {
sample_rate: 48000,
bitrate: VorbisBitrateMode::VBRQuality(6),
};
assert_eq!(cfg.silence_key(), "48000_q6");
}
#[test]
fn test_default_mux_uses_expected_key() {
let mux = OggMux::new();
let key = mux.vorbis_config.silence_key();
assert_eq!(key, "44100_320");
}
#[tokio::test]
async fn test_mux_shutdown_behavior() {
let mux = OggMux::new();
let (input_tx, output_rx) = mux.spawn();
drop(output_rx);
let _ = input_tx.send(Bytes::from_static(b"example")).await;
sleep(Duration::from_millis(200)).await;
let result = input_tx.send(Bytes::from_static(b"after close")).await;
assert!(result.is_err(), "Expected send to fail after shutdown");
}
}