From 10c75f04e1b8cdd54f032e19eb4638cbea6a439c Mon Sep 17 00:00:00 2001 From: Glenn Thompson Date: Thu, 9 Apr 2026 16:32:58 +0100 Subject: [PATCH] Stream sync, countdown timer, playback state fix, buffer bloat detection Multi-pronged attempt to fix cumulative lag between audio playback, now-playing display, and browser notifications: == Synchronization (work in progress) == - Server-side time-based metadata delay via get-listener-now-playing with configurable *browser-buffer-seconds* parameter - First track after restart syncs perfectly; subsequent tracks drift due to browser audio buffer bloat (buffer grows unbounded over time) - Added client-side buffer bloat detection: monitors audio.buffered vs audio.currentTime and auto-reconnects when buffer exceeds 15s - Added [STREAM-SYNC], [NOTIFY], [BUFFER] diagnostic logging to browser console for ongoing diagnosis == Countdown timer == - Server exposes remaining seconds via pipeline-track-remaining - now-playing JSON API includes 'remaining' field adjusted for browser buffer delay - Player frame shows [mm:ss] countdown next to track title - Main page now-playing area also shows countdown with its own polling (15s interval) and local 1-second ticker - LASS styles for .track-countdown and .track-countdown-mini == Playback state fix == - Fixed save-playback-state saving one track ahead of what was actually playing (was saving the track loading during crossfade) - Uses *pending-save-file* with one-track delay so the saved state reflects the track that was actually heard == Notifications == - All notification conditions working correctly per diagnostic logs - show-track-notification logs supported/permission/enabled/last/title - Notifications fire consistently on track changes --- cl-streamer | 2 +- frontend-partials.lisp | 14 ++-- parenscript/front-page.lisp | 47 ++++++++++++- parenscript/stream-player.lisp | 78 +++++++++++++++++++-- playlists/deep-focus.m3u | 113 ++++++++++++++++++++++++++++++ static/asteroid.css | 15 ++++ static/asteroid.lass | 13 ++++ stream-harmony.lisp | 41 ++++++++--- template/audio-player-frame.ctml | 1 + template/partial/now-playing.ctml | 3 +- 10 files changed, 303 insertions(+), 24 deletions(-) create mode 100644 playlists/deep-focus.m3u diff --git a/cl-streamer b/cl-streamer index 23b5ee1..8a5dde5 160000 --- a/cl-streamer +++ b/cl-streamer @@ -1 +1 @@ -Subproject commit 23b5ee1e35e6c967cab12751ac7b8930d21b3307 +Subproject commit 8a5dde598370b13965317f456999a85096832be6 diff --git a/frontend-partials.lisp b/frontend-partials.lisp index cec0665..d814037 100644 --- a/frontend-partials.lisp +++ b/frontend-partials.lisp @@ -120,12 +120,14 @@ (artist (getf parsed :artist)) (song (getf parsed :song)) (search-url (generate-music-search-url artist song))) - (api-output `(("status" . "success") - ("title" . ,title) - ("listeners" . ,(cdr (assoc :listeners now-playing-stats))) - ("track_id" . ,(cdr (assoc :track-id now-playing-stats))) - ("favorite_count" . ,favorite-count) - ("search_url" . ,search-url)))) + (let ((remaining (cdr (assoc :remaining now-playing-stats)))) + (api-output `(("status" . "success") + ("title" . ,title) + ("listeners" . ,(cdr (assoc :listeners now-playing-stats))) + ("track_id" . ,(cdr (assoc :track-id now-playing-stats))) + ("favorite_count" . ,favorite-count) + ("search_url" . ,search-url) + ,@(when remaining `(("remaining" . ,remaining))))))) (api-output `(("status" . "offline") ("title" . "Stream Offline") ("track_id" . nil))))))) diff --git a/parenscript/front-page.lisp b/parenscript/front-page.lisp index 9658fc3..f6e8572 100644 --- a/parenscript/front-page.lisp +++ b/parenscript/front-page.lisp @@ -866,7 +866,52 @@ (ps:chain console (log "Could not load recent requests:" error)))))))) ;; Load recent requests on page load - (load-recent-requests))) + (load-recent-requests) + + ;; Main page countdown timer + (defvar *main-remaining* nil) + + (defun format-countdown (seconds) + (let ((m (ps:chain -math (floor (/ seconds 60)))) + (s (ps:chain -math (floor (mod seconds 60))))) + (+ (if (< m 10) (+ "0" m) m) ":" (if (< s 10) (+ "0" s) s)))) + + (defun poll-now-playing () + (let ((mount (or (ps:chain local-storage (get-item "stream-mount")) "asteroid.mp3"))) + (ps:chain + (fetch (+ "/api/asteroid/partial/now-playing-json?mount=" mount)) + (then (lambda (response) + (if (ps:@ response ok) + (ps:chain response (json)) + nil))) + (then (lambda (data) + (when data + (let ((title (or (ps:@ data data title) (ps:@ data title))) + (remaining (or (ps:@ data data remaining) (ps:@ data remaining))) + (listeners (or (ps:@ data data listeners) (ps:@ data listeners))) + (title-el (ps:chain document (get-element-by-id "current-track-title"))) + (listener-el (ps:chain document (get-element-by-id "current-listeners")))) + (when (and title-el title) + (setf (ps:@ title-el text-content) title)) + (when (and listener-el listeners) + (setf (ps:@ listener-el text-content) listeners)) + (when remaining + (setf *main-remaining* remaining)))))) + (catch (lambda (error) nil))))) + + ;; Start polling and countdown ticker on the main page + (set-timeout poll-now-playing 2000) + (set-interval poll-now-playing 15000) + (set-interval + (lambda () + (let ((el (ps:chain document (get-element-by-id "track-countdown-main")))) + (when el + (if (and *main-remaining* (> *main-remaining* 0)) + (progn + (decf *main-remaining*) + (setf (ps:@ el text-content) (+ "[" (format-countdown *main-remaining*) "]"))) + (setf (ps:@ el text-content) ""))))) + 1000))) "Compiled JavaScript for front-page - generated at load time") (defun generate-front-page-js () diff --git a/parenscript/stream-player.lisp b/parenscript/stream-player.lisp index 62e2530..958059a 100644 --- a/parenscript/stream-player.lisp +++ b/parenscript/stream-player.lisp @@ -336,6 +336,30 @@ ;; Track last notified title to avoid duplicate notifications (defvar *last-notified-title* nil) + ;; Countdown timer state + (defvar *track-remaining-seconds* nil) + (defvar *countdown-interval* nil) + + (defun format-countdown (seconds) + (let ((m (ps:chain -math (floor (/ seconds 60)))) + (s (ps:chain -math (floor (mod seconds 60))))) + (+ (if (< m 10) (+ "0" m) m) ":" (if (< s 10) (+ "0" s) s)))) + + (defun start-countdown-ticker () + (when *countdown-interval* + (clear-interval *countdown-interval*)) + (setf *countdown-interval* + (set-interval + (lambda () + (let ((el (ps:chain document (get-element-by-id "track-countdown")))) + (when el + (if (and *track-remaining-seconds* (> *track-remaining-seconds* 0)) + (progn + (decf *track-remaining-seconds*) + (setf (ps:@ el text-content) (+ "[" (format-countdown *track-remaining-seconds*) "]"))) + (setf (ps:@ el text-content) ""))))) + 1000))) + ;; Check if notifications are enabled in localStorage (defun notifications-enabled-p () (= (ps:chain local-storage (get-item "notifications-enabled")) "true")) @@ -410,6 +434,12 @@ ;; Show a system notification for track change (defun show-track-notification (title body) + (ps:chain console (log "[NOTIFY] show-track-notification called:" + "supported=" (notifications-supported-p) + "permission=" (get-notification-permission) + "enabled=" (notifications-enabled-p) + "last=" *last-notified-title* + "title=" title)) (when (and (notifications-supported-p) (= (get-notification-permission) "granted") (notifications-enabled-p) @@ -422,6 +452,7 @@ :tag "asteroid-track-change" :renotify true :silent false))))) + (ps:chain console (log "[NOTIFY] Notification created successfully")) ;; Auto-close after 5 seconds (set-timeout (lambda () (ps:chain notification (close))) 5000) ;; Click to focus the window @@ -430,7 +461,7 @@ (ps:chain window (focus)) (ps:chain notification (close))))) (:catch (e) - (ps:chain console (log "Notification error:" e)))))) + (ps:chain console (log "[NOTIFY] Notification error:" e)))))) ;; Notify track change (called from update-mini-now-playing) (defun notify-track-change (title) @@ -514,16 +545,15 @@ (when el ;; Check if track changed and record to history + notify (when (not (= (ps:@ el text-content) title)) + (ps:chain console (log "[STREAM-SYNC] Title changed:" title)) (record-track-listen title) (notify-track-change title)) (setf (ps:@ el text-content) title) - ;; Check if this track is in user's favorites (check-favorite-status-mini)) (update-media-session title) (when track-id-el (let ((track-id (or (ps:@ data data track_id) (ps:@ data track_id)))) (setf (ps:@ track-id-el value) (or track-id "")))) - ;; Update favorite count display (let ((count-el (ps:chain document (get-element-by-id "favorite-count-mini"))) (fav-count (or (ps:@ data data favorite_count) (ps:@ data favorite_count) 0))) (when count-el @@ -531,7 +561,10 @@ ((= fav-count 0) (setf (ps:@ count-el text-content) "")) ((= fav-count 1) (setf (ps:@ count-el text-content) "1 ❤️")) (t (setf (ps:@ count-el text-content) (+ fav-count " ❤️")))))) - ;; Update MusicBrainz search link + ;; Sync countdown timer from server + (let ((remaining (or (ps:@ data data remaining) (ps:@ data remaining)))) + (when remaining + (setf *track-remaining-seconds* remaining))) (let ((mb-link (ps:chain document (get-element-by-id "mini-musicbrainz-link"))) (search-url (or (ps:@ data data search_url) (ps:@ data search_url)))) (when mb-link @@ -724,6 +757,35 @@ (catch (lambda (err) (ps:chain console (log "Reconnect failed:" err)))))) + ;; Buffer bloat detection and reset + (defvar *max-buffer-seconds* 15) + (defvar *buffer-check-interval* nil) + + (defun get-buffer-ahead (audio-element) + "Return seconds of audio buffered ahead of current playback position." + (ps:try + (when (and (ps:@ audio-element buffered) + (> (ps:@ audio-element buffered length) 0)) + (- (ps:chain audio-element buffered (end (- (ps:@ audio-element buffered length) 1))) + (ps:@ audio-element current-time))) + (:catch (e) 0))) + + (defun start-buffer-monitor (audio-element) + (when *buffer-check-interval* + (clear-interval *buffer-check-interval*)) + (setf *buffer-check-interval* + (set-interval + (lambda () + (when (and (not (ps:@ audio-element paused)) + (not *is-reconnecting*)) + (let ((ahead (get-buffer-ahead audio-element))) + (when (and ahead (> ahead 0)) + (ps:chain console (log (+ "[BUFFER] " (ps:chain ahead (to-fixed 1)) "s ahead"))) + (when (> ahead *max-buffer-seconds*) + (ps:chain console (log (+ "[BUFFER] Bloat detected (" (ps:chain ahead (to-fixed 1)) "s), resetting stream"))) + (reconnect-stream)))))) + 10000))) + ;; Attach event listeners to audio element (defun attach-audio-listeners (audio-element) (ps:chain audio-element @@ -907,8 +969,9 @@ :artist "Asteroid Radio" :album "Live Broadcast"))))) - ;; Attach event listeners + ;; Attach event listeners and buffer monitor (attach-audio-listeners audio-element) + (start-buffer-monitor audio-element) ;; Restore user channel preference (let ((channel-selector (ps:chain document (get-element-by-id "stream-channel"))) @@ -958,9 +1021,10 @@ (ps:chain console (log "Could not fetch channel name:" error)))))) 15000)) ;; Poll every 15 seconds - ;; Start now playing updates + ;; Start now playing updates and countdown ticker (set-timeout update-mini-now-playing 1000) - (set-interval update-mini-now-playing 15000)))) + (set-interval update-mini-now-playing 15000) + (start-countdown-ticker)))) ;; Initialize popout player (defun init-popout-player () diff --git a/playlists/deep-focus.m3u b/playlists/deep-focus.m3u new file mode 100644 index 0000000..8250341 --- /dev/null +++ b/playlists/deep-focus.m3u @@ -0,0 +1,113 @@ +#EXTM3U +#PLAYLIST:Deep Focus +#PHASE:Deep Focus +#DURATION:5 hours (approx) +#CURATOR:Asteroid Radio +#DESCRIPTION:Upbeat and cheerful electronic music for long coding sessions - IDM, shoegaze electronica, melodic techno, and ambient rhythms + +#EXTINF:-1,Tycho - Glider +/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/01 - Glider.flac +#EXTINF:-1,Boards of Canada - Spectrum +/app/music/Boards of Canada/A Few Old Tunes/01 - Spectrum.mp3 +#EXTINF:-1,Ulrich Schnauss - Melts into Air +/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/01. Melts into Air (2019 Version).flac +#EXTINF:-1,Plaid - Do Matter +/app/music/Plaid - The Digging Remedy (2016) [FLAC]/01 - Do Matter.flac +#EXTINF:-1,Four Tet - Two Thousand And Seventeen +/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/02 Two Thousand And Seventeen.flac +#EXTINF:-1,Kiasmos - Lit +/app/music/Kiasmos/2014 - Kiasmos/01 - Lit.flac +#EXTINF:-1,Proem - A Good Soaking +/app/music/Proem - Twelve Tails-(2021) @FLAC [16-48]/12 - A Good Soaking.flac +#EXTINF:-1,Cut Copy - Standing in the Middle of the Field +/app/music/Cut Copy - Haiku From Zero (2017) [FLAC] {2557864014}/01 - Standing in the Middle of the Field.flac +#EXTINF:-1,Marconi Union - Sleeper +/app/music/Marconi Union - Ghost Stations (2016 - WEB - FLAC)/01. Marconi Union - Sleeper.flac +#EXTINF:-1,Clark - Peak Magnetic +/app/music/Clark - Death Peak (2017) [FLAC]/03 - Peak Magnetic.flac +#EXTINF:-1,Tycho - Weather +/app/music/Tycho - Simulcast (2020) [WEB FLAC]/01 - Weather.flac +#EXTINF:-1,Boards of Canada - Happy Cycling +/app/music/Boards of Canada/A Few Old Tunes/06 - Happy Cycling.mp3 +#EXTINF:-1,Ulrich Schnauss & Jonas Munk - Asteroid 2467 +/app/music/Ulrich Schnauss & Jonas Munk - Eight Fragments Of An Illusion (2021) - WEB FLAC/01. Asteroid 2467.flac +#EXTINF:-1,Faux Tales - Avalon +/app/music/Faux Tales - 2015 - Kairos [FLAC] {Kensai Records KNS006 WEB}/3 - Avalon.flac +#EXTINF:-1,Vector Lovers - City Lights From A Train +/app/music/Vector Lovers/2005 - Capsule For One/01 - City Lights From A Train.mp3 +#EXTINF:-1,Plaid - Maru +/app/music/Plaid - Polymer (2019) [WEB FLAC]/03 - Maru.flac +#EXTINF:-1,Four Tet - Baby +/app/music/Four Tet - Sixteen Oceans (2020) {Text Records - TEXT051} [CD FLAC]/02 - Four Tet - Baby.flac +#EXTINF:-1,Bluetech - Laika +/app/music/Bluetech - Spacehop Chronicles Vol. 1 (flac)/01. Laika.flac +#EXTINF:-1,Kiasmos - Looped +/app/music/Kiasmos/2014 - Kiasmos/03 - Looped.flac +#EXTINF:-1,Proem - Snow Drifts +/app/music/Proem - Twelve Tails-(2021) @FLAC [16-48]/11 - Snow Drifts.flac +#EXTINF:-1,Thievery Corporation - Fragments (Tycho Remix) +/app/music/Thievery Corporation and Tycho - Fragments Ascension EP (flac)/2. Thievery Corporation - Fragments (Tycho Remix).flac +#EXTINF:-1,Ulrich Schnauss - Love Grows Out of Thin Air +/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/02. Love Grows Out of Thin Air (2019 Version).flac +#EXTINF:-1,Clark - Kiri's Glee +/app/music/Clark - Kiri Variations (2019) [WEB FLAC]/04 - Kiri's Glee.flac +#EXTINF:-1,Cut Copy - Airborne +/app/music/Cut Copy - Haiku From Zero (2017) [FLAC] {2557864014}/05 - Airborne.flac +#EXTINF:-1,Marconi Union - Riser +/app/music/Marconi Union - Ghost Stations (2016 - WEB - FLAC)/04. Marconi Union - Riser.flac +#EXTINF:-1,Boards of Canada - Forest Moon +/app/music/Boards of Canada/A Few Old Tunes/09 - Forest Moon.mp3 +#EXTINF:-1,Tycho - Ascension +/app/music/Thievery Corporation and Tycho - Fragments Ascension EP (flac)/3. Tycho - Ascension.flac +#EXTINF:-1,Plaid - Dancers +/app/music/Plaid - Polymer (2019) [WEB FLAC]/07 - Dancers.flac +#EXTINF:-1,Quaeschning & Ulrich Schnauss - A Calm but Steady Flow +/app/music/Quaeschning and Ulrich Schnauss - Synthwaves (2017) {vista003, GER, CD} [FLAC]/05 - A Calm but Steady Flow.flac +#EXTINF:-1,Bitcrush - Engale +/app/music/Bitcrush - Enarc (flac)/01 - Engale.flac +#EXTINF:-1,Four Tet - Parallel 1 +/app/music/Four Tet - Parallel (2020) - WEB FLAC/01. Parallel 1.flac +#EXTINF:-1,woob - INNA +/app/music/woob - Mass Distraction EP [WEB FLAC 24]/woob - Mass Distraction EP - 01 INNA.flac +#EXTINF:-1,Vector Lovers - Nostalgia 4 The Future +/app/music/Vector Lovers/2005 - Capsule For One/05 - Nostalgia 4 The Future.mp3 +#EXTINF:-1,Kiasmos - Swayed +/app/music/Kiasmos/2014 - Kiasmos/04 - Swayed.flac +#EXTINF:-1,Proem - Keep This Whole +/app/music/Proem - Twelve Tails-(2021) @FLAC [16-48]/10 - Keep This Whole.flac +#EXTINF:-1,Ulrich Schnauss - Her and the Sea +/app/music/Ulrich Schnauss - A Long Way To Fall - Rebound (2020) - WEB FLAC/01. Her and the Sea.flac +#EXTINF:-1,Faux Tales - Oceania +/app/music/Faux Tales - 2015 - Kairos [FLAC] {Kensai Records KNS006 WEB}/4 - Oceania.flac +#EXTINF:-1,Cut Copy - Stars Last Me a Lifetime +/app/music/Cut Copy - Haiku From Zero (2017) [FLAC] {2557864014}/04 - Stars Last Me a Lifetime.flac +#EXTINF:-1,Plaid - The Bee +/app/music/Plaid - The Digging Remedy (2016) [FLAC]/04 - The Bee.flac +#EXTINF:-1,Clark - Living Fantasy +/app/music/Clark - Death Peak (2017) [FLAC]/08 - Living Fantasy.flac +#EXTINF:-1,Tycho - Japan (Satin Jackets Remix) +/app/music/Tycho - Weather Remixes (2020) - WEB FLAC/03. Japan (Satin Jackets Remix).flac +#EXTINF:-1,Boards of Canada - Skimming Stones +/app/music/Boards of Canada/A Few Old Tunes/10 - Skimming Stones.mp3 +#EXTINF:-1,Ulrich Schnauss & Jonas Munk - Perpetual Motion +/app/music/Ulrich Schnauss & Jonas Munk - Eight Fragments Of An Illusion (2021) - WEB FLAC/04. Perpetual Motion.flac +#EXTINF:-1,Bluetech - Skybox +/app/music/Bluetech - Spacehop Chronicles Vol. 1 (flac)/04. Skybox.flac +#EXTINF:-1,God is an Astronaut - Epitaph +/app/music/God is an Astronaut - Epitaph (2018) WEB FLAC/01. Epitaph.flac +#EXTINF:-1,Seba & Ulrich Schnauss - M7 +/app/music/Seba & Ulrich Schnauss - Snöflingor EP [2017] [WEB_FLAC]/01. Seba & Ulrich Schnauss - M7.flac +#EXTINF:-1,Quaeschning & Ulrich Schnauss - Prism +/app/music/Quaeschning and Ulrich Schnauss - Synthwaves (2017) {vista003, GER, CD} [FLAC]/08 - Prism.flac +#EXTINF:-1,High Energy Protons - The Heavens (Monolith mix) +/app/music/High Energy Protons/03 - The Heavens (Monolith mix).mp3 +#EXTINF:-1,woob - Subterranean District +/app/music/woob - Tokyo Run - Series 8 Keycard - Lv.6 [2017] WEB [FLAC24] [16-44]/Tokyo Run - 24 Bit Masters/06 woob - Subterranean District.flac +#EXTINF:-1,Bitcrush - Two Go From There +/app/music/Bitcrush - Enarc (flac)/03 - Two Go From There.flac +#EXTINF:-1,Daft Punk - Derezzed +/app/music/Daft Punk - TRON Legacy - The Complete Edition (2020 - WEB - FLAC)/13 Derezzed.flac +#EXTINF:-1,God is an Astronaut - Komorebi +/app/music/God is an Astronaut - Epitaph (2018) WEB FLAC/05. Komorebi.flac +#EXTINF:-1,Daft Punk - The Son of Flynn +/app/music/Daft Punk - TRON Legacy - The Complete Edition (2020 - WEB - FLAC)/03 The Son of Flynn.flac diff --git a/static/asteroid.css b/static/asteroid.css index 621a5e6..44b3972 100644 --- a/static/asteroid.css +++ b/static/asteroid.css @@ -1372,6 +1372,14 @@ body.persistent-player-container .now-playing-mini{ min-width: 300px; } +body.persistent-player-container .track-countdown-mini{ + color: #888; + font-size: 0.75em; + font-family: monospace; + margin-left: 6px; + white-space: nowrap; +} + body.persistent-player-container .persistent-reconnect-btn{ background: transparent; color: #00ff00; @@ -1595,6 +1603,13 @@ body.popout-body .status-mini{ margin: 0 5px; } +.track-countdown{ + color: #888; + font-size: 0.8em; + font-family: monospace; + margin-left: 8px; +} + .now-playing-track{ display: flex; align-items: center; diff --git a/static/asteroid.lass b/static/asteroid.lass index e817f48..80b4e6b 100644 --- a/static/asteroid.lass +++ b/static/asteroid.lass @@ -1108,6 +1108,13 @@ :flex 1 :min-width "300px") + (.track-countdown-mini + :color "#888" + :font-size "0.75em" + :font-family "monospace" + :margin-left "6px" + :white-space "nowrap") + (.persistent-reconnect-btn :background transparent :color "#00ff00" @@ -1288,6 +1295,12 @@ (.craftering (a :margin "0 5px"))) + (.track-countdown + :color "#888" + :font-size "0.8em" + :font-family "monospace" + :margin-left "8px") + ;; Now playing favorite button (.now-playing-track :display "flex" diff --git a/stream-harmony.lisp b/stream-harmony.lisp index b8b2fa6..1ef8eb4 100644 --- a/stream-harmony.lisp +++ b/stream-harmony.lisp @@ -148,6 +148,11 @@ (setf *current-playlist-path* playlist-path) (log:info "Playlist now active: ~A" (file-namestring playlist-path))) +(defvar *pending-save-file* nil + "The file path that will be saved on the NEXT track change. + This one-track delay ensures we persist the track that was actually playing, + not the one being loaded during crossfade.") + (defun on-harmony-track-change (pipeline track-info) "Called by cl-streamer when a track changes. Updates recently-played lists and finds the track in the database." @@ -168,9 +173,10 @@ :track-id track-id) :curated) (setf *last-known-track-curated* display-title)) - ;; Persist current track for resume-on-restart - (when file-path - (save-playback-state file-path)) + ;; Save the PREVIOUS track (which was actually playing) and queue this one + (when *pending-save-file* + (save-playback-state *pending-save-file*)) + (setf *pending-save-file* file-path) (log:info "Track change: ~A (track-id: ~A)" display-title track-id))) (defun find-track-by-file-path (file-path) @@ -191,19 +197,38 @@ (defun harmony-now-playing (&optional (mount "asteroid.mp3")) "Get now-playing information from cl-streamer pipeline. - Returns an alist with now-playing data, or NIL if the pipeline is not running." + Uses the metadata timeline to report what listeners are actually hearing, + accounting for ring buffer and browser decode buffering." (when (and *harmony-pipeline* (cl-streamer/harmony:pipeline-current-track *harmony-pipeline*)) - (let* ((track-info (cl-streamer/harmony:pipeline-current-track *harmony-pipeline*)) - (display-title (or (getf track-info :display-title) "Unknown")) + (let* ((server (cl-streamer/harmony:pipeline-server *harmony-pipeline*)) + (listener-title (when server + (cl-streamer:get-listener-now-playing + server (format nil "/~A" mount)))) + (track-info (cl-streamer/harmony:pipeline-current-track *harmony-pipeline*)) + (display-title (or listener-title + (getf track-info :display-title) + "Unknown")) (listeners (cl-streamer:pipeline-listener-count *harmony-pipeline*)) (track-id (or (find-track-by-title display-title) - (find-track-by-file-path (getf track-info :file))))) + (find-track-by-file-path (getf track-info :file)))) + (raw-remaining (cl-streamer/harmony:pipeline-track-remaining *harmony-pipeline*)) + ;; Adjust for browser buffer delay - listener is further behind than pipeline + (remaining (when raw-remaining + (let ((adjusted (+ raw-remaining cl-streamer::*browser-buffer-seconds*))) + (max 0 (floor adjusted)))))) + ;; Diagnostic: log when listener-title differs from pipeline title + (let ((pipeline-title (getf track-info :display-title))) + (when (and listener-title pipeline-title + (not (string= listener-title pipeline-title))) + (log:info "[SYNC-DIAG] API returning ~S (pipeline has ~S, delay=~As)" + listener-title pipeline-title cl-streamer::*browser-buffer-seconds*))) `((:listenurl . ,(format nil "~A/~A" *stream-base-url* mount)) (:title . ,display-title) (:listeners . ,(or listeners 0)) (:track-id . ,track-id) - (:favorite-count . ,(or (get-track-favorite-count display-title) 0)))))) + (:favorite-count . ,(or (get-track-favorite-count display-title) 0)) + ,@(when remaining `((:remaining . ,remaining))))))) ;;; ---- Pipeline Lifecycle ---- diff --git a/template/audio-player-frame.ctml b/template/audio-player-frame.ctml index 10f8637..98b2165 100644 --- a/template/audio-player-frame.ctml +++ b/template/audio-player-frame.ctml @@ -39,6 +39,7 @@ Loading... +