From 6a06eba36abec1e5a467c309eb7b292758f2d207 Mon Sep 17 00:00:00 2001 From: Dustin Bahr Date: Thu, 25 Apr 2019 14:13:26 -0600 Subject: [PATCH] Different approach to re-creating the AVPlayer, and resetting the AVAudioSession, when a track causes a failure. --- Example/SwiftAudio/AudioController.swift | 34 +++++++++++++++++-- .../AVPlayerWrapper/AVPlayerWrapper.swift | 33 +++++++++++++++--- .../AVPlayerWrapperDelegate.swift | 2 +- SwiftAudio/Classes/AudioPlayer.swift | 4 +++ .../Classes/Observer/AVPlayerObserver.swift | 12 ++++++- .../Observer/AVPlayerTimeObserver.swift | 7 +++- 6 files changed, 83 insertions(+), 9 deletions(-) diff --git a/Example/SwiftAudio/AudioController.swift b/Example/SwiftAudio/AudioController.swift index 9426bdc..a648e56 100644 --- a/Example/SwiftAudio/AudioController.swift +++ b/Example/SwiftAudio/AudioController.swift @@ -10,7 +10,7 @@ import Foundation import SwiftAudio -class AudioController { +class AudioController : AudioPlayerDelegate { static let shared = AudioController() let player: QueuedAudioPlayer @@ -18,7 +18,13 @@ class AudioController { let sources: [AudioItem] = [ DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/67b51d90ffddd6bb3f095059997021b589845f81?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "33 \"GOD\"", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")), - DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")), + + // This track has a problem on purpose. It has problematic image metadata. + // It causes the AVPlayerItem and AVPlayer to change to a status of `.failed`. + // After this happens, the AVPlayer must be recreated, + // and the AVAudioSession category reset before playing other tracks. + DefaultAudioItem(audioUrl: "https://livestage.curiousmedia.com/public/pin/audio/problem-track-with-image-metadata.mp3", artist: "Fail", title: "Bad Image Metadata Failure", albumTitle: "Fail", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")), + DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/6f9999d909b017eabef97234dd7a206355720d9d?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "715 - CRΣΣKS", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")), DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/bf9bdd403c67fdbe06a582e7b292487c8cfd1f7e?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "____45_____", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")) ] @@ -35,8 +41,32 @@ class AudioController { .previous, .changePlaybackPosition ] + + player.delegate = self + try? audioSessionController.set(category: .playback) try? player.add(items: sources, playWhenReady: false) } + // MARK: - AudioPlayerDelegate + + func audioPlayer(playerDidChangeState state: AudioPlayerState) {} + + func audioPlayer(itemPlaybackEndedWithReason reason: PlaybackEndedReason) {} + + func audioPlayer(secondsElapsed seconds: Double) {} + + func audioPlayer(failedWithError error: Error?) { + print("AudioController failedWithError: ", error) + } + + func audioPlayer(seekTo seconds: Int, didFinish: Bool) {} + + func audioPlayer(didUpdateDuration duration: Double) {} + + func audioPlayerResetAudioSession() { + // Reset the audio session category because there was a failure to play a track. + // If the category is not reset, it seems to behave like the default category + try? audioSessionController.set(category: .playback) + } } diff --git a/SwiftAudio/Classes/AVPlayerWrapper/AVPlayerWrapper.swift b/SwiftAudio/Classes/AVPlayerWrapper/AVPlayerWrapper.swift index a09485e..d304d24 100755 --- a/SwiftAudio/Classes/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/SwiftAudio/Classes/AVPlayerWrapper/AVPlayerWrapper.swift @@ -26,9 +26,9 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { // MARK: - Properties - let avPlayer: AVPlayer - let playerObserver: AVPlayerObserver - let playerTimeObserver: AVPlayerTimeObserver + var avPlayer: AVPlayer + var playerObserver: AVPlayerObserver + var playerTimeObserver: AVPlayerTimeObserver let playerItemNotificationObserver: AVPlayerItemNotificationObserver let playerItemObserver: AVPlayerItemObserver @@ -192,11 +192,35 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { playerTimeObserver.unregisterForBoundaryTimeEvents() playerItemNotificationObserver.stopObservingCurrentItem() + //if the status of the current (about to be previous) item is .failed, + //we know that we need to reset + if (currentItem?.status == .failed) { + resetAfterFailure() + } + if !soft { avPlayer.replaceCurrentItem(with: nil) } } - + + private func resetAfterFailure() { + //reset time observer just in case + playerTimeObserver.unregisterForPeriodicEvents() + + // When AVPlayer status becomes .failed, + // the instance can no longer be used. + // A new instance must be created. + // https://developer.apple.com/documentation/avfoundation/avplayer/1388096-status + self.avPlayer = AVPlayer() + + //observe the new player instance + self.playerObserver.setPlayer(player: self.avPlayer) + self.playerTimeObserver.setPlayer(player: self.avPlayer, periodicObserverTimeInterval: timeEventFrequency.getTime()) + self.playerTimeObserver.registerForPeriodicTimeEvents() + + //have delegates reset the audio session to whatever they intend to use + self.delegate?.AVWrapperResetAudioSession() + } } extension AVPlayerWrapper: AVPlayerObserverDelegate { @@ -236,6 +260,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate { case .failed: self.delegate?.AVWrapper(failedWithError: avPlayer.error) + print("player status is .failed, will need to reset before playing again") break case .unknown: diff --git a/SwiftAudio/Classes/AVPlayerWrapper/AVPlayerWrapperDelegate.swift b/SwiftAudio/Classes/AVPlayerWrapper/AVPlayerWrapperDelegate.swift index ee7bee8..380caba 100644 --- a/SwiftAudio/Classes/AVPlayerWrapper/AVPlayerWrapperDelegate.swift +++ b/SwiftAudio/Classes/AVPlayerWrapper/AVPlayerWrapperDelegate.swift @@ -16,5 +16,5 @@ protocol AVPlayerWrapperDelegate: class { func AVWrapper(seekTo seconds: Int, didFinish: Bool) func AVWrapper(didUpdateDuration duration: Double) func AVWrapperItemDidPlayToEndTime() - + func AVWrapperResetAudioSession() } diff --git a/SwiftAudio/Classes/AudioPlayer.swift b/SwiftAudio/Classes/AudioPlayer.swift index 7f1efb5..664fc60 100755 --- a/SwiftAudio/Classes/AudioPlayer.swift +++ b/SwiftAudio/Classes/AudioPlayer.swift @@ -25,6 +25,7 @@ public protocol AudioPlayerDelegate: class { func audioPlayer(didUpdateDuration duration: Double) + func audioPlayerResetAudioSession() } public class AudioPlayer: AVPlayerWrapperDelegate { @@ -367,4 +368,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate { self.delegate?.audioPlayer(itemPlaybackEndedWithReason: .playedUntilEnd) } + func AVWrapperResetAudioSession() { + self.delegate?.audioPlayerResetAudioSession() + } } diff --git a/SwiftAudio/Classes/Observer/AVPlayerObserver.swift b/SwiftAudio/Classes/Observer/AVPlayerObserver.swift index 3812a29..efcc2c6 100644 --- a/SwiftAudio/Classes/Observer/AVPlayerObserver.swift +++ b/SwiftAudio/Classes/Observer/AVPlayerObserver.swift @@ -36,7 +36,7 @@ class AVPlayerObserver: NSObject { static let timeControlStatus = #keyPath(AVPlayer.timeControlStatus) } - let player: AVPlayer + var player: AVPlayer private let statusChangeOptions: NSKeyValueObservingOptions = [.new, .initial] private let timeControlStatusChangeOptions: NSKeyValueObservingOptions = [.new] var isObserving: Bool = false @@ -54,6 +54,16 @@ class AVPlayerObserver: NSObject { } } + func setPlayer(player: AVPlayer) { + if self.isObserving { + self.player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context) + self.player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context) + } + + self.player = player + self.isObserving = false + } + /** Start receiving events from this observer. diff --git a/SwiftAudio/Classes/Observer/AVPlayerTimeObserver.swift b/SwiftAudio/Classes/Observer/AVPlayerTimeObserver.swift index c30a0e0..9a5bf41 100644 --- a/SwiftAudio/Classes/Observer/AVPlayerTimeObserver.swift +++ b/SwiftAudio/Classes/Observer/AVPlayerTimeObserver.swift @@ -27,7 +27,7 @@ class AVPlayerTimeObserver { var boundaryTimeStartObserverToken: Any? var periodicTimeObserverToken: Any? - private let player: AVPlayer + private var player: AVPlayer /// The frequence to receive periodic time events. /// Setting this to a new value will trigger a re-registering to the periodic events of the player. @@ -45,6 +45,11 @@ class AVPlayerTimeObserver { self.player = player self.periodicObserverTimeInterval = periodicObserverTimeInterval } + + func setPlayer(player: AVPlayer, periodicObserverTimeInterval: CMTime) { + self.player = player + self.periodicObserverTimeInterval = periodicObserverTimeInterval + } /** Will register for the AVPlayer BoundaryTimeEvents, to trigger start and complete events.