Files
pmomusic/pmoaudio-ext/src/sinks/broadcast_pacing.rs
Eric Coissac 306e691c61 Refactor WebRenderer to use server-side streaming with OGG-FLAC sink
This commit refactors the WebRenderer to use a server-side streaming architecture with OGG-FLAC sink instead of the previous WebSocket-based approach. The changes include:

- Replaced WebSocket communication with HTTP streaming using DirectOggFlacSink
- Implemented a new pipeline architecture with dedicated handlers for UPnP commands
- Added new modules for registration, registry, and streaming
- Updated the renderer to work with a pipeline control system
- Removed old WebSocket session management
- Added support for HTTP streaming with gapless playback
- Updated dependencies and features for the new architecture

The WebRenderer now acts as a MediaRenderer UPnP device that serves audio streams via HTTP endpoints, with commands relayed to the audio pipeline through a new control system.
2026-02-26 20:05:42 +01:00

71 lines
2.2 KiB
Rust

//! Shared broadcast pacing logic for streaming sinks.
//!
//! Provides intelligent backpressure based on audio timing:
//! - Detects TopZeroSync (when audio timestamp resets to 0)
//! - Drops frames that are late (audio_ts < elapsed)
//! - Paces broadcast to match audio playback rate
use std::time::{Duration, Instant};
use tracing::trace;
/// Error returned when a frame should be skipped (too late)
#[derive(Debug)]
pub struct SkipFrame;
/// Manages broadcast pacing with TopZeroSync detection
pub struct BroadcastPacer {
/// Start time (reset on TopZeroSync)
start_time: Instant,
/// Maximum allowed lead time before sleeping (0 = no pacing)
max_lead_time: f64,
/// Label for logging (e.g., "FLAC" or "OGG")
label: String,
}
impl BroadcastPacer {
/// Create a new broadcast pacer
///
/// # Arguments
///
/// * `max_lead_time` - Maximum lead time in seconds (0 = no pacing)
/// * `label` - Label for logging
pub fn new(max_lead_time: f64, label: impl Into<String>) -> Self {
Self {
start_time: Instant::now(),
max_lead_time: max_lead_time.max(0.0),
label: label.into(),
}
}
/// Reset the pacer clock (call when audio timestamp resets to 0).
pub fn reset(&mut self) {
self.start_time = Instant::now();
trace!("{} broadcaster: pacer reset", self.label);
}
/// Check timing and apply pacing.
///
/// If the audio is ahead of real time by more than `max_lead_time`, sleeps
/// until the lead is within bounds. Returns `Err(SkipFrame)` if the chunk
/// is already late (audio_ts < elapsed - 1s grace).
pub async fn check_and_pace(&mut self, audio_timestamp: f64) -> Result<(), SkipFrame> {
if self.max_lead_time <= 0.0 {
return Ok(());
}
let elapsed = self.start_time.elapsed().as_secs_f64();
let lead = audio_timestamp - elapsed;
if lead > self.max_lead_time {
let sleep_secs = lead - self.max_lead_time;
trace!(
"{} broadcaster: audio ahead by {:.3}s, sleeping {:.3}s",
self.label, lead, sleep_secs
);
tokio::time::sleep(Duration::from_secs_f64(sleep_secs)).await;
}
Ok(())
}
}