mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-07 03:24:57 +02:00
Merge pull request #108 from rasmuslos/master
Move the audio-player to its own file
This commit is contained in:
commit
a42f94e367
3 changed files with 378 additions and 288 deletions
258
ios/App/App/AudioPlayer.swift
Normal file
258
ios/App/App/AudioPlayer.swift
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
//
|
||||||
|
// AudioPlayer.swift
|
||||||
|
// App
|
||||||
|
//
|
||||||
|
// Created by Rasmus Krämer on 07.03.22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AVFoundation
|
||||||
|
import UIKit
|
||||||
|
import MediaPlayer
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
class AudioPlayer: NSObject {
|
||||||
|
// enums and @objc are not compatible
|
||||||
|
@objc dynamic var status: Int
|
||||||
|
@objc dynamic var rate: Float
|
||||||
|
|
||||||
|
private var playerContext = 0
|
||||||
|
private var playerItemContext = 0
|
||||||
|
private var nowPlayingInfo: [String: Any] = [:]
|
||||||
|
|
||||||
|
private var playWhenReady: Bool
|
||||||
|
|
||||||
|
private var audioPlayer: AVPlayer
|
||||||
|
public var audiobook: Audiobook
|
||||||
|
|
||||||
|
init(audiobook: Audiobook, playWhenReady: Bool = false) {
|
||||||
|
self.playWhenReady = playWhenReady
|
||||||
|
self.audiobook = audiobook
|
||||||
|
self.audioPlayer = AVPlayer()
|
||||||
|
self.status = -1
|
||||||
|
self.rate = 0.0
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
initAudioSession()
|
||||||
|
setupRemoteTransportControls()
|
||||||
|
invokeMetadataUpdate()
|
||||||
|
|
||||||
|
// Listen to player events
|
||||||
|
self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: .new, context: &playerContext)
|
||||||
|
self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), options: .new, context: &playerContext)
|
||||||
|
|
||||||
|
let playerItem = AVPlayerItem(asset: createAsset())
|
||||||
|
playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: .new, context: &playerItemContext)
|
||||||
|
|
||||||
|
self.audioPlayer.replaceCurrentItem(with: playerItem)
|
||||||
|
seek(self.audiobook.startTime)
|
||||||
|
|
||||||
|
NSLog("Audioplayer ready")
|
||||||
|
}
|
||||||
|
deinit {
|
||||||
|
destroy()
|
||||||
|
}
|
||||||
|
func destroy() {
|
||||||
|
pause()
|
||||||
|
|
||||||
|
nowPlayingInfo = [:]
|
||||||
|
updateNowPlaying()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Methods
|
||||||
|
public func play() {
|
||||||
|
self.audioPlayer.play()
|
||||||
|
self.status = 1
|
||||||
|
self.rate = 1.0
|
||||||
|
|
||||||
|
updateNowPlaying()
|
||||||
|
}
|
||||||
|
public func pause() {
|
||||||
|
self.audioPlayer.pause()
|
||||||
|
self.status = 0
|
||||||
|
self.rate = 0.0
|
||||||
|
|
||||||
|
updateNowPlaying()
|
||||||
|
}
|
||||||
|
public func seek(_ to: Double) {
|
||||||
|
let continuePlaing = rate > 0.0
|
||||||
|
|
||||||
|
pause()
|
||||||
|
self.audioPlayer.seek(to: CMTime(seconds: to, preferredTimescale: 1000)) { completed in
|
||||||
|
if !completed {
|
||||||
|
NSLog("WARNING: seeking not completed (to \(to)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if continuePlaing {
|
||||||
|
self.play()
|
||||||
|
}
|
||||||
|
self.updateNowPlaying()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func setPlaybackRate(_ rate: Float) {
|
||||||
|
if(self.audioPlayer.rate != rate) {
|
||||||
|
self.audioPlayer.rate = rate
|
||||||
|
}
|
||||||
|
self.rate = rate
|
||||||
|
|
||||||
|
self.updateNowPlaying()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getCurrentTime() -> Double {
|
||||||
|
self.audioPlayer.currentTime().seconds
|
||||||
|
}
|
||||||
|
public func getDuration() -> Double {
|
||||||
|
self.audioPlayer.currentItem?.duration.seconds ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
private func createAsset() -> AVAsset {
|
||||||
|
let headers: [String: String] = [
|
||||||
|
"Authorization": "Bearer \(audiobook.token)"
|
||||||
|
]
|
||||||
|
|
||||||
|
return AVURLAsset(url: URL(string: audiobook.playlistUrl)!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
|
||||||
|
}
|
||||||
|
private func initAudioSession() {
|
||||||
|
do {
|
||||||
|
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: [.allowAirPlay])
|
||||||
|
try AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
} catch {
|
||||||
|
NSLog("Failed to set AVAudioSession category")
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func shouldFetchCover() -> Bool {
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] as? String != audiobook.streamId || nowPlayingInfo[MPMediaItemPropertyArtwork] == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Now playing
|
||||||
|
func setupRemoteTransportControls() {
|
||||||
|
let commandCenter = MPRemoteCommandCenter.shared()
|
||||||
|
|
||||||
|
commandCenter.playCommand.isEnabled = true
|
||||||
|
commandCenter.playCommand.addTarget { [unowned self] event in
|
||||||
|
play()
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
commandCenter.pauseCommand.isEnabled = true
|
||||||
|
commandCenter.pauseCommand.addTarget { [unowned self] event in
|
||||||
|
pause()
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
|
||||||
|
commandCenter.skipForwardCommand.isEnabled = true
|
||||||
|
commandCenter.skipForwardCommand.preferredIntervals = [30]
|
||||||
|
commandCenter.skipForwardCommand.addTarget { [unowned self] event in
|
||||||
|
guard let command = event.command as? MPSkipIntervalCommand else {
|
||||||
|
return .noSuchContent
|
||||||
|
}
|
||||||
|
|
||||||
|
seek(getCurrentTime() + command.preferredIntervals[0].doubleValue)
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
commandCenter.skipBackwardCommand.isEnabled = true
|
||||||
|
commandCenter.skipBackwardCommand.preferredIntervals = [30]
|
||||||
|
commandCenter.skipBackwardCommand.addTarget { [unowned self] event in
|
||||||
|
guard let command = event.command as? MPSkipIntervalCommand else {
|
||||||
|
return .noSuchContent
|
||||||
|
}
|
||||||
|
|
||||||
|
seek(getCurrentTime() - command.preferredIntervals[0].doubleValue)
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
|
||||||
|
commandCenter.changePlaybackPositionCommand.isEnabled = true
|
||||||
|
commandCenter.changePlaybackPositionCommand.addTarget { event in
|
||||||
|
guard let event = event as? MPChangePlaybackPositionCommandEvent else {
|
||||||
|
return .noSuchContent
|
||||||
|
}
|
||||||
|
|
||||||
|
self.seek(event.positionTime)
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func invokeMetadataUpdate() {
|
||||||
|
if !shouldFetchCover() && audiobook.artworkUrl != nil {
|
||||||
|
setMetadata(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let url = URL(string: audiobook.artworkUrl!) else { return }
|
||||||
|
getData(from: url) { [weak self] image in
|
||||||
|
guard let self = self,
|
||||||
|
let downloadedImage = image else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let artwork = MPMediaItemArtwork.init(boundsSize: downloadedImage.size, requestHandler: { _ -> UIImage in
|
||||||
|
return downloadedImage
|
||||||
|
})
|
||||||
|
|
||||||
|
self.setMetadata(artwork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func setMetadata(_ artwork: MPMediaItemArtwork?) {
|
||||||
|
if artwork != nil {
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
|
||||||
|
} else if shouldFetchCover() {
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyArtwork] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] = audiobook.streamId
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = URL(string: audiobook.playlistUrl)
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = "hls"
|
||||||
|
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyTitle] = audiobook.title
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyArtist] = audiobook.author ?? "unknown"
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = audiobook.series
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNowPlaying() {
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = getDuration()
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = getCurrentTime()
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate
|
||||||
|
nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0
|
||||||
|
|
||||||
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Observer
|
||||||
|
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||||
|
if context == &playerItemContext {
|
||||||
|
if keyPath == #keyPath(AVPlayer.status) {
|
||||||
|
guard let playerStatus = AVPlayerItem.Status(rawValue: (change?[.newKey] as? Int ?? -1)) else { return }
|
||||||
|
|
||||||
|
if playerStatus == .readyToPlay {
|
||||||
|
NSLog("pain \(self.audiobook.startTime)")
|
||||||
|
updateNowPlaying()
|
||||||
|
|
||||||
|
self.status = 0
|
||||||
|
if self.playWhenReady {
|
||||||
|
self.playWhenReady = false
|
||||||
|
self.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if context == &playerContext {
|
||||||
|
if keyPath == #keyPath(AVPlayer.rate) {
|
||||||
|
setPlaybackRate(change?[.newKey] as? Float ?? 1.0)
|
||||||
|
} else if keyPath == #keyPath(AVPlayer.currentItem) {
|
||||||
|
NSLog("WARNING: Item ended")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
ios/App/App/Audiobook.swift
Normal file
24
ios/App/App/Audiobook.swift
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
//
|
||||||
|
// AudioBook.swift
|
||||||
|
// App
|
||||||
|
//
|
||||||
|
// Created by Rasmus Krämer on 07.03.22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Audiobook {
|
||||||
|
var streamId: String
|
||||||
|
var audiobookId: String
|
||||||
|
var playlistUrl: String
|
||||||
|
|
||||||
|
var startTime: Double = 0.0
|
||||||
|
var duration: Double
|
||||||
|
|
||||||
|
var title: String
|
||||||
|
var series: String?
|
||||||
|
var author: String?
|
||||||
|
var artworkUrl: String?
|
||||||
|
|
||||||
|
var token: String
|
||||||
|
}
|
|
@ -3,369 +3,177 @@ import Capacitor
|
||||||
import MediaPlayer
|
import MediaPlayer
|
||||||
import AVKit
|
import AVKit
|
||||||
|
|
||||||
struct Audiobook {
|
|
||||||
var streamId = ""
|
|
||||||
var audiobookId = ""
|
|
||||||
var title = "No Title"
|
|
||||||
var author = "Unknown"
|
|
||||||
var playWhenReady = false
|
|
||||||
var startTime = 0.0
|
|
||||||
var cover = ""
|
|
||||||
var duration = 0
|
|
||||||
var series = ""
|
|
||||||
var playlistUrl = ""
|
|
||||||
var token = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc(MyNativeAudio)
|
@objc(MyNativeAudio)
|
||||||
public class MyNativeAudio: CAPPlugin {
|
public class MyNativeAudio: CAPPlugin {
|
||||||
var currentCall: CAPPluginCall?
|
var currentCall: CAPPluginCall?
|
||||||
var audioPlayer: AVPlayer!
|
var currentPlayer: AudioPlayer?
|
||||||
var audiobook: Audiobook?
|
|
||||||
|
|
||||||
// Key-value observing context
|
var playerContext = 0
|
||||||
private var playerItemContext = 0
|
|
||||||
private var playerState: PlayerState = .stopped
|
|
||||||
|
|
||||||
enum PlayerState {
|
|
||||||
case stopped
|
|
||||||
case playing
|
|
||||||
case paused
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func load() {
|
override public func load() {
|
||||||
NSLog("Load MyNativeAudio")
|
NSLog("Load MyNativeAudio")
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(stop), name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
|
// NotificationCenter.default.addObserver(self, selector: #selector(stop), name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
||||||
|
|
||||||
setupRemoteTransportControls()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func initPlayer(_ call: CAPPluginCall) {
|
@objc func initPlayer(_ call: CAPPluginCall) {
|
||||||
NSLog("Init Player")
|
NSLog("Init Player")
|
||||||
audiobook = Audiobook(
|
let audiobook = Audiobook(
|
||||||
streamId: call.getString("id") ?? "",
|
streamId: call.getString("id")!,
|
||||||
audiobookId: call.getString("audiobookId") ?? "",
|
audiobookId: call.getString("audiobookId")!,
|
||||||
|
playlistUrl: call.getString("playlistUrl")!,
|
||||||
|
|
||||||
|
startTime: (Double(call.getString("startTime") ?? "0") ?? 0.0) / 1000,
|
||||||
|
duration: call.getDouble("duration") ?? 0,
|
||||||
|
|
||||||
title: call.getString("title") ?? "No Title",
|
title: call.getString("title") ?? "No Title",
|
||||||
author: call.getString("author") ?? "Unknown",
|
series: call.getString("series"),
|
||||||
playWhenReady: call.getBool("playWhenReady", false),
|
author: call.getString("author"),
|
||||||
startTime: Double(call.getString("startTime") ?? "0") ?? 0.0,
|
artworkUrl: call.getString("cover"),
|
||||||
cover: call.getString("cover") ?? "",
|
|
||||||
duration: call.getInt("duration") ?? 0,
|
|
||||||
series: call.getString("series") ?? "",
|
|
||||||
playlistUrl: call.getString("playlistUrl") ?? "",
|
|
||||||
token: call.getString("token") ?? ""
|
token: call.getString("token") ?? ""
|
||||||
)
|
)
|
||||||
if (audiobook == nil) {
|
|
||||||
|
if self.currentPlayer != nil && self.currentPlayer?.audiobook.streamId == audiobook.streamId {
|
||||||
|
call.resolve(["success": true])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let headers: [String:String] = [
|
|
||||||
"Authorization": "Bearer \(audiobook!.token)"
|
|
||||||
]
|
|
||||||
let url = URL(string:audiobook!.playlistUrl)
|
|
||||||
let asset = AVURLAsset(
|
|
||||||
url: url!,
|
|
||||||
options: ["AVURLAssetHTTPHeaderFieldsKey": headers]
|
|
||||||
)
|
|
||||||
|
|
||||||
print("Playing audiobook url \(String(describing: url))")
|
self.currentPlayer = AudioPlayer(audiobook: audiobook, playWhenReady: call.getBool("playWhenReady", false))
|
||||||
|
self.currentPlayer!.addObserver(self, forKeyPath: #keyPath(AudioPlayer.status), options: .new, context: &playerContext)
|
||||||
// For play in background
|
|
||||||
do {
|
|
||||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: [.allowAirPlay])
|
|
||||||
try AVAudioSession.sharedInstance().setActive(true)
|
|
||||||
NSLog("[TEST] Session is Active")
|
|
||||||
} catch {
|
|
||||||
NSLog("[TEST] Failed to set BG Data")
|
|
||||||
print(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
let playerItem = AVPlayerItem(asset: asset)
|
|
||||||
playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: [.old, .new], context: &playerItemContext)
|
|
||||||
|
|
||||||
self.audioPlayer = AVPlayer(playerItem: playerItem)
|
|
||||||
seek(to: (audiobook?.startTime ?? 0.0) / 1000)
|
|
||||||
|
|
||||||
let time = self.audioPlayer.currentItem?.currentTime()
|
|
||||||
print("Audio Player Initialized \(String(describing: time))")
|
|
||||||
|
|
||||||
call.resolve(["success": true])
|
call.resolve(["success": true])
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||||
// Only handle observations for the playerItemContext
|
guard context == &playerContext else {
|
||||||
guard context == &playerItemContext else {
|
|
||||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyPath == #keyPath(AVPlayerItem.status) {
|
if keyPath == #keyPath(AudioPlayer.status) {
|
||||||
let status: AVPlayerItem.Status
|
sendMetadata()
|
||||||
if let statusNumber = change?[.newKey] as? NSNumber {
|
|
||||||
status = AVPlayerItem.Status(rawValue: statusNumber.intValue)!
|
|
||||||
print("AVPlayer Status Change \(String(status.rawValue))")
|
|
||||||
} else {
|
|
||||||
status = .unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch over status value
|
|
||||||
switch status {
|
|
||||||
case .readyToPlay:
|
|
||||||
NSLog("AVPlayer ready to play")
|
|
||||||
|
|
||||||
setNowPlayingMetadata()
|
|
||||||
sendMetadata()
|
|
||||||
|
|
||||||
if (audiobook?.playWhenReady == true) {
|
|
||||||
NSLog("AVPlayer playWhenReady == true")
|
|
||||||
play()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case .failed:
|
|
||||||
// Player item failed. See error.
|
|
||||||
break
|
|
||||||
case .unknown:
|
|
||||||
// Player item is not yet ready
|
|
||||||
break
|
|
||||||
@unknown default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func seekForward(_ call: CAPPluginCall) {
|
@objc func seekForward(_ call: CAPPluginCall) {
|
||||||
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
|
if self.currentPlayer == nil {
|
||||||
let destinationTime = getCurrentTime() + amount
|
call.resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
seek(to: destinationTime)
|
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
|
||||||
|
let destinationTime = self.currentPlayer!.getCurrentTime() + amount
|
||||||
|
|
||||||
|
self.currentPlayer!.seek(destinationTime)
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
@objc func seekBackward(_ call: CAPPluginCall) {
|
@objc func seekBackward(_ call: CAPPluginCall) {
|
||||||
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
|
if self.currentPlayer == nil {
|
||||||
let destinationTime = getCurrentTime() - amount
|
call.resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
seek(to: destinationTime)
|
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
|
||||||
|
let destinationTime = self.currentPlayer!.getCurrentTime() - amount
|
||||||
|
|
||||||
|
self.currentPlayer!.seek(destinationTime)
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
@objc func seekPlayer(_ call: CAPPluginCall) {
|
@objc func seekPlayer(_ call: CAPPluginCall) {
|
||||||
|
if self.currentPlayer == nil {
|
||||||
|
call.resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let seekTime = (Double(call.getString("timeMs", "0")) ?? 0) / 1000
|
let seekTime = (Double(call.getString("timeMs", "0")) ?? 0) / 1000
|
||||||
NSLog("Seek Player \(seekTime)")
|
NSLog("Seek Player \(seekTime)")
|
||||||
|
|
||||||
seek(to: seekTime)
|
self.currentPlayer!.seek(seekTime)
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func pausePlayer(_ call: CAPPluginCall) {
|
@objc func pausePlayer(_ call: CAPPluginCall) {
|
||||||
pause()
|
if self.currentPlayer == nil {
|
||||||
|
call.resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.currentPlayer!.pause()
|
||||||
|
|
||||||
|
sendPlaybackStatusUpdate(false)
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
@objc func playPlayer(_ call: CAPPluginCall) {
|
@objc func playPlayer(_ call: CAPPluginCall) {
|
||||||
play()
|
if self.currentPlayer == nil {
|
||||||
|
call.resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.currentPlayer!.play()
|
||||||
|
|
||||||
|
sendPlaybackStatusUpdate(true)
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func terminateStream(_ call: CAPPluginCall) {
|
@objc func terminateStream(_ call: CAPPluginCall) {
|
||||||
pause()
|
stop()
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
@objc func stop() {
|
@objc func stop() {
|
||||||
if let call = currentCall {
|
if let call = currentCall {
|
||||||
|
if self.currentPlayer != nil {
|
||||||
|
self.currentPlayer!.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.currentPlayer = nil
|
||||||
currentCall = nil;
|
currentCall = nil;
|
||||||
call.resolve([ "result": true ])
|
call.resolve([ "result": true ])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func getCurrentTime(_ call: CAPPluginCall) {
|
@objc func getCurrentTime(_ call: CAPPluginCall) {
|
||||||
let currTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
if self.currentPlayer == nil {
|
||||||
let buffTime = self.audioPlayer.currentItem?.currentTime().seconds ?? 0
|
call.resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
NSLog("AVPlayer getCurrentTime \(currTime)")
|
let currentTime = self.currentPlayer?.getCurrentTime() ?? 0
|
||||||
call.resolve([ "value": currTime * 1000, "bufferedTime": buffTime * 1000 ])
|
call.resolve([ "value": currentTime * 1000, "bufferedTime": currentTime * 1000 ])
|
||||||
}
|
}
|
||||||
@objc func getStreamSyncData(_ call: CAPPluginCall) {
|
@objc func getStreamSyncData(_ call: CAPPluginCall) {
|
||||||
let streamId = audiobook?.streamId ?? ""
|
if self.currentPlayer == nil {
|
||||||
call.resolve([ "isPlaying": false, "lastPauseTime": 0, "id": streamId ])
|
call.resolve([ "isPlaying": false as Any, "lastPauseTime": 0, "id": nil ])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
call.resolve([ "isPlaying": self.currentPlayer!.rate > 0.0, "lastPauseTime": 0, "id": self.currentPlayer?.audiobook.streamId as Any ])
|
||||||
}
|
}
|
||||||
@objc func setPlaybackSpeed(_ call: CAPPluginCall) {
|
@objc func setPlaybackSpeed(_ call: CAPPluginCall) {
|
||||||
|
if self.currentPlayer == nil {
|
||||||
|
call.resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let speed = call.getFloat("speed") ?? 0
|
let speed = call.getFloat("speed") ?? 0
|
||||||
NSLog("[TEST] Set Playback Speed \(speed)")
|
self.currentPlayer!.setPlaybackRate(speed)
|
||||||
audioPlayer.rate = speed
|
|
||||||
|
|
||||||
call.resolve()
|
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() {
|
func sendMetadata() {
|
||||||
|
if self.currentPlayer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
self.notifyListeners("onMetadata", data: [
|
self.notifyListeners("onMetadata", data: [
|
||||||
"duration": getDuration() * 1000,
|
"duration": self.currentPlayer!.getDuration() * 1000,
|
||||||
"currentTime": getCurrentTime() * 1000,
|
"currentTime": self.currentPlayer!.getCurrentTime() * 1000,
|
||||||
"stateName": "unknown"
|
"stateName": "unknown"
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
func sendPlaybackStatusUpdate(_ playing: Bool) {
|
||||||
@objc func appDidEnterBackground() {
|
self.notifyListeners("onPlayingUpdate", data: [
|
||||||
updateNowPlaying()
|
"value": playing
|
||||||
NSLog("[TEST] App Enter Backround")
|
])
|
||||||
}
|
|
||||||
@objc func appWillEnterForeground() {
|
|
||||||
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() {
|
|
||||||
let commandCenter = MPRemoteCommandCenter.shared()
|
|
||||||
|
|
||||||
commandCenter.playCommand.isEnabled = true
|
|
||||||
commandCenter.playCommand.addTarget { [unowned self] event in
|
|
||||||
play()
|
|
||||||
return .success
|
|
||||||
}
|
|
||||||
commandCenter.pauseCommand.isEnabled = true
|
|
||||||
commandCenter.pauseCommand.addTarget { [unowned self] event in
|
|
||||||
pause()
|
|
||||||
return .success
|
|
||||||
}
|
|
||||||
|
|
||||||
commandCenter.skipForwardCommand.isEnabled = true
|
|
||||||
commandCenter.skipForwardCommand.preferredIntervals = [30]
|
|
||||||
commandCenter.skipForwardCommand.addTarget { [unowned self] event in
|
|
||||||
guard let command = event.command as? MPSkipIntervalCommand else {
|
|
||||||
return .noSuchContent
|
|
||||||
}
|
|
||||||
|
|
||||||
seek(to: getCurrentTime() + command.preferredIntervals[0].doubleValue)
|
|
||||||
return .success
|
|
||||||
}
|
|
||||||
commandCenter.skipBackwardCommand.isEnabled = true
|
|
||||||
commandCenter.skipBackwardCommand.preferredIntervals = [30]
|
|
||||||
commandCenter.skipBackwardCommand.addTarget { [unowned self] event in
|
|
||||||
guard let command = event.command as? MPSkipIntervalCommand else {
|
|
||||||
return .noSuchContent
|
|
||||||
}
|
|
||||||
|
|
||||||
seek(to: getCurrentTime() - command.preferredIntervals[0].doubleValue)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 setNowPlayingMetadata() {
|
|
||||||
if audiobook?.cover != nil && shouldFetchCover() {
|
|
||||||
guard let url = URL(string: audiobook!.cover) else { return }
|
|
||||||
getData(from: url) { [weak self] image in
|
|
||||||
guard let self = self,
|
|
||||||
let downloadedImage = image else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let artwork = MPMediaItemArtwork.init(boundsSize: downloadedImage.size, requestHandler: { _ -> UIImage in
|
|
||||||
return downloadedImage
|
|
||||||
})
|
|
||||||
|
|
||||||
self.setNowPlayingMetadataWithImage(artwork)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setNowPlayingMetadataWithImage(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func setNowPlayingMetadataWithImage(_ artwork: MPMediaItemArtwork?) {
|
|
||||||
NSLog("%@", "**** Set track metadata: title \(audiobook?.title ?? "")")
|
|
||||||
|
|
||||||
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
|
|
||||||
var nowPlayingInfo = [String: Any]()
|
|
||||||
|
|
||||||
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[MPNowPlayingInfoPropertyMediaType] = "hls"
|
|
||||||
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
|
|
||||||
nowPlayingInfo[MPMediaItemPropertyTitle] = audiobook?.title ?? ""
|
|
||||||
nowPlayingInfo[MPMediaItemPropertyArtist] = audiobook?.author ?? ""
|
|
||||||
|
|
||||||
nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue