mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-09 05:18:40 +02:00
Merge pull request #1179 from indiefan/rate-manager
Refactor AudioPlayer to use a Rate Manager protocol.
This commit is contained in:
@@ -65,6 +65,9 @@
|
||||
E9E8814D28DA6B9000D750C1 /* PlayerTimeUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E8814C28DA6B9000D750C1 /* PlayerTimeUtilsTests.swift */; };
|
||||
E9E985F828B02D9400957F23 /* PlayerProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E985F728B02D9400957F23 /* PlayerProgress.swift */; };
|
||||
E9FA07E328C82848005520B0 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FA07E228C82848005520B0 /* Logger.swift */; };
|
||||
EACB38122BCCA1330060DA4A /* AudioPlayerRateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EACB38112BCCA1330060DA4A /* AudioPlayerRateManager.swift */; };
|
||||
EACB38142BCCA1410060DA4A /* LegacyAudioPlayerRateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EACB38132BCCA1410060DA4A /* LegacyAudioPlayerRateManager.swift */; };
|
||||
EACB38162BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EACB38152BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -142,6 +145,9 @@
|
||||
E9E8814C28DA6B9000D750C1 /* PlayerTimeUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTimeUtilsTests.swift; sourceTree = "<group>"; };
|
||||
E9E985F728B02D9400957F23 /* PlayerProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerProgress.swift; sourceTree = "<group>"; };
|
||||
E9FA07E228C82848005520B0 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
EACB38112BCCA1330060DA4A /* AudioPlayerRateManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioPlayerRateManager.swift; sourceTree = "<group>"; };
|
||||
EACB38132BCCA1410060DA4A /* LegacyAudioPlayerRateManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyAudioPlayerRateManager.swift; sourceTree = "<group>"; };
|
||||
EACB38152BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultedAudioPlayerRateManager.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -183,6 +189,9 @@
|
||||
3ABF6190280432610070250E /* player */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EACB38152BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift */,
|
||||
EACB38132BCCA1410060DA4A /* LegacyAudioPlayerRateManager.swift */,
|
||||
EACB38112BCCA1330060DA4A /* AudioPlayerRateManager.swift */,
|
||||
E9E8814828DA641B00D750C1 /* util */,
|
||||
3A200C1427D64D7E00CBF02E /* AudioPlayer.swift */,
|
||||
E9DFCBFA28C28F4A00B36356 /* AudioPlayerSleepTimer.swift */,
|
||||
@@ -521,6 +530,7 @@
|
||||
E9D5506228AC1CC900C746DD /* PlayerState.swift in Sources */,
|
||||
3AD4FCE728043E72006DB301 /* AbsDatabase.m in Sources */,
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
EACB38122BCCA1330060DA4A /* AudioPlayerRateManager.swift in Sources */,
|
||||
E9FA07E328C82848005520B0 /* Logger.swift in Sources */,
|
||||
3A90295F280968E700E1D427 /* PlaybackReport.swift in Sources */,
|
||||
E9D5505A28AC1C4500C746DD /* Folder.swift in Sources */,
|
||||
@@ -540,6 +550,7 @@
|
||||
3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */,
|
||||
E9D5507128AC1EC700C746DD /* DownloadItemPart.swift in Sources */,
|
||||
4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */,
|
||||
EACB38142BCCA1410060DA4A /* LegacyAudioPlayerRateManager.swift in Sources */,
|
||||
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */,
|
||||
3AB34053280829BF0039308B /* Extensions.swift in Sources */,
|
||||
E9D5505828AC1C1A00C746DD /* Library.swift in Sources */,
|
||||
@@ -548,6 +559,7 @@
|
||||
4DABC04F2B0139CA000F6264 /* User.swift in Sources */,
|
||||
4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */,
|
||||
E9D5506828AC1DC300C746DD /* LocalPodcastEpisode.swift in Sources */,
|
||||
EACB38162BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift in Sources */,
|
||||
E9D5505228AC1B5D00C746DD /* Chapter.swift in Sources */,
|
||||
E9D5506028AC1CA900C746DD /* PlaybackMetadata.swift in Sources */,
|
||||
E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */,
|
||||
|
||||
@@ -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,18 @@ class AudioPlayer: NSObject {
|
||||
self.audioPlayer.automaticallyWaitsToMinimizeStalling = true
|
||||
self.sessionId = sessionId
|
||||
self.status = .uninitialized
|
||||
self.rate = 0.0
|
||||
self.tmpRate = playbackRate
|
||||
|
||||
if #available(iOS 16.0, *) {
|
||||
self.rateManager = DefaultedAudioPlayerRateManager(audioPlayer: self.audioPlayer, defaultRate: playbackRate)
|
||||
} else {
|
||||
self.rateManager = LegacyAudioPlayerRateManager(audioPlayer: self.audioPlayer, defaultRate: playbackRate)
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.rateManager.rateChangedCompletion = self.updateNowPlaying
|
||||
self.rateManager.defaultRateChangedCompletion = self.setupTimeObservers
|
||||
|
||||
initAudioSession()
|
||||
setupRemoteTransportControls()
|
||||
|
||||
@@ -81,7 +86,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 +154,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 +335,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 +427,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 +692,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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// DefaultedAudioPlayerRateManager.swift
|
||||
// Audiobookshelf
|
||||
//
|
||||
// Created by Marke Hallowell on 4/14/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
class DefaultedAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
|
||||
internal let logger = AppLogger(category: "DefaultedAudioPlayerRateManager")
|
||||
|
||||
internal var audioPlayer: AVPlayer
|
||||
|
||||
// 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.audioPlayer = audioPlayer
|
||||
self.rateChangedCompletion = {}
|
||||
self.defaultRateChangedCompletion = {}
|
||||
self.rate = self.audioPlayer.rate
|
||||
self.defaultRate = defaultRate
|
||||
self.audioPlayer.defaultRate = defaultRate
|
||||
|
||||
super.init()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleObservedRateChange), name: AVPlayer.rateDidChangeNotification, object: self.audioPlayer)
|
||||
}
|
||||
|
||||
public func setPlaybackRate(_ rate: Float) {
|
||||
self.handlePlaybackRateChange(rate, observed: false)
|
||||
}
|
||||
|
||||
// No-op (player automatically resumes at last-known defaultRate)
|
||||
public func handlePlayEvent() { }
|
||||
|
||||
// MARK: - Destructor
|
||||
public func destroy() {
|
||||
NotificationCenter.default.removeObserver(self, name: AVPlayer.rateDidChangeNotification, object: self.audioPlayer)
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
internal func handlePlaybackRateChange(_ rate: Float, observed: Bool = false) {
|
||||
let playbackSpeedChanged = rate > 0.0 && rate != self.defaultRate
|
||||
|
||||
if playbackSpeedChanged {
|
||||
self.defaultRate = rate
|
||||
self.audioPlayer.defaultRate = rate
|
||||
self.defaultRateChangedCompletion()
|
||||
|
||||
// Check to see if we also need to make a temporary rate change to player
|
||||
if self.audioPlayer.rate > 0.0 {
|
||||
if self.audioPlayer.rate != rate {
|
||||
self.rate = rate
|
||||
self.audioPlayer.rate = rate
|
||||
self.rateChangedCompletion()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.rate = rate
|
||||
self.rateChangedCompletion()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iOS rate change notification handler
|
||||
@objc internal func handleObservedRateChange(notification: Notification) {
|
||||
// TODO: Consider handling cases individually (e.g. overall session impact?)
|
||||
/*
|
||||
guard let reason = notification.userInfo?[AVPlayer.rateDidChangeReasonKey] as? AVPlayer.RateDidChangeReason else {
|
||||
return
|
||||
}
|
||||
|
||||
switch reason {
|
||||
case .appBackgrounded:
|
||||
// App transitioned to the background.
|
||||
case .audioSessionInterrupted:
|
||||
// The system interrupts the app's audio session.
|
||||
case .setRateCalled:
|
||||
// The app set the player's rate.
|
||||
case .setRateFailed:
|
||||
// An attempt to change the player's rate failed.
|
||||
default:
|
||||
break
|
||||
}
|
||||
*/
|
||||
self.handlePlaybackRateChange(self.audioPlayer.rate, observed: true)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user