- Changes icon
- Improved state sync - Moved player to shared
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon-40.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-58.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-80.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-20.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-29.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-58.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-80.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-76.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-152.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-167.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-1024.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-120.png",
|
||||
"idiom" : "car",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-180.png",
|
||||
"idiom" : "car",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-48.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "notificationCenter",
|
||||
"scale" : "2x",
|
||||
"size" : "24x24",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-55.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "notificationCenter",
|
||||
"scale" : "2x",
|
||||
"size" : "27.5x27.5",
|
||||
"subtype" : "42mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-58.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "companionSettings",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-87.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "companionSettings",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "notificationCenter",
|
||||
"scale" : "2x",
|
||||
"size" : "33x33",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-80.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-88.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "44x44",
|
||||
"subtype" : "40mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "46x46",
|
||||
"subtype" : "41mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-100.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "50x50",
|
||||
"subtype" : "44mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "51x51",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-172.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "86x86",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-196.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "98x98",
|
||||
"subtype" : "42mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-216.png",
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "108x108",
|
||||
"subtype" : "44mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "117x117",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-1024.png",
|
||||
"idiom" : "watch-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-64.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "icon-1024.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 605 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 743 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 927 B |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>audiobookshelf-app</string>
|
||||
<string>Audiobookshelf</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -27,6 +27,10 @@
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INPlayMediaIntent</string>
|
||||
</array>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
//
|
||||
// AudioPlayer.swift
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 07.03.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
import MediaPlayer
|
||||
|
||||
class AudioPlayer: NSObject {
|
||||
// enums and @objc are not compatible
|
||||
@objc dynamic var status: Int
|
||||
@objc dynamic var rate: Float
|
||||
|
||||
private var tmpRate: Float = 1.0
|
||||
private var lastPlayTime: Double = 0.0
|
||||
|
||||
private var playerContext = 0
|
||||
private var playerItemContext = 0
|
||||
|
||||
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()
|
||||
NowPlayingInfo.setAudiobook(audiobook: audiobook)
|
||||
|
||||
// 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()
|
||||
audioPlayer.replaceCurrentItem(with: nil)
|
||||
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setActive(false)
|
||||
} catch {
|
||||
NSLog("Failed to set AVAudioSession inactive")
|
||||
print(error)
|
||||
}
|
||||
|
||||
DispatchQueue.main.sync {
|
||||
UIApplication.shared.endReceivingRemoteControlEvents()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
public func play(allowSeekBack: Bool = false) {
|
||||
if allowSeekBack {
|
||||
let diffrence = Date.timeIntervalSinceReferenceDate - lastPlayTime
|
||||
var time: Int?
|
||||
|
||||
if lastPlayTime == 0 {
|
||||
time = 5
|
||||
} else if diffrence < 6 {
|
||||
time = 2
|
||||
} else if diffrence < 12 {
|
||||
time = 10
|
||||
} else if diffrence < 30 {
|
||||
time = 15
|
||||
} else if diffrence < 180 {
|
||||
time = 20
|
||||
} else if diffrence < 3600 {
|
||||
time = 25
|
||||
} else {
|
||||
time = 29
|
||||
}
|
||||
|
||||
if time != nil {
|
||||
seek(getCurrentTime() - Double(time!))
|
||||
}
|
||||
}
|
||||
lastPlayTime = Date.timeIntervalSinceReferenceDate
|
||||
|
||||
self.audioPlayer.play()
|
||||
self.status = 1
|
||||
self.rate = self.tmpRate
|
||||
self.audioPlayer.rate = self.tmpRate
|
||||
|
||||
updateNowPlaying()
|
||||
}
|
||||
public func pause() {
|
||||
self.audioPlayer.pause()
|
||||
self.status = 0
|
||||
self.rate = 0.0
|
||||
|
||||
updateNowPlaying()
|
||||
lastPlayTime = Date.timeIntervalSinceReferenceDate
|
||||
}
|
||||
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, observed: Bool = false) {
|
||||
if self.audioPlayer.rate != rate {
|
||||
self.audioPlayer.rate = rate
|
||||
}
|
||||
if rate > 0.0 && !(observed && rate == 1) {
|
||||
self.tmpRate = 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Now playing
|
||||
func setupRemoteTransportControls() {
|
||||
DispatchQueue.main.sync {
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
}
|
||||
let commandCenter = MPRemoteCommandCenter.shared()
|
||||
|
||||
commandCenter.playCommand.isEnabled = true
|
||||
commandCenter.playCommand.addTarget { [unowned self] event in
|
||||
play(allowSeekBack: true)
|
||||
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
|
||||
}
|
||||
|
||||
commandCenter.changePlaybackRateCommand.isEnabled = true
|
||||
commandCenter.changePlaybackRateCommand.supportedPlaybackRates = [0.5, 0.75, 1.0, 1.25, 1.5, 2]
|
||||
commandCenter.changePlaybackRateCommand.addTarget { event in
|
||||
guard let event = event as? MPChangePlaybackRateCommandEvent else {
|
||||
return .noSuchContent
|
||||
}
|
||||
|
||||
self.setPlaybackRate(event.playbackRate)
|
||||
return .success
|
||||
}
|
||||
}
|
||||
|
||||
func updateNowPlaying() {
|
||||
NowPlayingInfo.update(duration: getDuration(), currentTime: getCurrentTime(), rate: rate)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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, observed: true)
|
||||
} else if keyPath == #keyPath(AVPlayer.currentItem) {
|
||||
NSLog("WARNING: Item ended")
|
||||
}
|
||||
} else {
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,6 @@ func parseSleepTime(millis: String?) -> Double {
|
||||
|
||||
@objc(MyNativeAudio)
|
||||
public class MyNativeAudio: CAPPlugin {
|
||||
var currentPlayer: AudioPlayer?
|
||||
|
||||
var playerContext = 0
|
||||
|
||||
var currentSleepTimer: Timer? = nil
|
||||
@@ -35,60 +33,67 @@ public class MyNativeAudio: CAPPlugin {
|
||||
)
|
||||
let playWhenReady = call.getBool("playWhenReady", false)
|
||||
|
||||
if currentPlayer != nil && currentPlayer?.audiobook.streamId == audiobook.streamId {
|
||||
if AudioPlayer.instance != nil && AudioPlayer.instance?.audiobook.streamId == audiobook.streamId {
|
||||
if playWhenReady {
|
||||
self.currentPlayer?.play()
|
||||
AudioPlayer.instance?.play()
|
||||
}
|
||||
|
||||
call.resolve(["success": true])
|
||||
return
|
||||
} else if currentPlayer != nil && currentPlayer?.audiobook.streamId != audiobook.streamId {
|
||||
} else if AudioPlayer.instance != nil && AudioPlayer.instance?.audiobook.streamId != audiobook.streamId {
|
||||
stop()
|
||||
}
|
||||
|
||||
currentPlayer = AudioPlayer(audiobook: audiobook, playWhenReady: playWhenReady)
|
||||
currentPlayer!.addObserver(self, forKeyPath: #keyPath(AudioPlayer.status), options: .new, context: &playerContext)
|
||||
AudioPlayer.instance = AudioPlayer(audiobook: audiobook, playWhenReady: playWhenReady)
|
||||
AudioPlayer.instance!.addObserver(self, forKeyPath: #keyPath(AudioPlayer.status), options: .new, context: &playerContext)
|
||||
|
||||
call.resolve(["success": true])
|
||||
}
|
||||
override public func load() {
|
||||
NSLog("Load MyNativeAudio")
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||
}
|
||||
|
||||
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
guard context == &playerContext else {
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
return
|
||||
}
|
||||
|
||||
NSLog("AudioPlayer state change: \(String(describing: keyPath))")
|
||||
|
||||
if keyPath == #keyPath(AudioPlayer.status) {
|
||||
if keyPath == #keyPath(AudioPlayer.status) || keyPath == #keyPath(AudioPlayer.rate) {
|
||||
sendMetadata()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func seekForward(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
if AudioPlayer.instance == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
|
||||
let destinationTime = self.currentPlayer!.getCurrentTime() + amount
|
||||
let destinationTime = AudioPlayer.instance!.getCurrentTime() + amount
|
||||
|
||||
self.currentPlayer!.seek(destinationTime)
|
||||
AudioPlayer.instance!.seek(destinationTime)
|
||||
call.resolve()
|
||||
}
|
||||
@objc func seekBackward(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
if AudioPlayer.instance == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let amount = (Double(call.getString("amount", "0")) ?? 0) / 1000
|
||||
let destinationTime = self.currentPlayer!.getCurrentTime() - amount
|
||||
let destinationTime = AudioPlayer.instance!.getCurrentTime() - amount
|
||||
|
||||
self.currentPlayer!.seek(destinationTime)
|
||||
AudioPlayer.instance!.seek(destinationTime)
|
||||
call.resolve()
|
||||
}
|
||||
@objc func seekPlayer(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
if AudioPlayer.instance == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
@@ -96,28 +101,28 @@ public class MyNativeAudio: CAPPlugin {
|
||||
let seekTime = (Double(call.getString("timeMs", "0")) ?? 0) / 1000
|
||||
NSLog("Seek Player \(seekTime)")
|
||||
|
||||
self.currentPlayer!.seek(seekTime)
|
||||
AudioPlayer.instance!.seek(seekTime)
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func pausePlayer(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
if AudioPlayer.instance == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
self.currentPlayer!.pause()
|
||||
AudioPlayer.instance!.pause()
|
||||
|
||||
sendPlaybackStatusUpdate(false)
|
||||
call.resolve()
|
||||
}
|
||||
@objc func playPlayer(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
if AudioPlayer.instance == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
self.currentPlayer!.play(allowSeekBack: true)
|
||||
AudioPlayer.instance!.play(allowSeekBack: true)
|
||||
|
||||
sendPlaybackStatusUpdate(true)
|
||||
call.resolve()
|
||||
@@ -128,10 +133,10 @@ public class MyNativeAudio: CAPPlugin {
|
||||
call.resolve()
|
||||
}
|
||||
@objc func stop(_ call: CAPPluginCall? = nil) {
|
||||
if self.currentPlayer != nil {
|
||||
self.currentPlayer!.destroy()
|
||||
if AudioPlayer.instance != nil {
|
||||
AudioPlayer.instance!.destroy()
|
||||
}
|
||||
self.currentPlayer = nil
|
||||
AudioPlayer.instance = nil
|
||||
|
||||
if call != nil {
|
||||
call!.resolve([ "result": true ])
|
||||
@@ -139,37 +144,37 @@ public class MyNativeAudio: CAPPlugin {
|
||||
}
|
||||
|
||||
@objc func getCurrentTime(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
if AudioPlayer.instance == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let currentTime = self.currentPlayer?.getCurrentTime() ?? 0
|
||||
let currentTime = AudioPlayer.instance?.getCurrentTime() ?? 0
|
||||
call.resolve([ "value": currentTime * 1000, "bufferedTime": currentTime * 1000 ])
|
||||
}
|
||||
@objc func getStreamSyncData(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
if AudioPlayer.instance == nil {
|
||||
call.resolve([ "isPlaying": false, "lastPauseTime": 0, "id": nil ])
|
||||
return
|
||||
}
|
||||
|
||||
call.resolve([ "isPlaying": self.currentPlayer!.rate > 0.0, "lastPauseTime": 0, "id": self.currentPlayer?.audiobook.streamId as Any ])
|
||||
call.resolve([ "isPlaying": AudioPlayer.instance!.rate > 0.0, "lastPauseTime": 0, "id": AudioPlayer.instance?.audiobook.streamId as Any ])
|
||||
}
|
||||
|
||||
@objc func setPlaybackSpeed(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
if AudioPlayer.instance == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let speed = call.getFloat("speed") ?? 0
|
||||
self.currentPlayer!.setPlaybackRate(speed)
|
||||
AudioPlayer.instance!.setPlaybackRate(speed)
|
||||
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func setSleepTimer(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
if AudioPlayer.instance == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
@@ -180,14 +185,14 @@ public class MyNativeAudio: CAPPlugin {
|
||||
call.resolve([ "success": true ])
|
||||
}
|
||||
@objc func increaseSleepTime(_ call: CAPPluginCall) {
|
||||
if self.currentPlayer == nil {
|
||||
if AudioPlayer.instance == nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
var time = self.remainingSleepDuration + parseSleepTime(millis: call.getString("time"))
|
||||
if time > self.currentPlayer!.getDuration() {
|
||||
time = self.currentPlayer!.getDuration()
|
||||
if time > AudioPlayer.instance!.getDuration() {
|
||||
time = AudioPlayer.instance!.getDuration()
|
||||
}
|
||||
|
||||
setSleepTimer(seconds: time)
|
||||
@@ -217,7 +222,7 @@ public class MyNativeAudio: CAPPlugin {
|
||||
}
|
||||
|
||||
func setSleepTimer(seconds: Double) {
|
||||
if currentPlayer == nil {
|
||||
if AudioPlayer.instance == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -239,7 +244,7 @@ public class MyNativeAudio: CAPPlugin {
|
||||
}
|
||||
}
|
||||
func updateSleepTime() {
|
||||
if currentPlayer == nil {
|
||||
if AudioPlayer.instance == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -248,10 +253,10 @@ public class MyNativeAudio: CAPPlugin {
|
||||
currentSleepTimer!.invalidate()
|
||||
}
|
||||
self.notifyListeners("onSleepTimerEnded", data: [
|
||||
"value": currentPlayer!.getCurrentTime(),
|
||||
"value": AudioPlayer.instance!.getCurrentTime(),
|
||||
])
|
||||
|
||||
currentPlayer!.pause()
|
||||
AudioPlayer.instance!.pause()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -261,16 +266,21 @@ public class MyNativeAudio: CAPPlugin {
|
||||
])
|
||||
}
|
||||
|
||||
func sendMetadata() {
|
||||
if self.currentPlayer == nil {
|
||||
@objc func sendMetadata() {
|
||||
if AudioPlayer.instance == nil {
|
||||
return
|
||||
}
|
||||
|
||||
NSLog("fired metadata update")
|
||||
|
||||
self.notifyListeners("onMetadata", data: [
|
||||
"duration": self.currentPlayer!.getDuration() * 1000,
|
||||
"currentTime": self.currentPlayer!.getCurrentTime() * 1000,
|
||||
"stateName": "unknown"
|
||||
"duration": AudioPlayer.instance!.getDuration() * 1000,
|
||||
"currentTime": AudioPlayer.instance!.getCurrentTime() * 1000,
|
||||
"stateName": "unknown",
|
||||
|
||||
"currentRate": AudioPlayer.instance!.rate
|
||||
])
|
||||
sendPlaybackStatusUpdate(AudioPlayer.instance!.rate != 0.0)
|
||||
}
|
||||
func sendPlaybackStatusUpdate(_ playing: Bool) {
|
||||
self.notifyListeners("onPlayingUpdate", data: [
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
//
|
||||
// NowPlaying.swift
|
||||
// App
|
||||
//
|
||||
// Created by Rasmus Krämer on 22.03.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
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 NowPlayingInfo {
|
||||
private static var nowPlayingInfo: [String: Any] = [:]
|
||||
private static var audiobook: Audiobook?
|
||||
|
||||
public static func setAudiobook(audiobook: Audiobook) {
|
||||
self.audiobook = audiobook
|
||||
setMetadata(nil)
|
||||
|
||||
if !shouldFetchCover() || audiobook.artworkUrl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = URL(string: audiobook.artworkUrl!) else { return }
|
||||
getData(from: url) { [self] image in
|
||||
guard let downloadedImage = image else {
|
||||
return
|
||||
}
|
||||
let artwork = MPMediaItemArtwork.init(boundsSize: downloadedImage.size, requestHandler: { _ -> UIImage in
|
||||
return downloadedImage
|
||||
})
|
||||
|
||||
self.setMetadata(artwork)
|
||||
}
|
||||
}
|
||||
public static func update(duration: Double, currentTime: Double, rate: Float) {
|
||||
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0
|
||||
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||
}
|
||||
public static func reset() {
|
||||
audiobook = nil
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
||||
}
|
||||
|
||||
private static func setMetadata(_ artwork: MPMediaItemArtwork?) {
|
||||
if self.audiobook == nil {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
private static func shouldFetchCover() -> Bool {
|
||||
audiobook != nil && (nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] as? String != audiobook!.streamId || nowPlayingInfo[MPMediaItemPropertyArtwork] == nil)
|
||||
}
|
||||
}
|
||||