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>
Host-facing:
- Info.plist requests a 440x340 expanded panel via the new
MioPluginPreferredWidth/MioPluginPreferredHeight keys (MioIsland
v2.1.8+). Old hosts ignore the keys and use their default.
UI:
- Fix vertical stretching of the playing card. Outer ZStack now centers
children instead of wrapping in a maxHeight:.infinity frame which was
letting an inner Spacer propagate fill-height up to the top-level VStack.
- Hero HStack clipped to album art height (128pt) so the meta column
can't bleed a fill-height hint upward either.
Data:
- Apple Music artwork is now fetched via a temp file (write artwork data
of current track to /tmp, load NSImage from disk). First-class cover art
instead of the generic music.note placeholder.
- apply(appleScript:) clears albumArt when the track identity changes so
the next refresh reloads cover art for the new track.
Latency:
- Poll interval 3s → 1.5s. Track changes typically reflect within 2s.
- Also subscribe to the legacy com.apple.iTunes.playerInfo distributed
notification in addition to com.apple.Music.playerInfo — some builds of
Music.app still emit the iTunes name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UI polish (ExpandedView rewrite):
- Horizontal hero row: 128×128 album art on the left, title/artist/
album + source badge on the right. Half the vertical footprint of
v2.0.0 at the same info density.
- Dropped the "NOW PLAYING" eyebrow (redundant with the source badge).
- Tightened outer padding 28 → 20, inter-section spacing 22-28 → 16.
- Play button 56 → 48, prev/next 44 → 36; still 44pt tap targets via
the invisible hover frame.
AppleScript timeout fix (the real bug, unrelated to UI):
- Every fetch() script now wraps the `tell application` block in
`with timeout of N seconds` (2s for Spotify/Music, 3s for Chrome).
- Music.app hanging was stalling the entire source router for 120s
(default AppleEvent timeout), freezing the UI on stale Spotify data.
- runAppleScript() suppresses error -1712 (errAETimeout) alongside
existing -600 / -1728 — expected, not noisy.
Info.plist: CFBundleShortVersionString 2.0.0 → 2.0.1,
CFBundleVersion 2 → 3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the v1.0.0 shell (MediaRemote-only, non-functional on macOS 15.4+)
with a layered design that handles four playback sources with sticky source
priority routing:
NowPlayingState (orchestrator, @MainActor, 3s poll + notifications)
├─ MediaRemote (private framework, dlopen)
├─ Spotify AppleScript (desktop)
├─ Apple Music AppleScript (desktop)
└─ Chrome JS injection (YouTube / SoundCloud / web music)
UI:
- Large album art with color-extracted gradient background
- Title / artist / album + source badge
- Draggable seek bar with hover-grow affordance
- Prev / Play·Pause (56pt lime button) / Next controls
- Header slot: 20x20 icon + 3-bar pseudo-spectrum that pulses while playing
- Bi-lingual (zh / en), follows host appLanguage
Graceful degradation:
- Host < v2.1.7 → upgrade banner (NSAppleEventsUsageDescription required)
- QQ Music / NetEase / Kugou detected → "desktop unsupported, try web version"
- Empty state with hint to play something in supported apps
Build layout:
Sources/ root (MioPlugin.swift contract + MusicPlugin principal)
Sources/sources/ data sources
Sources/ui/ SwiftUI views
Sources/support/ ChineseAppDetector / HostVersionCheck / Localization
build.sh now recursively finds .swift under Sources/.
Breaking-ish: plugin id ("music-player") and bundle ID preserved. Users on
v1.0.0 can upgrade in place via the plugin store.
Requires: MioIsland host >= v2.1.7 for full functionality.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>