Ajout de la gestion de la position en temps réel et amélioration de la gestion de la durée

Ajout de l'événement Position pour envoyer la position courante ~1/s pendant la lecture.

Modification de la gestion de la durée dans le registry pour ne pas écraser une durée déjà connue par la source (priorité à la source pour les flux radio).

Mise à jour du handler position_update pour utiliser update_duration au lieu de update_position, et suppression de la gestion de position_sec qui est maintenant gérée par PlayerEvent::Position.

Le serveur gère maintenant la position via les événements Position émis par PlayerSource, et la durée est uniquement utilisée comme fallback pour les sources sans durée connue.
This commit is contained in:
2026-03-01 23:55:40 +01:00
parent f57754b5f8
commit 2b0def806a
4 changed files with 29 additions and 7 deletions

View File

@@ -79,6 +79,8 @@ pub enum PlayerEvent {
Stopped,
/// Fin de piste (pour que le ControlPoint avance la queue)
TrackEnded,
/// Position courante (émise ~1/s pendant la lecture)
Position { position_sec: f64 },
/// Erreur lors de l'ouverture ou de la lecture
Error(String),
}
@@ -345,6 +347,9 @@ impl PlayerSourceLogic {
let source_stop = stop_token.child_token();
let source_stop_clone = source_stop.clone();
// Dernière seconde entière pour laquelle on a émis un Position
let mut last_reported_sec: i64 = -1;
// Spawner l'émission de la source dans une tâche séparée
let emit_task = tokio::spawn(async move {
source.emit_to_channel(&chunk_tx, &source_stop_clone).await
@@ -443,6 +448,14 @@ impl PlayerSourceLogic {
// Mettre à jour la position courante
if seg.is_audio_chunk() {
*paused_at_sec = seg.timestamp_sec;
// Émettre Position ~1/s
let sec = paused_at_sec.floor() as i64;
if sec != last_reported_sec {
last_reported_sec = sec;
let _ = self.event_tx.send(PlayerEvent::Position {
position_sec: *paused_at_sec,
});
}
}
// Envoyer au pipeline en aval
if let Err(e) = send_to_children("PlayerSource", output, seg).await {

View File

@@ -162,6 +162,9 @@ async fn run_event_listener(
s.playback_state = PlaybackState::Stopped;
s.position = None;
}
PlayerEvent::Position { position_sec } => {
state.write().position = Some(seconds_to_upnp_time(position_sec));
}
PlayerEvent::TrackEnded => {
#[cfg(feature = "pmoserver")]
{

View File

@@ -68,12 +68,14 @@ pub struct PositionUpdateRequest {
}
/// POST /api/webrenderer/{id}/position
/// position_sec est ignoré (géré par PlayerEvent::Position côté serveur).
/// duration_sec est utilisé comme fallback si la source ne connaît pas la durée (flux radio).
pub async fn position_update_handler(
State(registry): State<Arc<RendererRegistry>>,
Path(instance_id): Path<String>,
Json(req): Json<PositionUpdateRequest>,
) -> impl IntoResponse {
registry.update_position(&instance_id, req.position_sec, req.duration_sec);
registry.update_duration(&instance_id, req.duration_sec);
StatusCode::NO_CONTENT
}

View File

@@ -152,15 +152,19 @@ impl RendererRegistry {
.map(|i| i.device_instance.clone())
}
/// Met à jour la position et la durée depuis le navigateur (audio.currentTime).
pub fn update_position(&self, instance_id: &str, position_sec: f64, duration_sec: Option<f64>) {
/// Met à jour la durée depuis le navigateur.
/// La position est gérée par PlayerSource via PlayerEvent::Position.
/// On n'utilise duration_sec que si la source ne la connaît pas (flux radio sans durée).
pub fn update_duration(&self, instance_id: &str, duration_sec: Option<f64>) {
let instances = self.instances.read();
if let Some(instance) = instances.get(instance_id) {
let mut s = instance.state.write();
s.position = Some(crate::pipeline::seconds_to_upnp_time(position_sec));
if let Some(dur) = duration_sec {
if dur > 0.0 {
s.duration = Some(crate::pipeline::seconds_to_upnp_time(dur));
// N'écraser la durée que si elle n'est pas déjà connue (la source est prioritaire)
if s.duration.is_none() {
if let Some(dur) = duration_sec {
if dur > 0.0 {
s.duration = Some(crate::pipeline::seconds_to_upnp_time(dur));
}
}
}
}