Skip to content

fix(media-watcher): write playback as discrete per-player segments - #343

Open
0xbrayo wants to merge 3 commits into
ActivityWatch:masterfrom
0xbrayo:fix/media-watcher-concurrent-players
Open

0xbrayo wants to merge 3 commits into
ActivityWatch:masterfrom
0xbrayo:fix/media-watcher-concurrent-players

Conversation

@0xbrayo

@0xbrayo 0xbrayo commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Part of #333.

MediaWatcher sent playback as zero-duration heartbeats (pulsetime 60s) on every change and every 15s poll. The server only merges a heartbeat into the newest event in the bucket. With two media sessions playing at once (for example music plus a video), their heartbeats alternated, so neither ever merged, and every poll inserted two new zero-duration events instead of extending two events.

Change

  • New MediaPlaybackSegments tracks one open segment per media session (keyed by session token, so an app's two sessions stay separate) with a known start. It writes the segment once, with its real duration via insertEvent, when:
    • the track or playback state changes,
    • the session is destroyed, leaves the active list, or the listener disconnects,
    • the player pauses or stops without metadata, or reports an unknown state or empty metadata,
    • the segment reaches 5 minutes, after which a new segment starts.
  • The 5-minute chunking bounds how much is lost if the process is killed mid-episode, and how stale the bucket gets during long playback. Contiguous chunks from the same player still merge on the server when nothing else was written in between.
  • Pause, stop and buffering transitions are still recorded as zero-duration events, once per transition, as before.
  • All segment work, including the final writes on disconnect and destroy, runs on the watcher's handler thread, never the service's main thread.
  • Event data is unchanged: app, package, title, artist, album, state.

Behaviour change

Playing time now shows up when the segment closes, at most ~5 minutes plus one poll later, rather than being extended live by heartbeats.

Testing

  • New MediaPlaybackSegmentsTest covers:
    • two players polled together write one event each instead of an event per poll,
    • a track change ends the previous track,
    • long playback is written in bounded chunks,
    • a repeated pause is recorded once.
  • ./gradlew :mobile:testStandardDebugUnitTest passes.

@greptile-apps

greptile-apps Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issue was found.

Summary

The PR replaces media heartbeats with completed playback segments, tracked separately for each media session.

  • Playing segments close on track changes, playback changes, session removal, or the five-minute limit.
  • The latest changes put registration and teardown on the same worker thread.
  • The earlier findings were rechecked. No new actionable issue was found in the changes since the last review.

Diagram

sequenceDiagram
    participant Android
    participant Worker as MediaWatcher worker
    participant Segments as Per-session segments
    participant Store as Media bucket
    Android->>Worker: Connect and scan sessions
    Worker->>Segments: Observe playback
    Android->>Worker: Track change, pause, or session removal
    Worker->>Segments: Close affected segment
    Segments->>Store: Write start, duration, and media data
    Android->>Worker: Queue teardown at disconnect time
    Worker->>Worker: Finish running scan, then unregister callbacks
    Worker->>Segments: End remaining segments at disconnect time
    Segments->>Store: Write final playback
Loading

Reviews (3) · Last reviewed commit: "fix(media-watcher): serialize teardown w..." · Reviewed by Greptile

Comment thread mobile/src/main/java/net/activitywatch/android/watcher/MediaWatcher.kt Outdated
Comment thread mobile/src/main/java/net/activitywatch/android/watcher/MediaWatcher.kt Outdated
Comment thread mobile/src/main/java/net/activitywatch/android/watcher/MediaWatcher.kt Outdated
Comment thread mobile/src/main/java/net/activitywatch/android/watcher/MediaWatcher.kt Outdated
Playback was sent as heartbeats, but the server only merges a heartbeat
into the newest event in the bucket. With two players active their
heartbeats alternated, never merged, and every 15s poll inserted new
zero-duration events. Track each player's playing time as an open
segment and write it once with its real duration when the track or
state changes, the session ends, or it reaches 5 minutes.
@0xbrayo
0xbrayo force-pushed the fix/media-watcher-concurrent-players branch from ed6ce41 to e5691c8 Compare October 8, 2026 19:37
Key segments by media session token rather than package, so an app's
two sessions don't overwrite each other. Close a session's segment when
it leaves the active list, when it pauses or stops without metadata,
and when it reports an unknown state or empty metadata, so stopped time
is never counted as playing. Include album in the segment identity.
Run all segment work, including the final writes on disconnect and
destroy, on the handler thread so the blocking writes stay off the main
thread; the writes are stamped with the disconnect time.
@0xbrayo

0xbrayo commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

🤖 Claude, on behalf of @0xbrayo

@greptile review

Teardown cleared the maps on the main thread while a session scan could
be running on the handler thread, so the scan could register callbacks
again after disconnect. Run teardown on the handler thread after any
running scan, and guard callbacks, polls and session changes with a
connected flag so work queued before the disconnect can't reopen a
segment.
@0xbrayo

0xbrayo commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

🤖 Claude, on behalf of @0xbrayo

@greptile review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant