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.

Add MediaControlsManager and Now Playing updates

Integrate MPRemoteCommandCenter via a new MediaControlsManager. Wire
PlayerState to handle play/pause/next/previous/seek commands and call
updateNowPlayingInfo on track or playback status changes. Add async
artwork loading for Now Playing metadata. Include updated Xcode user
interface state.

+222 -18
macos/Rockbox.xcodeproj/project.xcworkspace/xcuserdata/tsirysandratraina.xcuserdatad/UserInterfaceState.xcuserstate

This is a binary file and will not be displayed.

+94
macos/Rockbox/Manager/MediaControlsManager.swift
··· 1 + // 2 + // MediaControlsManager.swift 3 + // Rockbox 4 + // 5 + // Created by Tsiry Sandratraina on 25/12/2025. 6 + // 7 + import MediaPlayer 8 + import AppKit 9 + 10 + class MediaControlsManager { 11 + 12 + static let shared = MediaControlsManager() 13 + 14 + var onPlay: (() -> Void)? 15 + var onPause: (() -> Void)? 16 + var onNext: (() -> Void)? 17 + var onPrevious: (() -> Void)? 18 + var onSeek: ((TimeInterval) -> Void)? 19 + 20 + private init() { 21 + setupRemoteCommandCenter() 22 + } 23 + 24 + func setupRemoteCommandCenter() { 25 + let commandCenter = MPRemoteCommandCenter.shared() 26 + 27 + commandCenter.playCommand.isEnabled = true 28 + commandCenter.playCommand.addTarget { [weak self] event in 29 + self?.onPlay?() 30 + return .success 31 + } 32 + 33 + commandCenter.pauseCommand.isEnabled = true 34 + commandCenter.pauseCommand.addTarget { [weak self] event in 35 + self?.onPause?() 36 + return .success 37 + } 38 + 39 + commandCenter.togglePlayPauseCommand.isEnabled = true 40 + commandCenter.togglePlayPauseCommand.addTarget { [weak self] event in 41 + return .success 42 + } 43 + 44 + commandCenter.nextTrackCommand.isEnabled = true 45 + commandCenter.nextTrackCommand.addTarget { [weak self] event in 46 + self?.onNext?() 47 + return .success 48 + } 49 + 50 + commandCenter.previousTrackCommand.isEnabled = true 51 + commandCenter.previousTrackCommand.addTarget { [weak self] event in 52 + self?.onPrevious?() 53 + return .success 54 + } 55 + 56 + commandCenter.changePlaybackPositionCommand.isEnabled = true 57 + commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in 58 + if let event = event as? MPChangePlaybackPositionCommandEvent { 59 + self?.onSeek?(event.positionTime) 60 + return .success 61 + } 62 + return .commandFailed 63 + } 64 + } 65 + 66 + func updateNowPlaying( 67 + title: String, 68 + artist: String? = nil, 69 + album: String? = nil, 70 + artwork: NSImage? = nil, 71 + duration: TimeInterval, 72 + currentTime: TimeInterval, 73 + isPlaying: Bool 74 + ) { 75 + var nowPlayingInfo = [String: Any]() 76 + 77 + nowPlayingInfo[MPMediaItemPropertyTitle] = title 78 + if let artist = artist { 79 + nowPlayingInfo[MPMediaItemPropertyArtist] = artist 80 + } 81 + if let album = album { 82 + nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = album 83 + } 84 + if let artwork = artwork { 85 + nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: artwork.size) { _ in artwork } 86 + } 87 + 88 + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration 89 + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime 90 + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 91 + 92 + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo 93 + } 94 + }
+128 -18
macos/Rockbox/State/PlayerState.swift
··· 40 40 set { currentTime = newValue * duration } 41 41 } 42 42 43 + init() { 44 + setupMediaControls() 45 + setInitialNowPlayingInfo() 46 + } 47 + 48 + private func setupMediaControls() { 49 + let manager = MediaControlsManager.shared 50 + manager.onPlay = { 51 + Task { 52 + try? await resume() 53 + } 54 + } 55 + 56 + manager.onPause = { 57 + Task { 58 + try? await pause() 59 + } 60 + } 61 + 62 + manager.onNext = { 63 + Task { 64 + try? await next() 65 + } 66 + } 67 + 68 + manager.onPrevious = { 69 + Task { 70 + try? await previous() 71 + } 72 + } 73 + 74 + manager.onSeek = { position in 75 + Task { 76 + try? await play(elapsed: Int64(position) * 1000) 77 + } 78 + } 79 + 80 + } 81 + 82 + private func setInitialNowPlayingInfo() { 83 + MediaControlsManager.shared.updateNowPlaying( 84 + title: "Not Playing", 85 + artist: "Rockbox", 86 + album: nil, 87 + artwork: nil, 88 + duration: 0, 89 + currentTime: 0, 90 + isPlaying: false 91 + ) 92 + } 93 + 43 94 // Upcoming tracks (after current) 44 95 var upNext: [Song] { 45 96 guard currentIndex + 1 < queue.count else { return [] } ··· 62 113 do { 63 114 isConnected = true 64 115 for try await response in currentTrackStream() { 116 + let previousTrack = self.currentTrack 65 117 self.currentTrack = Song( 66 118 cuid: response.id, 67 119 path: response.path, ··· 78 130 ) 79 131 self.duration = TimeInterval(response.length / 1000) 80 132 self.currentTime = TimeInterval(response.elapsed / 1000) 133 + 134 + if previousTrack.cuid != self.currentTrack.cuid { 135 + self.updateNowPlayingInfo() 136 + } 81 137 } 82 138 // Refresh queue when track changes 83 139 await self.fetchQueue() ··· 94 150 streamStatusTask = Task { 95 151 do { 96 152 for try await response in playbackStatusStream() { 153 + let previousIsPlaying = self.isPlaying 97 154 self.isPlaying = response.status == 1 98 155 self.status = response.status 156 + 157 + if previousIsPlaying != self.isPlaying { 158 + self.updateNowPlayingInfo() 159 + } 99 160 } 100 161 } catch is CancellationError { 101 162 // Ignored ··· 129 190 } 130 191 131 192 self.currentTrack = self.queue[self.currentIndex] 193 + self.updateNowPlayingInfo() 132 194 } 133 195 } catch is CancellationError { 134 196 // Ignored ··· 176 238 let globalStatus = try await fetchGlobalStatus() 177 239 self.currentTime = TimeInterval(globalStatus.resumeElapsed / 1000) 178 240 self.duration = TimeInterval(data.tracks[index].length / 1000) 241 + 242 + self.updateNowPlayingInfo() 179 243 } 180 244 } catch { 181 245 self.error = error ··· 257 321 Task { 258 322 do { 259 323 try await play(elapsed: position) 324 + self.updateNowPlayingInfo() 260 325 } catch { 261 326 self.error = error 262 327 } ··· 269 334 try await startPlaylist(position: Int32(index)) 270 335 self.currentIndex = index 271 336 self.currentTrack = queue[index] 337 + self.updateNowPlayingInfo() 272 338 } catch { 273 339 self.error = error 274 340 } ··· 331 397 } 332 398 } 333 399 } 334 - 400 + 335 401 func fetchSettings() { 336 - Task { 337 - do { 338 - let data = try await fetchGlobalSettings() 339 - switch data.repeatMode { 340 - case 0: 341 - repeatMode = .off 342 - case 1: 343 - repeatMode = .one 344 - case 2: 345 - repeatMode = .all 346 - default: 347 - repeatMode = .off 348 - } 349 - isShuffleEnabled = data.playlistShuffle 350 - } catch { 351 - self.error = error 352 - } 402 + Task { 403 + do { 404 + let data = try await fetchGlobalSettings() 405 + switch data.repeatMode { 406 + case 0: 407 + repeatMode = .off 408 + case 1: 409 + repeatMode = .one 410 + case 2: 411 + repeatMode = .all 412 + default: 413 + repeatMode = .off 414 + } 415 + isShuffleEnabled = data.playlistShuffle 416 + } catch { 417 + self.error = error 418 + } 419 + } 420 + } 421 + 422 + // MARK: - Update Now Playing Info 423 + 424 + func updateNowPlayingInfo() { 425 + // Load artwork asynchronously 426 + loadArtwork(from: currentTrack.albumArt) { [weak self] artwork in 427 + guard let self = self else { return } 428 + 429 + MediaControlsManager.shared.updateNowPlaying( 430 + title: self.currentTrack.title, 431 + artist: self.currentTrack.artist, 432 + album: self.currentTrack.album, 433 + artwork: artwork, 434 + duration: self.duration, 435 + currentTime: self.currentTime, 436 + isPlaying: self.isPlaying 437 + ) 438 + } 439 + } 440 + 441 + private func loadArtwork(from url: URL?, completion: @escaping (NSImage?) -> Void) { 442 + guard let url = url else { 443 + completion(nil) 444 + return 445 + } 446 + 447 + DispatchQueue.global(qos: .userInitiated).async { 448 + let image: NSImage? 449 + 450 + if url.isFileURL { 451 + image = NSImage(contentsOf: url) 452 + } else { 453 + if let data = try? Data(contentsOf: url) { 454 + image = NSImage(data: data) 455 + } else { 456 + image = nil 457 + } 353 458 } 459 + 460 + DispatchQueue.main.async { 461 + completion(image) 462 + } 463 + } 354 464 } 355 465 }