debug du lecteur web

This commit is contained in:
2025-11-27 20:27:52 +01:00
parent 3d1673157b
commit 9e8491710c
6 changed files with 314 additions and 54 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -194,6 +194,9 @@
@loadedmetadata="handleLoadedMetadata"
@durationchange="handleDurationChange"
@canplay="handleCanPlay"
@waiting="handleAudioWaiting"
@stalled="handleAudioStalled"
@progress="handleAudioProgress"
></audio>
</div>
</div>
@@ -251,12 +254,15 @@ const audioError = ref<string | null>(null)
const audioPlayer = ref<HTMLAudioElement | null>(null)
const currentTime = ref(0)
const duration = ref(0)
const lastTimeUpdateTs = ref<number | null>(null)
const lastLoggedTimeupdate = ref<number>(0)
const lastReadyState = ref<number | null>(null)
const lastNetworkState = ref<number | null>(null)
// Retry logic for fragile browser decoders
const MAX_AUDIO_RETRIES = 3
const RETRY_DELAY_MS = 800 // Réduit pour des transitions plus rapides
const retryCount = ref(0)
// Retry timer cleanup (auto-reload désactivé pour les streams live)
let retryTimer: ReturnType<typeof setTimeout> | null = null
let stallMonitor: ReturnType<typeof setInterval> | null = null
let stateMonitor: ReturnType<typeof setInterval> | null = null
// Metadata refresh via SSE
let metadataEventSource: EventSource | null = null
@@ -264,12 +270,69 @@ let metadataEventSource: EventSource | null = null
// Load sources on mount
onMounted(async () => {
await loadSources()
// Monitor pour détecter une éventuelle stagnation du currentTime
stallMonitor = setInterval(() => {
const p = audioPlayer.value
if (!p || !isPlaying.value) return
if (!lastTimeUpdateTs.value) return
const now = Date.now()
// Si aucun timeupdate depuis >5s et lecture en cours, logguer l'état du buffer
if (now - lastTimeUpdateTs.value > 5000) {
const buffered: string[] = []
for (let i = 0; i < p.buffered.length; i++) {
buffered.push(`${p.buffered.start(i).toFixed(3)}-${p.buffered.end(i).toFixed(3)}`)
}
console.debug('[PlayerDebug] stall-detected', {
ts: now,
currentTime: p.currentTime.toFixed(3),
duration: isFinite(p.duration) ? p.duration.toFixed(3) : 'NaN',
readyState: p.readyState,
networkState: p.networkState,
paused: p.paused,
buffered,
})
// Eviter le spam : reset le marqueur pour n'émettre qu'une fois par stagnation
lastTimeUpdateTs.value = null
}
}, 2000)
// Monitor périodique des changements de readyState/networkState
stateMonitor = setInterval(() => {
const p = audioPlayer.value
if (!p) return
const rs = p.readyState
const ns = p.networkState
if (rs !== lastReadyState.value || ns !== lastNetworkState.value) {
const buffered: string[] = []
for (let i = 0; i < p.buffered.length; i++) {
buffered.push(`${p.buffered.start(i).toFixed(3)}-${p.buffered.end(i).toFixed(3)}`)
}
console.debug('[PlayerDebug] state-change', {
ts: Date.now(),
currentTime: p.currentTime.toFixed(3),
readyState: rs,
networkState: ns,
paused: p.paused,
buffered,
})
lastReadyState.value = rs
lastNetworkState.value = ns
}
}, 1000)
})
// Cleanup on unmount
onUnmounted(() => {
stopMetadataRefresh()
clearRetryTimer()
if (stallMonitor) {
clearInterval(stallMonitor)
stallMonitor = null
}
if (stateMonitor) {
clearInterval(stateMonitor)
stateMonitor = null
}
})
/**
@@ -516,6 +579,18 @@ function handleAudioEnded() {
currentTime.value = 0
stopMetadataRefresh()
// On peut implémenter ici une logique de lecture automatique du prochain morceau
const p = audioPlayer.value
if (p) {
console.debug('[PlayerDebug] ended', {
ts: Date.now(),
currentTime: p.currentTime.toFixed(3),
duration: isFinite(p.duration) ? p.duration.toFixed(3) : 'NaN',
readyState: p.readyState,
networkState: p.networkState,
})
} else {
console.debug('[PlayerDebug] ended (no player)')
}
}
/**
@@ -530,16 +605,46 @@ function handleAudioError() {
? `Erreur de lecture audio (code ${errorCode})`
: 'Erreur de lecture audio inconnue'
if (selectedSource.value && currentTrack.value && retryCount.value < MAX_AUDIO_RETRIES) {
isPlaying.value = false
retryCount.value += 1
audioError.value = `${baseMessage} - tentative de reprise (${retryCount.value}/${MAX_AUDIO_RETRIES})`
scheduleRetryPlayback()
return
}
// Pour les streams live (Radio Paradise), ne pas recharger le flux : cela remet la progression à 0
// et crée un lag perceptible. On se contente d'afficher l'erreur et on tente un play() direct si possible.
audioError.value = baseMessage
isPlaying.value = false
// Tenter une reprise douce sans reload si le player a déjà des données bufferisées
const player = audioPlayer.value
if (player && !player.paused) {
// Rien à faire, il lit encore ou se relancera tout seul
return
}
if (player && player.readyState >= 2) {
console.debug('[PlayerDebug] error', {
ts: Date.now(),
message: baseMessage,
code: audio?.error?.code,
currentTime: player.currentTime.toFixed(3),
duration: isFinite(player.duration) ? player.duration.toFixed(3) : 'NaN',
readyState: player.readyState,
networkState: player.networkState,
buffered: (() => {
const arr: string[] = []
for (let i = 0; i < player.buffered.length; i++) {
arr.push(`${player.buffered.start(i).toFixed(3)}-${player.buffered.end(i).toFixed(3)}`)
}
return arr
})(),
})
void player.play().catch(() => {
// Si play échoue (user gesture requis ou decode error), on laisse l'erreur affichée
})
} else {
console.debug('[PlayerDebug] error (no retry)', {
ts: Date.now(),
message: baseMessage,
code: audio?.error?.code,
readyState: player?.readyState,
networkState: player?.networkState,
})
}
}
/**
@@ -557,6 +662,21 @@ function handleAudioPlay() {
function handleAudioPause() {
isPlaying.value = false
// Don't stop refresh on pause, user might resume
const p = audioPlayer.value
if (p) {
const buffered: string[] = []
for (let i = 0; i < p.buffered.length; i++) {
buffered.push(`${p.buffered.start(i).toFixed(3)}-${p.buffered.end(i).toFixed(3)}`)
}
console.debug('[PlayerDebug] pause', {
ts: Date.now(),
currentTime: p.currentTime.toFixed(3),
readyState: p.readyState,
networkState: p.networkState,
paused: p.paused,
buffered,
})
}
}
/**
@@ -565,6 +685,45 @@ function handleAudioPause() {
function handleTimeUpdate() {
if (audioPlayer.value) {
currentTime.value = audioPlayer.value.currentTime
lastTimeUpdateTs.value = Date.now()
// Log périodiquement l'état du buffer pour diagnostiquer les coupures
const logEvery = 2 // secondes (plus serré pour capturer les transitions)
if (Math.floor(currentTime.value) % logEvery === 0 && Math.floor(currentTime.value) !== lastLoggedTimeupdate.value) {
const p = audioPlayer.value
const buffered: string[] = []
for (let i = 0; i < p.buffered.length; i++) {
buffered.push(`${p.buffered.start(i).toFixed(3)}-${p.buffered.end(i).toFixed(3)}`)
}
console.debug('[PlayerDebug] timeupdate', {
ts: Date.now(),
currentTime: p.currentTime.toFixed(3),
duration: isFinite(p.duration) ? p.duration.toFixed(3) : 'NaN',
readyState: p.readyState,
networkState: p.networkState,
paused: p.paused,
buffered,
})
lastLoggedTimeupdate.value = Math.floor(currentTime.value)
}
// Si le temps recule (changement de piste) logguer explicitement l'état
if (currentTime.value < lastLoggedTimeupdate.value) {
const p = audioPlayer.value
const buffered: string[] = []
for (let i = 0; i < p.buffered.length; i++) {
buffered.push(`${p.buffered.start(i).toFixed(3)}-${p.buffered.end(i).toFixed(3)}`)
}
console.debug('[PlayerDebug] time-jump', {
ts: Date.now(),
currentTime: p.currentTime.toFixed(3),
duration: isFinite(p.duration) ? p.duration.toFixed(3) : 'NaN',
readyState: p.readyState,
networkState: p.networkState,
paused: p.paused,
buffered,
})
}
}
}
@@ -593,6 +752,60 @@ function handleCanPlay() {
console.log('Audio ready to play (canplay event)')
}
function handleAudioWaiting() {
const p = audioPlayer.value
if (!p) return
const buffered: string[] = []
for (let i = 0; i < p.buffered.length; i++) {
buffered.push(`${p.buffered.start(i).toFixed(3)}-${p.buffered.end(i).toFixed(3)}`)
}
console.debug('[PlayerDebug] waiting', {
ts: Date.now(),
currentTime: p.currentTime.toFixed(3),
duration: isFinite(p.duration) ? p.duration.toFixed(3) : 'NaN',
readyState: p.readyState,
networkState: p.networkState,
paused: p.paused,
buffered,
})
}
function handleAudioStalled() {
const p = audioPlayer.value
if (!p) return
const buffered: string[] = []
for (let i = 0; i < p.buffered.length; i++) {
buffered.push(`${p.buffered.start(i).toFixed(3)}-${p.buffered.end(i).toFixed(3)}`)
}
console.debug('[PlayerDebug] stalled', {
ts: Date.now(),
currentTime: p.currentTime.toFixed(3),
duration: isFinite(p.duration) ? p.duration.toFixed(3) : 'NaN',
readyState: p.readyState,
networkState: p.networkState,
paused: p.paused,
buffered,
})
}
function handleAudioProgress() {
const p = audioPlayer.value
if (!p) return
const buffered: string[] = []
for (let i = 0; i < p.buffered.length; i++) {
buffered.push(`${p.buffered.start(i).toFixed(3)}-${p.buffered.end(i).toFixed(3)}`)
}
console.debug('[PlayerDebug] progress', {
ts: Date.now(),
currentTime: p.currentTime.toFixed(3),
duration: isFinite(p.duration) ? p.duration.toFixed(3) : 'NaN',
readyState: p.readyState,
networkState: p.networkState,
paused: p.paused,
buffered,
})
}
/**
* Formatte le temps en MM:SS
*/
@@ -752,25 +965,11 @@ function clearRetryTimer() {
}
}
/**
* Réinitialise les compteurs de retry
*/
// Réinitialise l'état de retry (timer uniquement)
function resetAudioRetries() {
retryCount.value = 0
clearRetryTimer()
}
/**
* Lance un retry différé pour laisser le temps au backend de reproposer le flux
*/
function scheduleRetryPlayback() {
clearRetryTimer()
retryTimer = setTimeout(() => {
retryTimer = null
void retryPlayback()
}, RETRY_DELAY_MS)
}
/**
* (Ré)initialise la source audio et démarre la lecture
*/
@@ -852,20 +1051,6 @@ async function waitForSufficientBuffer(player: HTMLAudioElement): Promise<void>
})
}
/**
* Tente de relancer la lecture après une erreur (nouvelle résolution d'URI)
*/
async function retryPlayback() {
if (!selectedSource.value || !currentTrack.value) return
try {
const result = await resolveUri(selectedSource.value.id, currentTrack.value.id)
await startPlaybackFromUri(result.uri)
} catch (e: any) {
audioError.value = `Erreur lors de la reprise: ${e.message}`
isPlaying.value = false
}
}
</script>
<style scoped>

