diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index b91c66ce..78b66bd9 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -41,8 +41,9 @@ class AudioPlayer: NSObject { // Sleep timer values internal var sleepTimeChapterStopAt: Double? - internal var sleepTimeStopAt: Double? - internal var sleepTimeToken: Any? + internal var sleepTimeChapterToken: Any? + internal var sleepTimer: Timer? + internal var sleepTimeRemaining: Double? private var currentTrackIndex = 0 private var allPlayerItems:[AVPlayerItem] = [] @@ -168,8 +169,8 @@ class AudioPlayer: NSObject { await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: isPlaying, isStopping: false) } - // Update the sleep time, if set - if self.sleepTimeStopAt != nil { + if self.isSleepTimerSet() { + // Update the UI NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) } } @@ -264,12 +265,6 @@ class AudioPlayer: NSObject { 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 let lastPlayed = (session.updatedAt ?? 0)/1000 let currentTime = allowSeekBack ? calculateSeekBackTimeAtCurrentTime(session.currentTime, lastPlayed: lastPlayed) : session.currentTime @@ -282,16 +277,7 @@ class AudioPlayer: NSObject { let seekTime = currentTime - currentTrackStartOffset self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] completed in guard completed else { return } - guard let self = self else { return } - - // Start playback - self.audioPlayer.play() - self.rate = self.tmpRate - self.audioPlayer.rate = self.tmpRate - self.status = 1 - - // Update the progress - self.updateNowPlaying() + self?.resumePlayback() } } @@ -320,9 +306,24 @@ class AudioPlayer: NSObject { 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() { guard self.isInitialized() else { return } + NSLog("PAUSE: Pausing playback") self.audioPlayer.pause() Task { @@ -332,7 +333,6 @@ class AudioPlayer: NSObject { } self.status = 0 - self.rate = 0.0 updateNowPlaying() @@ -342,10 +342,7 @@ class AudioPlayer: NSObject { public func seek(_ to: Double, from: String) { let continuePlaying = rate > 0.0 - // Capture remaining sleep time before changing the track position or pausing - let sleepSecondsRemaining = self.getSleepTimeRemaining() - - pause() + self.pause() 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 let self = self else { return } - // Reschedule the sleep timer - if let currentTime = self.getCurrentTime() { - self.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining) + if continuePlaying { + self.resumePlayback() } - if continuePlaying { - self.play() - } self.updateNowPlaying() } } } 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) if self.audioPlayer.rate != rate { @@ -415,11 +406,6 @@ class AudioPlayer: NSObject { if playbackSpeedChanged { 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 self.setupTimeObserver() } diff --git a/ios/App/Shared/player/AudioPlayerSleepTimer.swift b/ios/App/Shared/player/AudioPlayerSleepTimer.swift index 85839984..96801559 100644 --- a/ios/App/Shared/player/AudioPlayerSleepTimer.swift +++ b/ios/App/Shared/player/AudioPlayerSleepTimer.swift @@ -12,54 +12,59 @@ extension AudioPlayer { // MARK: - Public API + public func isSleepTimerSet() -> Bool { + return self.isCountdownSleepTimerSet() || self.isChapterSleepTimerSet() + } + public func getSleepTimeRemaining() -> Double? { guard let currentTime = self.getCurrentTime() else { return nil } // Return the player time until sleep - var timeUntilSleep: Double? = nil + var sleepTimeRemaining: Double? = nil if let chapterStopAt = self.sleepTimeChapterStopAt { - timeUntilSleep = chapterStopAt - currentTime - } else if let stopAt = self.sleepTimeStopAt { - timeUntilSleep = stopAt - currentTime + sleepTimeRemaining = chapterStopAt - currentTime + } else if self.isCountdownSleepTimerSet() { + sleepTimeRemaining = self.sleepTimeRemaining } - // Scale the time until sleep based on the playback rate - if let timeUntilSleep = timeUntilSleep { - let timeUntilSleepScaled = timeUntilSleep / self.getPlaybackRate() - guard timeUntilSleepScaled.isNaN == false else { return nil } - - return timeUntilSleepScaled.rounded() - } else { + // Guard against invalid sleep timers + if sleepTimeRemaining?.isLess(than: 0) ?? false { + self.removeSleepTimer() return nil } + + return sleepTimeRemaining } - // Let iOS handle the sleep timer logic by letting us know when it's time to stop - public func setSleepTime(stopAt: Double, scaleBasedOnSpeed: Bool = false) { - NSLog("SLEEP TIMER: Scheduling for \(stopAt)") + public func setSleepTimer(secondsUntilSleep: Double) { + NSLog("SLEEP TIMER: Sleeping in \(secondsUntilSleep) seconds") + self.removeSleepTimer() + self.sleepTimeRemaining = secondsUntilSleep - // Reset any previous sleep timer - let isChapterSleepTimer = !scaleBasedOnSpeed - self.removeSleepTimer(resetStopAt: !isChapterSleepTimer) - - guard let currentTime = getCurrentTime() else { - NSLog("SLEEP TIMER: Failed to get currenTime") - return + DispatchQueue.runOnMainQueue { + self.sleepTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in + if self.isPlaying() { + self.decrementSleepTimerIfRunning() + } + } } - // Mark the time to stop playing - let scaledStopAt = self.calculateScaledStopAt(stopAt, currentTime: currentTime, scaleBasedOnSpeed: scaleBasedOnSpeed) - self.sleepTimeStopAt = scaledStopAt - let sleepTime = CMTime(seconds: scaledStopAt, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + // Update the UI + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) + } + + public func setChapterSleepTimer(stopAt: Double) { + NSLog("SLEEP TIMER: Scheduling for chapter end \(stopAt)") + self.removeSleepTimer() // Schedule the observation time + self.sleepTimeChapterStopAt = stopAt + let sleepTime = CMTime(seconds: stopAt, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) var times = [NSValue]() times.append(NSValue(time: sleepTime)) - self.sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: self.queue) { [weak self] in - NSLog("SLEEP TIMER: Pausing audio") - self?.pause() - self?.removeSleepTimer() + self.sleepTimeChapterToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: self.queue) { [weak self] in + self?.handleSleepEnd() } // Update the UI @@ -67,56 +72,66 @@ extension AudioPlayer { } public func increaseSleepTime(extraTimeInSeconds: Double) { - if let sleepTime = self.getSleepTimeRemaining(), let currentTime = getCurrentTime() { - let newSleepTimerPosition = currentTime + sleepTime + extraTimeInSeconds - if newSleepTimerPosition > currentTime { - self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) - } - } + self.removeChapterSleepTimer() + guard let sleepTimeRemaining = self.sleepTimeRemaining else { return } + self.sleepTimeRemaining = sleepTimeRemaining + extraTimeInSeconds + + // Update the UI + NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil) } public func decreaseSleepTime(removeTimeInSeconds: Double) { - if let sleepTime = self.getSleepTimeRemaining(), let currentTime = getCurrentTime() { - let newSleepTimerPosition = currentTime + sleepTime - removeTimeInSeconds - if newSleepTimerPosition > currentTime { - 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 - } + self.removeChapterSleepTimer() + guard let sleepTimeRemaining = self.sleepTimeRemaining else { return } + self.sleepTimeRemaining = sleepTimeRemaining - removeTimeInSeconds // 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 - internal func rescheduleSleepTimerAtTime(time: Double, secondsRemaining: Double?) { - guard self.isSleepTimerSet() else { return } - - // Cancel a chapter sleep timer that is no longer valid - if isChapterSleepTimerBeforeTime(time) { - return self.removeSleepTimer() + internal func decrementSleepTimerIfRunning() { + if var sleepTimeRemaining = self.sleepTimeRemaining { + sleepTimeRemaining -= 1 + self.sleepTimeRemaining = sleepTimeRemaining + + // Handle the sleep if the timer has expired + if sleepTimeRemaining <= 0 { + self.handleSleepEnd() + } } - - // Update the sleep timer - if !isChapterSleepTimer() { - guard let secondsRemaining = secondsRemaining else { return } - let newSleepTimerPosition = time + secondsRemaining - self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true) + } + + private func handleSleepEnd() { + NSLog("SLEEP TIMER: Pausing audio") + self.pause() + self.removeSleepTimer() + } + + private func removeChapterSleepTimer() { + if let token = self.sleepTimeChapterToken { + self.audioPlayer.removeTimeObserver(token) } + self.sleepTimeChapterToken = nil + self.sleepTimeChapterStopAt = nil } private func isChapterSleepTimerBeforeTime(_ time: Double) -> Bool { @@ -127,29 +142,12 @@ extension AudioPlayer { return false } - private func isSleepTimerSet() -> Bool { - return self.sleepTimeStopAt != nil + private func isCountdownSleepTimerSet() -> Bool { + return self.sleepTimeRemaining != nil } - private func isChapterSleepTimer() -> Bool { + private func isChapterSleepTimerSet() -> Bool { 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 - } - } - } diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index a41414fb..975b5331 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -70,13 +70,11 @@ class PlayerHandler { } public static func setSleepTime(secondsUntilSleep: Double) { - guard let currentTime = self.player?.getCurrentTime() else { return } - let stopAt = secondsUntilSleep + currentTime - self.player?.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: true) + self.player?.setSleepTimer(secondsUntilSleep: secondsUntilSleep) } public static func setChapterSleepTime(stopAt: Double) { - self.player?.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: false) + self.player?.setChapterSleepTimer(stopAt: stopAt) } public static func increaseSleepTime(increaseSeconds: Double) {