@@ -254,6 +254,32 @@ extension WebSocketServer {
254254 {
255255 let albumArt = ( music [ " albumArt " ] as? String ) ?? " "
256256 let likeStatus = ( music [ " likeStatus " ] as? String ) ?? " none "
257+ let isBuffering = ( music [ " isBuffering " ] as? Bool ) ?? false
258+
259+ // Android sends duration/position in ms; convert to seconds.
260+ // Using NSNumber because Swift's `as? Double` fails if the JSON parser inferred an Int.
261+ let durationSec = ( music [ " duration " ] as? NSNumber ) . map { $0. doubleValue / 1000.0 } ?? - 1.0
262+ var positionSec = ( music [ " position " ] as? NSNumber ) . map { $0. doubleValue / 1000.0 } ?? - 1.0
263+
264+ // Timestamp-based position correction:
265+ // Android includes the wall-clock ms when the position snapshot was taken.
266+ // We add the elapsed time since then (which includes WiFi transit) to get a
267+ // much more accurate "current" position — effectively NTP-style compensation.
268+ // Clamp: only correct for realistic WiFi delays (< 5s). Larger deltas likely
269+ // indicate clock skew between devices, which would worsen accuracy if applied.
270+ if positionSec >= 0 , playing, !isBuffering,
271+ let tsMs = music [ " positionTimestamp " ] as? NSNumber {
272+ let capturedAt = tsMs. doubleValue / 1000.0
273+ let nowSec = Date ( ) . timeIntervalSince1970
274+ let networkDelta = nowSec - capturedAt
275+ if networkDelta > - 2.0 && networkDelta < 5.0 {
276+ positionSec += max ( 0.0 , networkDelta)
277+ }
278+ }
279+ // Clamp to duration to prevent the seekbar going past the end
280+ if durationSec > 0 && positionSec > durationSec {
281+ positionSec = durationSec
282+ }
257283
258284 AppState . shared. status = DeviceStatus (
259285 battery: . init( level: level, isCharging: isCharging) ,
@@ -265,19 +291,38 @@ extension WebSocketServer {
265291 volume: volume,
266292 isMuted: isMuted,
267293 albumArt: albumArt,
268- likeStatus: likeStatus
294+ likeStatus: likeStatus,
295+ duration: durationSec,
296+ position: positionSec,
297+ isBuffering: isBuffering
269298 )
270299 )
271300
272- // Publish Android now-playing info to MPNowPlayingInfoCenter
273- var npInfo = NowPlayingInfo ( )
274- npInfo. title = title
275- npInfo. artist = artist
276- npInfo. isPlaying = playing
277- if let data = Data ( base64Encoded: albumArt) {
278- npInfo. artworkData = data
301+ // Publish Android now-playing info to MPNowPlayingInfoCenter only when
302+ // the user has opted in, because this requires playing silent audio which
303+ // causes multipoint Bluetooth headphones to route audio to the Mac.
304+ if UserDefaults . standard. syncAndroidPlaybackSeekbar {
305+ var npInfo = NowPlayingInfo ( )
306+ npInfo. title = title
307+ npInfo. artist = artist
308+ npInfo. isPlaying = playing
309+ if let data = Data ( base64Encoded: albumArt) {
310+ npInfo. artworkData = data
311+ }
312+ // Seekbar: Android sends duration/position in ms; MPNowPlayingInfoCenter needs seconds.
313+ // positionMs uses optDouble so missing/null safely falls back to -1.
314+ // NOTE: Use NSNumber because Swift's JSON parser returns an Int type for flat numbers.
315+ if let nsNum = music [ " duration " ] as? NSNumber , nsNum. doubleValue > 0 {
316+ npInfo. duration = nsNum. doubleValue / 1000.0
317+ }
318+ if let pMs = music [ " position " ] as? NSNumber , pMs. doubleValue >= 0 {
319+ npInfo. elapsedTime = pMs. doubleValue / 1000.0
320+ }
321+ NowPlayingPublisher . shared. update ( info: npInfo)
322+ } else {
323+ // If the setting is off, ensure any previously running session is cleared
324+ NowPlayingPublisher . shared. clear ( )
279325 }
280- NowPlayingPublisher . shared. update ( info: npInfo)
281326 }
282327 }
283328
0 commit comments