2024-05-07 04:30:57 -06:00
|
|
|
import Foundation
|
|
|
|
import MediaPlayer
|
|
|
|
|
|
|
|
class NowPlayingInfoCenterManager {
|
|
|
|
static let shared = NowPlayingInfoCenterManager()
|
|
|
|
|
|
|
|
private let SEEK_INTERVAL_SECONDS: Double = 10
|
|
|
|
|
|
|
|
private weak var currentPlayer: AVPlayer?
|
|
|
|
private var players = NSHashTable<AVPlayer>.weakObjects()
|
|
|
|
|
|
|
|
private var observers: [Int: NSKeyValueObservation] = [:]
|
|
|
|
private var playbackObserver: Any?
|
|
|
|
|
|
|
|
private var playTarget: Any?
|
|
|
|
private var pauseTarget: Any?
|
|
|
|
private var skipForwardTarget: Any?
|
|
|
|
private var skipBackwardTarget: Any?
|
|
|
|
private var playbackPositionTarget: Any?
|
|
|
|
private var seekTarget: Any?
|
2024-08-29 04:28:27 -06:00
|
|
|
private var togglePlayPauseTarget: Any?
|
2024-05-07 04:30:57 -06:00
|
|
|
|
2024-07-04 04:11:12 -06:00
|
|
|
private let remoteCommandCenter = MPRemoteCommandCenter.shared()
|
|
|
|
|
2024-05-07 04:30:57 -06:00
|
|
|
private var receivingRemoveControlEvents = false {
|
|
|
|
didSet {
|
|
|
|
if receivingRemoveControlEvents {
|
|
|
|
try? AVAudioSession.sharedInstance().setCategory(.playback)
|
|
|
|
try? AVAudioSession.sharedInstance().setActive(true)
|
|
|
|
UIApplication.shared.beginReceivingRemoteControlEvents()
|
|
|
|
} else {
|
|
|
|
UIApplication.shared.endReceivingRemoteControlEvents()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
deinit {
|
|
|
|
cleanup()
|
|
|
|
}
|
|
|
|
|
|
|
|
func registerPlayer(player: AVPlayer) {
|
|
|
|
if players.contains(player) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if receivingRemoveControlEvents == false {
|
|
|
|
receivingRemoveControlEvents = true
|
|
|
|
}
|
|
|
|
|
|
|
|
if let oldObserver = observers[player.hashValue] {
|
|
|
|
oldObserver.invalidate()
|
|
|
|
}
|
|
|
|
|
|
|
|
observers[player.hashValue] = observePlayers(player: player)
|
|
|
|
players.add(player)
|
|
|
|
|
|
|
|
if currentPlayer == nil {
|
|
|
|
setCurrentPlayer(player: player)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func removePlayer(player: AVPlayer) {
|
|
|
|
if !players.contains(player) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-06-07 05:54:14 -06:00
|
|
|
if let observer = observers[player.hashValue] {
|
2024-05-07 04:30:57 -06:00
|
|
|
observer.invalidate()
|
|
|
|
}
|
|
|
|
|
2024-06-07 05:54:14 -06:00
|
|
|
observers.removeValue(forKey: player.hashValue)
|
2024-05-07 04:30:57 -06:00
|
|
|
players.remove(player)
|
|
|
|
|
|
|
|
if currentPlayer == player {
|
|
|
|
currentPlayer = nil
|
2024-08-02 02:49:52 -06:00
|
|
|
updateNowPlayingInfo()
|
2024-05-07 04:30:57 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
if players.allObjects.isEmpty {
|
|
|
|
cleanup()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public func cleanup() {
|
|
|
|
observers.removeAll()
|
|
|
|
players.removeAllObjects()
|
|
|
|
|
|
|
|
if let playbackObserver {
|
|
|
|
currentPlayer?.removeTimeObserver(playbackObserver)
|
|
|
|
}
|
|
|
|
|
2024-07-04 04:11:12 -06:00
|
|
|
invalidateCommandTargets()
|
2024-05-07 04:30:57 -06:00
|
|
|
|
|
|
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
|
|
|
|
receivingRemoveControlEvents = false
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setCurrentPlayer(player: AVPlayer) {
|
|
|
|
if player == currentPlayer {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if let playbackObserver {
|
|
|
|
currentPlayer?.removeTimeObserver(playbackObserver)
|
|
|
|
}
|
|
|
|
|
|
|
|
currentPlayer = player
|
2024-07-04 04:11:12 -06:00
|
|
|
registerCommandTargets()
|
2024-05-07 04:30:57 -06:00
|
|
|
|
2024-08-02 02:49:52 -06:00
|
|
|
updateNowPlayingInfo()
|
2024-05-07 04:30:57 -06:00
|
|
|
playbackObserver = player.addPeriodicTimeObserver(
|
|
|
|
forInterval: CMTime(value: 1, timescale: 4),
|
|
|
|
queue: .global(),
|
|
|
|
using: { [weak self] _ in
|
2024-08-02 02:49:52 -06:00
|
|
|
self?.updateNowPlayingInfo()
|
2024-05-07 04:30:57 -06:00
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-07-04 04:11:12 -06:00
|
|
|
private func registerCommandTargets() {
|
|
|
|
invalidateCommandTargets()
|
2024-05-07 04:30:57 -06:00
|
|
|
|
2024-07-04 04:11:12 -06:00
|
|
|
playTarget = remoteCommandCenter.playCommand.addTarget { [weak self] _ in
|
2024-05-07 04:30:57 -06:00
|
|
|
guard let self, let player = self.currentPlayer else {
|
|
|
|
return .commandFailed
|
|
|
|
}
|
|
|
|
|
|
|
|
if player.rate == 0 {
|
|
|
|
player.play()
|
|
|
|
}
|
|
|
|
|
|
|
|
return .success
|
|
|
|
}
|
|
|
|
|
2024-07-04 04:11:12 -06:00
|
|
|
pauseTarget = remoteCommandCenter.pauseCommand.addTarget { [weak self] _ in
|
2024-05-07 04:30:57 -06:00
|
|
|
guard let self, let player = self.currentPlayer else {
|
|
|
|
return .commandFailed
|
|
|
|
}
|
|
|
|
|
|
|
|
if player.rate != 0 {
|
|
|
|
player.pause()
|
|
|
|
}
|
|
|
|
|
|
|
|
return .success
|
|
|
|
}
|
|
|
|
|
2024-07-04 04:11:12 -06:00
|
|
|
skipBackwardTarget = remoteCommandCenter.skipBackwardCommand.addTarget { [weak self] _ in
|
2024-05-07 04:30:57 -06:00
|
|
|
guard let self, let player = self.currentPlayer else {
|
|
|
|
return .commandFailed
|
|
|
|
}
|
2024-06-07 06:04:00 -06:00
|
|
|
let newTime = player.currentTime() - CMTime(seconds: self.SEEK_INTERVAL_SECONDS, preferredTimescale: .max)
|
2024-05-07 04:30:57 -06:00
|
|
|
player.seek(to: newTime)
|
|
|
|
return .success
|
|
|
|
}
|
|
|
|
|
2024-07-04 04:11:12 -06:00
|
|
|
skipForwardTarget = remoteCommandCenter.skipForwardCommand.addTarget { [weak self] _ in
|
2024-05-07 04:30:57 -06:00
|
|
|
guard let self, let player = self.currentPlayer else {
|
|
|
|
return .commandFailed
|
|
|
|
}
|
|
|
|
|
2024-06-07 06:04:00 -06:00
|
|
|
let newTime = player.currentTime() + CMTime(seconds: self.SEEK_INTERVAL_SECONDS, preferredTimescale: .max)
|
2024-05-07 04:30:57 -06:00
|
|
|
player.seek(to: newTime)
|
|
|
|
return .success
|
|
|
|
}
|
|
|
|
|
2024-07-04 04:11:12 -06:00
|
|
|
playbackPositionTarget = remoteCommandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
|
2024-05-07 04:30:57 -06:00
|
|
|
guard let self, let player = self.currentPlayer else {
|
|
|
|
return .commandFailed
|
|
|
|
}
|
|
|
|
if let event = event as? MPChangePlaybackPositionCommandEvent {
|
2024-08-12 05:58:40 -06:00
|
|
|
player.seek(to: CMTime(seconds: event.positionTime, preferredTimescale: .max))
|
2024-05-07 04:30:57 -06:00
|
|
|
return .success
|
|
|
|
}
|
|
|
|
return .commandFailed
|
|
|
|
}
|
2024-08-29 04:28:27 -06:00
|
|
|
|
|
|
|
// Handler for togglePlayPauseCommand, sent by Apple's Earpods wired headphones
|
|
|
|
togglePlayPauseTarget = remoteCommandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
|
|
|
|
guard let self, let player = self.currentPlayer else {
|
|
|
|
return .commandFailed
|
|
|
|
}
|
|
|
|
|
|
|
|
if player.rate == 0 {
|
|
|
|
player.play()
|
|
|
|
} else {
|
|
|
|
player.pause()
|
|
|
|
}
|
|
|
|
|
|
|
|
return .success
|
|
|
|
}
|
2024-05-07 04:30:57 -06:00
|
|
|
}
|
|
|
|
|
2024-07-04 04:11:12 -06:00
|
|
|
private func invalidateCommandTargets() {
|
|
|
|
remoteCommandCenter.playCommand.removeTarget(playTarget)
|
|
|
|
remoteCommandCenter.pauseCommand.removeTarget(pauseTarget)
|
|
|
|
remoteCommandCenter.skipForwardCommand.removeTarget(skipForwardTarget)
|
|
|
|
remoteCommandCenter.skipBackwardCommand.removeTarget(skipBackwardTarget)
|
|
|
|
remoteCommandCenter.changePlaybackPositionCommand.removeTarget(playbackPositionTarget)
|
2024-08-29 04:28:27 -06:00
|
|
|
remoteCommandCenter.togglePlayPauseCommand.removeTarget(togglePlayPauseTarget)
|
2024-05-07 04:30:57 -06:00
|
|
|
}
|
|
|
|
|
2024-08-02 02:49:52 -06:00
|
|
|
public func updateNowPlayingInfo() {
|
2024-05-07 04:30:57 -06:00
|
|
|
guard let player = currentPlayer, let currentItem = player.currentItem else {
|
2024-07-04 04:11:12 -06:00
|
|
|
invalidateCommandTargets()
|
2024-05-07 04:30:57 -06:00
|
|
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// commonMetadata is metadata from asset, externalMetadata is custom metadata set by user
|
2024-08-05 03:59:49 -06:00
|
|
|
// externalMetadata should override commonMetadata to allow override metadata from source
|
|
|
|
let metadata = {
|
|
|
|
let common = Dictionary(uniqueKeysWithValues: currentItem.asset.commonMetadata.map { ($0.identifier, $0) })
|
|
|
|
let external = Dictionary(uniqueKeysWithValues: currentItem.externalMetadata.map { ($0.identifier, $0) })
|
|
|
|
return Array((common.merging(external) { _, new in new }).values)
|
|
|
|
}()
|
2024-05-07 04:30:57 -06:00
|
|
|
|
2024-07-04 04:11:12 -06:00
|
|
|
let titleItem = AVMetadataItem.metadataItems(from: metadata, filteredByIdentifier: .commonIdentifierTitle).first?.stringValue ?? ""
|
2024-05-07 04:30:57 -06:00
|
|
|
|
2024-07-04 04:11:12 -06:00
|
|
|
let artistItem = AVMetadataItem.metadataItems(from: metadata, filteredByIdentifier: .commonIdentifierArtist).first?.stringValue ?? ""
|
2024-05-07 04:30:57 -06:00
|
|
|
|
|
|
|
// I have some issue with this - setting artworkItem when it not set dont return nil but also is crashing application
|
|
|
|
// this is very hacky workaround for it
|
2024-07-04 04:11:12 -06:00
|
|
|
let imgData = AVMetadataItem.metadataItems(from: metadata, filteredByIdentifier: .commonIdentifierArtwork).first?.dataValue
|
|
|
|
let image = imgData.flatMap { UIImage(data: $0) } ?? UIImage()
|
|
|
|
let artworkItem = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
|
|
|
|
|
2024-08-02 02:49:52 -06:00
|
|
|
let newNowPlayingInfo: [String: Any] = [
|
2024-07-04 04:11:12 -06:00
|
|
|
MPMediaItemPropertyTitle: titleItem,
|
|
|
|
MPMediaItemPropertyArtist: artistItem,
|
|
|
|
MPMediaItemPropertyArtwork: artworkItem,
|
|
|
|
MPMediaItemPropertyPlaybackDuration: currentItem.duration.seconds,
|
|
|
|
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentItem.currentTime().seconds.rounded(),
|
|
|
|
MPNowPlayingInfoPropertyPlaybackRate: player.rate,
|
|
|
|
MPNowPlayingInfoPropertyIsLiveStream: CMTIME_IS_INDEFINITE(currentItem.asset.duration),
|
|
|
|
]
|
2024-08-02 02:49:52 -06:00
|
|
|
let currentNowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
|
2024-05-07 04:30:57 -06:00
|
|
|
|
2024-08-02 02:49:52 -06:00
|
|
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = currentNowPlayingInfo.merging(newNowPlayingInfo) { _, new in new }
|
2024-05-07 04:30:57 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
private func findNewCurrentPlayer() {
|
|
|
|
if let newPlayer = players.allObjects.first(where: {
|
|
|
|
$0.rate != 0
|
|
|
|
}) {
|
|
|
|
setCurrentPlayer(player: newPlayer)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We will observe players rate to find last active player that info will be displayed
|
|
|
|
private func observePlayers(player: AVPlayer) -> NSKeyValueObservation {
|
|
|
|
return player.observe(\.rate) { [weak self] player, change in
|
|
|
|
guard let self else { return }
|
|
|
|
|
|
|
|
let rate = change.newValue
|
|
|
|
|
|
|
|
// case where there is new player that is not paused
|
|
|
|
// In this case event is triggered by non currentPlayer
|
2024-06-07 06:04:00 -06:00
|
|
|
if rate != 0 && self.currentPlayer != player {
|
|
|
|
self.setCurrentPlayer(player: player)
|
2024-05-07 04:30:57 -06:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// case where currentPlayer was paused
|
|
|
|
// In this case event is triggeret by currentPlayer
|
2024-06-07 06:04:00 -06:00
|
|
|
if rate == 0 && self.currentPlayer == player {
|
|
|
|
self.findNewCurrentPlayer()
|
2024-05-07 04:30:57 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|