Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recover from AVPlayer failed #60

Merged
merged 7 commits into from
May 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Example/SwiftAudio/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class ViewController: UIViewController {
controller.player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed)
controller.player.event.seek.addListener(self, handleAudioPlayerDidSeek)
controller.player.event.updateDuration.addListener(self, handleAudioPlayerUpdateDuration)
controller.player.event.didRecreateAVPlayer.addListener(self, handleAVPlayerRecreated)
}

@IBAction func togglePlay(_ sender: Any) {
Expand Down Expand Up @@ -116,4 +117,8 @@ class ViewController: UIViewController {
}
}

func handleAVPlayerRecreated() {
try? controller.audioSessionController.set(category: .playback)
}

}
18 changes: 17 additions & 1 deletion Example/Tests/AVPlayerObserverTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
beforeEach {
player = AVPlayer()
player.volume = 0.0
observer = AVPlayerObserver(player: player)
observer = AVPlayerObserver()
observer.player = player
observer.delegate = self
}

it("should not be observing", closure: {
expect(observer.isObserving).to(beFalse())
})

context("when observing has started", {
beforeEach {
observer.startObserving()
Expand Down Expand Up @@ -54,6 +59,17 @@ class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
expect(observer.isObserving).toEventually(beTrue())
})
})

context("when stopping observing", closure: {

beforeEach {
observer.stopObserving()
}

it("should not be observing", closure: {
expect(observer.isObserving).to(beFalse())
})
})
})

}
Expand Down
3 changes: 2 additions & 1 deletion Example/Tests/AVPlayerTimeObserverTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ class AVPlayerTimeObserverTests: QuickSpec {
player = AVPlayer()
player.automaticallyWaitsToMinimizeStalling = false
player.volume = 0
observer = AVPlayerTimeObserver(player: player, periodicObserverTimeInterval: TimeEventFrequency.everyQuarterSecond.getTime())
observer = AVPlayerTimeObserver(periodicObserverTimeInterval: TimeEventFrequency.everyQuarterSecond.getTime())
observer.player = player
}

