Ajout de la source Radio France avec : - Implémentation du trait MusicSource pour l'intégration UPnP - Routes HTTP API REST pour l'accès aux stations et flux AAC - Proxy streaming avec tracking des connexions - Génération dynamique de l'arborescence UPnP - Cache multi-niveaux (stations, métadonnées, covers) - Support des ~70 stations (standalone, groupes, radios ICI) Fichiers créés : - pmoradiofrance/src/source.rs (implémentation MusicSource) - pmoradiofrance/src/server_ext.rs (routes HTTP et proxy streaming) - pmoradiofrance/assets/radiofrance-logo.webp (logo placeholder) Fichiers modifiés : - pmoradiofrance/src/lib.rs (re-exports et modules) - pmoradiofrance/Cargo.toml (dépendances server) - Cargo.toml (workspace) - pmomediaserver/Cargo.toml (feature radiofrance) - PMOMusic/Cargo.toml (feature radiofrance) - PMOMusic/src/main.rs (enregistrement automatique) Tests et validation : compilation OK, pattern respecté, feature-gating cohérent
18 KiB
** Tu dois suivre scrupuleusement les règles définies dans le fichier @Rules.md **
** Cette tâche est une tâche de recherche et développement. Elle doit conduire à un prototype fonctionnel et/ou un rapport technique sur la faisabilité. **
Support du streaming AAC dans pmoflac
Contexte
Actuellement, pmoflac supporte le décodage streaming pour :
- ✅ MP3 (via
minimp3) - ✅ FLAC (via
claxon) - ✅ Ogg Vorbis (via
lewton) - ✅ Ogg Opus (via
opus) - ✅ WAV (parsing manuel)
- ✅ AIFF (parsing manuel)
Manque critique : Pas de support AAC, pourtant très utilisé pour :
- Streams radio live (Radio France, etc.)
- Podcasts
- Services de streaming musicaux
- Fichiers M4A/MP4
Problématique
Le décodage AAC en streaming infini (radio live) est actuellement impossible dans pmoflac, ce qui force à :
- Soit faire un proxy passthrough (pas de transcodage FLAC)
- Soit utiliser une redirection 302 (pas de tracking)
Cela empêche d'avoir une expérience uniforme où toutes les sources servent du FLAC.
Objectif
Investiguer et prototyper le support du décodage AAC streaming dans pmoflac, en s'inspirant de l'architecture existante (MP3, Ogg, etc.).
Recherches préliminaires
1. Symphonia avec ReadOnlySource
Symphonia est la bibliothèque Rust la plus complète pour le décodage audio. Elle fournit :
ReadOnlySource: Wrapper pour sources non-seekable (streams infinis)AdtsReader: Format reader spécifique pour ADTS (AAC streaming)symphonia-codec-aac: Décodeur AAC-LC (Low Complexity)
Points d'attention :
- Issue connue : Certains formats peuvent quand même réclamer le seek
- Nécessite de tester avec un vrai stream ADTS
2. Format ADTS
ADTS (Audio Data Transport Stream) est le format AAC conçu pour le streaming :
- Auto-synchronisant : chaque frame a un header (12 bits
0xFFF) - Pas de container nécessaire (MP4, M4A)
- Utilisé par les radios en streaming
- Chaque frame contient ses métadonnées (sample rate, channels, etc.)
Structure :
Frame 1: [ADTS Header 7-9 bytes][AAC Data]
Frame 2: [ADTS Header 7-9 bytes][AAC Data]
...
3. Alternative : fdk-aac
Bindings Rust pour fdk-aac (bibliothèque Fraunhofer) :
Avantages :
- ✅ Décodeur de référence (qualité maximale)
- ✅ Support explicite du streaming chunk-by-chunk
- ✅ Buffer interne géré automatiquement
- ✅ Pas besoin de seek
Inconvénients :
- ❌ Dépendance C (libfdk-aac)
- ❌ Licence restrictive (non-commerciale pour certaines versions)
- ❌ Compilation plus complexe
Plan d'investigation
Round 1 : Prototype Symphonia ADTS
Objectif : Tester si Symphonia peut décoder un stream AAC infini avec ReadOnlySource + AdtsReader.
Étapes
-
Créer un module de test :
pmoflac/tests/aac_streaming_test.rs -
Implémenter un décodeur basique :
use symphonia::core::io::{MediaSourceStream, ReadOnlySource}; use symphonia::default::get_probe; use symphonia_codec_aac::AdtsReader; async fn decode_aac_stream_test<R: AsyncRead + Unpin>( reader: R ) -> Result<Vec<u8>> { // Wrapper AsyncRead → Read synchrone (pattern pmoflac) let sync_reader = blocking_reader_from_async(reader); // ReadOnlySource pour stream infini let source = ReadOnlySource::new(sync_reader); let mss = MediaSourceStream::new(Box::new(source), Default::default()); // Probe avec hint AAC/ADTS let mut hint = Hint::new(); hint.with_extension("aac"); let mut format = get_probe() .format(&hint, mss, &Default::default(), &Default::default())?; // Récupérer le track audio let track = format.default_track().unwrap(); let mut decoder = symphonia::default::get_codecs() .make(&track.codec_params, &Default::default())?; let mut pcm_output = Vec::new(); // Décoder frame par frame (boucle infinie jusqu'à disconnect) loop { match format.next_packet() { Ok(packet) => { let decoded = decoder.decode(&packet)?; // Convertir en PCM et accumuler let samples = convert_to_pcm_bytes(decoded); pcm_output.extend_from_slice(&samples); } Err(symphonia::core::errors::Error::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => { break; // Stream fermé } Err(e) => return Err(e.into()), } } Ok(pcm_output) } -
Tester avec un fichier AAC ADTS statique :
- Télécharger un échantillon AAC ADTS
- Vérifier que le décodage fonctionne
- Comparer PCM output avec ffmpeg
-
Tester avec un stream Radio France live :
#[tokio::test] #[ignore = "Requires network"] async fn test_decode_radiofrance_stream() { let stream_url = "https://icecast.radiofrance.fr/fip-hifi.aac"; let response = reqwest::get(stream_url).await.unwrap(); let reader = response.bytes_stream(); // Lire 10 secondes de stream let pcm = decode_aac_stream_test(reader).await.unwrap(); assert!(!pcm.is_empty()); // Vérifier format PCM (44.1kHz ou 48kHz, stéréo, 16-bit) }
Critères de succès Round 1
- ✅ Le décodeur accepte un
ReadOnlySourcesans erreur de seek - ✅ Les frames ADTS sont correctement parsées
- ✅ Le décodage AAC → PCM fonctionne
- ✅ Un stream live (infini) peut être décodé sans plantage
- ✅ Le PCM output est valide (vérifiable avec
ffplay)
Livrables Round 1
- Module de test :
pmoflac/tests/aac_streaming_test.rs - Rapport technique :
Blackboard/Report/Support_AAC_streaming_pmoflac.md- Résultats des tests
- Problèmes rencontrés (seek, parsing, etc.)
- Métriques de performance (CPU, latence)
- Comparaison qualité avec ffmpeg
Round 2 : Intégration dans pmoflac (si Round 1 réussit)
Objectif : Intégrer le décodeur AAC dans l'architecture streaming de pmoflac.
Fichiers à créer/modifier
1. pmoflac/src/aac.rs (nouveau)
use symphonia::core::io::{MediaSourceStream, ReadOnlySource};
use tokio::sync::mpsc;
use crate::{
common::ChannelReader,
decoder_common::{spawn_ingest_task, spawn_writer_task, DecodedStream},
pcm::StreamInfo,
};
pub type AacDecodedStream = DecodedStream<AacError>;
#[derive(thiserror::Error, Debug)]
pub enum AacError {
#[error("AAC decode error: {0}")]
Decode(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Channel closed")]
ChannelClosed,
}
/// Décoder un stream AAC/ADTS en PCM
pub async fn decode_aac_stream<R>(reader: R) -> Result<AacDecodedStream, AacError>
where
R: AsyncRead + Unpin + Send + 'static,
{
// Suivre le pattern existant (MP3, FLAC, etc.)
let (ingest_tx, ingest_rx) = mpsc::channel(CHANNEL_CAPACITY);
spawn_ingest_task(reader, ingest_tx);
let (pcm_tx, pcm_rx) = mpsc::channel(CHANNEL_CAPACITY);
let (pcm_reader, pcm_writer) = tokio::io::duplex(DUPLEX_BUFFER_SIZE);
let (info_tx, info_rx) = oneshot::channel::<Result<StreamInfo, AacError>>();
let blocking_handle = tokio::task::spawn_blocking(move || -> Result<(), AacError> {
let mut channel_reader = ChannelReader::<AacError>::new(ingest_rx);
// ReadOnlySource pour stream infini
let source = ReadOnlySource::new(&mut channel_reader);
let mss = MediaSourceStream::new(Box::new(source), Default::default());
// Probe AAC/ADTS
let mut hint = Hint::new();
hint.with_extension("aac");
let mut format = get_probe()
.format(&hint, mss, &Default::default(), &Default::default())
.map_err(|e| AacError::Decode(e.to_string()))?;
let track = format.default_track()
.ok_or_else(|| AacError::Decode("No audio track found".into()))?;
let mut decoder = get_codecs()
.make(&track.codec_params, &Default::default())
.map_err(|e| AacError::Decode(e.to_string()))?;
// Extraire StreamInfo
let codec_params = &track.codec_params;
let info = StreamInfo {
sample_rate: codec_params.sample_rate.unwrap_or(48000),
channels: codec_params.channels.unwrap().count() as u8,
bits_per_sample: 16, // AAC decode to 16-bit PCM
total_samples: None, // Stream infini
max_block_size: 0,
min_block_size: 0,
};
if info_tx.send(Ok(info.clone())).is_err() {
return Ok(());
}
// Boucle de décodage
loop {
match format.next_packet() {
Ok(packet) => {
let decoded = decoder.decode(&packet)
.map_err(|e| AacError::Decode(e.to_string()))?;
// Convertir AudioBufferRef → bytes PCM
let pcm_bytes = convert_audio_buffer_to_bytes(decoded, &info);
if pcm_tx.blocking_send(Ok(pcm_bytes)).is_err() {
break; // Reader fermé
}
}
Err(symphonia::core::errors::Error::IoError(e))
if e.kind() == std::io::ErrorKind::UnexpectedEof => {
break; // Stream terminé normalement
}
Err(e) => {
let msg = e.to_string();
let _ = pcm_tx.blocking_send(Err(AacError::Decode(msg.clone())));
return Err(AacError::Decode(msg));
}
}
}
Ok(())
});
let writer_handle = spawn_writer_task(pcm_rx, pcm_writer, blocking_handle, "aac-decode");
let info = info_rx.await.map_err(|_| AacError::ChannelClosed)??;
let reader = ManagedAsyncReader::new("aac-decode-writer", pcm_reader, writer_handle);
Ok(DecodedStream::new(info, reader))
}
/// Convertir AudioBufferRef Symphonia → bytes PCM little-endian
fn convert_audio_buffer_to_bytes(
audio_buffer: AudioBufferRef,
info: &StreamInfo,
) -> Vec<u8> {
// Implémenter conversion selon le type de buffer
// (S16, S24, S32, F32, etc.) → i16 little-endian interleaved
// ...
}
2. pmoflac/src/lib.rs (modifier)
pub mod aac;
pub use aac::{decode_aac_stream, AacDecodedStream, AacError};
3. pmoflac/src/autodetect.rs (modifier)
Ajouter la détection AAC/ADTS :
fn detect_format(bytes: &[u8]) -> Option<DetectedFormat> {
// ... détections existantes ...
// Détecter ADTS AAC (syncword 0xFFF)
if is_adts(bytes) {
return Some(DetectedFormat::Aac);
}
None
}
fn is_adts(bytes: &[u8]) -> bool {
if bytes.len() < 2 {
return false;
}
// ADTS syncword: 12 bits à 1 (0xFFF)
bytes[0] == 0xFF && (bytes[1] & 0xF0) == 0xF0
}
pub enum DecodedAudioStream {
// ... variants existants ...
Aac(AacDecodedStream),
}
4. pmoflac/src/transcode.rs (modifier)
Ajouter AAC au transcodeur :
pub enum AudioCodec {
// ... codecs existants ...
Aac,
}
pub async fn transcode_to_flac_stream<R>(
reader: R,
options: TranscodeOptions,
) -> Result<TranscodeToFlac, TranscodeError>
where
R: AsyncRead + Unpin + Send + 'static,
{
// ... détection auto ...
match decoded {
// ... cas existants ...
DecodedAudioStream::Aac(stream) => {
transcode_from_decoded(AudioCodec::Aac, stream, options.encoder_options).await
}
}
}
5. pmoflac/Cargo.toml (modifier)
[dependencies]
# ... dépendances existantes ...
# AAC support
symphonia = { version = "0.5", features = ["aac", "isomp4"], optional = true }
symphonia-core = { version = "0.5", optional = true }
symphonia-codec-aac = { version = "0.5", optional = true }
[features]
default = ["mp3", "ogg", "opus", "wav", "aiff"]
aac = ["dep:symphonia", "dep:symphonia-core", "dep:symphonia-codec-aac"]
all = ["mp3", "ogg", "opus", "wav", "aiff", "aac"]
Tests Round 2
Tests unitaires :
#[tokio::test]
async fn test_decode_aac_to_pcm() {
let aac_data = include_bytes!("../test-data/sample.aac");
let stream = decode_aac_stream(&aac_data[..]).await.unwrap();
let info = stream.info();
assert_eq!(info.sample_rate, 48000);
assert_eq!(info.channels, 2);
// Lire quelques samples
let mut buffer = vec![0u8; 4096];
let mut reader = stream;
let n = reader.read(&mut buffer).await.unwrap();
assert!(n > 0);
}
Tests intégration :
#[tokio::test]
#[ignore = "Integration test - network required"]
async fn test_transcode_radiofrance_to_flac() {
let stream_url = "https://icecast.radiofrance.fr/fip-hifi.aac";
let response = reqwest::get(stream_url).await.unwrap();
let reader = response.bytes_stream();
let transcoded = transcode_to_flac_stream(
reader,
TranscodeOptions::default()
).await.unwrap();
assert_eq!(transcoded.input_codec(), AudioCodec::Aac);
assert_eq!(transcoded.input_stream_info().sample_rate, 48000);
// Lire 5 secondes de FLAC
let mut output = Vec::new();
let mut stream = transcoded.into_stream();
for _ in 0..50 {
let mut chunk = vec![0u8; 8192];
stream.read(&mut chunk).await.unwrap();
output.extend_from_slice(&chunk);
tokio::time::sleep(Duration::from_millis(100)).await;
}
assert!(output.len() > 100_000); // Au moins 100 KB de FLAC
}
Critères de succès Round 2
- ✅
decode_aac_stream()suit le pattern existant (MP3, Ogg, etc.) - ✅ Auto-détection AAC/ADTS fonctionne
- ✅ Transcodage AAC → FLAC streaming opérationnel
- ✅ Tests unitaires et intégration passent
- ✅ Documentation complète (doctests, exemples)
- ✅ Feature flag
aacpour compilation optionnelle
Round 3 : Intégration dans pmoradiofrance (si Round 2 réussit)
Objectif : Remplacer le proxy AAC passthrough par un transcodage FLAC.
Modifications
1. pmoradiofrance/src/server_ext.rs
Remplacer le proxy passthrough par un transcodage :
async fn proxy_stream(
Path(slug): Path<String>,
State(state): State<Arc<RadioFranceServerState>>
) -> Result<Response, (StatusCode, String)> {
let stream_url = state.client.get_stream_url(&slug).await
.map_err(|e| (StatusCode::NOT_FOUND, e.to_string()))?;
let response = reqwest::get(&stream_url).await
.map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
// Transcoder AAC → FLAC avec pmoflac
let transcoded = pmoflac::transcode_to_flac_stream(
response.bytes_stream(),
pmoflac::TranscodeOptions::default()
).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Enregistrer connexion active et démarrer metadata refresh
// ...
// Stream FLAC au lieu d'AAC
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "audio/flac".parse().unwrap());
headers.insert("Cache-Control", "no-cache".parse().unwrap());
Ok((headers, Body::from_stream(transcoded.into_stream())).into_response())
}
2. pmoradiofrance/src/playlist.rs
Changer le protocol_info pour FLAC :
// Avant (AAC)
protocol_info: "http-get:*:audio/aac:*"
// Après (FLAC)
protocol_info: "http-get:*:audio/flac:*"
sample_frequency: Some(info.sample_rate.to_string())
bits_per_sample: Some("16".to_string())
Critères de succès Round 3
- ✅ Radio France sert du FLAC au lieu d'AAC
- ✅ Uniformité : toutes les sources PMOMusic servent du FLAC
- ✅ Latence acceptable (<2s) pour le streaming live
- ✅ CPU raisonnable pour 2-3 streams simultanés sur LAN
- ✅ Métadonnées volatiles toujours mises à jour
Alternative : fdk-aac (si Symphonia échoue)
Si Symphonia ne fonctionne pas en streaming infini, explorer fdk-aac :
Avantages
- ✅ Décodeur de référence (meilleure qualité)
- ✅ Conçu pour le streaming
- ✅ Utilisé en production (Android, etc.)
Inconvénients
- ❌ Dépendance C (compilation complexe)
- ❌ Licence restrictive (vérifier compatibilité projet)
Prototype minimal
use fdk_aac::dec::{Decoder, DecoderParams};
pub async fn decode_aac_with_fdk<R>(reader: R) -> Result<AacDecodedStream>
where
R: AsyncRead + Unpin + Send + 'static,
{
// Similar pattern to pmoflac MP3 decoder
// spawn_blocking pour le décodeur C
// ...
}
Résultats attendus
Minimum viable (Round 1)
- ✅ Rapport technique sur la faisabilité du streaming AAC avec Symphonia
- ✅ Prototype fonctionnel (même basique)
- ✅ Identification des limitations et solutions de contournement
Objectif complet (Round 1-3)
- ✅ Support AAC/ADTS dans
pmoflac(feature flag optionnelle) - ✅ Transcodage AAC → FLAC streaming opérationnel
- ✅ Radio France servant du FLAC uniforme
- ✅ Documentation et tests complets
En cas d'échec
- ✅ Rapport détaillé des blocages techniques
- ✅ Recommandations alternatives (fdk-aac, attendre évolution Symphonia, etc.)
- ✅ Garder le proxy AAC passthrough actuel