Skip to content

Commit 49e1959

Browse files
committed
Enhance MediaInfoManager and Reporter for improved media playback monitoring
- Integrated Combine framework into MediaInfoManager for reactive media state changes. - Added notification handling for playback state changes, allowing the Reporter to respond to media updates. - Updated the Reporter class to utilize the new media monitoring capabilities, sending media info alongside window focus data. - Refactored the prepareSend method to accommodate optional media information, enhancing reporting flexibility. These changes improve the application's ability to track and report media playback status in real-time, enhancing user experience and functionality. Signed-off-by: Innei <tukon479@gmail.com>
1 parent 05f6eac commit 49e1959

3 files changed

Lines changed: 137 additions & 23 deletions

File tree

ProcessReporter/Core/MediaInfoManager/MediaInfoManager.swift

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Created by Innei on 2025/4/11.
44

55
import AppKit
6+
import Combine
67
import Foundation
78

89
// Recreating the MRContent classes in Swift
@@ -33,6 +34,78 @@ typealias MRMediaRemoteGetNowPlayingApplicationPIDFunction = @convention(c) (
3334
) -> Void
3435

3536
public class MediaInfoManager: NSObject {
37+
// Notification name for media playing state changes
38+
private static let playingStateChangedNotificationName =
39+
"kMRMediaRemoteNowPlayingApplicationIsPlayingDidChangeNotification"
40+
private static let applicationChangedNotificationName =
41+
"kMRMediaRemoteNowPlayingApplicationDidChangeNotification"
42+
private static let infoChangedNotificationName =
43+
"kMRMediaRemoteNowPlayingInfoDidChangeNotification"
44+
45+
// Callback for when playback state changes
46+
public typealias PlaybackStateChangedCallback = (MediaInfo) -> Void
47+
48+
// Store the callback
49+
private static var playbackStateChangedCallback: PlaybackStateChangedCallback?
50+
51+
// Observer for the notification
52+
private static var observer: Any?
53+
54+
// Static initializer to set up the notification center
55+
private static let setupOnce: Void = {
56+
loadMediaRemoteFramework()
57+
}()
58+
private static var cancellables = Set<AnyCancellable>()
59+
private static var mediaInfo: MediaInfo?
60+
61+
// Load MediaRemote framework and register for notifications
62+
private static func loadMediaRemoteFramework() {
63+
let url = URL(fileURLWithPath: "/System/Library/PrivateFrameworks/MediaRemote.framework")
64+
guard CFBundleCreate(kCFAllocatorDefault, url as CFURL) != nil else {
65+
print("Failed to load MediaRemote framework")
66+
return
67+
}
68+
69+
for name in [
70+
playingStateChangedNotificationName, applicationChangedNotificationName,
71+
infoChangedNotificationName,
72+
] {
73+
74+
NotificationCenter.default.publisher(for: Notification.Name(name)).sink { _ in
75+
if let callback = MediaInfoManager.playbackStateChangedCallback {
76+
DispatchQueue.main.async {
77+
guard let mediaInfo = MediaInfoManager.getMediaInfo() else {
78+
return
79+
}
80+
callback(mediaInfo)
81+
}
82+
}
83+
}.store(in: &cancellables)
84+
}
85+
}
86+
87+
// Setup the notification observer
88+
public static func startMonitoringPlaybackChanges(
89+
callback: @escaping PlaybackStateChangedCallback
90+
) {
91+
// Make sure framework is loaded
92+
_ = setupOnce
93+
94+
// Remove existing observer if there is one
95+
if let observer = observer {
96+
NotificationCenter.default.removeObserver(observer)
97+
}
98+
99+
// Store the callback
100+
playbackStateChangedCallback = callback
101+
}
102+
103+
// Stop monitoring playback changes
104+
public static func stopMonitoringPlaybackChanges() {
105+
cancellables.removeAll()
106+
playbackStateChangedCallback = nil
107+
}
108+
36109
public static func getMediaInfo() -> MediaInfo? {
37110
if let nowPlayingInfo = getNowPlayingInfo() {
38111
let name = nowPlayingInfo["name"] as? String
@@ -53,7 +126,8 @@ public class MediaInfoManager: NSObject {
53126
name: name, artist: artist, album: album, image: artworkData, duration: duration,
54127
elapsedTime: elapsedTime, processID: processID, processName: processName,
55128
executablePath: executablePath, playing: playing,
56-
applicationIdentifier: bundleID)
129+
applicationIdentifier: bundleID
130+
)
57131
}
58132
return nil
59133
}
@@ -73,17 +147,22 @@ public class MediaInfoManager: NSObject {
73147
// Get function pointers from the framework
74148
let getMRMediaRemoteGetNowPlayingInfo = unsafeBitCast(
75149
CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteGetNowPlayingInfo" as CFString),
76-
to: MRMediaRemoteGetNowPlayingInfoFunction.self)
150+
to: MRMediaRemoteGetNowPlayingInfoFunction.self
151+
)
77152

