From 414a8dc764b5fe09a4c857704e85825ed1891896 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Thu, 6 Jul 2023 00:11:18 -0500 Subject: [PATCH] Recover from mediaServicesWereResetNotification --- BookPlayer/AppDelegate.swift | 7 +-- BookPlayer/Player/PlayerManager.swift | 72 +++++++++++++++++++++------ 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/BookPlayer/AppDelegate.swift b/BookPlayer/AppDelegate.swift index 59b228ea..37217a54 100644 --- a/BookPlayer/AppDelegate.swift +++ b/BookPlayer/AppDelegate.swift @@ -65,7 +65,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { try? AVAudioSession.sharedInstance().setCategory( AVAudioSession.Category.playback, - mode: AVAudioSession.Mode(rawValue: convertFromAVAudioSessionMode(AVAudioSession.Mode.spokenAudio)), + mode: .spokenAudio, options: [] ) @@ -475,8 +475,3 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } } - -// Helper function inserted by Swift 4.2 migrator. -private func convertFromAVAudioSessionMode(_ input: AVAudioSession.Mode) -> String { - return input.rawValue -} diff --git a/BookPlayer/Player/PlayerManager.swift b/BookPlayer/Player/PlayerManager.swift index a977fed5..d18307b5 100755 --- a/BookPlayer/Player/PlayerManager.swift +++ b/BookPlayer/Player/PlayerManager.swift @@ -53,6 +53,8 @@ final class PlayerManager: NSObject, PlayerManagerProtocol { private var fadeTimer: Timer? + private var timeControlPassthroughPublisher = CurrentValueSubject(.paused) + private var timeControlSubscription: AnyCancellable? private var playableChapterSubscription: AnyCancellable? private var isPlayingSubscription: AnyCancellable? private var periodicTimeObserver: Any? @@ -97,17 +99,32 @@ final class PlayerManager: NSObject, PlayerManagerProtocol { self.userActivityManager = UserActivityManager(libraryService: libraryService) super.init() - self.setupPlayerInstance() + setupPlayerInstance() - NotificationCenter.default.addObserver(self, - selector: #selector(playerDidFinishPlaying(_:)), - name: .AVPlayerItemDidPlayToEndTime, - object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(playerDidFinishPlaying(_:)), + name: .AVPlayerItemDidPlayToEndTime, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleMediaServicesWereReset), + name: AVAudioSession.mediaServicesWereResetNotification, + object: nil + ) } func setupPlayerInstance() { + if let observer = periodicTimeObserver { + audioPlayer.removeTimeObserver(observer) + } + + audioPlayer = AVPlayer() + let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - self.periodicTimeObserver = self.audioPlayer.addPeriodicTimeObserver( + periodicTimeObserver = audioPlayer.addPeriodicTimeObserver( forInterval: interval, queue: DispatchQueue.main ) { [weak self] _ in @@ -117,7 +134,9 @@ final class PlayerManager: NSObject, PlayerManagerProtocol { } // Only route audio for AirPlay - self.audioPlayer.allowsExternalPlayback = false + audioPlayer.allowsExternalPlayback = false + + bindTimeControlPassthroughPublisher() } func currentItemPublisher() -> AnyPublisher { @@ -216,13 +235,8 @@ final class PlayerManager: NSObject, PlayerManagerProtocol { loadChapterTask?.cancel() // Recover in case of failure - if self.audioPlayer.status == .failed { - if let observer = self.periodicTimeObserver { - self.audioPlayer.removeTimeObserver(observer) - } - - self.audioPlayer = AVPlayer() - self.setupPlayerInstance() + if audioPlayer.status == .failed { + setupPlayerInstance() } // Preload item @@ -383,9 +397,19 @@ final class PlayerManager: NSObject, PlayerManagerProtocol { return self.audioPlayer.timeControlStatus == .playing } + /// We need an intermediate publisher for the `timeControlStatus`, as the `AVPlayer` instance can be recreated, + /// thus invalidating the registered observers for `isPlaying` + func bindTimeControlPassthroughPublisher() { + timeControlSubscription?.cancel() + timeControlSubscription = audioPlayer.publisher(for: \.timeControlStatus) + .sink { [weak self] timeControlStatus in + self?.timeControlPassthroughPublisher.send(timeControlStatus) + } + } + func bindPauseObserver() { self.isPlayingSubscription?.cancel() - self.isPlayingSubscription = self.audioPlayer.publisher(for: \.timeControlStatus) + self.isPlayingSubscription = timeControlPassthroughPublisher .delay(for: .seconds(0.1), scheduler: RunLoop.main, options: .none) .sink { timeControlStatus in if timeControlStatus == .paused { @@ -397,7 +421,7 @@ final class PlayerManager: NSObject, PlayerManagerProtocol { func isPlayingPublisher() -> AnyPublisher { return Publishers.CombineLatest3( - audioPlayer.publisher(for: \.timeControlStatus), + timeControlPassthroughPublisher, $playbackQueued, $isFetchingRemoteURL ) @@ -857,6 +881,22 @@ extension PlayerManager { private func loadAndRefreshURL(item: PlayableItem) { load(item, autoplay: playbackQueued == true, forceRefreshURL: true) } + + @objc + private func handleMediaServicesWereReset() { + /// Playback should be stopped, and wait for the user to activate it again + if isPlaying { + stopPlayback() + } + + try? AVAudioSession.sharedInstance().setCategory( + AVAudioSession.Category.playback, + mode: .spokenAudio, + options: [] + ) + + setupPlayerInstance() + } } // MARK: - BookMarks