Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e50ea48d6 | |||
| 92a804e9e4 | |||
| af4635d047 | |||
| addebef7f9 | |||
| 46022ef0d6 | |||
| 386dc5202c | |||
| 17166093c2 | |||
| c06b8ce64c |
@@ -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) {
|
||||
@@ -116,4 +117,8 @@ class ViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func handleAVPlayerRecreated() {
|
||||
try? controller.audioSessionController.set(category: .playback)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -235,6 +234,10 @@ class AVPlayerWrapperTests: QuickSpec {
|
||||
}
|
||||
|
||||
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
func AVWrapperDidRecreateAVPlayer() {
|
||||
|
||||
}
|
||||
|
||||
func AVWrapperItemDidPlayToEndTime() {
|
||||
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
@@ -26,7 +26,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
let avPlayer: AVPlayer
|
||||
var avPlayer: AVPlayer
|
||||
let playerObserver: AVPlayerObserver
|
||||
let playerTimeObserver: AVPlayerTimeObserver
|
||||
let playerItemNotificationObserver: AVPlayerItemNotificationObserver
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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 {
|
||||
|
||||
@@ -16,5 +16,6 @@ protocol AVPlayerWrapperDelegate: class {
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
|
||||
func AVWrapper(didUpdateDuration duration: Double)
|
||||
func AVWrapperItemDidPlayToEndTime()
|
||||
func AVWrapperDidRecreateAVPlayer()
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -341,4 +340,8 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
self.event.playbackEnd.emit(data: .playedUntilEnd)
|
||||
}
|
||||
|
||||
func AVWrapperDidRecreateAVPlayer() {
|
||||
self.event.didRecreateAVPlayer.emit(data: ())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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.
|
||||
@@ -41,8 +46,7 @@ class AVPlayerTimeObserver {
|
||||
|
||||
weak var delegate: AVPlayerTimeObserverDelegate?
|
||||
|
||||
init(player: AVPlayer, periodicObserverTimeInterval: CMTime) {
|
||||
self.player = player
|
||||
init(periodicObserverTimeInterval: CMTime) {
|
||||
self.periodicObserverTimeInterval = periodicObserverTimeInterval
|
||||
}
|
||||
|
||||
@@ -50,9 +54,11 @@ class AVPlayerTimeObserver {
|
||||
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()
|
||||
})
|
||||
@@ -62,10 +68,11 @@ 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
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +80,9 @@ class AVPlayerTimeObserver {
|
||||
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)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user