78153
let getMRMediaRemoteGetNowPlayingApplicationIsPlaying = unsafeBitCast(
79154
CFBundleGetFunctionPointerForName(
80-
bundle, "MRMediaRemoteGetNowPlayingApplicationIsPlaying" as CFString),
81-
to: MRMediaRemoteGetNowPlayingApplicationIsPlayingFunction.self)
155+
bundle, "MRMediaRemoteGetNowPlayingApplicationIsPlaying" as CFString
156+
),
157+
to: MRMediaRemoteGetNowPlayingApplicationIsPlayingFunction.self
158+
)
82159

83160
let getMRMediaRemoteGetNowPlayingApplicationPID = unsafeBitCast(
84161
CFBundleGetFunctionPointerForName(
85-
bundle, "MRMediaRemoteGetNowPlayingApplicationPID" as CFString),
86-
to: MRMediaRemoteGetNowPlayingApplicationPIDFunction.self)
162+
bundle, "MRMediaRemoteGetNowPlayingApplicationPID" as CFString
163+
),
164+
to: MRMediaRemoteGetNowPlayingApplicationPIDFunction.self
165+
)
87166

88167
// Get playing status
89168
var isPlaying = false

ProcessReporter/Core/Reporter/Reporter.swift

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,18 +174,51 @@ class Reporter {
174174
private func monitor() {
175175
ApplicationMonitor.shared.startMouseMonitoring()
176176
ApplicationMonitor.shared.startWindowFocusMonitoring()
177-
ApplicationMonitor.shared.onWindowFocusChanged = { [unowned self] info in
178-
if PreferencesDataModel.shared.focusReport.value {
177+
ApplicationMonitor.shared.onWindowFocusChanged = { [weak self] info in
178+
guard let self = self else { return }
179+
if PreferencesDataModel.shared.focusReport.value
180+
&& PreferencesDataModel.shared.enabledTypes.value.types.contains(.process)
181+
{
179182
self.prepareSend(windowInfo: info)
180183
}
181184
}
182185

186+
MediaInfoManager.startMonitoringPlaybackChanges { [weak self] mediaInfo in
187+
guard let self = self else { return }
188+
if PreferencesDataModel.shared.enabledTypes.value.types.contains(.media) {
189+
self.prepareSend(
190+
windowInfo: ApplicationMonitor.shared.getFocusedWindowInfo(),
191+
mediaInfo: mediaInfo
192+
)
193+
}
194+
}
183195
statusItemManager.toggleStatusItemIcon(.syncing)
184196
}
185197

186198
private var reporterInitializedTime: Date
187199

188-
private func prepareSend(windowInfo: FocusedWindowInfo) {
200+
private func prepareSend(
201+
windowInfo optionalWindowInfo: FocusedWindowInfo?,
202+
mediaInfo optionalMediaInfo: MediaInfo? = nil
203+
) {
204+
205+
var windowInfo: FocusedWindowInfo!
206+
if let optionalWindowInfo = optionalWindowInfo {
207+
windowInfo = optionalWindowInfo
208+
} else {
209+
windowInfo = ApplicationMonitor.shared.getFocusedWindowInfo()
210+
if windowInfo == nil {
211+
return
212+
}
213+
}
214+
215+
var mediaInfo: MediaInfo?
216+
if let optionalMediaInfo = optionalMediaInfo {
217+
mediaInfo = optionalMediaInfo
218+
} else {
219+
mediaInfo = MediaInfoManager.getMediaInfo()
220+
}
221+
189222
let appName = windowInfo.appName
190223
let now = Date()
191224
// Ignore the first 2 seconds after initialization to wait for the setting synchronization to complete
@@ -205,8 +238,6 @@ class Reporter {
205238
statusItemManager.toggleStatusItemIcon(.syncing)
206239
}
207240

208-
let mediaInfo = MediaInfoManager.getMediaInfo()
209-
210241
var dataModel = ReportModel(
211242
windowInfo: nil,
212243
integrations: [],
@@ -218,8 +249,8 @@ class Reporter {
218249
// Filter media name
219250

220251
if !cachedFilteredMediaAppNames.contains(mediaInfo.processName),
221-
!shouldIgnoreArtistNull
222-
|| (mediaInfo.artist != nil && !mediaInfo.artist!.isEmpty)
252+
!shouldIgnoreArtistNull
253+
|| (mediaInfo.artist != nil && !mediaInfo.artist!.isEmpty)
223254
{
224255
dataModel.setMediaInfo(mediaInfo)
225256
}
@@ -254,8 +285,8 @@ class Reporter {
254285

255286
let interval = PreferencesDataModel.shared.sendInterval.value
256287
timer = Timer.scheduledTimer(
257-
withTimeInterval: TimeInterval(interval.rawValue), repeats: true)
258-
{ [weak self] _ in
288+
withTimeInterval: TimeInterval(interval.rawValue), repeats: true
289+
) { [weak self] _ in
259290
Task { @MainActor in
260291
guard let self = self else { return }
261292
if let info = ApplicationMonitor.shared.getFocusedWindowInfo() {
@@ -284,7 +315,7 @@ class Reporter {
284315
let extensions: [ReporterExtension] = [
285316
MixSpaceReporterExtension(),
286317
S3ReporterExtension(),
287-
SlackReporterExtension()
318+
SlackReporterExtension(),
288319
]
289320

290321
for ext in extensions {
@@ -363,7 +394,8 @@ extension Reporter {
363394
let d3 = Observable.combineLatest(
364395
preferences.mixSpaceIntegration,
365396
preferences.s3Integration,
366-
preferences.slackIntegration).subscribe { [weak self] _ in
397+
preferences.slackIntegration
398+
).subscribe { [weak self] _ in
367399
guard let self = self else { return }
368400
Task {
369401
await self.updateExtensions()

ProcessReporter/Core/Utilities/ApplicationMonitor.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class ApplicationMonitor {
4343
NSWorkspace.shared.open(
4444
URL(
4545
string:
46-
"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
46+
"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
4747
)!)
4848
}
4949
}
@@ -58,14 +58,16 @@ class ApplicationMonitor {
5858
let appElement = AXUIElementCreateApplication(pid)
5959

6060
var mainWindow: CFTypeRef?
61-
let mainWindowError = AXUIElementCopyAttributeValue(appElement, kAXMainWindowAttribute as CFString, &mainWindow)
61+
let mainWindowError = AXUIElementCopyAttributeValue(
62+
appElement, kAXMainWindowAttribute as CFString, &mainWindow)
6263
let window = mainWindow
6364
guard mainWindowError == .success else {
6465
return nil
6566
}
6667

6768
var title: CFTypeRef?
68-
let titleError = AXUIElementCopyAttributeValue(window as! AXUIElement, kAXTitleAttribute as CFString, &title)
69+
let titleError = AXUIElementCopyAttributeValue(
70+
window as! AXUIElement, kAXTitleAttribute as CFString, &title)
6971
guard titleError == .success, let titleString = title as? String else {
7072
return nil
7173
}
@@ -75,15 +77,16 @@ class ApplicationMonitor {
7577

7678
func getFocusedWindowInfo() -> FocusedWindowInfo? {
7779
guard isAccessibilityEnabled() else {
78-
checkAndRequestAccessibilityPermissions()
80+
ToastManager.shared.error(
81+
"Accessibility permissions are required to monitor window changes.")
7982
return nil
8083
}
8184

8285
guard let app = NSWorkspace.shared.frontmostApplication else {
8386
return nil
8487
}
8588
let applicationIdentifier = app.bundleIdentifier ?? ""
86-
89+
8790
if IgnoreSystemApplication.contains(applicationIdentifier) {
8891
return nil
8992
}
@@ -143,7 +146,7 @@ class ApplicationMonitor {
143146
queue: .main
144147
) { [weak self] _ in
145148
guard let self = self,
146-
let windowInfo = self.getFocusedWindowInfo()
149+
let windowInfo = self.getFocusedWindowInfo()
147150
else {
148151
return
149152
}

0 commit comments

Comments
 (0)