import Cocoa import CoreGraphics struct MediaPlayer: Equatable { let name: String let bundleID: String static let known: [MediaPlayer] = [ MediaPlayer(name: "Spotify", bundleID: "com.spotify.client"), MediaPlayer(name: "Apple Music", bundleID: "com.apple.Music"), MediaPlayer(name: "Yandex Music", bundleID: "ru.yandex.music.mac.universal"), MediaPlayer(name: "VLC", bundleID: "org.videolan.vlc"), MediaPlayer(name: "IINA", bundleID: "com.colliderli.iina"), MediaPlayer(name: "Vox", bundleID: "com.coppertino.Vox"), ] } class MediaKeyMonitor { var eventTap: CFMachPort? private var runLoopSource: CFRunLoopSource? private var launchObserver: NSObjectProtocol? var isEnabled: Bool = true { didSet { isEnabled ? startWatching() : stopWatching() } } private let prefsKey = "preferredMediaPlayer" private let appleMusicBundleID = "com.apple.Music" // All known player bundle IDs for detection private let allPlayerBundleIDs: Set = Set(MediaPlayer.known.map(\.bundleID) + [ "com.apple.iTunes", "com.tidal.desktop", "com.amazon.music", "com.deezer.deezer-desktop", "com.plexamp.Plexamp", "app.foobar2000.mac", ]) var preferredBundleID: String { get { UserDefaults.standard.string(forKey: prefsKey) ?? "com.spotify.client" } set { UserDefaults.standard.set(newValue, forKey: prefsKey) } } func start() -> Bool { startWatching() startEventTap() return true } func stop() { stopWatching() stopEventTap() } // MARK: - App Launch Watcher (main mechanism, like noTunes) // Watches for Apple Music launching and kills it + launches preferred player private func startWatching() { guard launchObserver == nil else { return } // Kill Apple Music if it's already running and we don't want it if preferredBundleID != appleMusicBundleID { killAppleMusic() } launchObserver = NSWorkspace.shared.notificationCenter.addObserver( forName: NSWorkspace.didLaunchApplicationNotification, object: nil, queue: .main ) { [weak self] notification in self?.handleAppLaunch(notification) } } private func stopWatching() { if let observer = launchObserver { NSWorkspace.shared.notificationCenter.removeObserver(observer) launchObserver = nil } } private func handleAppLaunch(_ notification: Notification) { guard isEnabled else { return } guard preferredBundleID != appleMusicBundleID else { return } guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, app.bundleIdentifier == appleMusicBundleID else { return } // Hide immediately to prevent visual flash, then force-kill app.hide() app.forceTerminate() // Launch preferred player if not already running if !isPreferredPlayerRunning() { launchPreferredPlayer() } } private func killAppleMusic() { for app in NSWorkspace.shared.runningApplications { if app.bundleIdentifier == appleMusicBundleID { app.hide() app.forceTerminate() } } } // MARK: - CGEvent Tap (supplementary — intercepts key before rcd when possible) private func startEventTap() { guard eventTap == nil else { return } let trusted = AXIsProcessTrusted() if !trusted { promptAccessibility() return } let mask = CGEventMask(1 << CGEventType(rawValue: 14)!.rawValue) // NX_SYSDEFINED guard let tap = CGEvent.tapCreate( tap: .cgSessionEventTap, place: .headInsertEventTap, options: .defaultTap, eventsOfInterest: mask, callback: mediaKeyCallback, userInfo: Unmanaged.passUnretained(self).toOpaque() ) else { return } eventTap = tap runLoopSource = CFMachPortCreateRunLoopSource(nil, tap, 0) CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, .commonModes) CGEvent.tapEnable(tap: tap, enable: true) } private func stopEventTap() { if let source = runLoopSource { CFRunLoopRemoveSource(CFRunLoopGetMain(), source, .commonModes) runLoopSource = nil } if let tap = eventTap { CGEvent.tapEnable(tap: tap, enable: false) eventTap = nil } } // MARK: - Helpers func isAnyPlayerRunning() -> Bool { NSWorkspace.shared.runningApplications.contains { app in guard let bid = app.bundleIdentifier else { return false } return allPlayerBundleIDs.contains(bid) } } private func isPreferredPlayerRunning() -> Bool { NSWorkspace.shared.runningApplications.contains { $0.bundleIdentifier == preferredBundleID } } func launchPreferredPlayer() { guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: preferredBundleID) else { return } NSWorkspace.shared.openApplication(at: url, configuration: .init()) { _, _ in } } private func promptAccessibility() { let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] as CFDictionary AXIsProcessTrustedWithOptions(options) } } // NX_KEYTYPE constants private let NX_KEYTYPE_PLAY: Int32 = 16 private func mediaKeyCallback( proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, userInfo: UnsafeMutableRawPointer? ) -> Unmanaged? { guard let userInfo = userInfo else { return Unmanaged.passRetained(event) } let monitor = Unmanaged.fromOpaque(userInfo).takeUnretainedValue() // Re-enable if system disabled us if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput { if let tap = monitor.eventTap { CGEvent.tapEnable(tap: tap, enable: true) } return Unmanaged.passRetained(event) } guard monitor.isEnabled else { return Unmanaged.passRetained(event) } guard type.rawValue == 14 else { return Unmanaged.passRetained(event) } guard let nsEvent = NSEvent(cgEvent: event) else { return Unmanaged.passRetained(event) } guard nsEvent.subtype.rawValue == 8 else { return Unmanaged.passRetained(event) } let data1 = nsEvent.data1 let keyCode = Int32((data1 & 0xFFFF0000) >> 16) let keyDown = ((data1 & 0x0100) == 0) guard keyCode == NX_KEYTYPE_PLAY && keyDown else { return Unmanaged.passRetained(event) } // Play pressed — if no player running, launch preferred and consume if !monitor.isAnyPlayerRunning() { DispatchQueue.main.async { monitor.launchPreferredPlayer() } return nil } return Unmanaged.passRetained(event) }