View File

@@ -76,7 +76,7 @@ use pmoflac::{EncoderOptions, FlacEncodedStream};
use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf};
use tokio::sync::{mpsc, RwLock};
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, trace, warn};
use tracing::{debug, error, info, trace, warn};
use crate::byte_stream_reader::{PcmChunk};
use crate::chunk_to_pcm::chunk_to_pcm_bytes;
@@ -317,6 +317,25 @@ impl NodeLogic for StreamingFlacSinkLogic {
}
debug!("StreamingFlacSink: SyncMarker::TrackBoundary {:?}",metadata.read().await.get_duration().await);
let current_ts = *self.ctx.current_timestamp.read().await;
// Durée attendue du morceau qui se termine : on lit les métadonnées courantes du sink
let (prev_title, prev_artist, prev_expected) = {
let meta = self.ctx.metadata.read().await;
let title = meta.title.clone().unwrap_or_else(|| "Unknown".into());
let artist = meta.artist.clone().unwrap_or_else(|| "Unknown".into());
let expected = meta
.duration
.map(|d| format!("{:.3}s", d.as_secs_f64()))
.unwrap_or_else(|| "unknown".into());
(title, artist, expected)
};
info!(
"StreamingFlacSink: track complete ts={:.3}s (title=\"{}\" artist=\"{}\" expected={})",
current_ts,
prev_title,
prev_artist,
prev_expected
);
if self.ctx.restart_encoder_on_track_boundary {
// Only restart encoder if it's already initialized (not the first track)

View File

@@ -259,6 +259,12 @@ impl NodeLogic for PlaylistSourceLogic {
.ok()
.flatten()
.unwrap_or_else(|| "Untitled".to_string());
let expected_duration = metadata_guard
.get_duration()
.await
.ok()
.flatten()
.map(|d| d.as_secs_f64());
drop(metadata_guard);
let remaining = self.playlist_handle.remaining().await.unwrap_or(0);
tracing::info!(
@@ -270,7 +276,8 @@ impl NodeLogic for PlaylistSourceLogic {
let track_start = std::time::Instant::now();
tracing::debug!("PlaylistSourceLogic: emitting TrackBoundary");
let boundary = AudioSegment::new_track_boundary(0, 0.0, metadata);
let metadata_for_boundary = metadata.clone();
let boundary = AudioSegment::new_track_boundary(0, 0.0, metadata_for_boundary);
send_to_children(node_name, &output, boundary).await?;
// Obtenir le chemin du fichier
@@ -307,6 +314,7 @@ impl NodeLogic for PlaylistSourceLogic {
&stop_token,
&self.cache,
cache_pk,
expected_duration,
emit_top_zero,
)
.await
@@ -363,6 +371,7 @@ async fn decode_and_emit_track(
stop_token: &CancellationToken,
cache: &Arc<AudioCache>,
cache_pk: &str,
expected_duration_sec: Option<f64>,
emit_top_zero: bool,
) -> Result<(), AudioError> {
// Attendre que le fichier soit suffisamment gros pour le sniffing
@@ -527,6 +536,20 @@ async fn decode_and_emit_track(
);
}
let actual_duration = total_frames as f64 / stream_info.sample_rate as f64;
let expected_str = expected_duration_sec
.map(|d| format!("{:.3}s", d))
.unwrap_or_else(|| "unknown".to_string());
tracing::info!(
"PlaylistSource: emitted pk={} frames={} sr={}Hz bit_depth={} duration={:.3}s (expected={})",
cache_pk,
total_frames,
stream_info.sample_rate,
stream_info.bits_per_sample,
actual_duration,
expected_str,
);
Ok(())
}

View File

@@ -9,7 +9,10 @@ use axum::{
Json, Router,
body::Body,
extract::{Path, State},
http::StatusCode,
http::{
header::{ACCEPT_RANGES, CACHE_CONTROL, CONNECTION, CONTENT_TYPE},
StatusCode,
},
response::{IntoResponse, Response},
routing::get,
};
@@ -236,7 +239,10 @@ async fn stream_flac(
let stream = channel.subscribe_flac();
Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "audio/flac")
.header(CONTENT_TYPE, "audio/flac")
.header(CACHE_CONTROL, "no-store, no-transform")
.header(CONNECTION, "keep-alive")
.header(ACCEPT_RANGES, "none")
.body(Body::from_stream(ReaderStream::new(stream)))
.unwrap())
}
@@ -249,7 +255,10 @@ async fn stream_ogg(
let stream = channel.subscribe_ogg();
Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "audio/ogg")
.header(CONTENT_TYPE, "application/ogg")
.header(CACHE_CONTROL, "no-store, no-transform")
.header(CONNECTION, "keep-alive")
.header(ACCEPT_RANGES, "none")
.body(Body::from_stream(ReaderStream::new(stream)))
.unwrap())
}
@@ -278,7 +287,10 @@ async fn stream_history_flac(
})?;
Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "audio/flac")
.header(CONTENT_TYPE, "audio/flac")
.header(CACHE_CONTROL, "no-store, no-transform")
.header(CONNECTION, "keep-alive")
.header(ACCEPT_RANGES, "none")
.body(Body::from_stream(ReaderStream::new(stream)))
.unwrap())
}
@@ -298,7 +310,10 @@ async fn stream_history_ogg(
})?;
Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "audio/ogg")
.header(CONTENT_TYPE, "application/ogg")
.header(CACHE_CONTROL, "no-store, no-transform")
.header(CONNECTION, "keep-alive")
.header(ACCEPT_RANGES, "none")
.body(Body::from_stream(ReaderStream::new(stream)))
.unwrap())
}

