Compare commits

..

3 Commits

Author SHA1 Message Date
Jørgen Henrichsen e6ca8c9b1e Update README.
Add part about using Combine for events.
2019-06-21 13:48:11 +02:00
Jørgen Henrichsen 83318d3ff0 Make all error enums public. Add EventError. 2019-06-21 13:28:51 +02:00
Jørgen Henrichsen 3c5f858a94 Add support for Combine in events.
Add an `EventPublisher` and a publisher property in the `Event`-class.
Can be used to subscribe to events with a `Subscriber`.
2019-06-21 13:22:09 +02:00
12 changed files with 111 additions and 128 deletions
+24 -18
View File
@@ -68,9 +68,9 @@ class AudioPlayerTests: XCTestCase {
func test_AudioPlayer__state__pausing_source__should_be_paused() {
let expectation = XCTestExpectation()
listener.stateUpdate = { [weak audioPlayer] state in
listener.stateUpdate = { state in
switch state {
case .playing: audioPlayer?.pause()
case .playing: self.audioPlayer.pause()
case .paused: expectation.fulfill()
default: break
}
@@ -82,11 +82,11 @@ class AudioPlayerTests: XCTestCase {
func test_AudioPlayer__state__stopping_source__should_be_idle() {
let expectation = XCTestExpectation()
var hasBeenPlaying: Bool = false
listener.stateUpdate = { [weak audioPlayer] state in
listener.stateUpdate = { state in
switch state {
case .playing:
hasBeenPlaying = true
audioPlayer?.stop()
self.audioPlayer.stop()
case .idle:
if hasBeenPlaying {
expectation.fulfill()
@@ -117,6 +117,22 @@ class AudioPlayerTests: XCTestCase {
// wait(for: [expectation], timeout: 20.0)
// }
func test_AudioPlayer__currentTime__when_loading_source_with_intial_time__should_be_equal_to_initial_time() {
let expectation = XCTestExpectation()
let item = DefaultAudioItemInitialTime(audioUrl: LongSource.path, artist: nil, title: nil, albumTitle: nil, sourceType: .file, artwork: nil, initialTime: 4.0)
listener.stateUpdate = { state in
switch state {
case .ready:
if self.audioPlayer.currentTime == item.getInitialTime() {
expectation.fulfill()
}
default: break
}
}
try? audioPlayer.load(item: item, playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
// MARK: - Rate
func test_AudioPlayer__rate__should_be_0() {
@@ -125,11 +141,10 @@ class AudioPlayerTests: XCTestCase {
func test_AudioPlayer__rate__playing_source__should_be_1() {
let expectation = XCTestExpectation()
listener.stateUpdate = { [weak audioPlayer] state in
guard let audioPlayer = audioPlayer else { return }
listener.stateUpdate = { state in
switch state {
case .playing:
if audioPlayer.rate == 1.0 {
if self.audioPlayer.rate == 1.0 {
expectation.fulfill()
}
default: break
@@ -147,11 +162,10 @@ class AudioPlayerTests: XCTestCase {
func test_AudioPlayer__currentItem__loading_source__should_not_be_nil() {
let expectation = XCTestExpectation()
listener.stateUpdate = { [weak audioPlayer] state in
guard let audioPlayer = audioPlayer else { return }
listener.stateUpdate = { state in
switch state {
case .ready:
if audioPlayer.currentItem != nil {
if self.audioPlayer.currentItem != nil {
expectation.fulfill()
}
default: break
@@ -177,20 +191,12 @@ class AudioPlayerEventListener {
var secondsElapse: ((_ seconds: TimeInterval) -> Void)?
var seekCompletion: (() -> Void)?
weak var audioPlayer: AudioPlayer?
init(audioPlayer: AudioPlayer) {
audioPlayer.event.stateChange.addListener(self, handleDidUpdateState)
audioPlayer.event.seek.addListener(self, handleSeek)
audioPlayer.event.secondElapse.addListener(self, handleSecondsElapse)
}
deinit {
audioPlayer?.event.stateChange.removeListener(self)
audioPlayer?.event.seek.removeListener(self)
audioPlayer?.event.secondElapse.removeListener(self)
}
func handleDidUpdateState(state: AudioPlayerState) {
self.state = state
}
+14 -2
View File
@@ -25,13 +25,13 @@ SwiftAudio is available through [CocoaPods](http://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod 'SwiftAudio', '~> 0.9.3'
pod 'SwiftAudio', '~> 0.9.2'
```
### Carthage
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
```ruby
github "jorgenhenrichsen/SwiftAudio" ~> 0.9.3
github "jorgenhenrichsen/SwiftAudio" ~> 0.9.2
```
Then follow the rest of Carthage instructions on [adding a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
@@ -63,6 +63,18 @@ class MyCustomViewController: UIViewController {
}
```
If you want to use the [Combine](https://developer.apple.com/documentation/combine) framework for events, each event in the `AudioPlayer` has an `EventPublisher` that can be subscribed to:
```swift
let audioPlayer = AudioPlayer()
audioPlayer.event.stateChange.publisher.subscribe(subscriber: someSubscriber)
/// Using a Sink
audioPlayer.event.stateChange.publisher.sink { state in
/// Handle state change here.
}
```
**Important**: This requires iOS version equal to or later than 13.0. If an application needs to support older iOS versions it is recommended to use the regular `Event.addListener(listener:)` method.
#### QueuedAudioPlayer
The `QueuedAudioPlayer` is a subclass of `AudioPlayer` that maintains a queue of audio tracks.
```swift
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudio'
s.version = '0.9.3'
s.version = '0.9.2'
s.summary = 'Easy audio streaming for iOS'
# This description is used to generate tags and improve search results.
+5 -3
View File
@@ -10,18 +10,20 @@ import Foundation
public struct APError {
enum LoadError: Error {
public enum LoadError: Error {
case invalidSourceUrl(String)
}
enum PlaybackError: Error {
public enum PlaybackError: Error {
case noLoadedItem
}
enum QueueError: Error {
public enum QueueError: Error {
case noPreviousItem
case noNextItem
case invalidIndex(index: Int, message: String)
}
public enum EventError: Error {}
}
@@ -168,8 +168,6 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
}
}
func load(from url: URL, playWhenReady: Bool) {
reset(soft: true)
_playWhenReady = playWhenReady
@@ -178,15 +176,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
recreateAVPlayer()
}
// Set item
self._pendingAsset = AVURLAsset(url: url)
if let pendingAsset = _pendingAsset {
pendingAsset.loadValuesAsynchronously(forKeys: [Constants.assetPlayableKey], completionHandler: { [weak self] in
guard let self = self else {
return
}
pendingAsset.loadValuesAsynchronously(forKeys: [Constants.assetPlayableKey], completionHandler: {
var error: NSError? = nil
let status = pendingAsset.statusOfValue(forKey: Constants.assetPlayableKey, error: &error)
@@ -208,6 +201,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
break
case .failed:
// print("load asset failed")
if isPendingAsset {
self.delegate?.AVWrapper(failedWithError: error)
self._pendingAsset = nil
@@ -215,6 +209,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
break
case .cancelled:
// print("load asset cancelled")
break
default:
@@ -238,8 +233,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
playerTimeObserver.unregisterForBoundaryTimeEvents()
playerItemNotificationObserver.stopObservingCurrentItem()
self._pendingAsset?.cancelLoading()
self._pendingAsset = nil
if self._pendingAsset != nil {
self._pendingAsset?.cancelLoading()
self._pendingAsset = nil
}
if !soft {
avPlayer.replaceCurrentItem(with: nil)
+31 -2
View File
@@ -6,6 +6,7 @@
//
import Foundation
import Combine
extension AudioPlayer {
@@ -69,7 +70,7 @@ extension AudioPlayer {
class Invoker<EventData> {
// Signals false if the listener object is nil
/// Signals false if the listener object is nil. If `false` is signaled, the invoker should not be retained.
let invoke: (EventData) -> Bool
weak var listener: AnyObject?
@@ -114,7 +115,7 @@ extension AudioPlayer {
self.invokersSemaphore.signal()
}
}
func emit(data: EventData) {
eventQueue.async {
self.invokersSemaphore.wait()
@@ -125,6 +126,34 @@ extension AudioPlayer {
}
}
/**
The publisher for this event. Use this for subscription through the Combine framework.
*/
@available(iOS 13.0, *)
public lazy var publisher: EventPublisher<EventData> = EventPublisher(event: self)
}
}
@available(iOS 13.0, *)
extension AudioPlayer {
public class EventPublisher<EventData>: Publisher {
public typealias Output = EventData
public typealias Failure = APError.EventError
private weak var event: Event<EventData>?
init(event: Event<EventData>) {
self.event = event
}
public func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
event?.addListener(self) { (data) in
let _ = subscriber.receive(data)
}
}
}
}
@@ -22,15 +22,9 @@ class AVPlayerItemNotificationObserver {
private let notificationCenter: NotificationCenter = NotificationCenter.default
private(set) weak var observingItem: AVPlayerItem?
weak var observingItem: AVPlayerItem?
weak var delegate: AVPlayerItemNotificationObserverDelegate?
private(set) var isObserving: Bool = false
deinit {
stopObservingCurrentItem()
}
/**
Will start observing notifications from an item.
@@ -40,7 +34,6 @@ class AVPlayerItemNotificationObserver {
func startObserving(item: AVPlayerItem) {
stopObservingCurrentItem()
observingItem = item
isObserving = true
notificationCenter.addObserver(self, selector: #selector(itemDidPlayToEndTime), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item)
}
@@ -48,12 +41,10 @@ class AVPlayerItemNotificationObserver {
Stop receiving notifications for the current item.
*/
func stopObservingCurrentItem() {
guard let observingItem = observingItem, isObserving else {
return
if let observingItem = observingItem {
notificationCenter.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observingItem)
}
self.notificationCenter.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observingItem)
self.observingItem = nil
self.isObserving = false
observingItem = nil
}
@objc private func itemDidPlayToEndTime() {
@@ -30,34 +30,37 @@ class AVPlayerItemObserver: NSObject {
static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges)
}
private(set) var isObserving: Bool = false
var isObserving: Bool = false
private(set) weak var observingItem: AVPlayerItem?
weak var observingItem: AVPlayerItem?
weak var delegate: AVPlayerItemObserverDelegate?
deinit {
stopObservingCurrentItem()
if self.isObserving {
stopObservingCurrentItem()
}
}
/**
Start observing an item. Will remove self as observer from old item, if any.
Start observing an item. Will remove self as observer from old item.
- parameter item: The player item to observe.
*/
func startObserving(item: AVPlayerItem) {
self.stopObservingCurrentItem()
self.isObserving = true
self.observingItem = item
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context)
main.async {
if self.isObserving {
self.stopObservingCurrentItem()
}
self.isObserving = true
self.observingItem = item
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context)
}
}
func stopObservingCurrentItem() {
guard let observingItem = observingItem, isObserving else {
return
}
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context)
observingItem?.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
observingItem?.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context)
self.isObserving = false
self.observingItem = nil
}
@@ -36,9 +36,10 @@ class AVPlayerObserver: NSObject {
static let timeControlStatus = #keyPath(AVPlayer.timeControlStatus)
}
private let statusChangeOptions: NSKeyValueObservingOptions = [.new, .initial]
private let timeControlStatusChangeOptions: NSKeyValueObservingOptions = [.new]
private(set) var isObserving: Bool = false
var isObserving: Bool = false
weak var delegate: AVPlayerObserverDelegate?
weak var player: AVPlayer? {
@@ -65,12 +66,13 @@ class AVPlayerObserver: NSObject {
}
func stopObserving() {
guard let player = player, isObserving else {
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?) {
@@ -105,6 +107,7 @@ class AVPlayerObserver: NSObject {
}
private func handleTimeControlStatusChange(_ change: [NSKeyValueChangeKey: Any]?) {
let status: AVPlayer.TimeControlStatus
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayer.TimeControlStatus(rawValue: statusNumber.intValue)!
@@ -10,8 +10,10 @@ import Foundation
import AVFoundation
protocol AVPlayerTimeObserverDelegate: class {
func audioDidStart()
func timeEvent(time: CMTime)
}
/**
@@ -48,11 +50,6 @@ class AVPlayerTimeObserver {
self.periodicObserverTimeInterval = periodicObserverTimeInterval
}
deinit {
unregisterForPeriodicEvents()
unregisterForBoundaryTimeEvents()
}
/**
Will register for the AVPlayer BoundaryTimeEvents, to trigger start and complete events.
*/
@@ -79,30 +79,6 @@ public struct SkipIntervalCommand: RemoteCommandProtocol {
}
public struct FeedbackCommand: RemoteCommandProtocol {
public static let like = FeedbackCommand(id: "Like", commandKeyPath: \MPRemoteCommandCenter.likeCommand, handlerKeyPath: \RemoteCommandController.handleLikeCommand)
public static let dislike = FeedbackCommand(id: "Dislike", commandKeyPath: \MPRemoteCommandCenter.dislikeCommand, handlerKeyPath: \RemoteCommandController.handleDislikeCommand)
public static let bookmark = FeedbackCommand(id: "Bookmark", commandKeyPath: \MPRemoteCommandCenter.bookmarkCommand, handlerKeyPath: \RemoteCommandController.handleBookmarkCommand)
public typealias Command = MPFeedbackCommand
public let id: String
public var commandKeyPath: KeyPath<MPRemoteCommandCenter, MPFeedbackCommand>
public var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler>
func set(isActive: Bool, localizedTitle: String, localizedShortTitle: String) -> FeedbackCommand {
MPRemoteCommandCenter.shared()[keyPath: commandKeyPath].isActive = isActive
MPRemoteCommandCenter.shared()[keyPath: commandKeyPath].localizedTitle = localizedTitle
MPRemoteCommandCenter.shared()[keyPath: commandKeyPath].localizedShortTitle = localizedShortTitle
return self
}
}
public enum RemoteCommand {
case play
@@ -123,12 +99,6 @@ public enum RemoteCommand {
case skipBackward(preferredIntervals: [NSNumber])
case like(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
case dislike(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
case bookmark(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
/**
All values in an array for convenience.
Don't use for associated values.
@@ -144,9 +114,6 @@ public enum RemoteCommand {
.changePlaybackPosition,
.skipForward(preferredIntervals: []),
.skipBackward(preferredIntervals: []),
.like(isActive: false, localizedTitle: "", localizedShortTitle: ""),
.dislike(isActive: false, localizedTitle: "", localizedShortTitle: ""),
.bookmark(isActive: false, localizedTitle: "", localizedShortTitle: "")
]
}
@@ -64,12 +64,6 @@ public class RemoteCommandController {
case .changePlaybackPosition: self.enableCommand(ChangePlaybackPositionCommand.changePlaybackPosition)
case .skipForward(let preferredIntervals): self.enableCommand(SkipIntervalCommand.skipForward.set(preferredIntervals: preferredIntervals))
case .skipBackward(let preferredIntervals): self.enableCommand(SkipIntervalCommand.skipBackward.set(preferredIntervals: preferredIntervals))
case .like(let isActive, let localizedTitle, let localizedShortTitle):
self.enableCommand(FeedbackCommand.like.set(isActive: isActive, localizedTitle: localizedTitle, localizedShortTitle: localizedShortTitle))
case .dislike(let isActive, let localizedTitle, let localizedShortTitle):
self.enableCommand(FeedbackCommand.dislike.set(isActive: isActive, localizedTitle: localizedTitle, localizedShortTitle: localizedShortTitle))
case .bookmark(let isActive, let localizedTitle, let localizedShortTitle):
self.enableCommand(FeedbackCommand.bookmark.set(isActive: isActive, localizedTitle: localizedTitle, localizedShortTitle: localizedShortTitle))
}
}
@@ -84,9 +78,6 @@ public class RemoteCommandController {
case .changePlaybackPosition: self.disableCommand(ChangePlaybackPositionCommand.changePlaybackPosition)
case .skipForward(_): self.disableCommand(SkipIntervalCommand.skipForward)
case .skipBackward(_): self.disableCommand(SkipIntervalCommand.skipBackward)
case .like(_, _, _): self.disableCommand(FeedbackCommand.like)
case .dislike(_, _, _): self.disableCommand(FeedbackCommand.dislike)
case .bookmark(_, _, _): self.disableCommand(FeedbackCommand.bookmark)
}
}
@@ -101,9 +92,6 @@ public class RemoteCommandController {
public lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = self.handleChangePlaybackPositionCommandDefault
public lazy var handleNextTrackCommand: RemoteCommandHandler = self.handleNextTrackCommandDefault
public lazy var handlePreviousTrackCommand: RemoteCommandHandler = self.handlePreviousTrackCommandDefault
public lazy var handleLikeCommand: RemoteCommandHandler = self.handleLikeCommandDefault
public lazy var handleDislikeCommand: RemoteCommandHandler = self.handleDislikeCommandDefault
public lazy var handleBookmarkCommand: RemoteCommandHandler = self.handleBookmarkCommandDefault
private func handlePlayCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let audioPlayer = self.audioPlayer {
@@ -192,18 +180,6 @@ public class RemoteCommandController {
return MPRemoteCommandHandlerStatus.commandFailed
}
private func handleLikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
return MPRemoteCommandHandlerStatus.success
}
private func handleDislikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
return MPRemoteCommandHandlerStatus.success
}
private func handleBookmarkCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
return MPRemoteCommandHandlerStatus.success
}
private func getRemoteCommandHandlerStatus(forError error: Error) -> MPRemoteCommandHandlerStatus {
if let error = error as? APError.LoadError {
switch error {