Compare commits

...

14 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
David Chavez 40ea7ad2f9 Release 0.14.7 2022-02-24 08:49:31 +01:00
David Chavez f2f1c1236c Add tests for new seek improvements 2022-02-24 08:48:54 +01:00
Terkel a75f0d0201 fix: make moveItem public and accessible from outside the class (#9) 2022-02-23 21:40:39 +01:00
Jacob Spizziri 9e4e7f6807 fix(seek): fix an issue causing seek to fail if called immediatly after load (#11) 2022-02-23 21:27:38 +01:00
David Chavez dbd3b03989 Release 0.14.6 2021-11-06 14:38:13 +01:00
David Chavez 7e19604df7 Create LICENSE (#5)
* Create LICENSE

* Update LICENSE
2021-11-06 14:29:06 +01:00
18 changed files with 324 additions and 75 deletions
+10 -10
View File
@@ -44,9 +44,9 @@
607FACEC1AFB9204008FA782 /* AVPlayerObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */; };
9B05AA312660276400C7A389 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA302660276400C7A389 /* Quick */; };
9B05AA332660276400C7A389 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA322660276400C7A389 /* Nimble */; };
9B1D5E1E27C76F5C004CA883 /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */; };
9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */; };
9B521D0E2662937600EF0C3A /* MockDispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */; };
9B77D79426C522D0004BAF2F /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B77D79326C522D0004BAF2F /* SwiftAudioEx */; };
9B77D79626C52382004BAF2F /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B77D79526C52382004BAF2F /* SwiftAudioEx */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -94,7 +94,7 @@
607FACE51AFB9204008FA782 /* SwiftAudio_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftAudio_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerObserverTests.swift; sourceTree = "<group>"; };
9B05AA38266028D600C7A389 /* SwiftAudio */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SwiftAudio; path = ..; sourceTree = "<group>"; };
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftAudioEx; path = ..; sourceTree = "<group>"; };
9B521D0D2662937600EF0C3A /* MockDispatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDispatchQueue.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -103,7 +103,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9B77D79426C522D0004BAF2F /* SwiftAudioEx in Frameworks */,
9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -111,7 +111,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9B77D79626C52382004BAF2F /* SwiftAudioEx in Frameworks */,
9B1D5E1E27C76F5C004CA883 /* SwiftAudioEx in Frameworks */,
9B05AA312660276400C7A389 /* Quick in Frameworks */,
9B05AA332660276400C7A389 /* Nimble in Frameworks */,
);
@@ -222,7 +222,7 @@
9B05AA2F2660276400C7A389 /* Frameworks */ = {
isa = PBXGroup;
children = (
9B05AA38266028D600C7A389 /* SwiftAudio */,
9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -244,7 +244,7 @@
);
name = SwiftAudio_Example;
packageProductDependencies = (
9B77D79326C522D0004BAF2F /* SwiftAudioEx */,
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */,
);
productName = SwiftAudio;
productReference = 607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */;
@@ -267,7 +267,7 @@
packageProductDependencies = (
9B05AA302660276400C7A389 /* Quick */,
9B05AA322660276400C7A389 /* Nimble */,
9B77D79526C52382004BAF2F /* SwiftAudioEx */,
9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */,
);
productName = Tests;
productReference = 607FACE51AFB9204008FA782 /* SwiftAudio_Tests.xctest */;
@@ -674,11 +674,11 @@
package = 9B05AA2C2660274F00C7A389 /* XCRemoteSwiftPackageReference "Nimble" */;
productName = Nimble;
};
9B77D79326C522D0004BAF2F /* SwiftAudioEx */ = {
9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */ = {
isa = XCSwiftPackageProductDependency;
productName = SwiftAudioEx;
};
9B77D79526C52382004BAF2F /* SwiftAudioEx */ = {
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */ = {
isa = XCSwiftPackageProductDependency;
productName = SwiftAudioEx;
};
@@ -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)
}
+12 -1
View File
@@ -145,6 +145,17 @@ class AVPlayerWrapperTests: XCTestCase {
wrapper.load(from: Source.url, playWhenReady: false)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__seeking__should_seek_while_not_yet_loaded() {
let seekTime: TimeInterval = 5.0
let expectation = XCTestExpectation()
holder.didSeekTo = { seconds in
expectation.fulfill()
}
wrapper.load(from: Source.url, playWhenReady: false)
wrapper.seek(to: seekTime)
wait(for: [expectation], timeout: 20.0)
}
func test_AVPlayerWrapper__loading_source_with_initial_time__should_seek() {
let expectation = XCTestExpectation()
@@ -182,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))
}
}
}
+42
View File
@@ -0,0 +1,42 @@
MIT License
Copyright (c) 2021 Double Symmetry
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Copyright (c) 2018 Jørgen Henrichsen <jh.henrichs@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+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.5'
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
@@ -165,16 +169,21 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
}
func seek(to seconds: TimeInterval) {
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)) { (finished) in
if let _ = self._initialTime {
self._initialTime = nil
if self._playWhenReady {
self.play()
}
}
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
}
}
// if the player is loading then we need to defer seeking until it's ready.
if (self._state == AVPlayerWrapperState.loading) {
self._initialTime = seconds
} else {
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)) { (finished) in
if let _ = self._initialTime {
self._initialTime = nil
if self._playWhenReady {
self.play()
}
}
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
}
}
}
@@ -213,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
@@ -353,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 }
}
/**
+10 -6
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)
}
/**
@@ -178,7 +182,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
- parameter toIndex: The index to move the item to.
- throws: `APError.QueueError`
*/
func moveItem(fromIndex: Int, toIndex: Int) throws {
public func moveItem(fromIndex: Int, toIndex: Int) throws {
try queueManager.moveItem(fromIndex: fromIndex, toIndex: toIndex)
}
@@ -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)
}