Corrige le bug critique qui empêchait la mise en cache des covers pour
les fichiers courts (jingles, etc.) :
Le problème :
- Quand EndOfStream arrivait AVANT la fin du prebuffer, le code retournait
immédiatement sans copier les métadonnées ni cacher les covers
- Cela affectait particulièrement les fichiers courts (jingles) où le
prebuffer de 512KB n'était pas atteint avant la fin du fichier
La solution :
- Lorsque EndOfStream est reçu pendant le prebuffer, on ferme le pump mais
on CONTINUE à attendre que cache_future se termine pour obtenir le pk
- Une fois le pk obtenu, on copie les métadonnées et on cache les covers
normalement avant de retourner
- Utilise un flag end_of_stream_received et une Option<track_tx> pour gérer
le cas où track_tx est déjà fermé
Test validé :
✓ Les covers sont bien cachées même pour les fichiers courts
✓ Fichier de cover présent : 36e3e134b8de74e6c16f202e3b3b543d.orig.webp (38K)
✓ Logs montrent : "Successfully cached cover for pk ... with cover pk ..."
Améliore la gestion du cache des covers dans FlacCacheSink :
- Remplace les avertissements génériques par des logs détaillés (debug/info/warn)
- Corrige la gestion des erreurs en retirant le `let _ =` qui ignorait les résultats
- Ajoute des logs de debug pour tracer le processus de mise en cache des covers
- Améliore la gestion des erreurs avec des messages plus informatifs
Corrige la playlist de l'exemple play_and_cache :
- Remplace create_persistent_playlist par get_write_handle pour créer une playlist éphémère
- Une playlist persistante n'est pas nécessaire pour cet exemple de démonstration
The small buffer (8) was causing the pump to block frequently when the FLAC
encoder was slow to consume data. This created micro-pauses in the PCM stream
that resulted in audible clicks in the encoded FLAC files.
With a larger buffer (256), the pump can continue sending data without blocking,
ensuring continuous audio flow to the encoder and eliminating the clicks.
Previous fix consumed the TrackBoundary with drain_until_track_boundary(),
preventing the next track from being processed correctly. This caused
audio to stop after the first cached track.
Solution: Use a pump_closed flag instead of draining. When the pump
closes early (cache hit), set the flag and ignore subsequent chunks
until TrackBoundary. The TrackBoundary is then handled normally by
the existing code, allowing proper continuation to the next track.
This preserves the block structure and allows all tracks in a block
to be processed correctly, whether cached or not.
When a file was already in cache, add_from_reader() would return
immediately after reading only 1024 bytes to compute the pk. This
closed the flac_stream and pcm_tx, causing the pump to terminate
normally. However, the dispatcher treated track_tx.send() failure
as a fatal error, even though the pump had completed successfully.
Changes:
- In phase 3 post-prebuffer, when track_tx.send() fails, wait for
pump to complete and check its result
- If pump returned Ok(), drain remaining segments until TrackBoundary
- If pump returned Err(), propagate the error
- This allows graceful handling of cache hits while preserving error
detection for genuine pump failures
Fixes the "Pump task died" error when relaunching play_and_cache
with existing cached files.
Problem: The dispatcher was placed AFTER the prebuffer await, causing a deadlock:
- Pump waits for data on track_rx
- Cache waits for pump to produce PCM data
- Code awaits cache completion before reaching dispatcher
- Dispatcher never runs → pump never receives data → deadlock
Solution: Use tokio::select! to dispatch segments in parallel with prebuffer wait
Architecture now has 3 phases:
1. Phase 1: Dispatch chunks + await prebuffer (in parallel via select!)
2. Phase 2: Copy metadata + push to playlist (after prebuffer complete)
3. Phase 3: Continue dispatching until TrackBoundary
This fixes the "sans musique" blocking issue where the system would freeze
waiting for prebuffer that could never complete.
Problem: When TrackBoundary arrived, the pump was awaited before continuing,
causing file truncation when pcm_tx was dropped while data was still buffering.
Solution: Allow multiple pump tasks to run in parallel:
- Create dedicated channel (track_tx/track_rx) for each track's pump
- Main loop reads from rx and dispatches segments to current pump via track_tx
- When TrackBoundary arrives: drop track_tx (signals pump to finish) and immediately start new pump
- Old pump continues writing in background until all data is flushed
This prevents truncation in progressive cache scenario (radio streaming).
Changes in flac_cache_sink.rs:
- Replace pump_track_segments_owned() with pump_track_segments_from_channel()
- Remove rx ownership passing - each pump gets its own channel
- Dispatcher loop reads rx and forwards to active pump
- No await on pump completion - let it finish in background
Changes:
- Add TimerNode (pmoaudio/src/nodes/timer_node.rs): Rate-limits audio chunk flow based on timestamps with configurable max_lead_time
- Integrate TimerNode into play_and_cache.rs pipeline: PlaylistSource → TimerNode (3s pacing) → AudioSink
- Improve EOF retry in playlist_source.rs: Wait for prebuffer (512KB) before decoding, retry on temporary EOF with 200ms delay
- Export TimerNode in pmoaudio lib.rs and nodes/mod.rs
Known issue: Cache files may still be truncated when TrackBoundary arrives before pump completes flushing.
This requires allowing parallel write tasks as suggested.
Problem:
- PlaylistSource reads cached files faster than FlacCacheSink writes them
- FLAC decoder encounters EOF and stops playback prematurely
- First track doesn't play completely (stops at prebuffer point ~600ms)
- Needed to differentiate:
* Temporary EOF: file still being written (wait and retry)
* Real EOF: file completely written (stop decoding)
Solution:
1. Added Cache::is_download_complete() method (pmocache/src/cache.rs:735)
- Checks for existence of completion marker (.complete file)
- Marker created only when file is fully written and closed
- Fast synchronous check (no async overhead)
2. Modified decode_and_emit_track() (playlist_source.rs:337)
- On EOF: check if completion marker exists
- If no marker: file still being written → wait 50ms and retry read
- If marker exists: file complete → finish decoding
- Reduced wait from 100ms to 50ms for better responsiveness
Benefits:
✅ First track now plays completely (not just prebuffer portion)
✅ Progressive caching still works (playback starts at ~600ms)
✅ Proper EOF handling (no premature stops)
✅ Efficient polling (50ms retry interval)
✅ Works for both fresh downloads and cached files
Tested:
- Fresh download: EOF retries visible in logs every ~50ms
- File plays until completion marker created
- No premature track termination
Related to previous optimization (commit d8594e7) that made
prebuffer→playlist push immediate (76ms instead of 19s).
Problem:
- tokio::join!() waited for both cache_future AND pump_future to complete
- cache_future returned after prebuffer (~530ms)
- pump_future read entire first track (~19s)
- Track only pushed to playlist after both finished → 19s delay
Solution (Solution A from OPTIMIZATION_PREBUFFER_TO_PLAYLIST.md):
- Created pump_track_segments_owned() that takes ownership of rx and returns it
- Spawned pump in tokio::spawn to run independently
- Wait for cache_future alone → push to playlist immediately
- Wait for pump_handle later to recover rx for next track
Results (tested with play_and_cache --null-audio):
Before:
- Prebuffer → playlist: ~19s
- Prebuffer → playback: ~19.5s
After:
- Prebuffer → playlist: ~24ms
- Prebuffer → playback: ~76ms
- Improvement: 99.6% (250x faster!)
Target was <1s, achieved 76ms (13x better than target!)
Changes:
- Added pump_track_segments_owned() in flac_cache_sink.rs:516
- Modified FlacCacheSinkLogic::process() to use tokio::spawn pattern
- Added timing logs (INFO level) for prebuffer and playlist push
- rx ownership properly managed: moved to pump, returned, recovered
Tests passed:
✅ Prebuffer completes in ~530ms (512KB downloaded)
✅ Track pushed to playlist in ~24ms after prebuffer
✅ Playback starts in ~76ms after prebuffer
✅ rx properly recovered for next tracks
✅ No panics or deadlocks
Added comprehensive logging to FlacCacheSink::process():
- Process start
- Waiting for/receiving first audio chunk
- FLAC encoder creation
- Cache ingestion and pump parallel execution
- tokio::join! completion
- Track added to cache confirmation
Testing results show PROGRESSIVE CACHING WORKS:
✅ Prebuffer reached in 0.6 seconds
✅ Track added to cache with pk
✅ Download pipeline completes successfully
✅ Playlist receives track
✅ Playback starts
Current timing:
- t=0.6s: Prebuffer complete (512KB)
- t=3.6s: Track added to playlist (after pump completes)
- t=4.5s: Playback starts
The 3s delay is because tokio::join! waits for BOTH futures:
- cache_future (returns after prebuffer ~0.6s)
- pump_future (pumps entire first track ~3s)
For true 1-2s startup, would need to refactor to push to playlist
immediately after prebuffer, without waiting for pump to complete.
Problem: All FLAC files with the same format (44.1kHz, stereo, 16-bit)
had identical headers and thus the same pk (071c5713d5cf485ca688832207bef0f9).
This caused the cache to think all tracks were the same file, regardless
of channel selection or actual content.
Solution: Skip the FLAC header (first 512 bytes) and calculate the pk
from bytes 512-1024 (actual audio content) instead. This ensures each
track gets a unique pk based on its actual audio data, not just its
format header.
Changes:
- Modified add_from_reader_with_pk() to read 1024 bytes instead of 512
- Use bytes 512-1024 for pk calculation when explicit_pk is None
- This works even with poor metadata (empty artist/title)
- Maintains backward compatibility with explicit_pk parameter
Fixes the issue where changing radio channel played the same song.
When a file was already in cache, FlacCacheSink would drain all
remaining segments (which can take 13+ seconds - the full track
duration) BEFORE adding the track to the playlist. This caused
a long delay before playback could start.
The fix reorders operations to:
1. Copy metadata to cache (fast)
2. Add pk to playlist IMMEDIATELY (fast)
3. Drain remaining segments (slow, but playback already started)
This ensures the playlist receives tracks immediately, allowing
playback to start without waiting for segment drainage to complete.
Fixes the 13-second delay when playing already-cached files.
The PlaylistSource decoder was hitting EOF prematurely when reading
files that were still being downloaded (progressive cache). Instead
of stopping, it now checks if the download is still ongoing and waits
100ms before retrying.
This preserves the progressive cache behavior: playback can start as
soon as the prebuffer (512KB) is ready, and the decoder will
gracefully wait for more data to be written as the download continues.
Changes:
- Modified decode_and_emit_track() to accept cache and pk parameters
- When EOF is reached (read == 0), check if download is ongoing
- If download is ongoing, wait 100ms and retry instead of stopping
- Only break the loop when download is complete and EOF is reached
Fixes the issue where the decoder would stop prematurely on
partially downloaded files.
- Add .complete marker files to track completed downloads
- Check marker instead of file size for completion detection
- Drain segments when file already in cache to avoid pipeline errors
- Consolidate() now removes incomplete files without markers
- Add new_cache_with_consolidation() for automatic cleanup on startup
Quand un fichier est déjà en cache, add_from_reader() retourne immédiatement
sans lire le stream FLAC, ce qui ferme le channel PCM. Avant cette correction,
pump_track_segments() retournait une erreur SendError, causant l'échec du
pipeline download.
Changements :
- Dans pump_track_segments(), détecter quand le channel est fermé
- Retourner Ok avec StopReason::ChannelClosed au lieu d'une erreur
- Ceci permet au pipeline de se terminer gracieusement
Cette situation est normale et attendue quand le fichier est déjà en cache.
Corrections :
- Removed unused Cursor import
- Fixed borrow checker issues by using tokio::join! instead of tokio::spawn
- Kept progressive streaming approach with add_from_reader
La solution finale utilise tokio::join! pour exécuter pump_track_segments
et add_from_reader en parallèle, évitant ainsi les problèmes de lifetime
avec tokio::spawn tout en conservant le streaming progressif.
Cette correction implémente le cache progressif et le streaming pour permettre
un démarrage quasi immédiat de la lecture pendant le téléchargement.
## Changements dans FlacCacheSink (pmoaudio-ext)
Avant :
- Accumulait tout le FLAC en mémoire dans un buffer
- Attendait la fin complète de l'encodage avant d'ajouter au cache
- Ajoutait à la playlist seulement après ingestion complète
Après :
- Passe le flux FLAC directement à add_from_reader
- add_from_reader retourne dès que le prebuffer (512 KB) est atteint
- Le PK est ajouté à la playlist immédiatement après le prebuffer
- L'encodage et l'écriture continuent en arrière-plan
## Changements dans play_and_cache.rs
- Suppression du sleep de 2 secondes avant le démarrage de la lecture
- Ajout de commentaire expliquant le mécanisme de prebuffer
- La lecture démarre dès que le prebuffer est atteint (~1-2 secondes)
## Résultat
La musique démarre maintenant presque immédiatement après le début du
téléchargement (temps du prebuffer) au lieu d'attendre la fin du
téléchargement complet du premier morceau.
Corrections pour que l'exemple utilise les bonnes API:
- Utilisation directe de Cache::new() au lieu de méthodes de config
- Suppression des appels à root() qui n'existent pas
- Utilisation du singleton PlaylistManager() au lieu de new()
- Ajout de cache-sink comme dépendance de playlist feature dans pmoaudio-ext
L'exemple devrait maintenant compiler correctement avec:
cargo run --example play_and_cache --features full -- <channel_id>
Improve the architecture by configuring the playlist handle directly
in register_playlist() instead of deferring it to run().
Changes:
- Remove playlist_handle_pending field (no longer needed)
- register_playlist() now calls logic_mut() to configure immediately
- run() becomes a simple delegation with no configuration logic
- Follows proper pattern: configuration before run(), not during run()
This is cleaner than the previous approach which used a pending field
and transferred it during run(). The new approach:
1. User calls register_playlist() → directly configures logic
2. User calls run() → simple delegation to inner.run()
Architecture now properly separates configuration from execution.
- Add Node::logic_mut() method to allow post-construction configuration
of node logic before run() is called
- Fix FlacCacheSink to properly transfer playlist_handle_pending to
the inner logic using logic_mut()
- Resolves FIXME at flac_cache_sink.rs:656 about missing logic_mut()
This enables the playlist registration mechanism to work correctly:
1. User calls register_playlist() on FlacCacheSink
2. Handle is stored in playlist_handle_pending
3. During run(), handle is transferred to FlacCacheSinkLogic
4. Tracks are automatically added to playlist after caching