mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-28 13:58:23 +02:00
Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app
This commit is contained in:
commit
152904b7c7
7 changed files with 73 additions and 63 deletions
|
@ -783,8 +783,6 @@ export default {
|
||||||
|
|
||||||
console.log('received metadata update', data)
|
console.log('received metadata update', data)
|
||||||
|
|
||||||
if (data.currentRate && data.currentRate > 0) this.playbackSpeed = data.currentRate
|
|
||||||
|
|
||||||
this.timeupdate()
|
this.timeupdate()
|
||||||
},
|
},
|
||||||
// When a playback session is started the native android/ios will send the session
|
// When a playback session is started the native android/ios will send the session
|
||||||
|
|
|
@ -159,7 +159,7 @@ public class AbsAudioPlayer: CAPPlugin {
|
||||||
|
|
||||||
@objc func sendMetadata() {
|
@objc func sendMetadata() {
|
||||||
self.notifyListeners("onPlayingUpdate", data: [ "value": !PlayerHandler.paused ])
|
self.notifyListeners("onPlayingUpdate", data: [ "value": !PlayerHandler.paused ])
|
||||||
if let metadata = PlayerHandler.getMetdata() {
|
if let metadata = try? PlayerHandler.getMetdata()?.asDictionary() {
|
||||||
self.notifyListeners("onMetadata", data: metadata)
|
self.notifyListeners("onMetadata", data: metadata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,19 +211,19 @@ public class AbsAudioPlayer: CAPPlugin {
|
||||||
|
|
||||||
@objc func getSleepTimerTime(_ call: CAPPluginCall) {
|
@objc func getSleepTimerTime(_ call: CAPPluginCall) {
|
||||||
call.resolve([
|
call.resolve([
|
||||||
"value": PlayerHandler.getSleepTimeRemaining()
|
"value": PlayerHandler.getSleepTimeRemaining() ?? 0
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func sendSleepTimerEnded() {
|
@objc func sendSleepTimerEnded() {
|
||||||
self.notifyListeners("onSleepTimerEnded", data: [
|
self.notifyListeners("onSleepTimerEnded", data: [
|
||||||
"value": PlayerHandler.getCurrentTime()
|
"value": PlayerHandler.getCurrentTime() ?? 0
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func sendSleepTimerSet() {
|
@objc func sendSleepTimerSet() {
|
||||||
self.notifyListeners("onSleepTimerSet", data: [
|
self.notifyListeners("onSleepTimerSet", data: [
|
||||||
"value": PlayerHandler.getSleepTimeRemaining()
|
"value": PlayerHandler.getSleepTimeRemaining() ?? 0
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class PlaybackMetadata: Codable {
|
struct PlaybackMetadata: Codable {
|
||||||
var duration: Double = 0
|
let duration: Double
|
||||||
var currentTime: Double = 0
|
let currentTime: Double
|
||||||
var playerState: PlayerState = PlayerState.IDLE
|
let playerState: PlayerState
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,9 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum PlayerState: Codable {
|
enum PlayerState: String, Codable {
|
||||||
case IDLE
|
case idle = "IDLE"
|
||||||
case BUFFERING
|
case buffering = "BUFFERING"
|
||||||
case READY
|
case ready = "READY"
|
||||||
case ENDED
|
case ended = "ENDED"
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,21 +10,26 @@ import AVFoundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import MediaPlayer
|
import MediaPlayer
|
||||||
|
|
||||||
enum PlayMethod:Int {
|
enum PlayMethod: Int {
|
||||||
case directplay = 0
|
case directplay = 0
|
||||||
case directstream = 1
|
case directstream = 1
|
||||||
case transcode = 2
|
case transcode = 2
|
||||||
case local = 3
|
case local = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PlayerStatus: Int {
|
||||||
|
case uninitialized = -1
|
||||||
|
case paused = 0
|
||||||
|
case playing = 1
|
||||||
|
}
|
||||||
|
|
||||||
class AudioPlayer: NSObject {
|
class AudioPlayer: NSObject {
|
||||||
internal let queue = DispatchQueue(label: "ABSAudioPlayerQueue")
|
internal let queue = DispatchQueue(label: "ABSAudioPlayerQueue")
|
||||||
internal let logger = AppLogger(category: "AudioPlayer")
|
internal let logger = AppLogger(category: "AudioPlayer")
|
||||||
|
|
||||||
// enums and @objc are not compatible
|
private var status: PlayerStatus
|
||||||
@objc dynamic var status: Int
|
internal var rate: Float
|
||||||
@objc dynamic var rate: Float
|
|
||||||
|
|
||||||
private var tmpRate: Float = 1.0
|
private var tmpRate: Float = 1.0
|
||||||
|
|
||||||
private var playerContext = 0
|
private var playerContext = 0
|
||||||
|
@ -57,9 +62,9 @@ class AudioPlayer: NSObject {
|
||||||
self.playWhenReady = playWhenReady
|
self.playWhenReady = playWhenReady
|
||||||
self.initialPlaybackRate = playbackRate
|
self.initialPlaybackRate = playbackRate
|
||||||
self.audioPlayer = AVQueuePlayer()
|
self.audioPlayer = AVQueuePlayer()
|
||||||
self.audioPlayer.automaticallyWaitsToMinimizeStalling = false
|
self.audioPlayer.automaticallyWaitsToMinimizeStalling = true
|
||||||
self.sessionId = sessionId
|
self.sessionId = sessionId
|
||||||
self.status = -1
|
self.status = .uninitialized
|
||||||
self.rate = 0.0
|
self.rate = 0.0
|
||||||
self.tmpRate = playbackRate
|
self.tmpRate = playbackRate
|
||||||
|
|
||||||
|
@ -141,7 +146,7 @@ class AudioPlayer: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func isInitialized() -> Bool {
|
public func isInitialized() -> Bool {
|
||||||
return self.status != -1
|
return self.status != .uninitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getPlaybackSession() -> PlaybackSession? {
|
public func getPlaybackSession() -> PlaybackSession? {
|
||||||
|
@ -156,6 +161,9 @@ class AudioPlayer: NSObject {
|
||||||
let trackEnd = startOffset + duration
|
let trackEnd = startOffset + duration
|
||||||
if (time < trackEnd.rounded(.down)) {
|
if (time < trackEnd.rounded(.down)) {
|
||||||
return index
|
return index
|
||||||
|
} else if (index == self.allPlayerItems.count - 1) {
|
||||||
|
// Seeking past end of last item
|
||||||
|
return index
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
|
@ -254,7 +262,7 @@ class AudioPlayer: NSObject {
|
||||||
logger.log("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)")
|
logger.log("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)")
|
||||||
|
|
||||||
// Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session
|
// Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session
|
||||||
let firstReady = self.status < 0
|
let firstReady = self.status == .uninitialized
|
||||||
if firstReady && !self.playWhenReady {
|
if firstReady && !self.playWhenReady {
|
||||||
// Seek is async, and if we call this when also pressing play, we will get weird jumps in the scrub bar depending on timing
|
// Seek is async, and if we call this when also pressing play, we will get weird jumps in the scrub bar depending on timing
|
||||||
// Seeking to the correct position happens during play()
|
// Seeking to the correct position happens during play()
|
||||||
|
@ -267,7 +275,7 @@ class AudioPlayer: NSObject {
|
||||||
self.play(allowSeekBack: false, isInitializing: true)
|
self.play(allowSeekBack: false, isInitializing: true)
|
||||||
} else {
|
} else {
|
||||||
// Mark the player as ready
|
// Mark the player as ready
|
||||||
self.status = 0
|
self.status = .paused
|
||||||
}
|
}
|
||||||
} else if (playerItem.status == .failed) {
|
} else if (playerItem.status == .failed) {
|
||||||
logger.error("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
|
logger.error("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
|
||||||
|
@ -327,8 +335,8 @@ class AudioPlayer: NSObject {
|
||||||
self.audioPlayer.play()
|
self.audioPlayer.play()
|
||||||
self.audioPlayer.rate = self.tmpRate
|
self.audioPlayer.rate = self.tmpRate
|
||||||
}
|
}
|
||||||
self.status = 1
|
self.status = .playing
|
||||||
|
|
||||||
// Update the progress
|
// Update the progress
|
||||||
self.updateNowPlaying()
|
self.updateNowPlaying()
|
||||||
|
|
||||||
|
@ -351,27 +359,18 @@ class AudioPlayer: NSObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.status = 0
|
|
||||||
|
|
||||||
|
self.status = .paused
|
||||||
updateNowPlaying()
|
updateNowPlaying()
|
||||||
|
|
||||||
self.startPausedTimer()
|
self.startPausedTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func seek(_ to: Double, from: String) {
|
public func seek(_ to: Double, from: String) {
|
||||||
let continuePlaying = rate > 0.0
|
logger.log("SEEK: Seek to \(to) from \(from)")
|
||||||
|
|
||||||
self.pause()
|
|
||||||
|
|
||||||
logger.log("SEEK: Seek to \(to) from \(from) and continuePlaying(\(continuePlaying)")
|
|
||||||
|
|
||||||
guard let playbackSession = self.getPlaybackSession() else { return }
|
guard let playbackSession = self.getPlaybackSession() else { return }
|
||||||
|
|
||||||
let currentTrack = playbackSession.audioTracks[self.currentTrackIndex]
|
|
||||||
let ctso = currentTrack.startOffset ?? 0.0
|
|
||||||
let trackEnd = ctso + currentTrack.duration
|
|
||||||
logger.log("SEEK: Seek current track END = \(trackEnd)")
|
|
||||||
|
|
||||||
let indexOfSeek = getItemIndexForTime(time: to)
|
let indexOfSeek = getItemIndexForTime(time: to)
|
||||||
logger.log("SEEK: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
|
logger.log("SEEK: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
|
||||||
|
|
||||||
|
@ -383,8 +382,6 @@ class AudioPlayer: NSObject {
|
||||||
playbackSession.currentTime = to
|
playbackSession.currentTime = to
|
||||||
}
|
}
|
||||||
|
|
||||||
self.playWhenReady = continuePlaying // Only playWhenReady if already playing
|
|
||||||
self.status = -1
|
|
||||||
let playerItems = self.allPlayerItems[indexOfSeek..<self.allPlayerItems.count]
|
let playerItems = self.allPlayerItems[indexOfSeek..<self.allPlayerItems.count]
|
||||||
|
|
||||||
DispatchQueue.runOnMainQueue {
|
DispatchQueue.runOnMainQueue {
|
||||||
|
@ -393,26 +390,33 @@ class AudioPlayer: NSObject {
|
||||||
self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
|
self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
seekInCurrentTrack(to: to, playbackSession: playbackSession)
|
||||||
|
|
||||||
setupQueueItemStatusObserver()
|
setupQueueItemStatusObserver()
|
||||||
} else {
|
} else {
|
||||||
logger.log("SEEK: Seeking in current item \(to)")
|
seekInCurrentTrack(to: to, playbackSession: playbackSession)
|
||||||
let currentTrackStartOffset = playbackSession.audioTracks[self.currentTrackIndex].startOffset ?? 0.0
|
}
|
||||||
let seekTime = to - currentTrackStartOffset
|
|
||||||
|
// Only for use in here where we handle track selection
|
||||||
|
func seekInCurrentTrack(to: Double, playbackSession: PlaybackSession) {
|
||||||
|
let currentTrack = playbackSession.audioTracks[self.currentTrackIndex]
|
||||||
|
let ctso = currentTrack.startOffset ?? 0.0
|
||||||
|
let trackEnd = ctso + currentTrack.duration
|
||||||
|
logger.log("SEEK: Seeking in current item \(to) (track START = \(ctso) END = \(trackEnd))")
|
||||||
|
|
||||||
|
let boundedTime = min(max(to, ctso), trackEnd)
|
||||||
|
let seekTime = boundedTime - ctso
|
||||||
|
|
||||||
DispatchQueue.runOnMainQueue {
|
DispatchQueue.runOnMainQueue {
|
||||||
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in
|
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in
|
||||||
self?.logger.log("SEEK: Completion handler called and continuePlaying(\(continuePlaying)")
|
self?.logger.log("SEEK: Completion handler called")
|
||||||
guard completed else {
|
guard completed else {
|
||||||
self?.logger.log("SEEK: WARNING: seeking not completed (to \(seekTime)")
|
self?.logger.log("SEEK: WARNING: seeking not completed (to \(seekTime)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
if continuePlaying {
|
|
||||||
self.resumePlayback()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.updateNowPlaying()
|
self.updateNowPlaying()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -463,9 +467,18 @@ class AudioPlayer: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func isPlaying() -> Bool {
|
public func isPlaying() -> Bool {
|
||||||
return self.status > 0
|
return self.status == .playing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func getPlayerState() -> PlayerState {
|
||||||
|
switch status {
|
||||||
|
case .uninitialized:
|
||||||
|
return PlayerState.buffering
|
||||||
|
case .paused, .playing:
|
||||||
|
return PlayerState.ready
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
private func createAsset(itemId:String, track:AudioTrack) -> AVAsset? {
|
private func createAsset(itemId:String, track:AudioTrack) -> AVAsset? {
|
||||||
guard let playbackSession = self.getPlaybackSession() else { return nil }
|
guard let playbackSession = self.getPlaybackSession() else { return nil }
|
||||||
|
|
|
@ -121,16 +121,15 @@ class PlayerHandler {
|
||||||
player.seek(amount, from: "handler")
|
player.seek(amount, from: "handler")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getMetdata() -> [String: Any]? {
|
public static func getMetdata() -> PlaybackMetadata? {
|
||||||
guard let player = player else { return nil }
|
guard let player = player else { return nil }
|
||||||
guard player.isInitialized() else { return nil }
|
guard player.isInitialized() else { return nil }
|
||||||
|
|
||||||
return [
|
return PlaybackMetadata(
|
||||||
"duration": player.getDuration(),
|
duration: player.getDuration() ?? 0,
|
||||||
"currentTime": player.getCurrentTime(),
|
currentTime: player.getCurrentTime() ?? 0,
|
||||||
"playerState": !paused,
|
playerState: player.getPlayerState()
|
||||||
"currentRate": player.rate,
|
)
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helper logic
|
// MARK: - Helper logic
|
||||||
|
|
|
@ -171,7 +171,7 @@ class ApiClient {
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
ApiClient.postResource(endpoint: endpoint, parameters: parameters, decodable: PlaybackSession.self) { obj in
|
ApiClient.postResource(endpoint: endpoint, parameters: parameters, decodable: PlaybackSession.self) { obj in
|
||||||
var session = obj
|
let session = obj
|
||||||
|
|
||||||
session.serverConnectionConfigId = Store.serverConfig!.id
|
session.serverConnectionConfigId = Store.serverConfig!.id
|
||||||
session.serverAddress = Store.serverConfig!.address
|
session.serverAddress = Store.serverConfig!.address
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue