1
0
mirror of https://github.com/mpv-player/mpv synced 2025-02-17 04:58:06 +00:00
mpv/osdep/mac/remote_command_center.swift
der richter c1e0e32453 mac/remote: use swift closure instead of obj-c selector bridging
it's unnecessary to use the overhead of an obj-c selector and the
resulting bridging.
2025-01-04 13:33:15 +01:00

278 lines
11 KiB
Swift

/*
* This file is part of mpv.
*
* mpv is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* mpv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with mpv. If not, see <http://www.gnu.org/licenses/>.
*/
import Cocoa
import MediaPlayer
import QuickLookThumbnailing
extension RemoteCommandCenter {
typealias ConfigHandler = (MPRemoteCommandEvent) -> (MPRemoteCommandHandlerStatus)
enum KeyType {
case normal
case repeatable
}
struct Config {
let key: Int32
let type: KeyType
var state: NSEvent.EventType = .applicationDefined
let handler: ConfigHandler
init(key: Int32 = 0, type: KeyType = .normal, handler: @escaping ConfigHandler = { _ in return .commandFailed }) {
self.key = key
self.type = type
self.handler = handler
}
}
}
class RemoteCommandCenter: EventSubscriber {
unowned let appHub: AppHub
var event: EventHelper? { return appHub.event }
var input: InputHelper { return appHub.input }
var configs: [MPRemoteCommand: Config] = [:]
var disabledCommands: [MPRemoteCommand] = []
var isPaused: Bool = false { didSet { updateInfoCenter() } }
var duration: Double = 0 { didSet { updateInfoCenter() } }
var position: Double = 0 { didSet { updateInfoCenter() } }
var rate: Double = 1 { didSet { updateInfoCenter() } }
var title: String = "" { didSet { updateInfoCenter() } }
var chapter: String? { didSet { updateInfoCenter() } }
var album: String? { didSet { updateInfoCenter() } }
var artist: String? { didSet { updateInfoCenter() } }
var path: String?
let coverLock = NSLock()
var coverTime: UInt64 = mach_absolute_time()
var coverPath: String?
var cover: NSImage? { didSet { updateInfoCenter() } }
var coverThumb: NSImage? { didSet { updateInfoCenter() } }
var defaultCover: NSImage
let queue: DispatchQueue = DispatchQueue(label: "io.mpv.remote.queue")
var infoCenter: MPNowPlayingInfoCenter { return MPNowPlayingInfoCenter.default() }
var commandCenter: MPRemoteCommandCenter { return MPRemoteCommandCenter.shared() }
init(_ appHub: AppHub) {
self.appHub = appHub
defaultCover = appHub.getIcon()
configs = [
commandCenter.pauseCommand: Config(key: MP_KEY_PAUSEONLY, handler: keyHandler),
commandCenter.playCommand: Config(key: MP_KEY_PLAYONLY, handler: keyHandler),
commandCenter.stopCommand: Config(key: MP_KEY_STOP, handler: keyHandler),
commandCenter.nextTrackCommand: Config(key: MP_KEY_NEXT, handler: keyHandler),
commandCenter.previousTrackCommand: Config(key: MP_KEY_PREV, handler: keyHandler),
commandCenter.togglePlayPauseCommand: Config(key: MP_KEY_PLAY, handler: keyHandler),
commandCenter.seekForwardCommand: Config(key: MP_KEY_FORWARD, type: .repeatable, handler: keyHandler),
commandCenter.seekBackwardCommand: Config(key: MP_KEY_REWIND, type: .repeatable, handler: keyHandler),
commandCenter.changePlaybackPositionCommand: Config(handler: seekHandler)
]
disabledCommands = [
commandCenter.changePlaybackRateCommand,
commandCenter.changeRepeatModeCommand,
commandCenter.changeShuffleModeCommand,
commandCenter.skipForwardCommand,
commandCenter.skipBackwardCommand,
commandCenter.enableLanguageOptionCommand,
commandCenter.disableLanguageOptionCommand,
commandCenter.ratingCommand,
commandCenter.likeCommand,
commandCenter.dislikeCommand,
commandCenter.bookmarkCommand
]
for cmd in disabledCommands {
cmd.isEnabled = false
}
}
func registerEvents() {
event?.subscribe(self, event: .init(name: "duration", format: MPV_FORMAT_DOUBLE))
event?.subscribe(self, event: .init(name: "time-pos", format: MPV_FORMAT_DOUBLE))
event?.subscribe(self, event: .init(name: "speed", format: MPV_FORMAT_DOUBLE))
event?.subscribe(self, event: .init(name: "pause", format: MPV_FORMAT_FLAG))
event?.subscribe(self, event: .init(name: "media-title", format: MPV_FORMAT_STRING))
event?.subscribe(self, event: .init(name: "chapter-metadata/title", format: MPV_FORMAT_STRING))
event?.subscribe(self, event: .init(name: "metadata/by-key/album", format: MPV_FORMAT_STRING))
event?.subscribe(self, event: .init(name: "metadata/by-key/artist", format: MPV_FORMAT_STRING))
event?.subscribe(self, event: .init(name: "path", format: MPV_FORMAT_STRING))
event?.subscribe(self, event: .init(name: "track-list", format: MPV_FORMAT_NODE))
}
func start() {
for (cmd, config) in configs {
cmd.isEnabled = true
cmd.addTarget(handler: config.handler)
}
updateInfoCenter()
NotificationCenter.default.addObserver(
forName: NSApplication.willBecomeActiveNotification,
object: nil,
queue: nil) { _ in self.makeCurrent() }
}
func stop() {
for (cmd, _) in configs {
cmd.isEnabled = false
cmd.removeTarget(nil)
}
infoCenter.nowPlayingInfo = nil
infoCenter.playbackState = .unknown
NotificationCenter.default.removeObserver(
self,
name: NSApplication.willBecomeActiveNotification,
object: nil
)
}
func makeCurrent() {
infoCenter.playbackState = .paused
infoCenter.playbackState = .playing
updateInfoCenter()
}
func updateInfoCenter() {
let cover = cover ?? coverThumb ?? defaultCover
infoCenter.playbackState = isPaused ? .paused : .playing
infoCenter.nowPlayingInfo = (infoCenter.nowPlayingInfo ?? [:]).merging([
MPNowPlayingInfoPropertyMediaType: NSNumber(value: MPNowPlayingInfoMediaType.video.rawValue),
MPNowPlayingInfoPropertyPlaybackProgress: NSNumber(value: 0.0),
MPNowPlayingInfoPropertyPlaybackRate: NSNumber(value: isPaused ? 0 : rate),
MPNowPlayingInfoPropertyElapsedPlaybackTime: NSNumber(value: position),
MPMediaItemPropertyPlaybackDuration: NSNumber(value: duration),
MPMediaItemPropertyTitle: title,
MPMediaItemPropertyArtist: artist ?? chapter ?? "",
MPMediaItemPropertyAlbumTitle: album ?? "",
MPMediaItemPropertyArtwork: MPMediaItemArtwork(boundsSize: cover.size) { _ in return cover }
]) { (_, new) in new }
}
func updateCover(tracks: [Any?]) {
coverLock.withLock {
coverTime = mach_absolute_time()
coverPath = nil
cover = nil
coverThumb = nil
// read cover image on separate thread
queue.async { self.generateCover(tracks: tracks, time: self.coverTime) }
generateCoverThumb(time: self.coverTime)
}
}
func generateCover(tracks: [Any?], time: UInt64) {
var imageCoverPath: String?
var externalCoverPath: String?
for item in tracks {
guard let track = item as? [String: Any?] else { continue }
if (track["image"] as? Bool) == true {
// opened file is an image
if track["external"] as? Bool == false && track["albumart"] as? Bool == false && imageCoverPath == nil {
imageCoverPath = path
continue
}
// external cover
if let filename = track["external-filename"] as? String, externalCoverPath == nil {
externalCoverPath = filename
continue
}
}
}
guard let path = imageCoverPath ?? externalCoverPath, coverPath != path else { return }
var image = NSImage(contentsOf: URL(fileURLWithPath: path))
if let url = URL(string: path), image == nil {
image = NSImage(contentsOf: url)
}
coverLock.withLock {
guard time == coverTime else { return }
cover = image
coverPath = path
}
}
func generateCoverThumb(time: UInt64) {
guard let path = path else { return }
let request = QLThumbnailGenerator.Request(fileAt: URL(fileURLWithPath: path),
size: CGSize(width: 2000, height: 2000),
scale: 1,
representationTypes: .all)
QLThumbnailGenerator.shared.generateBestRepresentation(for: request) { thumbnail, _ in
self.coverLock.withLock {
guard time == self.coverTime else { return }
guard let image = thumbnail?.nsImage, thumbnail?.type != .icon else { return }
self.coverThumb = image
}
}
}
lazy var keyHandler: ConfigHandler = { event in
guard let config = self.configs[event.command] else {
return .commandFailed
}
var state = config.state
if config.type == .repeatable {
state = config.state == .keyDown ? .keyUp : .keyDown
self.configs[event.command]?.state = state
}
self.input.put(key: config.key, type: state)
return .success
}
lazy var seekHandler: ConfigHandler = { event in
guard let posEvent = event as? MPChangePlaybackPositionCommandEvent else {
return .commandFailed
}
let cmd = String(format: "seek %.02f absolute", posEvent.positionTime)
return self.input.command(cmd) ? .success : .commandFailed
}
func handle(event: EventHelper.Event) {
switch event.name {
case "time-pos":
let newPosition = max(event.double ?? 0, 0)
if Int((floor(newPosition) - floor(position)) / rate) != 0 {
position = newPosition
}
case "pause": isPaused = event.bool ?? false
case "duration": duration = event.double ?? 0
case "speed": rate = event.double ?? 1
case "media-title": title = event.string ?? ""
case "chapter-metadata/title": chapter = event.string
case "metadata/by-key/album": album = event.string
case "metadata/by-key/artist": artist = event.string
case "path": path = event.string
case "track-list": updateCover(tracks: event.array)
default: break
}
}
}