/* * 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 . */ import MediaPlayer extension RemoteCommandCenter { typealias ConfigHandler = (MPRemoteCommandEvent) -> (MPRemoteCommandHandlerStatus) enum KeyType { case normal case repeatable } struct Config { let key: Int32 let type: KeyType var state: UInt32 = 0 let handler: ConfigHandler init(key: Int32, type: KeyType = .normal, handler: @escaping ConfigHandler = { event in return .commandFailed }) { self.key = key self.type = type self.handler = handler } } } class RemoteCommandCenter: NSObject { var nowPlayingInfo: [String:Any] = [:] 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 = 0 { didSet { updateInfoCenter() } } var title: String = "" { didSet { updateInfoCenter() } } var infoCenter: MPNowPlayingInfoCenter { get { return MPNowPlayingInfoCenter.default() } } var commandCenter: MPRemoteCommandCenter { get { return MPRemoteCommandCenter.shared() } } @objc override init() { super.init() nowPlayingInfo = [ MPNowPlayingInfoPropertyMediaType: NSNumber(value: MPNowPlayingInfoMediaType.video.rawValue), MPNowPlayingInfoPropertyPlaybackProgress: NSNumber(value: 0.0), MPMediaItemPropertyAlbumTitle: "", MPMediaItemPropertyArtist: "", ] 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), ] disabledCommands = [ commandCenter.changePlaybackRateCommand, commandCenter.changeRepeatModeCommand, commandCenter.changeShuffleModeCommand, commandCenter.skipForwardCommand, commandCenter.skipBackwardCommand, commandCenter.changePlaybackPositionCommand, commandCenter.enableLanguageOptionCommand, commandCenter.disableLanguageOptionCommand, commandCenter.ratingCommand, commandCenter.likeCommand, commandCenter.dislikeCommand, commandCenter.bookmarkCommand, ] if let app = NSApp as? Application, let icon = app.getMPVIcon() { let albumArt = MPMediaItemArtwork(boundsSize: icon.size) { _ in return icon } nowPlayingInfo[MPMediaItemPropertyArtwork] = albumArt } for cmd in disabledCommands { cmd.isEnabled = false } } @objc func start() { for (cmd, config) in configs { cmd.isEnabled = true cmd.addTarget(handler: config.handler) } updateInfoCenter() NotificationCenter.default.addObserver( self, selector: #selector(self.makeCurrent), name: NSApplication.willBecomeActiveNotification, object: nil ) } @objc 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 ) } @objc func makeCurrent(notification: NSNotification) { infoCenter.playbackState = .paused infoCenter.playbackState = .playing updateInfoCenter() } func updateInfoCenter() { nowPlayingInfo.merge([ MPNowPlayingInfoPropertyPlaybackRate: NSNumber(value: isPaused ? 0 : rate), MPNowPlayingInfoPropertyElapsedPlaybackTime: NSNumber(value: position), MPMediaItemPropertyPlaybackDuration: NSNumber(value: duration), MPMediaItemPropertyTitle: title, ]) { (_, new) in new } infoCenter.nowPlayingInfo = nowPlayingInfo infoCenter.playbackState = isPaused ? .paused : .playing } 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 == MP_KEY_STATE_DOWN ? MP_KEY_STATE_UP : MP_KEY_STATE_DOWN self.configs[event.command]?.state = state } EventsResponder.sharedInstance().handleMPKey(config.key, withMask: Int32(state)) return .success } @objc func processEvent(_ event: UnsafeMutablePointer) { switch event.pointee.event_id { case MPV_EVENT_PROPERTY_CHANGE: handlePropertyChange(event) default: break } } func handlePropertyChange(_ event: UnsafeMutablePointer) { let pData = OpaquePointer(event.pointee.data) guard let property = UnsafePointer(pData)?.pointee else { return } switch String(cString: property.name) { case "pause" where property.format == MPV_FORMAT_FLAG: isPaused = LibmpvHelper.mpvFlagToBool(property.data) ?? false case "time-pos" where property.format == MPV_FORMAT_DOUBLE: let newPosition = max(LibmpvHelper.mpvDoubleToDouble(property.data) ?? 0, 0) if Int((floor(newPosition) - floor(position)) / rate) != 0 { position = newPosition } case "duration" where property.format == MPV_FORMAT_DOUBLE: duration = LibmpvHelper.mpvDoubleToDouble(property.data) ?? 0 case "speed" where property.format == MPV_FORMAT_DOUBLE: rate = LibmpvHelper.mpvDoubleToDouble(property.data) ?? 1 case "media-title" where [MPV_FORMAT_STRING, MPV_FORMAT_NONE].contains(property.format): title = LibmpvHelper.mpvStringArrayToString(property.data) ?? "" default: break } } }