From 336b2266e89a16ef3701390e75b8e13b111a564d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E7=BF=94=E5=AE=87?= Date: Mon, 20 Apr 2026 15:02:36 +0800 Subject: [PATCH] =?UTF-8?q?v2.0.4:=20latency=20razor=20=E2=80=94=20event-d?= =?UTF-8?q?riven=20+=20running-app=20gate=20+=20parallel=20probing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- Info.plist | 4 +- Sources/NowPlayingState.swift | 231 ++++++++++++++++++-- Sources/sources/AppleMusicAppleScript.swift | 9 + Sources/sources/ChromeWebSource.swift | 8 + Sources/sources/SpotifyAppleScript.swift | 11 +- 5 files changed, 238 insertions(+), 25 deletions(-) diff --git a/Info.plist b/Info.plist index ae24d67..a8f2804 100644 --- a/Info.plist +++ b/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 2.0.3 + 2.0.4 CFBundleVersion - 5 + 6 NSPrincipalClass MusicPlugin.MusicPlugin