Rewrite sleep timer logic again

This commit is contained in:
ronaldheft 2022-09-02 18:22:42 -04:00
parent dc8852eb0d
commit c14f6ec4c2
3 changed files with 111 additions and 129 deletions

View file

@ -41,8 +41,9 @@ class AudioPlayer: NSObject {
// Sleep timer values // Sleep timer values
internal var sleepTimeChapterStopAt: Double? internal var sleepTimeChapterStopAt: Double?
internal var sleepTimeStopAt: Double? internal var sleepTimeChapterToken: Any?
internal var sleepTimeToken: Any? internal var sleepTimer: Timer?
internal var sleepTimeRemaining: Double?
private var currentTrackIndex = 0 private var currentTrackIndex = 0
private var allPlayerItems:[AVPlayerItem] = [] private var allPlayerItems:[AVPlayerItem] = []
@ -168,8 +169,8 @@ class AudioPlayer: NSObject {
await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: isPlaying, isStopping: false) await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: isPlaying, isStopping: false)
} }
// Update the sleep time, if set if self.isSleepTimerSet() {
if self.sleepTimeStopAt != nil { // Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
} }
} }
@ -264,12 +265,6 @@ class AudioPlayer: NSObject {
return return
} }
// Capture remaining sleep time before changing the track position
let sleepSecondsRemaining = self.getSleepTimeRemaining()
// Stop the paused timer
self.stopPausedTimer()
// Determine where we are starting playback // Determine where we are starting playback
let lastPlayed = (session.updatedAt ?? 0)/1000 let lastPlayed = (session.updatedAt ?? 0)/1000
let currentTime = allowSeekBack ? calculateSeekBackTimeAtCurrentTime(session.currentTime, lastPlayed: lastPlayed) : session.currentTime let currentTime = allowSeekBack ? calculateSeekBackTimeAtCurrentTime(session.currentTime, lastPlayed: lastPlayed) : session.currentTime
@ -282,16 +277,7 @@ class AudioPlayer: NSObject {
let seekTime = currentTime - currentTrackStartOffset let seekTime = currentTime - currentTrackStartOffset
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] completed in self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] completed in
guard completed else { return } guard completed else { return }
guard let self = self else { return } self?.resumePlayback()
// Start playback
self.audioPlayer.play()
self.rate = self.tmpRate
self.audioPlayer.rate = self.tmpRate
self.status = 1
// Update the progress
self.updateNowPlaying()
} }
} }
@ -320,9 +306,24 @@ class AudioPlayer: NSObject {
return currentTime - time return currentTime - time
} }
private func resumePlayback() {
NSLog("PLAY: Resuming playback")
// Stop the paused timer
self.stopPausedTimer()
self.audioPlayer.play()
self.audioPlayer.rate = self.tmpRate
self.status = 1
// Update the progress
self.updateNowPlaying()
}
public func pause() { public func pause() {
guard self.isInitialized() else { return } guard self.isInitialized() else { return }
NSLog("PAUSE: Pausing playback")
self.audioPlayer.pause() self.audioPlayer.pause()
Task { Task {
@ -332,7 +333,6 @@ class AudioPlayer: NSObject {
} }
self.status = 0 self.status = 0
self.rate = 0.0
updateNowPlaying() updateNowPlaying()
@ -342,10 +342,7 @@ class AudioPlayer: NSObject {
public func seek(_ to: Double, from: String) { public func seek(_ to: Double, from: String) {
let continuePlaying = rate > 0.0 let continuePlaying = rate > 0.0
// Capture remaining sleep time before changing the track position or pausing self.pause()
let sleepSecondsRemaining = self.getSleepTimeRemaining()
pause()
NSLog("SEEK: Seek to \(to) from \(from)") NSLog("SEEK: Seek to \(to) from \(from)")
@ -386,22 +383,16 @@ class AudioPlayer: NSObject {
guard completed else { return NSLog("SEEK: WARNING: seeking not completed (to \(seekTime)") } guard completed else { return NSLog("SEEK: WARNING: seeking not completed (to \(seekTime)") }
guard let self = self else { return } guard let self = self else { return }
// Reschedule the sleep timer if continuePlaying {
if let currentTime = self.getCurrentTime() { self.resumePlayback()
self.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining)
} }
if continuePlaying {
self.play()
}
self.updateNowPlaying() self.updateNowPlaying()
} }
} }
} }
public func setPlaybackRate(_ rate: Float, observed: Bool = false) { public func setPlaybackRate(_ rate: Float, observed: Bool = false) {
// Capture remaining sleep time before changing the rate
let sleepSecondsRemaining = self.getSleepTimeRemaining()
let playbackSpeedChanged = rate > 0.0 && rate != self.tmpRate && !(observed && rate == 1) let playbackSpeedChanged = rate > 0.0 && rate != self.tmpRate && !(observed && rate == 1)
if self.audioPlayer.rate != rate { if self.audioPlayer.rate != rate {
@ -415,11 +406,6 @@ class AudioPlayer: NSObject {
if playbackSpeedChanged { if playbackSpeedChanged {
self.tmpRate = rate self.tmpRate = rate
// If we have an active sleep timer, reschedule based on rate
if let currentTime = self.getCurrentTime() {
self.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining)
}
// Setup the time observer again at the new rate // Setup the time observer again at the new rate
self.setupTimeObserver() self.setupTimeObserver()
} }

View file

@ -12,54 +12,59 @@ extension AudioPlayer {
// MARK: - Public API // MARK: - Public API
public func isSleepTimerSet() -> Bool {
return self.isCountdownSleepTimerSet() || self.isChapterSleepTimerSet()
}
public func getSleepTimeRemaining() -> Double? { public func getSleepTimeRemaining() -> Double? {
guard let currentTime = self.getCurrentTime() else { return nil } guard let currentTime = self.getCurrentTime() else { return nil }
// Return the player time until sleep // Return the player time until sleep
var timeUntilSleep: Double? = nil var sleepTimeRemaining: Double? = nil
if let chapterStopAt = self.sleepTimeChapterStopAt { if let chapterStopAt = self.sleepTimeChapterStopAt {
timeUntilSleep = chapterStopAt - currentTime sleepTimeRemaining = chapterStopAt - currentTime
} else if let stopAt = self.sleepTimeStopAt { } else if self.isCountdownSleepTimerSet() {
timeUntilSleep = stopAt - currentTime sleepTimeRemaining = self.sleepTimeRemaining
} }
// Scale the time until sleep based on the playback rate // Guard against invalid sleep timers
if let timeUntilSleep = timeUntilSleep { if sleepTimeRemaining?.isLess(than: 0) ?? false {
let timeUntilSleepScaled = timeUntilSleep / self.getPlaybackRate() self.removeSleepTimer()
guard timeUntilSleepScaled.isNaN == false else { return nil }
return timeUntilSleepScaled.rounded()
} else {
return nil return nil
} }
return sleepTimeRemaining
} }
// Let iOS handle the sleep timer logic by letting us know when it's time to stop public func setSleepTimer(secondsUntilSleep: Double) {
public func setSleepTime(stopAt: Double, scaleBasedOnSpeed: Bool = false) { NSLog("SLEEP TIMER: Sleeping in \(secondsUntilSleep) seconds")
NSLog("SLEEP TIMER: Scheduling for \(stopAt)") self.removeSleepTimer()
self.sleepTimeRemaining = secondsUntilSleep
// Reset any previous sleep timer DispatchQueue.runOnMainQueue {
let isChapterSleepTimer = !scaleBasedOnSpeed self.sleepTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.removeSleepTimer(resetStopAt: !isChapterSleepTimer) if self.isPlaying() {
self.decrementSleepTimerIfRunning()
guard let currentTime = getCurrentTime() else { }
NSLog("SLEEP TIMER: Failed to get currenTime") }
return
} }
// Mark the time to stop playing // Update the UI
let scaledStopAt = self.calculateScaledStopAt(stopAt, currentTime: currentTime, scaleBasedOnSpeed: scaleBasedOnSpeed) NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
self.sleepTimeStopAt = scaledStopAt }
let sleepTime = CMTime(seconds: scaledStopAt, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
public func setChapterSleepTimer(stopAt: Double) {
NSLog("SLEEP TIMER: Scheduling for chapter end \(stopAt)")
self.removeSleepTimer()
// Schedule the observation time // Schedule the observation time
self.sleepTimeChapterStopAt = stopAt
let sleepTime = CMTime(seconds: stopAt, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
var times = [NSValue]() var times = [NSValue]()
times.append(NSValue(time: sleepTime)) times.append(NSValue(time: sleepTime))
self.sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: self.queue) { [weak self] in self.sleepTimeChapterToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: self.queue) { [weak self] in
NSLog("SLEEP TIMER: Pausing audio") self?.handleSleepEnd()
self?.pause()
self?.removeSleepTimer()
} }
// Update the UI // Update the UI
@ -67,56 +72,66 @@ extension AudioPlayer {
} }
public func increaseSleepTime(extraTimeInSeconds: Double) { public func increaseSleepTime(extraTimeInSeconds: Double) {
if let sleepTime = self.getSleepTimeRemaining(), let currentTime = getCurrentTime() { self.removeChapterSleepTimer()
let newSleepTimerPosition = currentTime + sleepTime + extraTimeInSeconds guard let sleepTimeRemaining = self.sleepTimeRemaining else { return }
if newSleepTimerPosition > currentTime { self.sleepTimeRemaining = sleepTimeRemaining + extraTimeInSeconds
self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true)
} // Update the UI
} NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
} }
public func decreaseSleepTime(removeTimeInSeconds: Double) { public func decreaseSleepTime(removeTimeInSeconds: Double) {
if let sleepTime = self.getSleepTimeRemaining(), let currentTime = getCurrentTime() { self.removeChapterSleepTimer()
let newSleepTimerPosition = currentTime + sleepTime - removeTimeInSeconds guard let sleepTimeRemaining = self.sleepTimeRemaining else { return }
if newSleepTimerPosition > currentTime { self.sleepTimeRemaining = sleepTimeRemaining - removeTimeInSeconds
self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true)
}
}
}
public func removeSleepTimer(resetStopAt: Bool = true) {
if resetStopAt {
self.sleepTimeStopAt = nil
self.sleepTimeChapterStopAt = nil
}
if let token = self.sleepTimeToken {
self.audioPlayer.removeTimeObserver(token)
self.sleepTimeToken = nil
}
// Update the UI // Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: self) NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
}
public func removeSleepTimer() {
self.sleepTimer?.invalidate()
self.sleepTimer = nil
self.removeChapterSleepTimer()
self.sleepTimeRemaining = nil
// Update the UI after a delay, to avoid a race condition when changing chapters
DispatchQueue.runOnMainQueue {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in
if !self.isSleepTimerSet() {
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: self)
}
}
}
} }
// MARK: - Internal helpers // MARK: - Internal helpers
internal func rescheduleSleepTimerAtTime(time: Double, secondsRemaining: Double?) { internal func decrementSleepTimerIfRunning() {
guard self.isSleepTimerSet() else { return } if var sleepTimeRemaining = self.sleepTimeRemaining {
sleepTimeRemaining -= 1
self.sleepTimeRemaining = sleepTimeRemaining
// Cancel a chapter sleep timer that is no longer valid // Handle the sleep if the timer has expired
if isChapterSleepTimerBeforeTime(time) { if sleepTimeRemaining <= 0 {
return self.removeSleepTimer() self.handleSleepEnd()
}
} }
}
// Update the sleep timer private func handleSleepEnd() {
if !isChapterSleepTimer() { NSLog("SLEEP TIMER: Pausing audio")
guard let secondsRemaining = secondsRemaining else { return } self.pause()
let newSleepTimerPosition = time + secondsRemaining self.removeSleepTimer()
self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) }
private func removeChapterSleepTimer() {
if let token = self.sleepTimeChapterToken {
self.audioPlayer.removeTimeObserver(token)
} }
self.sleepTimeChapterToken = nil
self.sleepTimeChapterStopAt = nil
} }
private func isChapterSleepTimerBeforeTime(_ time: Double) -> Bool { private func isChapterSleepTimerBeforeTime(_ time: Double) -> Bool {
@ -127,29 +142,12 @@ extension AudioPlayer {
return false return false
} }
private func isSleepTimerSet() -> Bool { private func isCountdownSleepTimerSet() -> Bool {
return self.sleepTimeStopAt != nil return self.sleepTimeRemaining != nil
} }
private func isChapterSleepTimer() -> Bool { private func isChapterSleepTimerSet() -> Bool {
return self.sleepTimeChapterStopAt != nil return self.sleepTimeChapterStopAt != nil
} }
private func getPlaybackRate() -> Double {
// Consider paused as playing at 1x
return Double(self.rate > 0 ? self.rate : 1)
}
private func calculateScaledStopAt(_ stopAt: Double, currentTime: Double, scaleBasedOnSpeed: Bool) -> Double {
if scaleBasedOnSpeed {
// Calculate the scaled time to stop at
let secondsUntilStopAt1x = stopAt - currentTime
let secondsUntilSleep = secondsUntilStopAt1x * self.getPlaybackRate()
NSLog("SLEEP TIMER: Adjusted based on playback speed of \(self.getPlaybackRate()) to \(secondsUntilSleep)")
return currentTime + secondsUntilSleep
} else {
return stopAt
}
}
} }

View file

@ -70,13 +70,11 @@ class PlayerHandler {
} }
public static func setSleepTime(secondsUntilSleep: Double) { public static func setSleepTime(secondsUntilSleep: Double) {
guard let currentTime = self.player?.getCurrentTime() else { return } self.player?.setSleepTimer(secondsUntilSleep: secondsUntilSleep)
let stopAt = secondsUntilSleep + currentTime
self.player?.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: true)
} }
public static func setChapterSleepTime(stopAt: Double) { public static func setChapterSleepTime(stopAt: Double) {
self.player?.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: false) self.player?.setChapterSleepTimer(stopAt: stopAt)
} }
public static func increaseSleepTime(increaseSeconds: Double) { public static func increaseSleepTime(increaseSeconds: Double) {