-
Notifications
You must be signed in to change notification settings - Fork 1k
/
Music.swift
137 lines (112 loc) · 5.91 KB
/
Music.swift
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
//
// Music.swift
// Aerial
//
// Created by Guillaume Louel on 29/06/2021.
// Copyright © 2021 Guillaume Louel. All rights reserved.
//
import Foundation
import AppKit
typealias MusicCallback = (SongInfo) -> Void
struct SongInfo {
let name: String
let artist: String
let album: String
let artwork: NSImage?
}
// swiftlint:disable:next type_body_length
class Music {
static let instance: Music = Music()
var callbacks = [MusicCallback]()
var wasSetup = false
// This is called once at init to set our observer
func setup() {
if !wasSetup {
debugLog("🎧 registering private callback")
// Load framework
let bundle = CFBundleCreate(kCFAllocatorDefault, NSURL(fileURLWithPath: "/System/Library/PrivateFrameworks/MediaRemote.framework"))
// Get a Swift function for MRMediaRemoteRegisterForNowPlayingNotifications
guard let MRMediaRemoteRegisterForNowPlayingNotificationsPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteRegisterForNowPlayingNotifications" as CFString) else { return }
typealias MRMediaRemoteRegisterForNowPlayingNotificationsFunction = @convention(c) (DispatchQueue) -> Void
let MRMediaRemoteRegisterForNowPlayingNotifications = unsafeBitCast(MRMediaRemoteRegisterForNowPlayingNotificationsPointer, to: MRMediaRemoteRegisterForNowPlayingNotificationsFunction.self)
// Call the register function
MRMediaRemoteRegisterForNowPlayingNotifications(DispatchQueue.main)
DispatchQueue.main.async {
// Register App state change callback
NotificationCenter.default.addObserver(self,
selector: #selector(Music.mediaRemoteAppStateChange(_:)),
name: NSNotification.Name("kMRMediaRemoteNowPlayingApplicationIsPlayingDidChangeNotification"), object: nil)
// Register playback info change callback
NotificationCenter.default.addObserver(self,
selector: #selector(Music.mediaRemoteCallback(_:)),
name: NSNotification.Name("kMRMediaRemoteNowPlayingInfoDidChangeNotification"), object: nil)
}
wasSetup = true
}
}
// Callback to get paused status from some apps that may not update info pause on change
@objc func mediaRemoteAppStateChange(_ aNotification: Notification) {
debugLog("🎧 app state change")
if let userInfo = aNotification.userInfo {
if let rate = userInfo["kMRMediaRemoteNowPlayingApplicationIsPlayingUserInfoKey"] as? Double {
if rate == 0 {
debugLog("🎧 playback is paused, clearing")
// Pause the thing
for callback in self.callbacks {
callback(SongInfo(name: "", artist: "", album: "", artwork: nil))
}
}
}
}
}
// General info change callback
@objc func mediaRemoteCallback(_ aNotification: Notification?) {
var album = ""
var name = ""
var artist = ""
var artwork: NSImage?
debugLog("🎧 media remote callback")
// Load framework
let bundle = CFBundleCreate(kCFAllocatorDefault, NSURL(fileURLWithPath: "/System/Library/PrivateFrameworks/MediaRemote.framework"))
// Get a Swift function for MRMediaRemoteGetNowPlayingInfo
guard let MRMediaRemoteGetNowPlayingInfoPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteGetNowPlayingInfo" as CFString) else { return }
typealias MRMediaRemoteGetNowPlayingInfoFunction = @convention(c) (DispatchQueue, @escaping ([String: Any]) -> Void) -> Void
let MRMediaRemoteGetNowPlayingInfo = unsafeBitCast(MRMediaRemoteGetNowPlayingInfoPointer, to: MRMediaRemoteGetNowPlayingInfoFunction.self)
// Get song info
MRMediaRemoteGetNowPlayingInfo(DispatchQueue.main, { (information) in
debugLog("🎧 audio info")
if let info = information["kMRMediaRemoteNowPlayingInfoPlaybackRate"] as? Double {
if (info != 0.0) {
// Player is running
if let info = information["kMRMediaRemoteNowPlayingInfoArtist"] as? String {
artist = info
}
if let info = information["kMRMediaRemoteNowPlayingInfoTitle"] as? String {
name = info
}
if let info = information["kMRMediaRemoteNowPlayingInfoAlbum"] as? String {
album = info
}
// try to grab image from the keys
if information.keys.contains("kMRMediaRemoteNowPlayingInfoArtworkData") {
if let _artwork = NSImage(data: information["kMRMediaRemoteNowPlayingInfoArtworkData"] as! Data) {
artwork = _artwork
}
}
debugLog("🎧 " + artist + " - " + name + " (" + album + ")" + ((artwork != nil) ? " with artwork " : " without artwork"))
} else {
debugLog("🎧 Player is paused")
}
}
// Let everyone who wants to know that we have a new song playing !
for callback in self.callbacks {
callback(SongInfo(name: name, artist: artist, album: album, artwork: artwork))
}
})
}
// MARK: - Callbacks
func addCallback(_ callback:@escaping MusicCallback) {
debugLog("🎧 Adding music callback")
callbacks.append(callback)
}
}