From b5f83ef6c86e08841ae7a07a489651f0a3ac9f1f Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sun, 5 Jul 2026 17:06:55 +0800 Subject: [PATCH] stream: compute now-playing duration/position off the HTTP thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /nowplaying handler called source.duration/source.elapsed on the live crossfaded source. Since it is polled every second by the web player, those cross-thread source queries contended with the crossfade's own clock during a transition and wedged the streaming clock — a few seconds of audio then a frozen buffer looping on air, only ever with a listener connected. Capture duration and the track start time in a synchronous on_metadata callback (which runs in the clock thread, where touching the source is safe) and have /nowplaying read those refs instead — position is just the wall-clock time since the track started. No handler touches the source outside its clock anymore, so the 3 s crossfade is safe again. Co-Authored-By: Claude Opus 4.8 --- stream/radio.liq | 17 +++++++++++++++++ stream/web.liq | 7 +++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/stream/radio.liq b/stream/radio.liq index 12d5bb3..8a46fcb 100644 --- a/stream/radio.liq +++ b/stream/radio.liq @@ -217,6 +217,23 @@ radio.on_metadata( end ) +# Durée et instant de départ du morceau courant, pour la règle de scrobble du +# player (« démarré au début et écouté à ~90 % »). CAPTURÉS ICI, DANS LE THREAD +# D'HORLOGE via un on_metadata SYNCHRONE : interroger source.duration/ +# source.elapsed depuis un handler HTTP (thread harbor) entrait en contention +# avec l'horloge du crossfade pendant une transition et figeait l'antenne. On ne +# touche donc plus la source hors de son horloge : /nowplaying (web.liq) se +# contente de lire ces refs — position = temps écoulé depuis cur_started. +cur_duration = ref(0.) +cur_started = ref(time()) +radio.on_metadata( + synchronous=true, + fun(_) -> begin + cur_duration := source.duration(radio) + cur_started := time() + end +) + # --- Sortie : flux MP3 sur http://:8000/radio.mp3 --- output.harbor( %mp3(bitrate=192), diff --git a/stream/web.liq b/stream/web.liq index bbe017e..d1d6fe9 100644 --- a/stream/web.liq +++ b/stream/web.liq @@ -95,11 +95,14 @@ harbor.http.register( # /scrobble pour prouver qu'il parle bien du titre à l'antenne. # `duration`/`position` (secondes, arrondies) permettent au player de décider # d'un scrobble « écouté à 90 %, démarré au début ». duration=0 si inconnue. + # Ces valeurs sont pré-calculées dans le thread d'horloge (voir cur_duration/ + # cur_started dans radio.liq) : on ne doit JAMAIS appeler source.duration/ + # source.elapsed ici, ça figerait l'antenne pendant un crossfade. resp.json({ title=m["title"], artist=m["artist"], url=m["url"], origin=m["origin"], file=path.basename(m["filename"]), - duration=int_of_float(source.duration(radio)), - position=int_of_float(source.elapsed(radio)) + duration=int_of_float(cur_duration()), + position=int_of_float(time() - cur_started()) }) end )