Skip to content

Commit

Permalink
Different approach to re-creating the AVPlayer, and resetting the AVA…
Browse files Browse the repository at this point in the history
…udioSession, when a track causes a failure.
  • Loading branch information
curiousdustin committed Apr 25, 2019
1 parent bb015f0 commit 6a06eba
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 9 deletions.
34 changes: 32 additions & 2 deletions Example/SwiftAudio/AudioController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,21 @@ import Foundation
import SwiftAudio


class AudioController {
class AudioController : AudioPlayerDelegate {

static let shared = AudioController()
let player: QueuedAudioPlayer
let audioSessionController = AudioSessionController.shared

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"))
]
Expand All @@ -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)
}
}
33 changes: 29 additions & 4 deletions SwiftAudio/Classes/AVPlayerWrapper/AVPlayerWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ protocol AVPlayerWrapperDelegate: class {
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
func AVWrapper(didUpdateDuration duration: Double)
func AVWrapperItemDidPlayToEndTime()

func AVWrapperResetAudioSession()
}
4 changes: 4 additions & 0 deletions SwiftAudio/Classes/AudioPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public protocol AudioPlayerDelegate: class {

func audioPlayer(didUpdateDuration duration: Double)

func audioPlayerResetAudioSession()
}

public class AudioPlayer: AVPlayerWrapperDelegate {
Expand Down Expand Up @@ -367,4 +368,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
self.delegate?.audioPlayer(itemPlaybackEndedWithReason: .playedUntilEnd)
}

func AVWrapperResetAudioSession() {
self.delegate?.audioPlayerResetAudioSession()
}
}
12 changes: 11 additions & 1 deletion SwiftAudio/Classes/Observer/AVPlayerObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
7 changes: 6 additions & 1 deletion SwiftAudio/Classes/Observer/AVPlayerTimeObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down

0 comments on commit 6a06eba

Please sign in to comment.