Implémentation du OGG chaining pour les flux audio

Cette modification introduit le support du OGG chaining dans le flux audio, permettant une transition transparente entre les pistes sans interruption. Cela inclut la gestion des encodeurs successifs dans le même canal Bytes, l'annulation des encodeurs précédents lors des transitions, et le maintien d'une connexion persistante avec backpressure TCP. Les modifications affectent les composants de sink et de pipeline audio, ainsi que les endpoints d'écoute.
This commit is contained in:
2026-02-28 12:15:02 +01:00
parent eedb0b6a0e
commit f9056f92e0
4 changed files with 191 additions and 76 deletions

View File

@@ -5,15 +5,17 @@
//! //!
//! # Cycle de vie //! # Cycle de vie
//! //!
//! - **Play** : le navigateur appelle `GET /stream`. `connect()` crée un nouveau //! - **Play** : le navigateur appelle `GET /stream`. `connect()` crée un channel
//! canal PCM + pipe duplex + encodeur FLAC + wrapper OGG, installe le sender //! Bytes (bytes_tx/rx), une task de forwarding qui copie les Bytes dans un
//! dans le sink, et notifie le sink via `client_notify`. Le flux reste ouvert : //! DuplexStream, et lance le premier encodeur OGG-FLAC. Le `bytes_tx` est
//! les morceaux s'enchaînent en gapless. //! stocké dans le sink pour le chaining TrackBoundary.
//! - **Stop** : le navigateur ferme la connexion. Le pipe se rompt, l'encodeur //! - **Stop** : le navigateur ferme la connexion. Le DuplexStream se rompt,
//! s'arrête. Le sink voit `pcm_tx.send()` échouer, passe le sender à `None`, //! la task de forwarding se termine, le bytes_tx devient invalide. Le sink
//! et **bloque** sur `client_notify` jusqu'au prochain Play. //! voit `pcm_tx.send()` échouer et bloque sur `client_notify`.
//! - **Play suivant** : `connect()` → nouveau pipe → `client_notify.notify_one()` //! - **TrackBoundary** : le sink ferme le `pcm_tx` courant (EOF → encodeur écrit
//! → le sink se débloque et reprend la consommation des segments. //! EOS OGG), attend la fin de l'encodeur, puis relance un nouvel encodeur
//! dans le même `bytes_tx` (OGG chaining : nouvelle BOS OGG dans le même flux HTTP).
//! - **Play suivant** : `connect()` → nouveau DuplexStream + channel → nouveau pipe.
//! //!
//! # Architecture //! # Architecture
//! //!
@@ -21,11 +23,10 @@
//! AudioSegment I24 @ 96 kHz //! AudioSegment I24 @ 96 kHz
//! ↓ NodeLogic::process() [bloque si pas de client] //! ↓ NodeLogic::process() [bloque si pas de client]
//! chunk_to_pcm_bytes() → PCM 24-bit LE //! chunk_to_pcm_bytes() → PCM 24-bit LE
//! ↓ Arc<Mutex<Option<mpsc::Sender<PcmChunk>>>> //! ↓ SharedPcmTx
//! ByteStreamReader (AsyncRead) //! ByteStreamReader → encode_flac_stream() → OGG pages
//! ↓ encode_flac_stream() //! ↓ SharedBytesTx (mpsc::Sender<Bytes>) ← persistant entre encodeurs
//! ↓ broadcast_ogg_flac_stream() → wrapping OGG pages //! [forwarding task] → tokio::io::DuplexStream
//! ↓ tokio::io::duplex pipe (256 KB)
//! ↓ DirectOggFlacStream (AsyncRead) → Body HTTP //! ↓ DirectOggFlacStream (AsyncRead) → Body HTTP
//! ``` //! ```
@@ -43,6 +44,7 @@ use pmoaudio::{
use pmoflac::{EncoderOptions, PcmFormat}; use pmoflac::{EncoderOptions, PcmFormat};
use tokio::io::{AsyncRead, ReadBuf}; use tokio::io::{AsyncRead, ReadBuf};
use tokio::sync::{mpsc, watch, Mutex}; use tokio::sync::{mpsc, watch, Mutex};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::{debug, warn}; use tracing::{debug, warn};
@@ -57,10 +59,17 @@ pub const DIRECT_OGG_FLAC_BITS_PER_SAMPLE: u8 = 24;
/// Capacité du pipe duplex (~256 KB). /// Capacité du pipe duplex (~256 KB).
const PIPE_CAPACITY: usize = 256 * 1024; const PIPE_CAPACITY: usize = 256 * 1024;
/// Capacité du channel Bytes intermédiaire.
const BYTES_CHANNEL_CAPACITY: usize = 64;
// ─── Shared state ───────────────────────────────────────────────────────────── // ─── Shared state ─────────────────────────────────────────────────────────────
type SharedPcmTx = Arc<Mutex<Option<mpsc::Sender<PcmChunk>>>>; type SharedPcmTx = Arc<Mutex<Option<mpsc::Sender<PcmChunk>>>>;
/// Canal Bytes persistant entre les encodeurs successifs (OGG chaining).
/// Le sink y envoie les pages OGG ; une task de forwarding les copie dans le DuplexStream.
type SharedBytesTx = Arc<Mutex<Option<mpsc::Sender<Bytes>>>>;
/// Handle de la task encodeur courante.
type SharedEncoderTask = Arc<Mutex<Option<JoinHandle<()>>>>;
// ─── Handle public ──────────────────────────────────────────────────────────── // ─── Handle public ────────────────────────────────────────────────────────────
@@ -72,48 +81,76 @@ pub struct DirectOggFlacHandle {
client_notify_internal: Arc<tokio::sync::Notify>, client_notify_internal: Arc<tokio::sync::Notify>,
first_byte_tx: Arc<watch::Sender<bool>>, first_byte_tx: Arc<watch::Sender<bool>>,
encoder_options: EncoderOptions, encoder_options: EncoderOptions,
/// Position de lecture courante (mise à jour par ByteStreamReader).
current_timestamp: Arc<tokio::sync::RwLock<f64>>, current_timestamp: Arc<tokio::sync::RwLock<f64>>,
/// Canal Bytes persistant partagé avec la logic du sink pour le OGG chaining.
bytes_tx: SharedBytesTx,
/// Task encodeur courante partagée avec la logic du sink.
encoder_task: SharedEncoderTask,
} }
impl DirectOggFlacHandle { impl DirectOggFlacHandle {
/// Crée un nouveau pipe OGG-FLAC et retourne le flux côté lecture. /// Crée un nouveau flux OGG-FLAC pour le client HTTP.
/// Débloque le sink s'il attendait un client. /// Remplace toute connexion précédente.
pub async fn connect(&self) -> DirectOggFlacStream { pub async fn connect(&self) -> DirectOggFlacStream {
let connect_count_before = *self.client_connect_tx.borrow(); let connect_count_before = *self.client_connect_tx.borrow();
debug!("DirectOggFlacHandle::connect() called, connect_count={}", connect_count_before); debug!("DirectOggFlacHandle::connect() called, connect_count={}", connect_count_before);
let (pcm_tx, pcm_rx) = mpsc::channel::<PcmChunk>(8); // Annuler l'encodeur précédent s'il tourne encore
// Réinitialiser le timestamp à 0 pour la nouvelle connexion if let Some(old_task) = self.encoder_task.lock().await.take() {
old_task.abort();
}
// Créer le channel Bytes persistant (OGG chaining)
let (bytes_tx, bytes_rx) = mpsc::channel::<Bytes>(BYTES_CHANNEL_CAPACITY);
*self.bytes_tx.lock().await = Some(bytes_tx.clone());
// Créer le DuplexStream vers le client HTTP
let (mut pipe_writer, pipe_reader) = tokio::io::duplex(PIPE_CAPACITY);
// Task de forwarding : Bytes → DuplexStream
// Se termine quand bytes_rx est fermé (bytes_tx droppé) ou pipe cassé
tokio::spawn(async move {
use tokio::io::AsyncWriteExt;
let mut rx = bytes_rx;
while let Some(bytes) = rx.recv().await {
if pipe_writer.write_all(&bytes).await.is_err() {
debug!("DirectOggFlacStream forwarder: pipe broken, client disconnected");
break;
}
}
debug!("DirectOggFlacStream forwarder: done");
});
// Réinitialiser les signaux
let _ = self.first_byte_tx.send(false);
*self.current_timestamp.write().await = 0.0; *self.current_timestamp.write().await = 0.0;
// Créer le premier pcm_tx + encodeur
let (pcm_tx, pcm_rx) = mpsc::channel::<PcmChunk>(8);
let current_dur = Arc::new(tokio::sync::RwLock::new(0.0f64)); let current_dur = Arc::new(tokio::sync::RwLock::new(0.0f64));
// Partager current_timestamp avec ByteStreamReader : il sera mis à jour
// avec le timestamp absolu du segment audio (position dans le fichier source).
let pcm_reader = ByteStreamReader::new(pcm_rx, self.current_timestamp.clone(), current_dur); let pcm_reader = ByteStreamReader::new(pcm_rx, self.current_timestamp.clone(), current_dur);
let (pipe_writer, pipe_reader) = tokio::io::duplex(PIPE_CAPACITY);
let _ = self.first_byte_tx.send(false);
debug!("DirectOggFlacHandle::connect() first_byte reset to false");
*self.pcm_tx.lock().await = Some(pcm_tx); *self.pcm_tx.lock().await = Some(pcm_tx);
debug!("DirectOggFlacHandle::connect() pcm_tx installed");
let new_count = connect_count_before.wrapping_add(1); let new_count = connect_count_before.wrapping_add(1);
let _ = self.client_connect_tx.send(new_count); let _ = self.client_connect_tx.send(new_count);
debug!("DirectOggFlacHandle::connect() client_connect_count -> {}", new_count);
self.client_notify_internal.notify_one(); self.client_notify_internal.notify_one();
debug!("DirectOggFlacHandle::connect() client_connect_count -> {}", new_count);
let options = self.encoder_options.clone(); let options = self.encoder_options.clone();
let current_timestamp = self.current_timestamp.clone(); let current_timestamp = self.current_timestamp.clone();
tokio::spawn(async move { let shared_bytes_tx = self.bytes_tx.clone();
debug!("DirectOggFlacHandle: encoder+ogg task started");
if let Err(e) = run_ogg_encoder(pcm_reader, pipe_writer, options, current_timestamp).await { let handle = tokio::spawn(async move {
debug!("DirectOggFlacStream encoder stopped: {}", e); debug!("DirectOggFlacHandle: initial encoder task started");
if let Err(e) = run_ogg_encoder(pcm_reader, bytes_tx, shared_bytes_tx, options, current_timestamp).await {
debug!("DirectOggFlacHandle: initial encoder stopped: {}", e);
} }
debug!("DirectOggFlacHandle: encoder+ogg task ended"); debug!("DirectOggFlacHandle: initial encoder task ended");
}); });
*self.encoder_task.lock().await = Some(handle);
debug!("DirectOggFlacHandle::connect() returning DirectOggFlacStream"); debug!("DirectOggFlacHandle::connect() returning DirectOggFlacStream");
DirectOggFlacStream { DirectOggFlacStream {
inner: pipe_reader, inner: pipe_reader,
@@ -125,7 +162,6 @@ impl DirectOggFlacHandle {
self.first_byte_tx.subscribe() self.first_byte_tx.subscribe()
} }
/// Retourne la position de lecture courante en secondes.
pub async fn current_position_sec(&self) -> f64 { pub async fn current_position_sec(&self) -> f64 {
*self.current_timestamp.read().await *self.current_timestamp.read().await
} }
@@ -172,6 +208,13 @@ impl AsyncRead for DirectOggFlacStream {
struct DirectOggFlacSinkLogic { struct DirectOggFlacSinkLogic {
pcm_tx: SharedPcmTx, pcm_tx: SharedPcmTx,
client_notify: Arc<tokio::sync::Notify>, client_notify: Arc<tokio::sync::Notify>,
encoder_options: EncoderOptions,
encoder_task: SharedEncoderTask,
bytes_tx: SharedBytesTx,
current_timestamp: Arc<tokio::sync::RwLock<f64>>,
/// Vrai dès qu'au moins un chunk audio a été encodé dans le stream courant.
/// Empêche le OGG chaining sur le TrackBoundary initial (avant tout audio).
has_encoded_frames: bool,
} }
#[async_trait] #[async_trait]
@@ -234,9 +277,21 @@ impl NodeLogic for DirectOggFlacSinkLogic {
seg.timestamp_sec, seg.timestamp_sec,
); );
*self.pcm_tx.lock().await = None; *self.pcm_tx.lock().await = None;
self.has_encoded_frames = false;
} else {
self.has_encoded_frames = true;
} }
} }
_AudioSegment::Sync(marker) => match marker.as_ref() { _AudioSegment::Sync(marker) => match marker.as_ref() {
SyncMarker::TrackBoundary { .. } => {
if self.has_encoded_frames {
debug!("DirectOggFlacSink: TrackBoundary — OGG chaining");
self.has_encoded_frames = false;
self.do_track_boundary().await;
} else {
debug!("DirectOggFlacSink: TrackBoundary ignored (no frames encoded yet)");
}
}
SyncMarker::EndOfStream => { SyncMarker::EndOfStream => {
debug!("DirectOggFlacSink: EndOfStream"); debug!("DirectOggFlacSink: EndOfStream");
} }
@@ -256,11 +311,69 @@ impl NodeLogic for DirectOggFlacSinkLogic {
} }
} }
impl DirectOggFlacSinkLogic {
/// OGG chaining : ferme l'encodeur courant (EOF → EOS OGG), attend sa fin,
/// puis relance un nouvel encodeur dans le même channel Bytes (nouvelle BOS OGG).
async fn do_track_boundary(&mut self) {
// 1. Fermer le pcm_tx courant → EOF dans ByteStreamReader → encodeur écrit EOS OGG
{
let mut guard = self.pcm_tx.lock().await;
*guard = None;
}
// 2. Attendre la fin de la task encodeur courante
let old_task = self.encoder_task.lock().await.take();
if let Some(handle) = old_task {
let _ = handle.await;
debug!("DirectOggFlacSink: previous encoder task joined");
}
// 3. Vérifier que le bytes_tx est encore valide (client pas déconnecté)
let bytes_tx = {
let guard = self.bytes_tx.lock().await;
guard.clone()
};
let Some(bytes_tx) = bytes_tx else {
debug!("DirectOggFlacSink: bytes_tx gone (client disconnected), skip OGG chaining");
return;
};
// 4. Nouveau pcm_tx + ByteStreamReader
let (pcm_tx, pcm_rx) = mpsc::channel::<PcmChunk>(8);
let current_dur = Arc::new(tokio::sync::RwLock::new(0.0f64));
let pcm_reader = ByteStreamReader::new(
pcm_rx,
self.current_timestamp.clone(),
current_dur,
);
*self.pcm_tx.lock().await = Some(pcm_tx);
// 5. Relancer l'encodeur dans le même bytes_tx (OGG chaining : nouvelle BOS OGG)
let options = self.encoder_options.clone();
let current_timestamp = self.current_timestamp.clone();
let shared_bytes_tx = self.bytes_tx.clone();
let handle = tokio::spawn(async move {
debug!("DirectOggFlacSink: chained encoder task started");
if let Err(e) = run_ogg_encoder(pcm_reader, bytes_tx, shared_bytes_tx, options, current_timestamp).await {
debug!("DirectOggFlacSink: chained encoder stopped: {}", e);
}
debug!("DirectOggFlacSink: chained encoder task ended");
});
*self.encoder_task.lock().await = Some(handle);
debug!("DirectOggFlacSink: OGG chaining complete, new encoder started");
}
}
// ─── Encodeur FLAC + wrapper OGG ───────────────────────────────────────────── // ─── Encodeur FLAC + wrapper OGG ─────────────────────────────────────────────
/// Encode PCM → FLAC → OGG et envoie les pages OGG dans `bytes_tx`.
/// Quand le channel devient invalide (client déconnecté), nettoie `shared_bytes_tx`.
async fn run_ogg_encoder( async fn run_ogg_encoder(
pcm_reader: ByteStreamReader, pcm_reader: ByteStreamReader,
mut pipe_writer: tokio::io::DuplexStream, bytes_tx: mpsc::Sender<Bytes>,
shared_bytes_tx: SharedBytesTx,
options: EncoderOptions, options: EncoderOptions,
_current_timestamp: Arc<tokio::sync::RwLock<f64>>, _current_timestamp: Arc<tokio::sync::RwLock<f64>>,
) -> Result<(), AudioError> { ) -> Result<(), AudioError> {
@@ -274,56 +387,56 @@ async fn run_ogg_encoder(
.await .await
.map_err(|e| AudioError::ProcessingError(format!("FLAC encoder init: {}", e)))?; .map_err(|e| AudioError::ProcessingError(format!("FLAC encoder init: {}", e)))?;
// Lire le header FLAC et construire les pages OGG d'en-tête
let flac_header = read_flac_header(&mut flac_stream).await?; let flac_header = read_flac_header(&mut flac_stream).await?;
let sample_rate = extract_sample_rate_from_streaminfo(&flac_header)?; let _sample_rate = extract_sample_rate_from_streaminfo(&flac_header)?;
let stream_serial: u32 = rand::random(); let stream_serial: u32 = rand::random();
let mut ogg = OggPageWriter::new(stream_serial); let mut ogg = OggPageWriter::new(stream_serial);
// Page BOS (identification OGG-FLAC)
let ogg_flac_id = create_ogg_flac_identification(&flac_header)?; let ogg_flac_id = create_ogg_flac_identification(&flac_header)?;
let bos_page = Bytes::from(ogg.create_page(&ogg_flac_id, true, false, false)); let bos_page = Bytes::from(ogg.create_page(&ogg_flac_id, true, false, false));
// Page Vorbis Comment
let vorbis_comment = create_empty_vorbis_comment(); let vorbis_comment = create_empty_vorbis_comment();
let comment_page = Bytes::from(ogg.create_page(&vorbis_comment, false, false, false)); let comment_page = Bytes::from(ogg.create_page(&vorbis_comment, false, false, false));
pipe_writer.write_all(&bos_page).await macro_rules! send_or_cleanup {
.map_err(|e| AudioError::IoError(format!("OGG BOS write: {}", e)))?; ($page:expr) => {
pipe_writer.write_all(&comment_page).await if bytes_tx.send($page).await.is_err() {
.map_err(|e| AudioError::IoError(format!("OGG comment write: {}", e)))?; debug!("DirectOggFlacSink: bytes_tx broken, client disconnected");
*shared_bytes_tx.lock().await = None;
return Ok(());
}
};
}
send_or_cleanup!(bos_page);
send_or_cleanup!(comment_page);
use tokio::io::AsyncReadExt;
use crate::sinks::flac_frame_utils::{validate_frame_header_crc, parse_flac_block_size};
// Lire les frames FLAC et les encapsuler dans des pages OGG
let sample_rate_f64 = sample_rate as f64;
let mut encoded_samples = 0u64; let mut encoded_samples = 0u64;
let mut read_buffer = vec![0u8; 16384]; let mut read_buffer = vec![0u8; 16384];
let mut accumulator: Vec<u8> = Vec::with_capacity(32768); let mut accumulator: Vec<u8> = Vec::with_capacity(32768);
use tokio::io::{AsyncReadExt, AsyncWriteExt};
loop { loop {
match flac_stream.read(&mut read_buffer).await { match flac_stream.read(&mut read_buffer).await {
Ok(0) => { Ok(0) => {
// EOF : page EOS finale // EOF : page EOS finale
let eos_page = Bytes::from(ogg.create_page(&accumulator, false, true, false)); let eos_page = Bytes::from(ogg.create_page(&accumulator, false, true, false));
let _ = pipe_writer.write_all(&eos_page).await; let _ = bytes_tx.send(eos_page).await;
break; break;
} }
Ok(n) => { Ok(n) => {
accumulator.extend_from_slice(&read_buffer[..n]); accumulator.extend_from_slice(&read_buffer[..n]);
loop { loop {
if accumulator.len() < 4 { if accumulator.len() < 4 { break; }
break;
}
// Trouver les positions de sync FLAC
let mut sync_data: Vec<(usize, u32)> = Vec::new(); let mut sync_data: Vec<(usize, u32)> = Vec::new();
for i in 0..accumulator.len() - 1 { for i in 0..accumulator.len() - 1 {
let b1 = accumulator[i]; let b1 = accumulator[i];
let b2 = accumulator[i + 1]; let b2 = accumulator[i + 1];
if b1 == 0xFF && b2 >= 0xF8 && b2 <= 0xFE { if b1 == 0xFF && b2 >= 0xF8 && b2 <= 0xFE {
use crate::sinks::flac_frame_utils::{validate_frame_header_crc, parse_flac_block_size};
if validate_frame_header_crc(&accumulator, i) { if validate_frame_header_crc(&accumulator, i) {
if let Some(samples) = parse_flac_block_size(&accumulator, i) { if let Some(samples) = parse_flac_block_size(&accumulator, i) {
sync_data.push((i, samples)); sync_data.push((i, samples));
@@ -332,9 +445,7 @@ async fn run_ogg_encoder(
} }
} }
if sync_data.len() < 2 { if sync_data.len() < 2 { break; }
break;
}
let first_start = sync_data[0].0; let first_start = sync_data[0].0;
let first_samples = sync_data[0].1; let first_samples = sync_data[0].1;
@@ -346,19 +457,17 @@ async fn run_ogg_encoder(
} }
let frame: Vec<u8> = accumulator.drain(0..second_start).collect(); let frame: Vec<u8> = accumulator.drain(0..second_start).collect();
encoded_samples = encoded_samples.saturating_add(first_samples as u64); encoded_samples = encoded_samples.saturating_add(first_samples as u64);
ogg.add_samples(first_samples as u64); ogg.add_samples(first_samples as u64);
let ogg_page = Bytes::from(ogg.create_page(&frame, false, false, false)); let ogg_page = Bytes::from(ogg.create_page(&frame, false, false, false));
if pipe_writer.write_all(&ogg_page).await.is_err() { if bytes_tx.send(ogg_page).await.is_err() {
// Client déconnecté — le pipe HTTP s'est rompu
warn!( warn!(
samples = encoded_samples, "DirectOggFlacSink: bytes_tx broken after {} samples ({:.3}s), client disconnected",
"DirectOggFlacSink: OGG pipe broken after {} samples ({:.3}s), client disconnected",
encoded_samples, encoded_samples,
encoded_samples as f64 / DIRECT_OGG_FLAC_SAMPLE_RATE as f64, encoded_samples as f64 / DIRECT_OGG_FLAC_SAMPLE_RATE as f64,
); );
*shared_bytes_tx.lock().await = None;
return Ok(()); return Ok(());
} }
} }
@@ -375,7 +484,7 @@ async fn run_ogg_encoder(
Ok(()) Ok(())
} }
// ─── OGG helpers (copiés de streaming_ogg_flac_sink) ───────────────────────── // ─── OGG helpers ─────────────────────────────────────────────────────────────
struct OggPageWriter { struct OggPageWriter {
stream_serial: u32, stream_serial: u32,
@@ -521,10 +630,17 @@ impl DirectOggFlacSink {
let (first_byte_tx, _) = watch::channel(false); let (first_byte_tx, _) = watch::channel(false);
let first_byte_tx = Arc::new(first_byte_tx); let first_byte_tx = Arc::new(first_byte_tx);
let current_timestamp = Arc::new(tokio::sync::RwLock::new(0.0f64)); let current_timestamp = Arc::new(tokio::sync::RwLock::new(0.0f64));
let bytes_tx: SharedBytesTx = Arc::new(Mutex::new(None));
let encoder_task: SharedEncoderTask = Arc::new(Mutex::new(None));
let logic = DirectOggFlacSinkLogic { let logic = DirectOggFlacSinkLogic {
pcm_tx: pcm_tx.clone(), pcm_tx: pcm_tx.clone(),
client_notify: client_notify_internal.clone(), client_notify: client_notify_internal.clone(),
encoder_options: encoder_options.clone(),
encoder_task: encoder_task.clone(),
bytes_tx: bytes_tx.clone(),
current_timestamp: current_timestamp.clone(),
has_encoded_frames: false,
}; };
let sink = Self { let sink = Self {
@@ -538,6 +654,8 @@ impl DirectOggFlacSink {
first_byte_tx, first_byte_tx,
encoder_options, encoder_options,
current_timestamp, current_timestamp,
bytes_tx,
encoder_task,
}; };
(sink, handle) (sink, handle)

View File

@@ -10,8 +10,8 @@ use std::sync::Arc;
use pmoaudio::{AudioSegment, PositionTrackerNode, ResamplingNode, ToI24Node}; use pmoaudio::{AudioSegment, PositionTrackerNode, ResamplingNode, ToI24Node};
use pmometadata::{MemoryTrackMetadata, TrackMetadata}; use pmometadata::{MemoryTrackMetadata, TrackMetadata};
use pmoaudio_ext::sinks::{ use pmoaudio_ext::sinks::{
OggFlacStreamHandle, StreamingOggFlacSink, DirectOggFlacHandle, DirectOggFlacSink,
DIRECT_OGG_FLAC_BITS_PER_SAMPLE, DIRECT_OGG_FLAC_SAMPLE_RATE, DIRECT_OGG_FLAC_SAMPLE_RATE,
}; };
use pmoaudio_ext::UriSource; use pmoaudio_ext::UriSource;
use pmoflac::EncoderOptions; use pmoflac::EncoderOptions;
@@ -59,10 +59,10 @@ impl PipelineHandle {
/// Pipeline audio complet pour une instance WebRenderer. /// Pipeline audio complet pour une instance WebRenderer.
/// ///
/// Créé au `POST /register`. Le flux OGG-FLAC est accessible via `flac_handle` /// Créé au `POST /register`. Le flux OGG-FLAC est accessible via `flac_handle`
/// (multi-clients, chaque `subscribe()` retourne un nouveau flux depuis le point courant). /// (mono-client, chaque `connect()` crée un nouveau flux avec backpressure TCP).
pub struct InstancePipeline { pub struct InstancePipeline {
/// Handle vers le sink OGG-FLAC — clonable, chaque subscribe() donne un flux live. /// Handle vers le sink OGG-FLAC — clonable, connect() crée un nouveau flux.
pub flac_handle: OggFlacStreamHandle, pub flac_handle: DirectOggFlacHandle,
pub pipeline_handle: PipelineHandle, pub pipeline_handle: PipelineHandle,
} }
@@ -82,10 +82,7 @@ impl InstancePipeline {
// La backpressure remonte naturellement depuis le pipe duplex jusqu'à la source. // La backpressure remonte naturellement depuis le pipe duplex jusqu'à la source.
use pmoaudio::pipeline::AudioPipelineNode; use pmoaudio::pipeline::AudioPipelineNode;
let (sink, flac_handle) = StreamingOggFlacSink::new( let (sink, flac_handle) = DirectOggFlacSink::new(EncoderOptions::default());
EncoderOptions::default(),
DIRECT_OGG_FLAC_BITS_PER_SAMPLE,
);
// Nœud de suivi de position : lit le timestamp des chunks sortant vers le sink // Nœud de suivi de position : lit le timestamp des chunks sortant vers le sink
let (mut position_tracker, position_handle) = PositionTrackerNode::new(); let (mut position_tracker, position_handle) = PositionTrackerNode::new();

View File

@@ -29,8 +29,8 @@ pub struct WebRendererInstance {
pub udn: String, pub udn: String,
pub device_instance: Arc<DeviceInstance>, pub device_instance: Arc<DeviceInstance>,
pub state: SharedState, pub state: SharedState,
/// Handle vers le sink OGG-FLAC — clonable, chaque subscribe() donne un flux live. /// Handle vers le sink OGG-FLAC — clonable, connect() crée un nouveau flux avec backpressure.
pub flac_handle: pmoaudio_ext::sinks::OggFlacStreamHandle, pub flac_handle: pmoaudio_ext::sinks::DirectOggFlacHandle,
pub pipeline: PipelineHandle, pub pipeline: PipelineHandle,
pub created_at: SystemTime, pub created_at: SystemTime,
} }
@@ -116,11 +116,11 @@ impl RendererRegistry {
Ok((stream_url, udn)) Ok((stream_url, udn))
} }
/// Retourne le OggFlacStreamHandle pour l'endpoint /stream (clonable). /// Retourne le DirectOggFlacHandle pour l'endpoint /stream (clonable).
pub fn get_flac_handle( pub fn get_flac_handle(
&self, &self,
instance_id: &str, instance_id: &str,
) -> Option<pmoaudio_ext::sinks::OggFlacStreamHandle> { ) -> Option<pmoaudio_ext::sinks::DirectOggFlacHandle> {
self.instances self.instances
.read() .read()
.get(instance_id) .get(instance_id)

View File

@@ -44,7 +44,7 @@ pub async fn stream_handler(
} }
}; };
let stream = handle.subscribe(); let stream = handle.connect().await;
info!(instance_id = %instance_id, "FLAC stream started"); info!(instance_id = %instance_id, "FLAC stream started");