Implement Seekbar sync for now playing#237
Conversation
- Guarded behind button called 'Sync Android playback seekbar' - Also added a tooltip - Added the seekbar in Menu bar widget and also the phone display - Requires changes in android app
There was a problem hiding this comment.
Pull request overview
Implements initial Android playback seekbar synchronization by extending the device music status payload with duration/position/buffering state, adding a seekbar UI, and introducing a macOS Now Playing publisher (MPNowPlayingInfoCenter) so third-party UI (e.g., Control Center / boringNotch) can display Android media state.
Changes:
- Added a new opt-in setting for “Sync Android playback seekbar” and wired it into the media UI.
- Extended
DeviceStatus.Musicand WebSocket/BLE plumbing to carry duration/position/buffering, plus outgoing “seekTo” control. - Added
NowPlayingPublisherto publish Android media into macOS Now Playing and forward system media commands back to Android.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| airsync-mac/Screens/Settings/SettingsFeaturesView.swift | Adds the new seekbar-sync toggle and clears now-playing state when disabled. |
| airsync-mac/Screens/HomeScreen/PhoneView/MediaPlayerView.swift | Adds an in-app seekbar UI bound to centralized AppState.mediaPosition. |
| airsync-mac/Model/DeviceStatus.swift | Extends music model with duration/position/buffering fields. |
| airsync-mac/Model/Device.swift | Updates mock music data to include the new fields. |
| airsync-mac/Core/WebSocket/WebSocketServer+Outgoing.swift | Adds outgoing seekTo plus a helper for forwarding system media commands to Android actions. |
| airsync-mac/Core/WebSocket/WebSocketServer+Handlers.swift | Parses duration/position/buffering; corrects position using timestamp; updates AppState and publishes to Now Playing when enabled. |
| airsync-mac/Core/Util/MacInfo/MacInfoSyncManager.swift | Prevents feedback loops by ignoring AirSync’s own published Now Playing entry. |
| airsync-mac/Core/Storage/UserDefaults.swift | Adds a persisted syncAndroidPlaybackSeekbar setting with documentation. |
| airsync-mac/Core/Media/NowPlayingPublisher.swift | New publisher that drives MPNowPlayingInfoCenter + silent audio + MPRemoteCommandCenter forwarding. |
| airsync-mac/Core/BLE/BLETransportBridge.swift | Populates new music fields with “not available” defaults for BLE. |
| airsync-mac/Core/AppState.swift | Adds centralized seekbar state + timer-driven position progression + seek handling. |
| airsync-mac/airsync_macApp.swift | Starts NowPlayingPublisher during app initialization. |
Comments suppressed due to low confidence (2)
airsync-mac/Core/WebSocket/WebSocketServer+Handlers.swift:332
- This base64 decode runs on the status-update hot path and (given WebSocketServer dispatch behavior) is likely on the main thread. Decoding artwork every update can be expensive and cause UI hitching. Consider caching the last albumArt string/hash and only decoding when it changes, and/or doing the decode off the main thread before publishing to MPNowPlayingInfoCenter.
if let data = Data(base64Encoded: albumArt) {
npInfo.artworkData = data
}
airsync-mac/Core/WebSocket/WebSocketServer+Handlers.swift:340
- Duration/position are already parsed, corrected (positionTimestamp compensation), and clamped into durationSec/positionSec above, but MPNowPlayingInfoCenter is being fed fresh values from the raw JSON again. This likely makes the system seekbar lag/overshoot vs the UI slider. Reuse durationSec/positionSec when setting npInfo.duration/elapsedTime so the published now-playing state matches the corrected seekbar state.
if let nsNum = music["duration"] as? NSNumber, nsNum.doubleValue > 0 {
npInfo.duration = nsNum.doubleValue / 1000.0
}
if let pMs = music["position"] as? NSNumber, pMs.doubleValue >= 0 {
npInfo.elapsedTime = pMs.doubleValue / 1000.0
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Initialize NowPlayingPublisher for MPNowPlayingInfoCenter integration | ||
| NowPlayingPublisher.shared.start() | ||
|
|
| /// Call once at app startup. Sets up remote commands and starts silent audio. | ||
| func start() { | ||
| registerRemoteCommands() | ||
| // Start silent audio immediately so the app is ALWAYS audio-eligible. | ||
| // If we wait until the first play command, macOS sees us publish | ||
| // MPNowPlayingInfoCenter data without backing audio and fires a pauseCommand | ||
| // to "correct" the state — which is the root cause of the glitch loop. | ||
| startSilentAudio() | ||
| } |
| /// Update now-playing with Android media info. | ||
| /// During the 1-second window after the user clicks a button, we ignore incoming | ||
| /// status updates. This protects our instant optimistic UI from being overwritten | ||
| /// by stale network packets that Android dispatched before the command took effect. | ||
| func update(info: NowPlayingInfo) { | ||
| let timeSinceCommand = Date().timeIntervalSince(lastCommandSentAt) | ||
| if timeSinceCommand < 1.0 { | ||
| return | ||
| } | ||
|
|
||
| currentInfo = info | ||
|
|
||
| // Always publish metadata on the main thread (MPNowPlayingInfoCenter requirement) | ||
| DispatchQueue.main.async { | ||
| self.lastStateUpdateAt = Date() | ||
| self.publishToNowPlayingInfoCenter(info: info) | ||
| } | ||
| // Silent audio is always running (started in start()), nothing to do here. | ||
| } |
| if UserDefaults.standard.syncAndroidPlaybackSeekbar { | ||
| var npInfo = NowPlayingInfo() | ||
| npInfo.title = title | ||
| npInfo.artist = artist |
| // | ||
|
|
||
| import SwiftUI | ||
| import Combine |
|
@Mudit200408
|
Hey, Sorry about that i think i had accidentally missed out some refactors in the commit, can u check it out again the seekbar changes in both android as well as mac, i have force pushed some changes |
|
Awesome! It works now! There's one thing I'd like to have which is to separate the seekbar controls and the silent audio trick to advertise the playback for macOS. Currently it seems both are controls with one toggle. But I don't want to delay this PR anymore so I'll separate it. Thanks! <3 |


All Conflicts FIXED!!
Although do a test before for confirmation, its working fine for me