context("has started boundary time observing", {
Expand Down
11 changes: 7 additions & 4 deletions Example/Tests/AVPlayerWrapperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ class AVPlayerWrapperTests: QuickSpec {
var wrapper: AVPlayerWrapper!

beforeEach {
let player = AVPlayer()
player.automaticallyWaitsToMinimizeStalling = false
player.volume = 0.0
wrapper = AVPlayerWrapper(avPlayer: player)
wrapper = AVPlayerWrapper()
wrapper.automaticallyWaitsToMinimizeStalling = false
wrapper.volume = 0.0
wrapper.bufferDuration = 0.0001
}

Expand Down Expand Up @@ -235,6 +234,10 @@ class AVPlayerWrapperTests: QuickSpec {
}

class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
func AVWrapperDidRecreateAVPlayer() {

}

func AVWrapperItemDidPlayToEndTime() {

}
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ SwiftAudio is available through [CocoaPods](http://cocoapods.org). To install
it, simply add the following line to your Podfile:

```ruby
pod 'SwiftAudio', '~> 0.8.0'
pod 'SwiftAudio', '~> 0.9.0'
```

### Carthage
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
```ruby
github "jorgenhenrichsen/SwiftAudio" ~> 0.8.0
github "jorgenhenrichsen/SwiftAudio" ~> 0.9.0
```
Then follow the rest of Carthage instructions on [adding a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).

Expand Down
2 changes: 1 addition & 1 deletion SwiftAudio.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Pod::Spec.new do |s|
s.name = 'SwiftAudio'
s.version = '0.8.0'
s.version = '0.9.0'
s.summary = 'Easy audio streaming for iOS'

# This description is used to generate tags and improve search results.
Expand Down
26 changes: 21 additions & 5 deletions SwiftAudio/Classes/AVPlayerWrapper/AVPlayerWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {

// MARK: - Properties

let avPlayer: AVPlayer
var avPlayer: AVPlayer
let playerObserver: AVPlayerObserver
let playerTimeObserver: AVPlayerTimeObserver
let playerItemNotificationObserver: AVPlayerItemNotificationObserver
Expand All @@ -46,10 +46,12 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
}
}

public init(avPlayer: AVPlayer = AVPlayer()) {
self.avPlayer = avPlayer
self.playerObserver = AVPlayerObserver(player: avPlayer)
self.playerTimeObserver = AVPlayerTimeObserver(player: avPlayer, periodicObserverTimeInterval: timeEventFrequency.getTime())
public init() {
self.avPlayer = AVPlayer()
self.playerObserver = AVPlayerObserver()
self.playerObserver.player = avPlayer
self.playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime())
self.playerTimeObserver.player = avPlayer
self.playerItemNotificationObserver = AVPlayerItemNotificationObserver()
self.playerItemObserver = AVPlayerItemObserver()

Expand Down Expand Up @@ -167,6 +169,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
func load(from url: URL, playWhenReady: Bool) {
reset(soft: true)
_playWhenReady = playWhenReady

if currentItem?.status == .failed {
recreateAVPlayer()
}

// Set item
let currentAsset = AVURLAsset(url: url)
Expand Down Expand Up @@ -199,6 +205,16 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
}
}

/// Will recreate the AVPlayer instance. Used when the current one fails.
private func recreateAVPlayer() {
let player = AVPlayer()
playerObserver.player = player
playerTimeObserver.player = player
playerTimeObserver.registerForPeriodicTimeEvents()
avPlayer = player
delegate?.AVWrapperDidRecreateAVPlayer()
}

}

extension AVPlayerWrapper: AVPlayerObserverDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ protocol AVPlayerWrapperDelegate: class {
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
func AVWrapper(didUpdateDuration duration: Double)
func AVWrapperItemDidPlayToEndTime()
func AVWrapperDidRecreateAVPlayer()

}
9 changes: 6 additions & 3 deletions SwiftAudio/Classes/AudioPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,9 @@ public class AudioPlayer: AVPlayerWrapperDelegate {

- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
*/
public init(avPlayer: AVPlayer = AVPlayer(),
nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
public init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
remoteCommandController: RemoteCommandController = RemoteCommandController()) {
self._wrapper = AVPlayerWrapper(avPlayer: avPlayer)
self._wrapper = AVPlayerWrapper()
self.nowPlayingInfoController = nowPlayingInfoController
self.remoteCommandController = remoteCommandController

Expand Down Expand Up @@ -341,4 +340,8 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
self.event.playbackEnd.emit(data: .playedUntilEnd)
}

func AVWrapperDidRecreateAVPlayer() {
self.event.didRecreateAVPlayer.emit(data: ())
}

}
11 changes: 10 additions & 1 deletion SwiftAudio/Classes/Event.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ extension AudioPlayer {
public typealias FailEventData = (Error?)
public typealias SeekEventData = (seconds: Int, didFinish: Bool)
public typealias UpdateDurationEventData = (Double)
public typealias DidRecreateAVPlayerEventData = ()

public struct EventHolder {

Expand All @@ -37,7 +38,8 @@ extension AudioPlayer {
public let secondElapse: AudioPlayer.Event<SecondElapseEventData> = AudioPlayer.Event()

/**
Emitted when the player encounters an error.
Emitted when the player encounters an error. This will ultimately result in the AVPlayer instance to be recreated.
If this event is emitted, it means you will need to load a new item in some way. Calling play() will not resume playback.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let fail: AudioPlayer.Event<FailEventData> = AudioPlayer.Event()
Expand All @@ -54,6 +56,13 @@ extension AudioPlayer {
*/
public let updateDuration: AudioPlayer.Event<UpdateDurationEventData> = AudioPlayer.Event()

/**
Emitted when the underlying AVPlayer instance is recreated. Recreation happens if the current player fails.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
- Note: It can be necessary to set the AVAudioSession's category again when this event is emitted.
*/
public let didRecreateAVPlayer: AudioPlayer.Event<()> = AudioPlayer.Event()

}

public typealias EventClosure<EventData> = (EventData) -> Void
Expand Down
40 changes: 22 additions & 18 deletions SwiftAudio/Classes/Observer/AVPlayerObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,39 +36,43 @@ class AVPlayerObserver: NSObject {
static let timeControlStatus = #keyPath(AVPlayer.timeControlStatus)
}

let player: AVPlayer

private let statusChangeOptions: NSKeyValueObservingOptions = [.new, .initial]
private let timeControlStatusChangeOptions: NSKeyValueObservingOptions = [.new]
var isObserving: Bool = false

weak var delegate: AVPlayerObserverDelegate?

init(player: AVPlayer) {
self.player = player
weak var player: AVPlayer? {
willSet {
self.stopObserving()
}
}

deinit {
if self.isObserving {
self.player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context)
self.player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context)
}
self.stopObserving()
}

/**
Start receiving events from this observer.

- Important: If this observer is already receiving events, it will first be removed. Never remove this observer manually.
*/
func startObserving() {
main.async {
if self.isObserving {
self.player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context)
self.player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context)
}
self.isObserving = true
self.player.addObserver(self, forKeyPath: AVPlayerKeyPath.status, options: self.statusChangeOptions, context: &AVPlayerObserver.context)
self.player.addObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, options: self.timeControlStatusChangeOptions, context: &AVPlayerObserver.context)
guard let player = player else {
return
}
self.stopObserving()
self.isObserving = true
player.addObserver(self, forKeyPath: AVPlayerKeyPath.status, options: self.statusChangeOptions, context: &AVPlayerObserver.context)
player.addObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, options: self.timeControlStatusChangeOptions, context: &AVPlayerObserver.context)
}

