Compare commits

..

8 Commits

Author SHA1 Message Date
David Chavez 92554a187c Release 0.15.0 2022-04-01 23:54:08 +02:00
David Chavez 473651f357 Support mp3 embedded chapters 2022-04-01 23:47:46 +02:00
David Chavez db2f3e9af7 Remove obsolete code 2022-04-01 23:22:26 +02:00
David Chavez a9f831a258 Fix bug in addItems at index and add tests 2022-04-01 21:18:52 +02:00
David Chavez cc3840d81e Fix next/previous with repeat modes 2022-04-01 20:47:54 +02:00
David Chavez 5307090ea3 Replace deprecated “timedMetadata" KVO 2022-04-01 17:47:57 +02:00
David Chavez bdaee8b18f Extract more information from interruptions 2022-04-01 00:14:47 +02:00
David Chavez 84d359bc4f Update README.md 2022-02-24 09:14:36 +01:00
16 changed files with 245 additions and 54 deletions
@@ -47,9 +47,9 @@ class AVPlayerItemObserverTests: QuickSpec {
}
class AVPlayerItemObserverDelegateHolder: AVPlayerItemObserverDelegate {
var receivedMetadata: ((_ metadata: [AVMetadataItem]) -> Void)?
func item(didReceiveMetadata metadata: [AVMetadataItem]) {
var receivedMetadata: ((_ metadata: [AVTimedMetadataGroup]) -> Void)?
func item(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
receivedMetadata?(metadata)
}
+1 -1
View File
@@ -193,7 +193,7 @@ class AVPlayerWrapperTests: XCTestCase {
}
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem]) {
func AVWrapper(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
}
@@ -52,11 +52,12 @@ class AudioSessionControllerTests: QuickSpec {
}
describe("its delegate") {
context("when a interruption arrives") {
context("when a ended interruption arrives") {
var delegate: AudioSessionControllerDelegateImplementation!
beforeEach {
let notification = Notification(name: AVAudioSession.interruptionNotification, object: nil, userInfo: [
AVAudioSessionInterruptionTypeKey: UInt(0)
AVAudioSessionInterruptionTypeKey: UInt(0),
AVAudioSessionInterruptionOptionKey: UInt(1),
])
delegate = AudioSessionControllerDelegateImplementation()
audioSessionController.delegate = delegate
@@ -64,7 +65,23 @@ class AudioSessionControllerTests: QuickSpec {
}
it("should eventually be updated with the interruption type") {
expect(delegate.interruptionType).toEventuallyNot(beNil())
expect(delegate.interruptionType).toEventually(equal(InterruptionType.ended(shouldResume: true)))
}
}
context("when a begin interruption arrives") {
var delegate: AudioSessionControllerDelegateImplementation!
beforeEach {
let notification = Notification(name: AVAudioSession.interruptionNotification, object: nil, userInfo: [
AVAudioSessionInterruptionTypeKey: UInt(1),
])
delegate = AudioSessionControllerDelegateImplementation()
audioSessionController.delegate = delegate
audioSessionController.handleInterruption(notification: notification)
}
it("should eventually be updated with the interruption type") {
expect(delegate.interruptionType).toEventually(equal(InterruptionType.began))
}
}
@@ -91,10 +108,9 @@ class AudioSessionControllerTests: QuickSpec {
}
class AudioSessionControllerDelegateImplementation: AudioSessionControllerDelegate {
var interruptionType: InterruptionType? = nil
var interruptionType: AVAudioSession.InterruptionType? = nil
func handleInterruption(type: AVAudioSession.InterruptionType) {
func handleInterruption(type: InterruptionType) {
self.interruptionType = type
}
}
+47
View File
@@ -70,6 +70,53 @@ class QueueManagerTests: QuickSpec {
}
}
describe("when adding at index") {
context("adding item at index 0 when queue is empty") {
it("should add element successfully") {
try manager.addItems([3], at: 0)
expect(manager.current).to(equal(3))
}
}
context("adding item at index") {
beforeEach {
manager.addItems([3, 1])
}
context("current [element count]") {
it("should add element successfully") {
try manager.addItems([5], at: manager.items.count)
expect(manager.items.last).to(equal(5))
}
}
context("before the [current index]") {
it("should add element successfully") {
try manager.addItems([5], at: 0)
expect(manager.current).to(equal(3))
expect(manager.currentIndex).to(equal(1))
}
}
context("after the [current index]") {
it("should add element successfully") {
try manager.addItems([5], at: 1)
expect(manager.current).to(equal(3))
expect(manager.currentIndex).to(equal(0))
}
}
context("at [current index]") {
it("should add element successfully") {
try manager.next()
try manager.addItems([5], at: 1)
expect(manager.current).to(equal(1))
expect(manager.currentIndex).to(equal(2))
}
}
}
}
context("when adding one item") {
+83 -3
View File
@@ -167,10 +167,90 @@ class QueuedAudioPlayerTests: QuickSpec {
}
}
describe("onNext") {
context("player was playing") {
beforeEach {
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()], playWhenReady: true)
}
context("then calling next()") {
beforeEach {
try? audioPlayer.next()
}
it("should go to next item and play") {
expect(audioPlayer.nextItems.count).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
}
}
context("player was paused") {
beforeEach {
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
audioPlayer.pause()
}
context("then calling next()") {
beforeEach {
try? audioPlayer.next()
}
it("should go to next item and play") {
expect(audioPlayer.nextItems.count).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.ready))
}
}
}
}
describe("onPrevious") {
context("player was playing") {
beforeEach {
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()], playWhenReady: true)
try? audioPlayer.next()
}
context("then calling previous()") {
beforeEach {
try? audioPlayer.previous()
}
it("should go to next item and play") {
expect(audioPlayer.nextItems.count).toEventually(equal(1))
expect(audioPlayer.currentIndex).toEventually(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
}
}
context("player was paused") {
beforeEach {
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
try? audioPlayer.next()
audioPlayer.pause()
}
context("then calling previous()") {
beforeEach {
try? audioPlayer.previous()
}
it("should go to next item and play") {
expect(audioPlayer.nextItems.count).toEventually(equal(1))
expect(audioPlayer.currentIndex).toEventually(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.ready))
}
}
}
}
describe("its repeat mode") {
context("when adding 2 items") {
beforeEach {
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()], playWhenReady: true)
}
context("then setting repeat mode off") {
@@ -244,9 +324,9 @@ class QueuedAudioPlayerTests: QuickSpec {
try? audioPlayer.next()
}
it("should move to next item but should not play") {
it("should move to next item and should play") {
expect(audioPlayer.nextItems.count).to(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.ready))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ To see the audio player in action, run the example project!
To run the example project, clone the repo, and run `pod install` from the Example directory first.
## Requirements
iOS 10.0+
iOS 11.0+
## Installation
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioEx'
s.version = '0.14.7'
s.version = '0.15.0'
s.summary = 'Easy audio streaming for iOS'
s.description = <<-DESC
SwiftAudioEx is an audio player written in Swift, making it simpler to work with audio playback from streams and files.
@@ -89,6 +89,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
get { return avPlayer.automaticallyWaitsToMinimizeStalling }
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
}
var willPlayWhenReady: Bool {
return _playWhenReady
}
var currentTime: TimeInterval {
let seconds = avPlayer.currentTime().seconds
@@ -218,8 +222,18 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
self.playerObserver.startObserving()
self.playerItemNotificationObserver.startObserving(item: currentItem)
self.playerItemObserver.startObserving(item: currentItem)
for format in pendingAsset.availableMetadataFormats {
self.delegate?.AVWrapper(didReceiveMetadata: pendingAsset.metadata(forFormat: format))
if pendingAsset.availableChapterLocales.count > 0 {
for locale in pendingAsset.availableChapterLocales {
let chapters = pendingAsset.chapterMetadataGroups(withTitleLocale: locale, containingItemsWithCommonKeys: nil)
self.delegate?.AVWrapper(didReceiveMetadata: chapters)
}
} else {
for format in pendingAsset.availableMetadataFormats {
let timeRange = CMTimeRange(start: CMTime(seconds: 0, preferredTimescale: 1000), end: pendingAsset.duration)
let group = AVTimedMetadataGroup(items: pendingAsset.metadata(forFormat: format), timeRange: timeRange)
self.delegate?.AVWrapper(didReceiveMetadata: [group])
}
}
}
break
@@ -358,8 +372,8 @@ extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
func item(didUpdateDuration duration: Double) {
self.delegate?.AVWrapper(didUpdateDuration: duration)
}
func item(didReceiveMetadata metadata: [AVMetadataItem]) {
func item(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
self.delegate?.AVWrapper(didReceiveMetadata: metadata)
}
@@ -9,14 +9,14 @@ import Foundation
import MediaPlayer
protocol AVPlayerWrapperDelegate: class {
protocol AVPlayerWrapperDelegate: AnyObject {
func AVWrapper(didChangeState state: AVPlayerWrapperState)
func AVWrapper(secondsElapsed seconds: Double)
func AVWrapper(failedWithError error: Error?)
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
func AVWrapper(didUpdateDuration duration: Double)
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem])
func AVWrapper(didReceiveMetadata metadata: [AVTimedMetadataGroup])
func AVWrapperItemDidPlayToEndTime()
func AVWrapperDidRecreateAVPlayer()
@@ -9,9 +9,11 @@ import Foundation
import AVFoundation
protocol AVPlayerWrapperProtocol: class {
protocol AVPlayerWrapperProtocol: AnyObject {
var state: AVPlayerWrapperState { get }
var willPlayWhenReady: Bool { get }
var currentItem: AVPlayerItem? { get }
+6 -2
View File
@@ -52,6 +52,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
// MARK: - Getters from AVPlayerWrapper
internal var willPlayWhenReady: Bool {
return wrapper.willPlayWhenReady
}
/**
The elapsed playback time of the current item.
@@ -365,8 +369,8 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
func AVWrapper(didUpdateDuration duration: Double) {
self.event.updateDuration.emit(data: duration)
}
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem]) {
func AVWrapper(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
self.event.receiveMetadata.emit(data: metadata)
}
@@ -8,11 +8,14 @@
import Foundation
import AVFoundation
public protocol AudioSessionControllerDelegate: class {
func handleInterruption(type: AVAudioSession.InterruptionType)
public enum InterruptionType: Equatable {
case began
case ended(shouldResume: Bool)
}
public protocol AudioSessionControllerDelegate: AnyObject {
func handleInterruption(type: InterruptionType)
}
/**
Simple controller for the `AVAudioSession`. If you need more advanced options, just use the `AVAudioSession` directly.
@@ -112,7 +115,19 @@ public class AudioSessionController {
return
}
self.delegate?.handleInterruption(type: type)
switch type {
case .began:
self.delegate?.handleInterruption(type: .began)
case .ended:
guard let typeValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else {
self.delegate?.handleInterruption(type: .ended(shouldResume: false))
return
}
let options = AVAudioSession.InterruptionOptions(rawValue: typeValue)
self.delegate?.handleInterruption(type: .ended(shouldResume: options.contains(.shouldResume)))
@unknown default: return
}
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ extension AudioPlayer {
public typealias FailEventData = (Error?)
public typealias SeekEventData = (seconds: Int, didFinish: Bool)
public typealias UpdateDurationEventData = (Double)
public typealias MetadataEventData = ([AVMetadataItem])
public typealias MetadataEventData = ([AVTimedMetadataGroup])
public typealias DidRecreateAVPlayerEventData = ()
public typealias QueueIndexEventData = (previousIndex: Int?, newIndex: Int?)
@@ -8,7 +8,7 @@
import Foundation
import AVFoundation
protocol AVPlayerItemObserverDelegate: class {
protocol AVPlayerItemObserverDelegate: AnyObject {
/**
Called when the observed item updates the duration.
@@ -18,7 +18,7 @@ protocol AVPlayerItemObserverDelegate: class {
/**
Called when the observed item receives metadata
*/
func item(didReceiveMetadata metadata: [AVMetadataItem])
func item(didReceiveMetadata metadata: [AVTimedMetadataGroup])
}
@@ -29,11 +29,11 @@ class AVPlayerItemObserver: NSObject {
private static var context = 0
private let main: DispatchQueue = .main
private let metadataOutput: AVPlayerItemMetadataOutput
private struct AVPlayerItemKeyPath {
static let duration = #keyPath(AVPlayerItem.duration)
static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges)
static let timedMetadata = #keyPath(AVPlayerItem.timedMetadata)
}
private(set) var isObserving: Bool = false
@@ -41,6 +41,13 @@ class AVPlayerItemObserver: NSObject {
private(set) weak var observingItem: AVPlayerItem?
weak var delegate: AVPlayerItemObserverDelegate?
override init() {
metadataOutput = AVPlayerItemMetadataOutput()
super.init()
metadataOutput.setDelegate(self, queue: main)
}
deinit {
stopObservingCurrentItem()
}
@@ -51,12 +58,12 @@ class AVPlayerItemObserver: NSObject {
- parameter item: The player item to observe.
*/
func startObserving(item: AVPlayerItem) {
self.stopObservingCurrentItem()
self.isObserving = true
self.observingItem = item
stopObservingCurrentItem()
isObserving = true
observingItem = item
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.timedMetadata, options: [.new], context: &AVPlayerItemObserver.context)
item.add(metadataOutput)
}
func stopObservingCurrentItem() {
@@ -65,8 +72,8 @@ class AVPlayerItemObserver: NSObject {
}
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context)
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.timedMetadata, context: &AVPlayerItemObserver.context)
self.isObserving = false
observingItem.remove(metadataOutput)
isObserving = false
self.observingItem = nil
}
@@ -79,21 +86,22 @@ class AVPlayerItemObserver: NSObject {
switch observedKeyPath {
case AVPlayerItemKeyPath.duration:
if let duration = change?[.newKey] as? CMTime {
self.delegate?.item(didUpdateDuration: duration.seconds)
delegate?.item(didUpdateDuration: duration.seconds)
}
case AVPlayerItemKeyPath.loadedTimeRanges:
if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration {
self.delegate?.item(didUpdateDuration: duration.seconds)
delegate?.item(didUpdateDuration: duration.seconds)
}
case AVPlayerItemKeyPath.timedMetadata:
if let metadata = change?[.newKey] as? [AVMetadataItem] {
self.delegate?.item(didReceiveMetadata: metadata)
}
default: break
}
}
}
extension AVPlayerItemObserver: AVPlayerItemMetadataOutputPushDelegate {
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
delegate?.item(didReceiveMetadata: groups)
}
}
+5 -4
View File
@@ -94,12 +94,13 @@ class QueueManager<T> {
- parameter at: The index to insert the items at.
*/
public func addItems(_ items: [T], at index: Int) throws {
guard index >= 0 && _items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "Index for addition has to be positive and smaller than the count of current items (\(_items.count))")
guard index >= 0 && _items.count >= index else {
throw APError.QueueError.invalidIndex(index: index, message: "Index to insert at has to be non-negative and equal to or smaller than the number of items: (\(_items.count))")
}
_items.insert(contentsOf: items, at: index)
if (_currentIndex >= index) { _currentIndex = _currentIndex + items.count }
if (_currentIndex >= index && _items.count != 1) { _currentIndex = _currentIndex + items.count }
}
/**
+9 -5
View File
@@ -123,14 +123,16 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
- throws: `APError`
*/
public func next() throws {
let shouldPlayWhenReady = (playerState == .loading) ? willPlayWhenReady : [.buffering, .playing].contains(playerState)
do {
let nextItem = try queueManager.next()
event.playbackEnd.emit(data: .skippedToNext)
try self.load(item: nextItem, playWhenReady: repeatMode != .track)
try self.load(item: nextItem, playWhenReady: shouldPlayWhenReady)
} catch APError.QueueError.noNextItem {
if repeatMode == .queue {
event.playbackEnd.emit(data: .skippedToNext)
try jumpToItem(atIndex: 0, playWhenReady: true)
try jumpToItem(atIndex: 0, playWhenReady: shouldPlayWhenReady)
} else {
throw APError.QueueError.noNextItem
}
@@ -143,9 +145,11 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
Step to the previous item in the queue.
*/
public func previous() throws {
let shouldPlayWhenReady = (playerState == .loading) ? willPlayWhenReady : [.buffering, .playing].contains(playerState)
let previousItem = try queueManager.previous()
event.playbackEnd.emit(data: .skippedToPrevious)
try self.load(item: previousItem, playWhenReady: repeatMode != .track)
try self.load(item: previousItem, playWhenReady: shouldPlayWhenReady)
}
/**
@@ -205,7 +209,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
case .off:
do {
let nextItem = try queueManager.next()
try self.load(item: nextItem, playWhenReady: repeatMode != .track)
try self.load(item: nextItem, playWhenReady: true)
} catch { /* playback finished */ }
case .track:
seek(to: 0)
@@ -213,7 +217,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
case .queue:
do {
let nextItem = try queueManager.next()
try self.load(item: nextItem, playWhenReady: repeatMode != .track)
try self.load(item: nextItem, playWhenReady: true)
} catch {
try? jumpToItem(atIndex: 0, playWhenReady: true)
}