Rewrite sleep timer

This commit is contained in:
ronaldheft
2022-09-02 16:31:47 -04:00
parent 34e68593bc
commit 586405f9de
5 changed files with 209 additions and 193 deletions
+34 -144
View File
@@ -18,7 +18,7 @@ enum PlayMethod:Int {
}
class AudioPlayer: NSObject {
private let queue = DispatchQueue(label: "ABSAudioPlayerQueue")
internal let queue = DispatchQueue(label: "ABSAudioPlayerQueue")
// enums and @objc are not compatible
@objc dynamic var status: Int
@@ -32,15 +32,17 @@ class AudioPlayer: NSObject {
private var playWhenReady: Bool
private var initialPlaybackRate: Float
private var audioPlayer: AVQueuePlayer
internal var audioPlayer: AVQueuePlayer
private var sessionId: String
private var timeObserverToken: Any?
private var queueObserver:NSKeyValueObservation?
private var queueItemStatusObserver:NSKeyValueObservation?
private var sleepTimeStopAt: Double?
private var sleepTimeToken: Any?
// Sleep timer values
internal var sleepTimeChapterStopAt: Double?
internal var sleepTimeStopAt: Double?
internal var sleepTimeToken: Any?
private var currentTrackIndex = 0
private var allPlayerItems:[AVPlayerItem] = []
@@ -263,7 +265,7 @@ class AudioPlayer: NSObject {
}
// Capture remaining sleep time before changing the track position
let sleepSecondsRemaining = PlayerHandler.remainingSleepTime
let sleepSecondsRemaining = self.getSleepTimeRemaining()
// Stop the paused timer
self.stopPausedTimer()
@@ -279,19 +281,17 @@ class AudioPlayer: NSObject {
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()
}
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()
}
}
@@ -342,22 +342,22 @@ 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()
NSLog("Seek to \(to) from \(from)")
NSLog("SEEK: Seek to \(to) from \(from)")
guard let playbackSession = self.getPlaybackSession() else { return }
let currentTrack = playbackSession.audioTracks[self.currentTrackIndex]
let ctso = currentTrack.startOffset ?? 0.0
let trackEnd = ctso + currentTrack.duration
NSLog("Seek current track END = \(trackEnd)")
// Capture remaining sleep time before changing the track position
let sleepSecondsRemaining = PlayerHandler.remainingSleepTime
NSLog("SEEK: Seek current track END = \(trackEnd)")
let indexOfSeek = getItemIndexForTime(time: to)
NSLog("Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
NSLog("SEEK: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
// Reconstruct queue if seeking to a different track
if (self.currentTrackIndex != indexOfSeek) {
@@ -378,32 +378,30 @@ class AudioPlayer: NSObject {
setupQueueItemStatusObserver()
} else {
NSLog("Seeking in current item \(to)")
NSLog("SEEK: Seeking in current item \(to)")
let currentTrackStartOffset = playbackSession.audioTracks[self.currentTrackIndex].startOffset ?? 0.0
let seekTime = to - currentTrackStartOffset
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in
if !completed {
NSLog("WARNING: seeking not completed (to \(seekTime)")
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?.play()
}
self?.updateNowPlaying()
// If we have an active sleep timer, reschedule based on seek, since seek is fuzzy
// This needs to occur after play() to capture the correct playback rate
if let currentTime = self?.getCurrentTime() {
self?.rescheduleSleepTimerAtTime(time: currentTime, secondsRemaining: sleepSecondsRemaining)
self.play()
}
self.updateNowPlaying()
}
}
}
public func setPlaybackRate(_ rate: Float, observed: Bool = false) {
// Capture remaining sleep time before changing the rate
let sleepSecondsRemaining = PlayerHandler.remainingSleepTime
let sleepSecondsRemaining = self.getSleepTimeRemaining()
let playbackSpeedChanged = rate > 0.0 && rate != self.tmpRate && !(observed && rate == 1)
if self.audioPlayer.rate != rate {
@@ -427,114 +425,6 @@ class AudioPlayer: NSObject {
}
}
public func getSleepStopAt() -> Double? {
return self.sleepTimeStopAt
}
// 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)")
// Reset any previous sleep timer
self.removeSleepTimer()
guard let currentTime = getCurrentTime() else {
NSLog("Failed to get currenTime")
return
}
// Mark the time to stop playing
if scaleBasedOnSpeed {
// Consider paused as playing at 1x
let rate = Double(self.rate > 0 ? self.rate : 1)
// Calculate the scaled time to stop at
let timeUntilSleep = (stopAt - currentTime) * rate
self.sleepTimeStopAt = currentTime + timeUntilSleep
NSLog("SLEEP TIMER: Adjusted based on playback speed of \(rate) to \(self.sleepTimeStopAt!)")
} else {
self.sleepTimeStopAt = stopAt
}
guard let sleepTimeStopAt = self.sleepTimeStopAt else { return }
let sleepTime = CMTime(seconds: sleepTimeStopAt, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
// Schedule the observation time
var times = [NSValue]()
times.append(NSValue(time: sleepTime))
sleepTimeToken = self.audioPlayer.addBoundaryTimeObserver(forTimes: times, queue: queue) { [weak self] in
NSLog("SLEEP TIMER: Pausing audio")
self?.pause()
PlayerHandler.sleepTimerChapterStopTime = nil
self?.removeSleepTimer()
}
// Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
}
private func rescheduleSleepTimerAtTime(time: Double, secondsRemaining: Int?) {
// Not a chapter sleep timer
let hadToCancelChapterSleepTimer = decideIfChapterSleepTimerNeedsToBeCanceled(time: time)
guard !hadToCancelChapterSleepTimer else { return }
guard PlayerHandler.sleepTimerChapterStopTime == nil else { return }
// Verify sleep timer is set
guard self.sleepTimeToken != nil else { return }
// Update the sleep timer
if let secondsRemaining = secondsRemaining {
let newSleepTimerPosition = time + Double(secondsRemaining)
self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true)
}
}
private func decideIfChapterSleepTimerNeedsToBeCanceled(time: Double) -> Bool {
if let chapterSleepTime = PlayerHandler.sleepTimerChapterStopTime {
let sleepIsBeforeCurrentTime = Double(chapterSleepTime) <= time
if sleepIsBeforeCurrentTime {
PlayerHandler.sleepTimerChapterStopTime = nil
self.removeSleepTimer()
return true
}
}
return false
}
public func increaseSleepTime(extraTimeInSeconds: Double) {
if let sleepTime = PlayerHandler.remainingSleepTime, let currentTime = getCurrentTime() {
let newSleepTimerPosition = currentTime + Double(sleepTime) + extraTimeInSeconds
if newSleepTimerPosition > currentTime {
self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true)
}
}
}
public func decreaseSleepTime(removeTimeInSeconds: Double) {
if let sleepTime = PlayerHandler.remainingSleepTime, let currentTime = getCurrentTime() {
let newSleepTimerPosition = currentTime + Double(sleepTime) - removeTimeInSeconds
guard newSleepTimerPosition > currentTime else { return }
if newSleepTimerPosition > currentTime {
self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true)
}
}
}
public func removeSleepTimer() {
self.sleepTimeStopAt = nil
if let token = sleepTimeToken {
self.audioPlayer.removeTimeObserver(token)
sleepTimeToken = nil
}
// Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: self)
}
public func getCurrentTime() -> Double? {
guard let playbackSession = self.getPlaybackSession() else { return nil }
let currentTrackTime = self.audioPlayer.currentTime().seconds
@@ -0,0 +1,155 @@
//
// AudioPlayerSleepTimer.swift
// App
//
// Created by Ron Heft on 9/2/22.
//
import Foundation
import AVFoundation
extension AudioPlayer {
// MARK: - Public API
public func getSleepTimeRemaining() -> Double? {
guard let currentTime = self.getCurrentTime() else { return nil }
// Return the player time until sleep
var timeUntilSleep: Double? = nil
if let chapterStopAt = self.sleepTimeChapterStopAt {
timeUntilSleep = chapterStopAt - currentTime
} else if let stopAt = self.sleepTimeStopAt {
timeUntilSleep = stopAt - currentTime
}
// 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 {
return nil
}
}
// 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)")
// 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
}
// 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))
// Schedule the observation time
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()
}
// Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
}
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)
}
}
}
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
}
// Update the UI
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()
}
// Update the sleep timer
if !isChapterSleepTimer() {
guard let secondsRemaining = secondsRemaining else { return }
let newSleepTimerPosition = time + secondsRemaining
self.setSleepTime(stopAt: newSleepTimerPosition, scaleBasedOnSpeed: true)
}
}
private func isChapterSleepTimerBeforeTime(_ time: Double) -> Bool {
if let chapterStopAt = self.sleepTimeChapterStopAt {
return chapterStopAt <= time
}
return false
}
private func isSleepTimerSet() -> Bool {
return self.sleepTimeStopAt != nil
}
private func isChapterSleepTimer() -> 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
}
}
}
+7 -39
View File
@@ -11,8 +11,6 @@ import RealmSwift
class PlayerHandler {
private static var player: AudioPlayer?
public static var sleepTimerChapterStopTime: Int? = nil
public static func startPlayback(sessionId: String, playWhenReady: Bool, playbackRate: Float) {
guard let session = Database.shared.getPlaybackSession(id: sessionId) else { return }
@@ -59,34 +57,6 @@ class PlayerHandler {
}
}
public static var remainingSleepTime: Int? {
get {
guard let player = player else { return nil }
guard let currentTime = player.getCurrentTime() else { return nil }
// Return the player time until sleep
var timeUntilSleep: Double? = nil
if let sleepTimerChapterStopTime = sleepTimerChapterStopTime {
timeUntilSleep = Double(sleepTimerChapterStopTime) - currentTime
} else if let stopAt = player.getSleepStopAt() {
timeUntilSleep = stopAt - currentTime
}
// Scale the time until sleep based on the playback rate
if let timeUntilSleep = timeUntilSleep {
// Consider paused as playing at 1x
let rate = Double(player.rate > 0 ? player.rate : 1)
let timeUntilSleepScaled = timeUntilSleep / rate
guard timeUntilSleepScaled.isNaN == false else { return nil }
return Int(timeUntilSleepScaled.rounded())
} else {
return nil
}
}
}
public static func getCurrentTime() -> Double? {
self.player?.getCurrentTime()
}
@@ -95,31 +65,29 @@ class PlayerHandler {
self.player?.setPlaybackRate(speed)
}
public static func getSleepTimeRemaining() -> Double? {
return self.player?.getSleepTimeRemaining()
}
public static func setSleepTime(secondsUntilSleep: Double) {
guard let player = player else { return }
guard let currentTime = player.getCurrentTime() else { return }
guard let currentTime = self.player?.getCurrentTime() else { return }
let stopAt = secondsUntilSleep + currentTime
player.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: true)
self.player?.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: true)
}
public static func setChapterSleepTime(stopAt: Double) {
guard let player = player else { return }
self.sleepTimerChapterStopTime = Int(stopAt)
player.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: false)
self.player?.setSleepTime(stopAt: stopAt, scaleBasedOnSpeed: false)
}
public static func increaseSleepTime(increaseSeconds: Double) {
self.sleepTimerChapterStopTime = nil
self.player?.increaseSleepTime(extraTimeInSeconds: increaseSeconds)
}
public static func decreaseSleepTime(decreaseSeconds: Double) {
self.sleepTimerChapterStopTime = nil
self.player?.decreaseSleepTime(removeTimeInSeconds: decreaseSeconds)
}
public static func cancelSleepTime() {
PlayerHandler.sleepTimerChapterStopTime = nil
self.player?.removeSleepTimer()
}