Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92554a187c | |||
| 473651f357 | |||
| db2f3e9af7 | |||
| a9f831a258 | |||
| cc3840d81e | |||
| 5307090ea3 | |||
| bdaee8b18f | |||
| 84d359bc4f | |||
| 40ea7ad2f9 | |||
| f2f1c1236c | |||
| a75f0d0201 | |||
| 9e4e7f6807 | |||
| dbd3b03989 | |||
| 7e19604df7 |
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user