-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathMediaKeyInterceptor.swift
More file actions
143 lines (122 loc) · 4.66 KB
/
Copy pathMediaKeyInterceptor.swift
File metadata and controls
143 lines (122 loc) · 4.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//
// MediaKeyInterceptor.swift
// Remotastic
//
// Intercepts system media key events at HID level to reliably prevent default handling.
// Re-enables tap when disabled by timeout/sleep and on wake.
//
import Cocoa
import CoreGraphics
class MediaKeyInterceptor {
private var eventTap: CFMachPort?
private var runLoopSource: CFRunLoopSource?
private var wakeObserver: NSObjectProtocol?
var onMediaKey: ((MediaKeyType) -> Bool)?
enum MediaKeyType {
case playPause, next, previous, volumeUp, volumeDown, mute
}
func start() {
let eventMask: CGEventMask = 1 << 14 // NX_SYSDEFINED
// HID-level tap intercepts media keys before the system handles them (more reliable than session tap).
guard let tap = CGEvent.tapCreate(
tap: .cghidEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: eventMask,
callback: { (proxy, type, event, refcon) -> Unmanaged<CGEvent>? in
guard let refcon = refcon else { return Unmanaged.passRetained(event) }
let interceptor = Unmanaged<MediaKeyInterceptor>.fromOpaque(refcon).takeUnretainedValue()
return interceptor.handleEvent(proxy: proxy, type: type, event: event)
},
userInfo: Unmanaged.passUnretained(self).toOpaque()
) else {
return
}
eventTap = tap
runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
if let source = runLoopSource {
CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes)
CGEvent.tapEnable(tap: tap, enable: true)
}
// Re-enable tap after sleep/wake (system often disables taps during sleep).
wakeObserver = NSWorkspace.shared.notificationCenter.addObserver(
forName: NSWorkspace.didWakeNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.reenableTap()
}
}
func stop() {
if let obs = wakeObserver {
NSWorkspace.shared.notificationCenter.removeObserver(obs)
wakeObserver = nil
}
if let tap = eventTap {
CGEvent.tapEnable(tap: tap, enable: false)
}
if let source = runLoopSource {
CFRunLoopRemoveSource(CFRunLoopGetMain(), source, .commonModes)
}
eventTap = nil
runLoopSource = nil
}
/// Re-enable the event tap after it was disabled by timeout or sleep.
func reenableTap() {
guard let tap = eventTap else { return }
CGEvent.tapEnable(tap: tap, enable: true)
}
private func handleEvent(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent) -> Unmanaged<CGEvent>? {
// Re-enable tap when system disables it (timeout or user input); then consume the event.
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
reenableTap()
return nil
}
// NX_SYSDEFINED = 14
guard type.rawValue == 14 else {
return Unmanaged.passRetained(event)
}
// Get NSEvent to parse the media key
guard let nsEvent = NSEvent(cgEvent: event) else {
return Unmanaged.passRetained(event)
}
// Check subtype 8 = media key event
guard nsEvent.subtype.rawValue == 8 else {
return Unmanaged.passRetained(event)
}
// Parse the key code from data1
let keyCode = Int32((nsEvent.data1 & 0xFFFF0000) >> 16)
let keyFlags = nsEvent.data1 & 0x0000FFFF
let keyState = (keyFlags & 0xFF00) >> 8
let isKeyDown = keyState == 0x0A
// Only handle key down events
guard isKeyDown else {
return Unmanaged.passRetained(event)
}
// Identify the media key
var mediaKey: MediaKeyType?
switch keyCode {
case NX_KEYTYPE_PLAY:
mediaKey = .playPause
case NX_KEYTYPE_NEXT, NX_KEYTYPE_FAST:
mediaKey = .next
case NX_KEYTYPE_PREVIOUS, NX_KEYTYPE_REWIND:
mediaKey = .previous
case NX_KEYTYPE_SOUND_UP:
mediaKey = .volumeUp
case NX_KEYTYPE_SOUND_DOWN:
mediaKey = .volumeDown
case NX_KEYTYPE_MUTE:
mediaKey = .mute
default:
break
}
if let key = mediaKey, let handler = onMediaKey, handler(key) {
return nil // Consume event
}
return Unmanaged.passRetained(event)
}
deinit {
stop()
}
}