@@ -219,6 +219,72 @@ <h2>
219219 const prefetchThresholdSeconds = { { prefetch_threshold_seconds } } ;
220220 let prefetchTriggered = false ;
221221
222+ // Remote logging for debugging on phone/car displays
223+ const REMOTE_LOGGING_ENABLED = true ;
224+ const LOG_BATCH_INTERVAL = { { client_log_batch_interval } } ; // Milliseconds between log batches (configurable via .env)
225+ let logBuffer = [ ] ;
226+ let logBatchTimer = null ;
227+
228+ // Remote logging function - sends logs to server
229+ function remoteLog ( level , message , context = { } ) {
230+ const timestamp = new Date ( ) . toISOString ( ) ;
231+
232+ // Add to buffer
233+ logBuffer . push ( {
234+ level : level ,
235+ message : message ,
236+ timestamp : timestamp ,
237+ context : context
238+ } ) ;
239+
240+ // Also log to console
241+ const consoleMethod = console [ level ] || console . log ;
242+ if ( Object . keys ( context ) . length > 0 ) {
243+ consoleMethod ( `[${ level . toUpperCase ( ) } ]` , message , context ) ;
244+ } else {
245+ consoleMethod ( `[${ level . toUpperCase ( ) } ]` , message ) ;
246+ }
247+
248+ // Start batch timer if not already running
249+ if ( REMOTE_LOGGING_ENABLED && ! logBatchTimer ) {
250+ logBatchTimer = setTimeout ( flushLogs , LOG_BATCH_INTERVAL ) ;
251+ }
252+ }
253+
254+ // Send buffered logs to server
255+ async function flushLogs ( ) {
256+ if ( logBuffer . length === 0 ) {
257+ logBatchTimer = null ;
258+ return ;
259+ }
260+
261+ const logsToSend = [ ...logBuffer ] ;
262+ logBuffer = [ ] ;
263+ logBatchTimer = null ;
264+
265+ try {
266+ await fetch ( '/admin/client-logs' , {
267+ method : 'POST' ,
268+ headers : {
269+ 'Content-Type' : 'application/json' ,
270+ } ,
271+ body : JSON . stringify ( logsToSend ) ,
272+ } ) ;
273+ } catch ( e ) {
274+ // Silently fail - don't want logging to break the app
275+ console . debug ( 'Failed to send remote logs:' , e ) ;
276+ }
277+ }
278+
279+ // Flush logs on page unload
280+ window . addEventListener ( 'beforeunload' , ( ) => {
281+ if ( logBuffer . length > 0 ) {
282+ // Use sendBeacon for reliability during page unload
283+ const blob = new Blob ( [ JSON . stringify ( logBuffer ) ] , { type : 'application/json' } ) ;
284+ navigator . sendBeacon ( '/admin/client-logs' , blob ) ;
285+ }
286+ } ) ;
287+
222288 // Stream resilience configuration
223289 let retryCount = 0 ;
224290 let maxRetries = 50 ; // Increased for long network transitions (5G↔WiFi)
@@ -559,6 +625,53 @@ <h2>
559625 console . log ( 'Stream loading suspended (browser buffered enough)' ) ;
560626 } ) ;
561627
628+ // Handle when duration becomes available
629+ player . addEventListener ( 'loadedmetadata' , function ( ) {
630+ remoteLog ( 'log' , '[MediaSession] Metadata loaded' , { duration : player . duration } ) ;
631+
632+ // Immediately set position state when duration becomes available
633+ if ( 'mediaSession' in navigator && navigator . mediaSession . setPositionState ) {
634+ if ( player . duration && ! isNaN ( player . duration ) && isFinite ( player . duration ) ) {
635+ try {
636+ navigator . mediaSession . setPositionState ( {
637+ duration : player . duration ,
638+ playbackRate : player . playbackRate ,
639+ position : player . currentTime
640+ } ) ;
641+ remoteLog ( 'log' , '[MediaSession] Initial position state set for car display' , {
642+ duration : player . duration ,
643+ position : player . currentTime
644+ } ) ;
645+ } catch ( e ) {
646+ remoteLog ( 'warn' , '[MediaSession] Failed to set initial position state' , { error : e . message } ) ;
647+ }
648+ }
649+ }
650+ } ) ;
651+
652+ player . addEventListener ( 'durationchange' , function ( ) {
653+ remoteLog ( 'log' , '[MediaSession] Duration changed' , { duration : player . duration } ) ;
654+
655+ // Update position state when duration changes
656+ if ( 'mediaSession' in navigator && navigator . mediaSession . setPositionState ) {
657+ if ( player . duration && ! isNaN ( player . duration ) && isFinite ( player . duration ) ) {
658+ try {
659+ navigator . mediaSession . setPositionState ( {
660+ duration : player . duration ,
661+ playbackRate : player . playbackRate ,
662+ position : player . currentTime
663+ } ) ;
664+ remoteLog ( 'log' , '[MediaSession] Position state updated after duration change' , {
665+ duration : player . duration ,
666+ position : player . currentTime
667+ } ) ;
668+ } catch ( e ) {
669+ remoteLog ( 'warn' , '[MediaSession] Failed to update position state' , { error : e . message } ) ;
670+ }
671+ }
672+ }
673+ } ) ;
674+
562675 // Prefetch next queue item when current track is nearing its end
563676 let lastPositionUpdate = 0 ;
564677 player . addEventListener ( 'timeupdate' , async function ( ) {
@@ -577,6 +690,15 @@ <h2>
577690 // Some browsers don't support this or have restrictions
578691 console . debug ( '[MediaSession] setPositionState not supported or failed:' , e . message ) ;
579692 }
693+ } else {
694+ // Debug why position state isn't being set (only log once per 10 seconds)
695+ if ( now - lastPositionUpdate > 10000 ) {
696+ remoteLog ( 'warn' , '[MediaSession] Cannot set position state' , {
697+ duration : player . duration ,
698+ isNaN : isNaN ( player . duration ) ,
699+ isFinite : isFinite ( player . duration )
700+ } ) ;
701+ }
580702 }
581703 }
582704
688810
689811 navigator . mediaSession . metadata = new MediaMetadata ( metadataOptions ) ;
690812
691- console . log ( ' [MediaSession] Updated metadata: ', {
813+ remoteLog ( 'log' , ' [MediaSession] Updated metadata', {
692814 title : metadataOptions . title ,
693815 artist : metadataOptions . artist ,
694816 hasArtwork : ! ! metadataOptions . artwork ,
@@ -697,16 +819,16 @@ <h2>
697819 } catch ( e ) {
698820 // Fallback: set metadata without artwork
699821 // This fixes compatibility with strict car systems like Tesla
700- console . warn ( ' [MediaSession] Failed to set metadata with artwork, trying without: ', e ) ;
822+ remoteLog ( 'warn' , ' [MediaSession] Failed to set metadata with artwork, trying without', { error : e . message } ) ;
701823 try {
702824 navigator . mediaSession . metadata = new MediaMetadata ( {
703825 title : trackInfo . title || 'YouTube Audio' ,
704826 artist : trackInfo . channel || 'YouTube' ,
705827 album : 'YouTube Radio'
706828 } ) ;
707- console . log ( '[MediaSession] Set metadata without artwork (fallback)' ) ;
829+ remoteLog ( 'log' , '[MediaSession] Set metadata without artwork (fallback)' ) ;
708830 } catch ( fallbackError ) {
709- console . error ( ' [MediaSession] Failed to set metadata even without artwork: ', fallbackError ) ;
831+ remoteLog ( 'error' , ' [MediaSession] Failed to set metadata even without artwork', { error : fallbackError . message } ) ;
710832 }
711833 }
712834
@@ -729,6 +851,14 @@ <h2>
729851 navigator . mediaSession . setActionHandler ( 'seekforward' , ( details ) => {
730852 player . currentTime = Math . max ( 0 , player . currentTime + ( details . seekOffset || 15 ) ) ;
731853 } ) ;
854+
855+ // Handle seek to specific position (for scrubber/progress bar in car displays)
856+ navigator . mediaSession . setActionHandler ( 'seekto' , ( details ) => {
857+ if ( details . seekTime !== null && ! isNaN ( details . seekTime ) ) {
858+ player . currentTime = details . seekTime ;
859+ remoteLog ( 'log' , '[MediaSession] Seeked to position' , { seekTime : details . seekTime } ) ;
860+ }
861+ } ) ;
732862 }
733863
734864 // Queue Management
0 commit comments