From f8a9033738e52370783f53a0d47a7d9903645f85 Mon Sep 17 00:00:00 2001 From: Marke Hallowell Date: Sun, 14 Apr 2024 16:12:55 -0700 Subject: [PATCH] Add an AudioPlayerRateManager implementation for iOS 16+. --- ios/App/Shared/player/AudioPlayer.swift | 7 +- .../DefaultedAudioPlayerRateManager.swift | 94 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 ios/App/Shared/player/DefaultedAudioPlayerRateManager.swift diff --git a/ios/App/Shared/player/AudioPlayer.swift b/ios/App/Shared/player/AudioPlayer.swift index 41b385cc..a2e99634 100644 --- a/ios/App/Shared/player/AudioPlayer.swift +++ b/ios/App/Shared/player/AudioPlayer.swift @@ -62,7 +62,12 @@ class AudioPlayer: NSObject { self.audioPlayer.automaticallyWaitsToMinimizeStalling = true self.sessionId = sessionId self.status = .uninitialized - self.rateManager = LegacyAudioPlayerRateManager(audioPlayer: self.audioPlayer, defaultRate: 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() diff --git a/ios/App/Shared/player/DefaultedAudioPlayerRateManager.swift b/ios/App/Shared/player/DefaultedAudioPlayerRateManager.swift new file mode 100644 index 00000000..1e45906a --- /dev/null +++ b/ios/App/Shared/player/DefaultedAudioPlayerRateManager.swift @@ -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) + } +}