From edb25f5bcd012a1c77d8a60c0b9853d24b79f8e8 Mon Sep 17 00:00:00 2001 From: Marke Hallowell Date: Sun, 14 Apr 2024 11:45:03 -0700 Subject: [PATCH] Refactor AudioPlayer to use an AudioPlayerRateManager protocol for managing rate/defaultRate state management. Implements a LegacyAudioPlayerRateManager implementation with existing (pre-iOS 16) behavior. --- ios/App/Shared/player/AudioPlayer.swift | 46 +++------- .../player/AudioPlayerRateManager.swift | 23 +++++ .../Shared/player/AudioPlayerSleepTimer.swift | 2 +- .../player/LegacyAudioPlayerRateManager.swift | 84 +++++++++++++++++++ ios/App/Shared/player/PlayerHandler.swift | 2 +- 5 files changed, 121 insertions(+), 36 deletions(-) create mode 100644 ios/App/Shared/player/AudioPlayerRateManager.swift create mode 100644 ios/App/Shared/player/LegacyAudioPlayerRateManager.swift diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index e9086775..41b385cc 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -29,9 +29,7 @@ class AudioPlayer: NSObject { internal let logger = AppLogger(category: "AudioPlayer") private var status: PlayerStatus - internal var rate: Float - - private var tmpRate: Float = 1.0 + internal var rateManager: AudioPlayerRateManager private var playerContext = 0 private var playerItemContext = 0 @@ -64,11 +62,13 @@ class AudioPlayer: NSObject { self.audioPlayer.automaticallyWaitsToMinimizeStalling = true self.sessionId = sessionId self.status = .uninitialized - self.rate = 0.0 - self.tmpRate = playbackRate + self.rateManager = LegacyAudioPlayerRateManager(audioPlayer: self.audioPlayer, defaultRate: playbackRate) super.init() + self.rateManager.rateChangedCompletion = self.updateNowPlaying + self.rateManager.defaultRateChangedCompletion = self.setupTimeObservers + initAudioSession() setupRemoteTransportControls() @@ -81,7 +81,6 @@ class AudioPlayer: NSObject { // Listen to player events self.setupAudioSessionNotifications() - self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: .new, context: &playerContext) self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), options: .new, context: &playerContext) for track in playbackSession.audioTracks { @@ -150,7 +149,6 @@ class AudioPlayer: NSObject { } // Remove observers - self.audioPlayer.removeObserver(self, forKeyPath: #keyPath(AVPlayer.rate), context: &playerContext) self.audioPlayer.removeObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), context: &playerContext) self.removeTimeObservers() @@ -332,7 +330,7 @@ class AudioPlayer: NSObject { self.markAudioSessionAs(active: true) DispatchQueue.runOnMainQueue { self.audioPlayer.play() - self.audioPlayer.rate = self.tmpRate + self.rateManager.handlePlayEvent() } self.status = .playing @@ -424,25 +422,8 @@ class AudioPlayer: NSObject { } } - public func setPlaybackRate(_ rate: Float, observed: Bool = false) { - let playbackSpeedChanged = rate > 0.0 && rate != self.tmpRate && !(observed && rate == 1) - - if self.audioPlayer.rate != rate { - logger.log("setPlaybakRate rate changed from \(self.audioPlayer.rate) to \(rate)") - DispatchQueue.runOnMainQueue { - self.audioPlayer.rate = rate - } - } - - self.rate = rate - self.updateNowPlaying() - - if playbackSpeedChanged { - self.tmpRate = rate - - // Setup the time observer again at the new rate - self.setupTimeObservers() - } + public func setPlaybackRate(_ rate: Float) { + self.rateManager.setPlaybackRate(rate) } public func setChapterTrack() { @@ -706,24 +687,21 @@ class AudioPlayer: NSObject { NowPlayingInfo.shared.update( duration: currentChapter.getRelativeChapterEndTime(), currentTime: currentChapter.getRelativeChapterCurrentTime(sessionCurrentTime: session.currentTime), - rate: rate, - defaultRate: tmpRate, + rate: self.rateManager.rate, + defaultRate: self.rateManager.defaultRate, chapterName: currentChapter.title, chapterNumber: (session.chapters.firstIndex(of: currentChapter) ?? 0) + 1, chapterCount: session.chapters.count ) } else if let duration = self.getDuration(), let currentTime = self.getCurrentTime() { - NowPlayingInfo.shared.update(duration: duration, currentTime: currentTime, rate: rate, defaultRate: tmpRate) + NowPlayingInfo.shared.update(duration: duration, currentTime: currentTime, rate: self.rateManager.rate, defaultRate: self.rateManager.defaultRate) } } // MARK: - Observer public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if context == &playerContext { - if keyPath == #keyPath(AVPlayer.rate) { - logger.log("playerContext observer player rate") - self.setPlaybackRate(change?[.newKey] as? Float ?? 1.0, observed: true) - } else if keyPath == #keyPath(AVPlayer.currentItem) { + if keyPath == #keyPath(AVPlayer.currentItem) { NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil) logger.log("WARNING: Item ended") } diff --git a/ios/App/Shared/player/AudioPlayerRateManager.swift b/ios/App/Shared/player/AudioPlayerRateManager.swift new file mode 100644 index 00000000..db4d43ed --- /dev/null +++ b/ios/App/Shared/player/AudioPlayerRateManager.swift @@ -0,0 +1,23 @@ +// +// AudioPlayerRateManager.swift +// Audiobookshelf +// +// Created by Marke Hallowell on 4/14/24. +// + +import Foundation +import AVFoundation + +protocol AudioPlayerRateManager { + var rate: Float { get } + var defaultRate: Float { get } + var rateChangedCompletion: () -> Void { get set } + var defaultRateChangedCompletion: () -> Void { get set } + + init(audioPlayer: AVPlayer, defaultRate: Float) + + func setPlaybackRate(_ rate: Float) + + // Callback for play events (e.g. LegacyAudioPlayerRateManager uses this set rate immediately after playback resumes) + func handlePlayEvent() -> Void +} diff --git a/ios/App/Shared/player/AudioPlayerSleepTimer.swift b/ios/App/Shared/player/AudioPlayerSleepTimer.swift index a84a7023..28cfdf06 100644 --- a/ios/App/Shared/player/AudioPlayerSleepTimer.swift +++ b/ios/App/Shared/player/AudioPlayerSleepTimer.swift @@ -22,7 +22,7 @@ extension AudioPlayer { // Return the player time until sleep var sleepTimeRemaining: Double? = nil if let chapterStopAt = self.sleepTimeChapterStopAt { - sleepTimeRemaining = (chapterStopAt - currentTime) / Double(self.rate > 0 ? self.rate : 1.0) + sleepTimeRemaining = (chapterStopAt - currentTime) / Double(self.rateManager.rate > 0 ? self.rateManager.rate : 1.0) } else if self.isCountdownSleepTimerSet() { sleepTimeRemaining = self.sleepTimeRemaining } diff --git a/ios/App/Shared/player/LegacyAudioPlayerRateManager.swift b/ios/App/Shared/player/LegacyAudioPlayerRateManager.swift new file mode 100644 index 00000000..3912d8c7 --- /dev/null +++ b/ios/App/Shared/player/LegacyAudioPlayerRateManager.swift @@ -0,0 +1,84 @@ +// +// LegacyAudioPlayerRateManager.swift +// Audiobookshelf +// +// Created by Marke Hallowell on 4/14/24. +// + +import Foundation +import AVFoundation + +class LegacyAudioPlayerRateManager: NSObject, AudioPlayerRateManager { + internal let logger = AppLogger(category: "AudioPlayer") + + internal var audioPlayer: AVPlayer + + internal var managerContext = 0 + + // MARK: - AudioPlayerRateManager + public private(set) var defaultRate: Float + public private(set) var rate: Float + public var rateChangedCompletion: () -> Void + public var defaultRateChangedCompletion: () -> Void + + required init(audioPlayer: AVPlayer, defaultRate: Float) { + self.rate = 0.0 + self.defaultRate = defaultRate + self.audioPlayer = audioPlayer + self.rateChangedCompletion = {} + self.defaultRateChangedCompletion = {} + + super.init() + + self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: .new, context: &managerContext) + } + + public func setPlaybackRate(_ rate: Float) { + self.handlePlaybackRateChange(rate, observed: false) + } + + public func handlePlayEvent() { + DispatchQueue.runOnMainQueue { + self.audioPlayer.rate = self.defaultRate + } + } + + // MARK: - Destructor + public func destroy() { + // Remove Observer + self.audioPlayer.removeObserver(self, forKeyPath: #keyPath(AVPlayer.rate), context: &managerContext) + } + + // MARK: - Internal + internal func handlePlaybackRateChange(_ rate: Float, observed: Bool = false) { + let playbackSpeedChanged = rate > 0.0 && rate != self.defaultRate && !(observed && rate == 1) + + if self.audioPlayer.rate != rate { + logger.log("setPlaybakRate rate changed from \(self.audioPlayer.rate) to \(rate)") + DispatchQueue.runOnMainQueue { + self.audioPlayer.rate = rate + } + } + + self.rate = rate + self.rateChangedCompletion() + + if playbackSpeedChanged { + self.defaultRate = rate + self.defaultRateChangedCompletion() + } + } + + // MARK: - Observer + public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if context == &managerContext { + if keyPath == #keyPath(AVPlayer.rate) { + logger.log("playerContext observer player rate") + self.handlePlaybackRateChange(change?[.newKey] as? Float ?? 1.0, observed: true) + } + } else { + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + return + } + } +} diff --git a/ios/App/Shared/player/PlayerHandler.swift b/ios/App/Shared/player/PlayerHandler.swift index c895a2c3..4cc9ccb1 100644 --- a/ios/App/Shared/player/PlayerHandler.swift +++ b/ios/App/Shared/player/PlayerHandler.swift @@ -38,7 +38,7 @@ class PlayerHandler { public static var paused: Bool { get { guard let player = player else { return true } - return player.rate == 0.0 + return player.rateManager.rate == 0.0 } set(paused) { if paused {