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.

Sync playback status and update Now Playing

Add updatePlaybackState to change rate/position/duration without
touching
artwork. Add fetchPlaybackStatus to query the server. Wrap
MPRemoteCommand
callbacks in @MainActor Tasks. Play/pause now queries server status
(with an
optimistic UI toggle) and updates now playing info early so macOS sees
the
correct playbackRate for media keys.

+76 -31
macos/Rockbox.xcodeproj/project.xcworkspace/xcuserdata/tsirysandratraina.xcuserdatad/UserInterfaceState.xcuserstate

This is a binary file and will not be displayed.

+11
macos/Rockbox/Manager/MediaControlsManager.swift
··· 96 96 } 97 97 } 98 98 99 + // Updates only the playback-state fields (rate, position, duration) without 100 + // touching artwork. Called immediately when play/pause state changes so macOS 101 + // always sees the correct playbackRate and routes the right media key command. 102 + func updatePlaybackState(isPlaying: Bool, currentTime: TimeInterval, duration: TimeInterval) { 103 + var info = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]() 104 + info[MPMediaItemPropertyPlaybackDuration] = duration 105 + info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime 106 + info[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 107 + MPNowPlayingInfoCenter.default().nowPlayingInfo = info 108 + } 109 + 99 110 func updateNowPlaying( 100 111 title: String, 101 112 artist: String? = nil,
+14
macos/Rockbox/Services/PlaybackService.swift
··· 76 76 } 77 77 } 78 78 79 + func fetchPlaybackStatus(host: String = "127.0.0.1", port: Int = 6061) async throws -> Int32 { 80 + try await withGRPCClient( 81 + transport: .http2NIOPosix( 82 + target: .dns(host: host, port: port), 83 + transportSecurity: .plaintext 84 + ) 85 + ) { grpcClient in 86 + let playback = Rockbox_V1alpha1_PlaybackService.Client(wrapping: grpcClient) 87 + let req = Rockbox_V1alpha1_StatusRequest() 88 + let response = try await playback.status(req) 89 + return response.status 90 + } 91 + } 92 + 79 93 func currentTrackStream() -> AsyncThrowingStream<Rockbox_V1alpha1_CurrentTrackResponse, Error> { 80 94 AsyncThrowingStream { continuation in 81 95 Task {
+51 -31
macos/Rockbox/State/PlayerState.swift
··· 47 47 48 48 private func setupMediaControls() { 49 49 let manager = MediaControlsManager.shared 50 - manager.onPlay = { 51 - Task { 52 - try? await resume() 53 - } 50 + 51 + // MPRemoteCommandCenter callbacks are plain () -> Void closures that may be 52 + // invoked outside the @MainActor. Wrap every callback in a @MainActor Task 53 + // so that isPlaying is read from the correct actor context. 54 + manager.onPlay = { [weak self] in 55 + Task { @MainActor [weak self] in self?.playOrPause() } 54 56 } 55 57 56 - manager.onPause = { 57 - Task { 58 - try? await pause() 59 - } 58 + manager.onPause = { [weak self] in 59 + Task { @MainActor [weak self] in self?.playOrPause() } 60 60 } 61 61 62 62 manager.onTogglePlayPause = { [weak self] in 63 - self?.playOrPause() 63 + Task { @MainActor [weak self] in self?.playOrPause() } 64 64 } 65 65 66 66 manager.onNext = { 67 - Task { 68 - try? await next() 69 - } 67 + Task { try? await next() } 70 68 } 71 69 72 70 manager.onPrevious = { 73 - Task { 74 - try? await previous() 75 - } 71 + Task { try? await previous() } 76 72 } 77 73 78 74 manager.onSeek = { position in 79 - Task { 80 - try? await play(elapsed: Int64(position) * 1000) 81 - } 75 + Task { try? await play(elapsed: Int64(position) * 1000) } 82 76 } 83 77 } 84 78 ··· 135 129 self.currentTime = TimeInterval(response.elapsed / 1000) 136 130 137 131 if previousTrack.cuid != self.currentTrack.cuid { 132 + // A track change always means playback just started. Set isPlaying 133 + // immediately so the first media key press is correct without waiting 134 + // for the status stream (which may never fire). 135 + self.isPlaying = true 138 136 self.updateNowPlayingInfo() 139 137 } 140 138 } ··· 242 240 self.currentTime = TimeInterval(globalStatus.resumeElapsed / 1000) 243 241 self.duration = TimeInterval(data.tracks[index].length / 1000) 244 242 243 + // Sync the actual server playing state so the first media key press 244 + // is correct even when the status stream never fires. 245 + let serverStatus = try await fetchPlaybackStatus() 246 + self.isPlaying = serverStatus == 1 247 + self.status = serverStatus 248 + 245 249 self.updateNowPlayingInfo() 246 250 } 247 251 } catch { ··· 299 303 } 300 304 301 305 func playOrPause() { 306 + // Don't rely on isPlaying — the status stream may never fire. 307 + // Query the real server state first, then act on it. 308 + isPlaying.toggle() // Optimistic toggle for immediate UI feedback 309 + updateNowPlayingInfo() 310 + 302 311 Task { 303 312 do { 304 - let globalStatus = try await fetchGlobalStatus() 305 - if globalStatus.resumeIndex > -1 && status == 0 { 306 - try await resumeTrack() 307 - return 313 + let serverStatus = try await fetchPlaybackStatus() 314 + if serverStatus == 1 { 315 + try await pause() 316 + } else { 317 + try await resume() 308 318 } 309 - 310 - if isPlaying { 311 - try await pause() 312 - return 319 + await MainActor.run { [weak self] in 320 + guard let self = self else { return } 321 + self.isPlaying = serverStatus != 1 322 + self.updateNowPlayingInfo() 313 323 } 314 - 315 - try await resume() 316 324 } catch { 317 - self.error = error 325 + await MainActor.run { [weak self] in 326 + guard let self = self else { return } 327 + self.isPlaying.toggle() // revert optimistic toggle 328 + self.error = error 329 + } 318 330 } 319 331 } 320 332 } ··· 425 437 // MARK: - Update Now Playing Info 426 438 427 439 func updateNowPlayingInfo() { 428 - // Load artwork asynchronously 440 + // Update playbackRate immediately so macOS routes the correct media key command. 441 + // Without this, a stale rate=0.0 causes macOS to send playCommand (instead of 442 + // pauseCommand) while the song is playing, which makes Rockbox restart the track. 443 + MediaControlsManager.shared.updatePlaybackState( 444 + isPlaying: isPlaying, 445 + currentTime: currentTime, 446 + duration: duration 447 + ) 448 + 449 + // Then update full metadata + artwork asynchronously. 429 450 loadArtwork(from: currentTrack.albumArt) { [weak self] artwork in 430 451 guard let self = self else { return } 431 - 432 452 MediaControlsManager.shared.updateNowPlaying( 433 453 title: self.currentTrack.title, 434 454 artist: self.currentTrack.artist,