Massively improved the MPNowPlayingInfoCenter

This commit is contained in:
Rasmus Krämer
2022-03-06 19:24:07 +01:00
parent 979b74f516
commit 999315ca91
+191 -227
View File
@@ -3,16 +3,6 @@ import Capacitor
import MediaPlayer import MediaPlayer
import AVKit import AVKit
extension UIImage {
func imageWith(newSize: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: newSize)
let image = renderer.image { _ in
self.draw(in: CGRect.init(origin: CGPoint.zero, size: newSize))
}
return image.withRenderingMode(self.renderingMode)
}
}
struct Audiobook { struct Audiobook {
var streamId = "" var streamId = ""
var audiobookId = "" var audiobookId = ""
@@ -29,41 +19,31 @@ struct Audiobook {
@objc(MyNativeAudio) @objc(MyNativeAudio)
public class MyNativeAudio: CAPPlugin { public class MyNativeAudio: CAPPlugin {
var avPlayer: AVPlayer!
var currentCall: CAPPluginCall? var currentCall: CAPPluginCall?
var audioPlayer: AVPlayer! var audioPlayer: AVPlayer!
var audiobook: Audiobook? var audiobook: Audiobook?
// Key-value observing context
private var playerItemContext = 0
private var playerState: PlayerState = .stopped
enum PlayerState { enum PlayerState {
case stopped case stopped
case playing case playing
case paused case paused
} }
private var playerState: PlayerState = .stopped
// Key-value observing context
private var playerItemContext = 0
override public func load() { override public func load() {
NSLog("Load MyNativeAudio") NSLog("Load MyNativeAudio")
NotificationCenter.default.addObserver(self, selector: #selector(stop), NotificationCenter.default.addObserver(self, selector: #selector(stop), name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
name:Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
selector: #selector(appDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(appWillEnterForeground),
name: UIApplication.willEnterForegroundNotification, object: nil)
setupRemoteTransportControls() setupRemoteTransportControls()
} }
@objc func initPlayer(_ call: CAPPluginCall) { @objc func initPlayer(_ call: CAPPluginCall) {
NSLog("Init Player") NSLog("Init Player")
audiobook = Audiobook( audiobook = Audiobook(
streamId: call.getString("id") ?? "", streamId: call.getString("id") ?? "",
audiobookId: call.getString("audiobookId") ?? "", audiobookId: call.getString("audiobookId") ?? "",
title: call.getString("title") ?? "No Title", title: call.getString("title") ?? "No Title",
@@ -94,7 +74,6 @@ public class MyNativeAudio: CAPPlugin {
// For play in background // For play in background
do { do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: [.allowAirPlay]) try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: [.allowAirPlay])
NSLog("[TEST] Playback OK")
try AVAudioSession.sharedInstance().setActive(true) try AVAudioSession.sharedInstance().setActive(true)
NSLog("[TEST] Session is Active") NSLog("[TEST] Session is Active")
} catch { } catch {
@@ -103,17 +82,10 @@ public class MyNativeAudio: CAPPlugin {
} }
let playerItem = AVPlayerItem(asset: asset) let playerItem = AVPlayerItem(asset: asset)
playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: [.old, .new], context: &playerItemContext)
// Register as an observer of the player item's status property
playerItem.addObserver(self,
forKeyPath: #keyPath(AVPlayerItem.status),
options: [.old, .new],
context: &playerItemContext)
self.audioPlayer = AVPlayer(playerItem: playerItem) self.audioPlayer = AVPlayer(playerItem: playerItem)
seek(to: (audiobook?.startTime ?? 0.0) / 1000)
let startTime = CMTime(seconds: (audiobook?.startTime ?? 0.0) / 1000, preferredTimescale: 1000)
self.audioPlayer.seek(to: startTime)
let time = self.audioPlayer.currentItem?.currentTime() let time = self.audioPlayer.currentItem?.currentTime()
print("Audio Player Initialized \(String(describing: time))") print("Audio Player Initialized \(String(describing: time))")
@@ -121,139 +93,10 @@ public class MyNativeAudio: CAPPlugin {
call.resolve(["success": true]) call.resolve(["success": true])
} }
@objc func seekForward(_ call: CAPPluginCall) { public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
let duration = self.audioPlayer.currentItem?.duration.seconds ?? 0
let currentTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
var destinationTime = currentTime + amount
if (destinationTime > duration) { destinationTime = duration }
let time = CMTime(seconds:destinationTime,preferredTimescale: 1000)
self.audioPlayer.seek(to: time)
call.resolve()
}
@objc func seekBackward(_ call: CAPPluginCall) {
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
let currentTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
var destinationTime = currentTime - amount
if (destinationTime < 0) { destinationTime = 0 }
let time = CMTime(seconds:destinationTime,preferredTimescale: 1000)
self.audioPlayer.seek(to: time)
call.resolve()
}
@objc func seekPlayer(_ call: CAPPluginCall) {
var seekTime = (Int(call.getString("timeMs", "0")) ?? 0) / 1000
NSLog("Seek Player \(seekTime)")
if (seekTime < 0) { seekTime = 0 }
let time = CMTime(seconds:Double(seekTime),preferredTimescale: 1000)
self.audioPlayer.seek(to: time)
call.resolve()
}
@objc func pausePlayer(_ call: CAPPluginCall) {
pause()
call.resolve()
}
@objc func playPlayer(_ call: CAPPluginCall) {
play()
call.resolve()
}
@objc func terminateStream(_ call: CAPPluginCall) {
pause()
call.resolve()
}
@objc func stop() {
if let call = currentCall {
currentCall = nil;
call.resolve([ "result": true])
}
}
@objc func getCurrentTime(_ call: CAPPluginCall) {
let currTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
let buffTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
NSLog("AVPlayer getCurrentTime \(currTime)")
call.resolve([ "value": currTime * 1000, "bufferedTime": buffTime * 1000 ])
}
@objc func getStreamSyncData(_ call: CAPPluginCall) {
let streamId = audiobook?.streamId ?? ""
call.resolve([ "isPlaying": false, "lastPauseTime": 0, "id": streamId ])
}
@objc func setPlaybackSpeed(_ call: CAPPluginCall) {
let speed = call.getFloat("speed") ?? 0
NSLog("[TEST] Set Playback Speed \(speed)")
audioPlayer.rate = speed
call.resolve()
}
func play() {
audioPlayer.play()
self.notifyListeners("onPlayingUpdate", data: [
"value": true
])
sendMetadata()
playerState = .playing
updateNowPlaying()
}
func pause() {
audioPlayer.pause()
self.notifyListeners("onPlayingUpdate", data: [
"value": false
])
sendMetadata()
playerState = .paused
}
func currentTime() -> Double {
return self.audioPlayer.currentItem?.currentTime().seconds ?? 0
}
func duration() -> Double {
return self.audioPlayer.currentItem?.duration.seconds ?? 0
}
func playbackRate() -> Float {
return self.audioPlayer.rate
}
func sendMetadata() {
let currTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
let duration = self.audioPlayer.currentItem?.duration.seconds ?? 0
self.notifyListeners("onMetadata", data: [
"duration": duration * 1000,
"currentTime": currTime * 1000,
"stateName": "unknown"
])
}
public override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
// Only handle observations for the playerItemContext // Only handle observations for the playerItemContext
guard context == &playerItemContext else { guard context == &playerItemContext else {
super.observeValue(forKeyPath: keyPath, super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
of: object,
change: change,
context: context)
return return
} }
@@ -268,42 +111,166 @@ public class MyNativeAudio: CAPPlugin {
// Switch over status value // Switch over status value
switch status { switch status {
case .readyToPlay: case .readyToPlay:
// Player item is ready to play. NSLog("AVPlayer ready to play")
NSLog("AVPlayer ready to play")
setNowPlayingMetadata() setNowPlayingMetadata()
sendMetadata() sendMetadata()
if (audiobook?.playWhenReady == true) { if (audiobook?.playWhenReady == true) {
NSLog("AVPlayer playWhenReady == true") NSLog("AVPlayer playWhenReady == true")
play() play()
} }
break break
case .failed: case .failed:
// Player item failed. See error. // Player item failed. See error.
break break
case .unknown: case .unknown:
// Player item is not yet ready // Player item is not yet ready
break break
@unknown default: @unknown default:
break break
} }
} }
} }
@objc func seekForward(_ call: CAPPluginCall) {
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
let destinationTime = getCurrentTime() + amount
seek(to: destinationTime)
call.resolve()
}
@objc func seekBackward(_ call: CAPPluginCall) {
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
let destinationTime = getCurrentTime() - amount
seek(to: destinationTime)
call.resolve()
}
@objc func seekPlayer(_ call: CAPPluginCall) {
let seekTime = (Double(call.getString("timeMs", "0")) ?? 0) / 1000
NSLog("Seek Player \(seekTime)")
seek(to: seekTime)
call.resolve()
}
@objc func pausePlayer(_ call: CAPPluginCall) {
pause()
call.resolve()
}
@objc func playPlayer(_ call: CAPPluginCall) {
play()
call.resolve()
}
@objc func terminateStream(_ call: CAPPluginCall) {
pause()
call.resolve()
}
@objc func stop() {
if let call = currentCall {
currentCall = nil;
call.resolve([ "result": true ])
}
}
@objc func getCurrentTime(_ call: CAPPluginCall) {
let currTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
let buffTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
NSLog("AVPlayer getCurrentTime \(currTime)")
call.resolve([ "value": currTime * 1000, "bufferedTime": buffTime * 1000 ])
}
@objc func getStreamSyncData(_ call: CAPPluginCall) {
let streamId = audiobook?.streamId ?? ""
call.resolve([ "isPlaying": false, "lastPauseTime": 0, "id": streamId ])
}
@objc func setPlaybackSpeed(_ call: CAPPluginCall) {
let speed = call.getFloat("speed") ?? 0
NSLog("[TEST] Set Playback Speed \(speed)")
audioPlayer.rate = speed
call.resolve()
}
func play() {
audioPlayer.play()
playerState = .playing
updateNowPlaying()
sendMetadata()
self.notifyListeners("onPlayingUpdate", data: [
"value": true
])
}
func pause() {
audioPlayer.pause()
playerState = .paused
updateNowPlaying()
sendMetadata()
self.notifyListeners("onPlayingUpdate", data: [
"value": false
])
}
func seek(to: Double) {
var seekTime = to
if seekTime < 0 {
seekTime = 0
} else if seekTime > getDuration() {
seekTime = getDuration()
}
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { finished in
self.updateNowPlaying()
}
}
func getCurrentTime() -> Double {
return self.audioPlayer.currentItem?.currentTime().seconds ?? 0
}
func getDuration() -> Double {
return self.audioPlayer.currentItem?.duration.seconds ?? 0
}
func getPlaybackRate() -> Float {
return self.audioPlayer.rate
}
func sendMetadata() {
self.notifyListeners("onMetadata", data: [
"duration": getDuration() * 1000,
"currentTime": getCurrentTime() * 1000,
"stateName": "unknown"
])
}
@objc func appDidEnterBackground() { @objc func appDidEnterBackground() {
updateNowPlaying() updateNowPlaying()
NSLog("[TEST] App Enter Backround") NSLog("[TEST] App Enter Backround")
} }
@objc func appWillEnterForeground() { @objc func appWillEnterForeground() {
NSLog("[TEST] App Will Enter Foreground") NSLog("[TEST] App Will Enter Foreground")
} }
func getData(from url: URL, completion: @escaping (UIImage?) -> Void) {
URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in
if let data = data {
completion(UIImage(data:data))
}
})
.resume()
}
func shouldFetchCover() -> Bool {
let nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
return nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] as? String != audiobook?.streamId
}
func setupRemoteTransportControls() { func setupRemoteTransportControls() {
// Get the shared MPRemoteCommandCenter
let commandCenter = MPRemoteCommandCenter.shared() let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.isEnabled = true commandCenter.playCommand.isEnabled = true
@@ -324,9 +291,7 @@ public class MyNativeAudio: CAPPlugin {
return .noSuchContent return .noSuchContent
} }
self.audioPlayer.seek(to: CMTime(seconds: currentTime() + command.preferredIntervals[0].doubleValue, preferredTimescale: 1000)) seek(to: getCurrentTime() + command.preferredIntervals[0].doubleValue)
updateNowPlaying()
return .success return .success
} }
commandCenter.skipBackwardCommand.isEnabled = true commandCenter.skipBackwardCommand.isEnabled = true
@@ -336,23 +301,36 @@ public class MyNativeAudio: CAPPlugin {
return .noSuchContent return .noSuchContent
} }
self.audioPlayer.seek(to: CMTime(seconds: currentTime() - command.preferredIntervals[0].doubleValue, preferredTimescale: 1000)) seek(to: getCurrentTime() - command.preferredIntervals[0].doubleValue)
updateNowPlaying() return .success
}
commandCenter.changePlaybackPositionCommand.isEnabled = true
commandCenter.changePlaybackPositionCommand.addTarget { event in
guard let event = event as? MPChangePlaybackPositionCommandEvent else {
return .noSuchContent
}
self.seek(to: event.positionTime)
return .success return .success
} }
} }
func updateNowPlaying() {
NSLog("%@", "**** Set playback info: rate \(getPlaybackRate()), position \(getCurrentTime()), duration \(getDuration())")
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
var nowPlayingInfo = nowPlayingInfoCenter.nowPlayingInfo ?? [String: Any]()
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = getDuration()
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = getCurrentTime()
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = getPlaybackRate()
nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0
nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo
}
func getData(from url: URL, completion: @escaping (UIImage?) -> Void) {
URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in
if let data = data {
completion(UIImage(data:data))
}
})
.resume()
}
func setNowPlayingMetadata() { func setNowPlayingMetadata() {
if (audiobook?.cover != nil) { if audiobook?.cover != nil && shouldFetchCover() {
guard let url = URL(string: audiobook!.cover) else { return } guard let url = URL(string: audiobook!.cover) else { return }
getData(from: url) { [weak self] image in getData(from: url) { [weak self] image in
guard let self = self, guard let self = self,
@@ -370,38 +348,24 @@ public class MyNativeAudio: CAPPlugin {
} }
} }
func setNowPlayingMetadataWithImage(_ artwork: MPMediaItemArtwork?) { func setNowPlayingMetadataWithImage(_ artwork: MPMediaItemArtwork?) {
NSLog("%@", "**** Set track metadata: title \(audiobook?.title ?? "")")
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default() let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
var nowPlayingInfo = [String: Any]() var nowPlayingInfo = [String: Any]()
NSLog("%@", "**** Set track metadata: title \(audiobook?.title ?? "")") if artwork != nil {
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
} else if shouldFetchCover() {
nowPlayingInfo[MPMediaItemPropertyArtwork] = nil
}
nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] = audiobook?.streamId
nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = audiobook?.playlistUrl != nil ? URL(string: audiobook!.playlistUrl) : nil nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = audiobook?.playlistUrl != nil ? URL(string: audiobook!.playlistUrl) : nil
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = "hls" nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = "hls"
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
nowPlayingInfo[MPMediaItemPropertyTitle] = audiobook?.title ?? "" nowPlayingInfo[MPMediaItemPropertyTitle] = audiobook?.title ?? ""
nowPlayingInfo[MPMediaItemPropertyArtist] = audiobook?.author ?? "" nowPlayingInfo[MPMediaItemPropertyArtist] = audiobook?.author ?? ""
if (artwork != nil) {
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
}
nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo
}
func updateNowPlaying() {
if (playerState != .playing) {
NSLog("[TEST] Not current playing so not updating now playing info")
return
}
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
var nowPlayingInfo = nowPlayingInfoCenter.nowPlayingInfo ?? [String: Any]()
NSLog("%@", "**** Set playback info: rate \(playbackRate()), position \(currentTime()), duration \(duration())")
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration()
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime()
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = playbackRate()
nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0
nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo
} }
} }