Implémentation du playback gapless avec préchargement des pistes

Cette modification implémente un système de playback gapless pour les renderers web, permettant une transition fluide entre les pistes. Le changement inclut l'ajout d'un moteur audio qui utilise deux éléments <audio> en ping-pong connectés à un AudioContext pour le contrôle du volume/mute et le contrôle du flux gapless. Les commandes UPnP ont été mises à jour pour gérer le préchargement des pistes suivantes via SetNextAVTransportURI. Le backend a été modifié pour gérer les transitions gapless et précharger les pistes suivantes. Des ajustements ont également été apportés au serveur pour permettre les requêtes CORS nécessaires à ce fonctionnement.
This commit is contained in:
2026-02-21 10:16:49 +01:00
parent 03775f7574
commit fbe7291b43
10 changed files with 493 additions and 102 deletions

View File

@@ -878,6 +878,10 @@ impl ControlPoint {
renderer.set_last_metadata(Some(metadata));
renderer.set_playback_source(PlaybackSource::FromQueue);
// Prefetch next track if supported (gapless playback)
self.prefetch_next_track(&renderer, renderer_id);
Ok(())
}
@@ -972,6 +976,51 @@ impl ControlPoint {
}
}
/// Prefetch the next track for a renderer (gapless playback support).
///
/// Called by the WebRenderer backend after a TrackEnded event to trigger
/// SetNextAVTransportURI for the next-next track (N+2).
pub fn prefetch_next_for_renderer(&self, renderer_id: &DeviceId) {
if let Some(renderer) = self.music_renderer_by_id(renderer_id) {
self.prefetch_next_track(&renderer, renderer_id);
}
}
/// Advances the queue index by one for a gapless transition, then prefetches the next-next track.
///
/// Called by the WebRenderer backend after a gapless TrackEnded event. In the gapless case,
/// the browser has already advanced to the next track autonomously, so the backend queue
/// index must be updated to stay in sync. Then we prefetch track N+2 for the next gapless
/// transition.
pub fn advance_queue_and_prefetch(&self, renderer_id: &DeviceId) {
if let Some(renderer) = self.music_renderer_by_id(renderer_id) {
// Advance the queue index to match the browser's gapless transition
match renderer.advance_queue_index() {
Ok(true) => {
debug!(
renderer = renderer_id.0.as_str(),
"advance_queue_and_prefetch: queue index advanced for gapless transition"
);
// Now prefetch the next-next track (N+2)
self.prefetch_next_track(&renderer, renderer_id);
}
Ok(false) => {
debug!(
renderer = renderer_id.0.as_str(),
"advance_queue_and_prefetch: no next track in queue, skipping prefetch"
);
}
Err(err) => {
debug!(
renderer = renderer_id.0.as_str(),
error = %err,
"advance_queue_and_prefetch: failed to advance queue index"
);
}
}
}
}
/// Jumps to a specific index in the queue and starts playback.
pub fn play_queue_index(
&self,
@@ -1019,6 +1068,10 @@ impl ControlPoint {
}
renderer.set_playback_source(PlaybackSource::FromQueue);
// Prefetch next track if supported (gapless playback)
self.prefetch_next_track(&renderer, renderer_id);
Ok(())
}

View File

@@ -879,6 +879,21 @@ impl MusicRenderer {
Ok(())
}
/// Advance the queue index by one without starting playback.
///
/// Used by the WebRenderer gapless path: the browser autonomously transitions
/// to the next track, so only the backend queue pointer needs to be updated
/// to stay in sync.
pub fn advance_queue_index(&self) -> Result<bool, ControlPointError> {
let mut backend = self.lock_backend_for("advance_queue_index");
let advanced = backend.advance()?;
drop(backend);
if advanced {
self.emit_queue_updated();
}
Ok(advanced)
}
/// Play from a specific index in the queue.
pub fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
// Reset the has_played flag before starting playback to prevent