func stopObserving() {
guard let player = player, self.isObserving else {
return
}
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context)
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context)
self.isObserving = false

}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
Expand Down
35 changes: 22 additions & 13 deletions SwiftAudio/Classes/Observer/AVPlayerTimeObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ class AVPlayerTimeObserver {
var boundaryTimeStartObserverToken: Any?
var periodicTimeObserverToken: Any?

private let player: AVPlayer
weak var player: AVPlayer? {
willSet {
unregisterForBoundaryTimeEvents()
unregisterForPeriodicEvents()
}
}

/// 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 @@ -41,18 +46,19 @@ class AVPlayerTimeObserver {

weak var delegate: AVPlayerTimeObserverDelegate?

init(player: AVPlayer, periodicObserverTimeInterval: CMTime) {
self.player = player
init(periodicObserverTimeInterval: CMTime) {
self.periodicObserverTimeInterval = periodicObserverTimeInterval
}

/**
Will register for the AVPlayer BoundaryTimeEvents, to trigger start and complete events.
*/
func registerForBoundaryTimeEvents() {

guard let player = player else {
return
}
unregisterForBoundaryTimeEvents()
let startBoundaryTimes: [NSValue] = [AVPlayerTimeObserver.startBoundaryTime].map({NSValue(time: $0)})

boundaryTimeStartObserverToken = player.addBoundaryTimeObserver(forTimes: startBoundaryTimes, queue: nil, using: { [weak self] in
self?.delegate?.audioDidStart()
})
Expand All @@ -62,17 +68,21 @@ class AVPlayerTimeObserver {
Unregister from the boundary events of the player.
*/
func unregisterForBoundaryTimeEvents() {
if let boundaryTimeStartObserverToken = boundaryTimeStartObserverToken {
player.removeTimeObserver(boundaryTimeStartObserverToken)
self.boundaryTimeStartObserverToken = nil
guard let player = player, let boundaryTimeStartObserverToken = boundaryTimeStartObserverToken else {
return
}
player.removeTimeObserver(boundaryTimeStartObserverToken)
self.boundaryTimeStartObserverToken = nil
}

/**
Start observing periodic time events.
Will trigger unregisterForPeriodicEvents() first to avoid multiple subscriptions.
*/
func registerForPeriodicTimeEvents() {
guard let player = player else {
return
}
unregisterForPeriodicEvents()
periodicTimeObserverToken = player.addPeriodicTimeObserver(forInterval: periodicObserverTimeInterval, queue: nil, using: { (time) in
self.delegate?.timeEvent(time: time)
Expand All @@ -83,12 +93,11 @@ class AVPlayerTimeObserver {
Unregister for periodic events.
*/
func unregisterForPeriodicEvents() {
if let periodicTimeObserverToken = periodicTimeObserverToken {
self.player.removeTimeObserver(periodicTimeObserverToken)
self.periodicTimeObserverToken = nil
guard let player = player, let periodicTimeObserverToken = periodicTimeObserverToken else {
return
}
player.removeTimeObserver(periodicTimeObserverToken)
self.periodicTimeObserverToken = nil
}



}