904b9b3d-c0eb-42f3-acef-958.../Sources
徐翔宇 336b2266e8 v2.0.4: latency razor — event-driven + running-app gate + parallel probing
Target: push state-change detection latency under 200ms in the common case,
and cold start under 2s.

Changes:

1. Event-driven primary path, poll becomes safety-net
   - Poll interval 1.5s → 15s. Was firing 40 AppleScript probes per minute
     on a Mac that's playing nothing.
   - MediaRemote notifications + DistributedNotificationCenter broadcasts
     (com.spotify.client.PlaybackStateChanged,
      com.apple.Music.playerInfo, com.apple.iTunes.playerInfo)
     already handle track changes in <100ms. The 1.5s poll was just
     backup, and now 15s is enough backup.

2. NSWorkspace launch/terminate observers
   - New observers on NSWorkspace.didLaunchApplicationNotification +
     didTerminateApplicationNotification. When Spotify, Apple Music, or
     Chrome launches / quits, refresh fires immediately instead of
     waiting for the next poll. Beats the old path by up to 15s on
     first-launch-of-day scenarios.

3. Running-app gate (NSWorkspace.runningApplications)
   - Each source now exposes `static var isRunning` via
     NSWorkspace.shared.runningApplications.contains(bundleId).
   - Router checks before probing. AppleScript `with timeout of 2 seconds`
     still trips when the target app isn't running, so avoiding those
     probes saves up to 6s per refresh on a clean Mac.

4. MediaRemote 15.4+ entitlement memoization
   - When MRMediaRemoteGetNowPlayingInfo returns an empty dict AND at
     least one player app is running (likelyBlocked heuristic), mark
     MediaRemote blocked for 60s and skip in the router. Saves ~50ms
     per refresh on restricted macOS versions and lets the first-pass
     AppleScript probe happen without a preceding MR round-trip.
   - Retries every 60s in case the gate state changes (macOS minor
     update / user-granted entitlement).

5. Parallel fallback probing
   - Old router was serial: MediaRemote → Spotify → Music → Chrome.
     Cold start worst-case 4-6s when all three AppleScript sources
     trip their 2s timeouts.
   - New router uses `async let` to fan out every live candidate
     concurrently. First-in-priority-order non-nil result wins.
     Cold start worst-case now ≈ slowest single AppleScript probe.

6. Sticky-source fast path survives
   - When the last-successful source is still a live candidate
     (its app still running, MR still not blocked), try it alone
     first. On steady-state playback this is one round-trip per
     refresh, same as before.

7. Transport control perceived latency
   - scheduleRefresh(after: 0.3) → 0.1 for togglePlay/next/prev/seek.
     UI already flips optimistically; the 100ms re-sync is enough
     to catch the real app state without feeling laggy.

Reference: Atoll (github.com/Ebullioscopic/Atoll) uses a bundled
mediaremote-adapter framework + Perl stream client to bypass the
macOS 15.4 MediaRemote entitlement gate entirely. That's a bigger
lift and left for a future phase — this commit wrings out the latency
that's achievable without that adapter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:02:36 +08:00
..
sources v2.0.4: latency razor — event-driven + running-app gate + parallel probing 2026-04-20 15:02:36 +08:00
support v2.0.0: full rewrite with multi-source NowPlaying 2026-04-19 02:27:21 +08:00
ui v2.0.3: compact panel via host size hint + Apple Music artwork + faster poll 2026-04-19 20:42:14 +08:00
MioPlugin.swift feat: MioIsland music player plugin — reads system NowPlaying 2026-04-11 23:37:11 +08:00
MusicPlugin.swift v2.0.3: compact panel via host size hint + Apple Music artwork + faster poll 2026-04-19 20:42:14 +08:00
NowPlayingState.swift v2.0.4: latency razor — event-driven + running-app gate + parallel probing 2026-04-20 15:02:36 +08:00