Implement Seekbar sync for now playing#109
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds seekbar synchronization support for “Now Playing” by sending duration/position/buffering state over the existing status channel and handling incoming seek commands, with additional suppression logic intended to prevent stale-position “jump back” behavior after a remote seek.
Changes:
- Added a new
"seekTo"media control action over WebSocket and aMediaControlUtil.seekTo()helper. - Extended now playing models + status JSON payload to include
duration,position,isBuffering, andpositionTimestamp. - Added seek-related suppression and a position-jump heuristic in
SyncManagerto trigger syncs on significant local position changes.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt | Adds handling for "seekTo" media control messages and activates seek suppression before executing seek. |
| app/src/main/java/com/sameerasw/airsync/utils/SyncManager.kt | Adds seek suppression, tracks last sync time, and introduces position-jump detection to trigger syncs after local seeks. |
| app/src/main/java/com/sameerasw/airsync/utils/MediaControlUtil.kt | Introduces seekTo() via MediaController.TransportControls. |
| app/src/main/java/com/sameerasw/airsync/utils/JsonUtil.kt | Extends status JSON with seekbar fields and escapes title/artist. |
| app/src/main/java/com/sameerasw/airsync/utils/DeviceInfoUtil.kt | Plumbs new media fields from MediaNotificationListener into AudioInfo and status JSON. |
| app/src/main/java/com/sameerasw/airsync/service/MediaNotificationListener.kt | Computes duration/position/buffering state and a wall-clock timestamp for the position snapshot; adjusts suppression bypass logic for state changes. |
| app/src/main/java/com/sameerasw/airsync/domain/model/DeviceStatus.kt | Extends AudioInfo and MediaInfo with duration/position/buffering/timestamp fields. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+113
to
+125
| // Position-jump detection: if positionMs is more than 8 seconds away from | ||
| // what we'd expect based on normal playback since the last sync, the user | ||
| // seeked on Android — trigger an immediate sync so the Mac can update. | ||
| // Guard: lastSyncTimeMs == 0 means we haven't done a successful sync yet | ||
| // (can happen if performInitialSync failed). Skip detection to avoid a | ||
| // huge elapsedMs causing a spurious forced sync. | ||
| if (!shouldSync && currentAudio.isPlaying && last.positionMs >= 0 && currentAudio.positionMs >= 0 && lastSyncTimeMs > 0) { | ||
| val elapsedMs = System.currentTimeMillis() - lastSyncTimeMs | ||
| val expectedPositionMs = last.positionMs + elapsedMs | ||
| val positionDelta = kotlin.math.abs(currentAudio.positionMs - expectedPositionMs) | ||
| if (positionDelta > 8_000L) { | ||
| shouldSync = true | ||
| Log.d(TAG, "Position jump detected (delta=${positionDelta}ms), syncing") |
Comment on lines
+113
to
+125
| // Position-jump detection: if positionMs is more than 8 seconds away from | ||
| // what we'd expect based on normal playback since the last sync, the user | ||
| // seeked on Android — trigger an immediate sync so the Mac can update. | ||
| // Guard: lastSyncTimeMs == 0 means we haven't done a successful sync yet | ||
| // (can happen if performInitialSync failed). Skip detection to avoid a | ||
| // huge elapsedMs causing a spurious forced sync. | ||
| if (!shouldSync && currentAudio.isPlaying && last.positionMs >= 0 && currentAudio.positionMs >= 0 && lastSyncTimeMs > 0) { | ||
| val elapsedMs = System.currentTimeMillis() - lastSyncTimeMs | ||
| val expectedPositionMs = last.positionMs + elapsedMs | ||
| val positionDelta = kotlin.math.abs(currentAudio.positionMs - expectedPositionMs) | ||
| if (positionDelta > 8_000L) { | ||
| shouldSync = true | ||
| Log.d(TAG, "Position jump detected (delta=${positionDelta}ms), syncing") |
Comment on lines
+126
to
+127
| val durationMs = metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: -1L | ||
|
|
Comment on lines
+133
to
+142
| if (positionMs >= 0 && playbackState != null) { | ||
| // Map elapsedRealtime of the last state update → wall-clock ms | ||
| val elapsedAtUpdate = playbackState.lastPositionUpdateTime | ||
| val elapsedNow = android.os.SystemClock.elapsedRealtime() | ||
| val wallNow = System.currentTimeMillis() | ||
| // Wall-clock instant when playbackState.position was last set | ||
| val wallAtUpdate = wallNow - (elapsedNow - elapsedAtUpdate) | ||
| // Advance position to now (if playing) or leave frozen (if paused/buffering) | ||
| if (isPlaying) { | ||
| val timeDelta = elapsedNow - elapsedAtUpdate |
Comment on lines
+643
to
+650
| fun onMediaStateChanged(context: Context, isPlayingChanged: Boolean = false) { | ||
| if (shouldSuppressMediaUpdate()) { | ||
| Log.d(TAG, "Media state change suppressed due to recent skip command") | ||
| // Always let play/pause state changes through, even during seek/skip suppression. | ||
| // This ensures "pause immediately after Mac seek" works correctly — the Mac will | ||
| // stop its local timer without waiting for the suppression window to expire. | ||
| if (isPlayingChanged) { | ||
| Log.d(TAG, "isPlaying changed during suppression — bypassing to sync") | ||
| checkAndSyncDeviceStatus(context) |
Handle mediaControl seekTo commands on Android and sync playback position metadata back to macOS. - add seekTo transport control support - include duration, position, timestamp, and buffering in status payloads - update sync logic so seekbar state can reconcile after remote scrubs
Owner
|
Thank you! |
Mudit200408
pushed a commit
to Mudit200408/airsync-android
that referenced
this pull request
May 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
All Conflicts FIXED!!