View File

@@ -13,7 +13,10 @@ use std::{fs, sync::Arc};
use axum::{
body::Body,
extract::{Path, State},
http::StatusCode,
http::{
header::{ACCEPT_RANGES, CACHE_CONTROL, CONNECTION, CONTENT_TYPE},
StatusCode,
},
response::{IntoResponse, Response},
routing::get,
Json, Router,
@@ -180,7 +183,10 @@ async fn stream_flac(
let stream = channel.subscribe_flac();
Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "audio/flac")
.header(CONTENT_TYPE, "audio/flac")
.header(CACHE_CONTROL, "no-store, no-transform")
.header(CONNECTION, "keep-alive")
.header(ACCEPT_RANGES, "none")
.body(Body::from_stream(ReaderStream::new(stream)))
.unwrap())
}
@@ -193,7 +199,10 @@ async fn stream_ogg(
let stream = channel.subscribe_ogg();
Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "audio/ogg")
.header(CONTENT_TYPE, "application/ogg")
.header(CACHE_CONTROL, "no-store, no-transform")
.header(CONNECTION, "keep-alive")
.header(ACCEPT_RANGES, "none")
.body(Body::from_stream(ReaderStream::new(stream)))
.unwrap())
}
@@ -206,7 +215,10 @@ async fn stream_icy(
let stream = channel.subscribe_icy();
Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "audio/flac")
.header(CONTENT_TYPE, "audio/flac")
.header(CACHE_CONTROL, "no-store, no-transform")
.header(CONNECTION, "keep-alive")
.header(ACCEPT_RANGES, "none")
.header("icy-metaint", "16000")
.body(Body::from_stream(ReaderStream::new(stream)))
.unwrap())
@@ -236,7 +248,10 @@ async fn stream_history_flac(
})?;
Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "audio/flac")
.header(CONTENT_TYPE, "audio/flac")
.header(CACHE_CONTROL, "no-store, no-transform")
.header(CONNECTION, "keep-alive")
.header(ACCEPT_RANGES, "none")
.body(Body::from_stream(ReaderStream::new(stream)))
.unwrap())
}
@@ -256,7 +271,10 @@ async fn stream_history_ogg(
})?;
Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "audio/ogg")
.header(CONTENT_TYPE, "application/ogg")
.header(CACHE_CONTROL, "no-store, no-transform")
.header(CONNECTION, "keep-alive")
.header(ACCEPT_RANGES, "none")
.body(Body::from_stream(ReaderStream::new(stream)))
.unwrap())
}