mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-29 14:47:21 +02:00
Merge pull request #355 from ronaldheft/ios-bug-seek-multi-track
Fixes iOS player seek issues with multi-track books
This commit is contained in:
@@ -194,35 +194,49 @@ class AudioPlayer: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setupQueueItemStatusObserver() {
|
private func setupQueueItemStatusObserver() {
|
||||||
|
NSLog("queueStatusObserver: Setting up")
|
||||||
|
|
||||||
|
// Listen for player item updates
|
||||||
self.queueItemStatusObserver?.invalidate()
|
self.queueItemStatusObserver?.invalidate()
|
||||||
self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { (playerItem, change) in
|
self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { playerItem, change in
|
||||||
guard let playbackSession = self.getPlaybackSession() else {
|
self.handleQueueItemStatus(playerItem: playerItem)
|
||||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (playerItem.status == .readyToPlay) {
|
|
||||||
NSLog("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)")
|
|
||||||
self.updateNowPlaying()
|
|
||||||
|
|
||||||
// Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session
|
|
||||||
let firstReady = self.status < 0
|
|
||||||
if firstReady || self.playWhenReady {
|
|
||||||
self.seek(playbackSession.currentTime, from: "queueItemStatusObserver")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark the player as ready
|
|
||||||
self.status = 0
|
|
||||||
|
|
||||||
// Start the player, if requested
|
|
||||||
if self.playWhenReady {
|
|
||||||
self.playWhenReady = false
|
|
||||||
self.play()
|
|
||||||
}
|
|
||||||
} else if (playerItem.status == .failed) {
|
|
||||||
NSLog("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
|
|
||||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Ensure we didn't miss a player item update during initialization
|
||||||
|
if let playerItem = self.audioPlayer.currentItem {
|
||||||
|
self.handleQueueItemStatus(playerItem: playerItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleQueueItemStatus(playerItem: AVPlayerItem) {
|
||||||
|
NSLog("queueStatusObserver: Current item status changed")
|
||||||
|
guard let playbackSession = self.getPlaybackSession() else {
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (playerItem.status == .readyToPlay) {
|
||||||
|
NSLog("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
|
||||||
|
let firstReady = self.status < 0
|
||||||
|
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
|
||||||
|
// Seeking to the correct position happens during play()
|
||||||
|
self.seek(playbackSession.currentTime, from: "queueItemStatusObserver")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the player as ready
|
||||||
|
self.status = 0
|
||||||
|
|
||||||
|
// Start the player, if requested
|
||||||
|
if self.playWhenReady {
|
||||||
|
self.playWhenReady = false
|
||||||
|
self.play()
|
||||||
|
}
|
||||||
|
} else if (playerItem.status == .failed) {
|
||||||
|
NSLog("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startPausedTimer() {
|
private func startPausedTimer() {
|
||||||
@@ -243,59 +257,67 @@ class AudioPlayer: NSObject {
|
|||||||
// MARK: - Methods
|
// MARK: - Methods
|
||||||
public func play(allowSeekBack: Bool = false) {
|
public func play(allowSeekBack: Bool = false) {
|
||||||
guard self.isInitialized() else { return }
|
guard self.isInitialized() else { return }
|
||||||
|
guard let session = self.getPlaybackSession() else {
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Capture remaining sleep time before changing the track position
|
// Capture remaining sleep time before changing the track position
|
||||||
let sleepSecondsRemaining = PlayerHandler.remainingSleepTime
|
let sleepSecondsRemaining = PlayerHandler.remainingSleepTime
|
||||||
|
|
||||||
if allowSeekBack, let session = self.getPlaybackSession() {
|
// Stop the paused timer
|
||||||
let lastPlayed = (session.updatedAt ?? 0)/1000
|
|
||||||
let difference = Date.timeIntervalSinceReferenceDate - lastPlayed
|
|
||||||
var time: Int?
|
|
||||||
|
|
||||||
if lastPlayed == 0 {
|
|
||||||
time = 5
|
|
||||||
} else if difference < 6 {
|
|
||||||
time = 2
|
|
||||||
} else if difference < 12 {
|
|
||||||
time = 10
|
|
||||||
} else if difference < 30 {
|
|
||||||
time = 15
|
|
||||||
} else if difference < 180 {
|
|
||||||
time = 20
|
|
||||||
} else if difference < 3600 {
|
|
||||||
time = 25
|
|
||||||
} else {
|
|
||||||
time = 29
|
|
||||||
}
|
|
||||||
|
|
||||||
if time != nil {
|
|
||||||
guard let currentTime = self.getCurrentTime() else {
|
|
||||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seek(currentTime - Double(time!), from: "play")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.stopPausedTimer()
|
self.stopPausedTimer()
|
||||||
|
|
||||||
Task {
|
// Determine where we are starting playback
|
||||||
if let currentTime = self.getCurrentTime() {
|
let lastPlayed = (session.updatedAt ?? 0)/1000
|
||||||
await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: self.isPlaying(), isStopping: false)
|
let currentTime = allowSeekBack ? calculateSeekBackTimeAtCurrentTime(session.currentTime, lastPlayed: lastPlayed) : session.currentTime
|
||||||
|
|
||||||
|
// Sync our new playback position
|
||||||
|
Task { await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: self.isPlaying(), isStopping: false) }
|
||||||
|
|
||||||
|
// Start playback, with a seek, for as smooth a scrub bar start as possible
|
||||||
|
let currentTrackStartOffset = session.audioTracks[self.currentTrackIndex].startOffset ?? 0.0
|
||||||
|
let seekTime = currentTime - currentTrackStartOffset
|
||||||
|
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] completed in
|
||||||
|
if completed, let self = self {
|
||||||
|
// Start playback
|
||||||
|
self.audioPlayer.play()
|
||||||
|
self.rate = self.tmpRate
|
||||||
|
self.audioPlayer.rate = self.tmpRate
|
||||||
|
self.status = 1
|
||||||
|
|
||||||
|
// If we have an active sleep timer, reschedule based on rate
|
||||||
|
self.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining)
|
||||||
|
|
||||||
|
// Update the progress
|
||||||
|
self.updateNowPlaying()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.audioPlayer.play()
|
private func calculateSeekBackTimeAtCurrentTime(_ currentTime: Double, lastPlayed: Double) -> Double {
|
||||||
self.status = 1
|
let difference = Date.timeIntervalSinceReferenceDate - lastPlayed
|
||||||
self.rate = self.tmpRate
|
var time: Double = 0
|
||||||
self.audioPlayer.rate = self.tmpRate
|
|
||||||
|
|
||||||
// If we have an active sleep timer, reschedule based on rate
|
// Scale seek back time based on how long since last play
|
||||||
if let currentTime = self.getCurrentTime() {
|
if lastPlayed == 0 {
|
||||||
self.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining)
|
time = 5
|
||||||
|
} else if difference < 6 {
|
||||||
|
time = 2
|
||||||
|
} else if difference < 12 {
|
||||||
|
time = 10
|
||||||
|
} else if difference < 30 {
|
||||||
|
time = 15
|
||||||
|
} else if difference < 180 {
|
||||||
|
time = 20
|
||||||
|
} else if difference < 3600 {
|
||||||
|
time = 25
|
||||||
|
} else {
|
||||||
|
time = 29
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNowPlaying()
|
// Wind the clock back
|
||||||
|
return currentTime - time
|
||||||
}
|
}
|
||||||
|
|
||||||
public func pause() {
|
public func pause() {
|
||||||
|
|||||||
Reference in New Issue
Block a user