Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Avoid clearing UI for incomplete stream data

Introduce needsFullResume to detect daemon restarts and perform
playlist_resume (resumeTrack) instead of audio_resume when appropriate.
Skip stream events with empty id/title and guard against queue index
out-of-bounds to avoid blanking the UI until metadata is available.

+28 -1
macos/Rockbox.xcodeproj/project.xcworkspace/xcuserdata/tsirysandratraina.xcuserdatad/UserInterfaceState.xcuserstate

This is a binary file and will not be displayed.

+28 -1
macos/Rockbox/State/PlayerState.swift
··· 34 34 private var streamTask: Task<Void, Never>? 35 35 private var streamStatusTask: Task<Void, Never>? 36 36 private var streamPlaylistTask: Task<Void, Never>? 37 + // True when the daemon was (re)started and audio is stopped; a full 38 + // playlist_resume is required instead of a plain audio_resume. 39 + private var needsFullResume: Bool = false 37 40 38 41 var progress: Double { 39 42 get { duration > 0 ? currentTime / duration : 0 } ··· 110 113 do { 111 114 isConnected = true 112 115 for try await response in currentTrackStream() { 116 + // Skip events where the server hasn't loaded track metadata yet. 117 + // This covers two cases: 118 + // • id empty — truly no active track (just-started daemon) 119 + // • title empty — id echoed from last track but tags not loaded 120 + // Both would otherwise blank out title/artist in the UI. 121 + guard !response.id.isEmpty, !response.title.isEmpty else { continue } 113 122 let previousTrack = self.currentTrack 114 123 self.currentTrack = Song( 115 124 cuid: response.id, ··· 128 137 self.duration = TimeInterval(response.length / 1000) 129 138 self.currentTime = TimeInterval(response.elapsed / 1000) 130 139 140 + // Server is delivering real track data — no longer need a full resume. 141 + self.needsFullResume = false 131 142 if previousTrack.cuid != self.currentTrack.cuid { 132 143 // A track change always means playback just started. Set isPlaying 133 144 // immediately so the first media key press is correct without waiting ··· 190 201 ) 191 202 } 192 203 193 - self.currentTrack = self.queue[self.currentIndex] 204 + // Guard against an index past the queue end and against tracks 205 + // whose metadata hasn't been loaded by the server yet (title empty). 206 + // The currentTrackStream will supply proper metadata once loaded. 207 + guard self.currentIndex < self.queue.count else { continue } 208 + let candidate = self.queue[self.currentIndex] 209 + guard !candidate.title.isEmpty else { continue } 210 + self.currentTrack = candidate 194 211 self.updateNowPlayingInfo() 195 212 } 196 213 } catch is CancellationError { ··· 245 262 let serverStatus = try await fetchPlaybackStatus() 246 263 self.isPlaying = serverStatus == 1 247 264 self.status = serverStatus 265 + // If audio is stopped (e.g. after daemon restart) but we have a 266 + // persisted playlist, a plain audio_resume won't work — we need 267 + // playlist_resume to reload the track from saved state. 268 + self.needsFullResume = serverStatus != 1 248 269 249 270 self.updateNowPlayingInfo() 250 271 } ··· 313 334 let serverStatus = try await fetchPlaybackStatus() 314 335 if serverStatus == 1 { 315 336 try await pause() 337 + } else if needsFullResume { 338 + // Daemon was restarted: audio engine is stopped, not merely paused. 339 + // playlist_resume (resumeTrack) reloads the track from saved state; 340 + // plain audio_resume would be a no-op here. 341 + try await resumeTrack() 342 + needsFullResume = false 316 343 } else { 317 344 try await resume() 318 345 }