Compare commits

..

42 Commits

Author SHA1 Message Date
David Chavez 8276f38b1b Release 0.15.2 2022-05-07 21:14:56 +02:00
David Akpan fcd5790e1e fix/ios-hls-live-duration (#18) 2022-05-07 08:59:17 +02:00
Jacob Spizziri ead7c0962e fix(audioplayer): fix loadArtwork method to unset artwork value if no image is given (#17)
https://github.com/doublesymmetry/react-native-track-player/issues/1511
2022-04-30 00:51:14 +02:00
David Chavez 7ff34271e8 Release 0.15.1 2022-04-22 23:11:59 +02:00
David Chavez 4f7a5b02a6 Fix: Bug - repeat mode and queue index event (#16) 2022-04-22 23:11:01 +02:00
David Chavez af803339dc More syntax updates and simplification 2022-04-03 13:16:43 +02:00
David Chavez a5bf6eb1dd Use timeDomain as default audioTimePitchAlgorithm 2022-04-03 12:35:30 +02:00
David Chavez 5e0c27b990 More syntax improvements 2022-04-03 12:24:18 +02:00
David Chavez 6079234942 More syntax updates 2022-04-03 12:13:39 +02:00
David Chavez e74b5ffe4d Syntax improvements 2022-04-03 11:49:23 +02:00
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
David Chavez 481130dc58 Release 0.14.5 2021-10-25 14:08:31 +02:00
David Chavez 300b34afa3 Do not emit paused state when changing tracks 2021-10-25 14:08:01 +02:00
David Chavez da3af0e9db Release 0.14.4 2021-09-28 10:58:23 +02:00
David Chavez d9eb313c1b Deprecate syncRemoteCommandsWithCommandCenter 2021-09-28 10:57:36 +02:00
David Chavez cca7f68da4 Increase deployment target for Test Target 2021-09-28 10:12:22 +02:00
David Chavez 7ed74b80ec Release 0.14.2 2021-09-28 10:04:01 +02:00
David Chavez 2773e4bfec Trigger skip and jump events only when actually taking action 2021-09-28 09:57:24 +02:00
David Chavez 77dc8f4ff1 Fix flickering elapsed time on a lock screen after pause 2021-09-28 09:41:04 +02:00
David Chavez accdf2c00c Rename exposed SPM package name 2021-09-28 09:31:31 +02:00
David Chavez 542d3a5764 Remove syncRemoteCommandsWithCommandCenter
Removed in favor of a didSet on remoteCommands property
2021-09-28 09:28:14 +02:00
David Chavez 4131e54f3e Create FUNDING.yml 2021-09-28 09:19:46 +02:00
David Chavez 03c4a7310f Release 0.14.2 2021-09-25 00:17:58 +02:00
David Chavez 9d2d2594a1 Replace commands based on diff to avoid iOS 15 issues 2021-09-25 00:17:31 +02:00
David Chavez 4e790876cb Release 0.14.1 2021-09-23 10:51:50 +02:00
David Chavez b19d01bdfc Allow manual resyncing of command center commands 2021-09-23 10:51:21 +02:00
David Chavez 3c8ecb353c Release 0.14.0 2021-09-16 17:50:55 +02:00
David Chavez cafd513468 Raise minimum deployment target to iOS11
Due to breaking change in Swift 5.5 & Xcode 13
2021-09-16 17:50:43 +02:00
David Chavez 7b8a4f318d Add tests for repeat mode 2021-08-19 16:27:41 +02:00
31 changed files with 1009 additions and 450 deletions
+12
View File
@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: DoubleSymmetry
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+14 -12
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 */;
@@ -532,7 +532,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = HPNZWPB9JK;
INFOPLIST_FILE = SwiftAudio/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -551,7 +551,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = HPNZWPB9JK;
INFOPLIST_FILE = SwiftAudio/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -577,6 +577,7 @@
"$(inherited)",
);
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -598,6 +599,7 @@
"$(inherited)",
);
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -672,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;
};
+6 -1
View File
@@ -31,6 +31,7 @@ class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
controller.player.event.stateChange.addListener(self, handleAudioPlayerStateChange)
controller.player.event.playbackEnd.addListener(self, handleAudioPlayerPlaybackEnd(data:))
controller.player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed)
controller.player.event.seek.addListener(self, handleAudioPlayerDidSeek)
controller.player.event.updateDuration.addListener(self, handleAudioPlayerUpdateDuration)
@@ -106,7 +107,7 @@ class ViewController: UIViewController {
// MARK: - AudioPlayer Event Handlers
func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
print(data)
print("state=\(data)")
DispatchQueue.main.async {
self.setPlayButtonState(forAudioPlayerState: data)
switch data {
@@ -126,6 +127,10 @@ class ViewController: UIViewController {
}
}
}
func handleAudioPlayerPlaybackEnd(data: AudioPlayer.PlaybackEndEventData) {
print("playEndReason=\(data)")
}
func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
if !isScrubbing {
@@ -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") {
+376
View File
@@ -3,6 +3,21 @@ import Nimble
@testable import SwiftAudioEx
extension QueuedAudioPlayer {
class SeekEventListener {
var eventResult: (Int, Bool) = (-1, false)
func handleEvent(seconds: Int, didFinish: Bool) { eventResult = (seconds, didFinish) }
}
func seekWithExpectation(to time: Double) {
let eventListener = SeekEventListener()
event.seek.addListener(eventListener, eventListener.handleEvent)
seek(to: time)
expect(eventListener.eventResult).toEventually(equal((0, true)))
}
}
class QueuedAudioPlayerTests: QuickSpec {
override func spec() {
describe("A QueuedAudioPlayer") {
@@ -166,6 +181,367 @@ class QueuedAudioPlayerTests: QuickSpec {
}
}
describe("onNext") {
context("player was playing") {
beforeEach {
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
}
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 not 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 previous item and play") {
expect(audioPlayer.nextItems.count).toEventually(equal(1))
expect(audioPlayer.previousItems.count).toEventually(equal(0))
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 previous item and not play") {
expect(audioPlayer.nextItems.count).toEventually(equal(1))
expect(audioPlayer.previousItems.count).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.ready))
}
}
}
}
class TestEventListener {
var eventResult: (Int?, Int?) = (-1, -1)
func handleEvent(previousIndex: Int?, nextIndex: Int?) { eventResult = (previousIndex, nextIndex) }
}
describe("its repeat mode") {
context("when adding 2 items") {
beforeEach {
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()], playWhenReady: true)
}
context("then setting repeat mode off") {
beforeEach {
audioPlayer.repeatMode = .off
}
context("allow playback to end normally") {
beforeEach {
audioPlayer.seekWithExpectation(to: 0.0682)
}
it("should move to next item") {
let eventListener = TestEventListener()
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
expect(audioPlayer.nextItems.count).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
expect(eventListener.eventResult).toEventually(equal((0, 1)))
}
context("allow playback to end again") {
beforeEach {
audioPlayer.seekWithExpectation(to: 0.0682)
}
it("should stop playback normally") {
let eventListener = TestEventListener()
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
expect(audioPlayer.nextItems.count).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.paused))
expect(eventListener.eventResult).toEventually(equal((1, nil)))
}
}
}
context("then calling next()") {
it("should move to next item") {
let eventListener = TestEventListener()
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
try? audioPlayer.next()
expect(audioPlayer.nextItems.count).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
expect(eventListener.eventResult).toEventually(equal((0, 1)))
}
context("then calling next() again") {
it("should fail") {
try? audioPlayer.next()
expect(try audioPlayer.next()).to(throwError())
}
}
}
}
context("then setting repeat mode track") {
beforeEach {
audioPlayer.repeatMode = .track
}
context("allow playback to end") {
beforeEach {
audioPlayer.seekWithExpectation(to: 0.0682)
}
it("should restart current item") {
let eventListener = TestEventListener()
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
expect(audioPlayer.currentTime).toEventually(equal(0))
expect(audioPlayer.nextItems.count).toEventually(equal(1))
expect(audioPlayer.currentIndex).toEventually(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
expect(eventListener.eventResult).toEventually(equal((0, 0)))
}
}
context("then calling next()") {
it("should move to next item and should play") {
let eventListener = TestEventListener()
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
try? audioPlayer.next()
expect(audioPlayer.nextItems.count).to(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
expect(eventListener.eventResult).toEventually(equal((0, 1)))
}
}
}
context("then setting repeat mode queue") {
beforeEach {
audioPlayer.repeatMode = .queue
}
context("allow playback to end") {
beforeEach {
audioPlayer.seekWithExpectation(to: 0.0682)
}
it("should move to next item and should play") {
let eventListener = TestEventListener()
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
expect(audioPlayer.nextItems.count).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
expect(eventListener.eventResult).toEventually(equal((0, 1)))
}
context("allow playback to end again") {
beforeEach {
audioPlayer.seekWithExpectation(to: 0.0682)
}
it("should move to first track and should play") {
let eventListener = TestEventListener()
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
expect(audioPlayer.nextItems.count).toEventually(equal(1))
expect(audioPlayer.currentIndex).toEventually(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
expect(eventListener.eventResult).toEventually(equal((1, 0)))
}
}
}
context("then calling next()") {
it("should move to next item and should play") {
let eventListener = TestEventListener()
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
try? audioPlayer.next()
expect(audioPlayer.nextItems.count).to(equal(0))
expect(audioPlayer.currentIndex).to(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
expect(eventListener.eventResult).toEventually(equal((0, 1)))
}
context("then calling next() again") {
beforeEach {
try? audioPlayer.next()
}
it("should move to first track and should play") {
let eventListener = TestEventListener()
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
try? audioPlayer.next()
expect(audioPlayer.nextItems.count).to(equal(1))
expect(audioPlayer.currentIndex).to(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
expect(eventListener.eventResult).toEventually(equal((1, 0)))
}
}
}
}
}
context("when adding 1 items") {
beforeEach {
try? audioPlayer.add(item: ShortSource.getAudioItem(), playWhenReady: true)
}
context("then setting repeat mode off") {
beforeEach {
audioPlayer.repeatMode = .off
}
context("allow playback to end normally") {
beforeEach {
audioPlayer.seekWithExpectation(to: 0.0682)
}
it("should stop playback normally") {
let eventListener = TestEventListener()
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
expect(audioPlayer.nextItems.count).toEventually(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.paused))
expect(eventListener.eventResult).toEventually(equal((0, nil)))
}
}
context("then calling next()") {
it("should fail") {
try? audioPlayer.next()
expect(try audioPlayer.next()).to(throwError())
}
}
}
context("then setting repeat mode track") {
beforeEach {
audioPlayer.repeatMode = .track
}
context("allow playback to end") {
beforeEach {
audioPlayer.seekWithExpectation(to: 0.0682)
}
it("should restart current item") {
let eventListener = TestEventListener()
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
expect(audioPlayer.currentTime).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
expect(eventListener.eventResult).toEventually(equal((0, 0)))
}
}
context("then calling next()") {
it("should restart current item") {
let eventListener = TestEventListener()
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
expect(audioPlayer.currentTime).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
expect(eventListener.eventResult).toEventually(equal((0, 0)))
}
}
}
context("then setting repeat mode queue") {
beforeEach {
audioPlayer.repeatMode = .queue
}
context("allow playback to end") {
beforeEach {
audioPlayer.seekWithExpectation(to: 0.0682)
}
it("should restart current item") {
let eventListener = TestEventListener()
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
expect(audioPlayer.currentTime).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
expect(eventListener.eventResult).toEventually(equal((0, 0)))
}
}
context("then calling next()") {
it("should restart current item") {
let eventListener = TestEventListener()
audioPlayer.event.queueIndex.addListener(eventListener, eventListener.handleEvent)
// workaround: seek not to beggining, for 0 expecations to correctly fail if necessary.
audioPlayer.seekWithExpectation(to: 0.05)
try? audioPlayer.next()
expect(audioPlayer.currentTime).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
expect(eventListener.eventResult).toEventually(equal((0, 0)))
}
}
}
}
}
}
}
}
+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.
+2 -2
View File
@@ -2,8 +2,8 @@
import PackageDescription
let package = Package(
name: "SwiftAudio",
platforms: [.iOS(.v10)],
name: "SwiftAudioEx",
platforms: [.iOS(.v11)],
products: [
.library(
name: "SwiftAudioEx",
+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
+2 -2
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioEx'
s.version = '0.13.2'
s.version = '0.15.2'
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.
@@ -20,7 +20,7 @@ DESC
'Jørgen Henrichsen' => 'jh.henrichs@gmail.com', }
s.source = { :git => 'https://github.com/DoubleSymmetry/SwiftAudioEx.git', :tag => s.version.to_s }
s.ios.deployment_target = '10.0'
s.ios.deployment_target = '11.0'
s.swift_version = '5.0'
s.source_files = 'SwiftAudioEx/Classes/**/*'
end
+1
View File
@@ -22,6 +22,7 @@ public struct APError {
case noPreviousItem
case noNextItem
case invalidIndex(index: Int, message: String)
case noNextWhenRepeatModeTrack
}
}
@@ -26,65 +26,51 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
// MARK: - Properties
var avPlayer: AVPlayer
let playerObserver: AVPlayerObserver
let playerTimeObserver: AVPlayerTimeObserver
let playerItemNotificationObserver: AVPlayerItemNotificationObserver
let playerItemObserver: AVPlayerItemObserver
/**
True if the last call to load(from:playWhenReady) had playWhenReady=true.
*/
fileprivate var _playWhenReady: Bool = true
fileprivate var _initialTime: TimeInterval?
fileprivate var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
didSet {
if oldValue != _state {
self.delegate?.AVWrapper(didChangeState: _state)
}
}
}
fileprivate var avPlayer = AVPlayer()
private let playerObserver = AVPlayerObserver()
internal let playerTimeObserver: AVPlayerTimeObserver
private let playerItemNotificationObserver = AVPlayerItemNotificationObserver()
private let playerItemObserver = AVPlayerItemObserver()
fileprivate var initialTime: TimeInterval?
fileprivate var pendingAsset: AVAsset? = nil
/// True when the track was paused for the purpose of switching tracks
fileprivate var pausedForLoad: Bool = false
public init() {
self.avPlayer = AVPlayer()
self.playerObserver = AVPlayerObserver()
self.playerObserver.player = avPlayer
self.playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime())
self.playerTimeObserver.player = avPlayer
self.playerItemNotificationObserver = AVPlayerItemNotificationObserver()
self.playerItemObserver = AVPlayerItemObserver()
self.playerObserver.delegate = self
self.playerTimeObserver.delegate = self
self.playerItemNotificationObserver.delegate = self
self.playerItemObserver.delegate = self
playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime())
playerTimeObserver.player = avPlayer
playerObserver.player = avPlayer
playerObserver.delegate = self
playerTimeObserver.delegate = self
playerItemNotificationObserver.delegate = self
playerItemObserver.delegate = self
// disabled since we're not making use of video playback
self.avPlayer.allowsExternalPlayback = false;
avPlayer.allowsExternalPlayback = false;
playerTimeObserver.registerForPeriodicTimeEvents()
}
// MARK: - AVPlayerWrapperProtocol
var state: AVPlayerWrapperState {
return _state
}
var reasonForWaitingToPlay: AVPlayer.WaitingReason? {
return avPlayer.reasonForWaitingToPlay
fileprivate(set) var state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
didSet {
if oldValue != state {
delegate?.AVWrapper(didChangeState: state)
}
}
}
/**
True if the last call to load(from:playWhenReady) had playWhenReady=true.
*/
fileprivate(set) var playWhenReady: Bool = true
var currentItem: AVPlayerItem? {
return avPlayer.currentItem
}
var _pendingAsset: AVAsset? = nil
var automaticallyWaitsToMinimizeStalling: Bool {
get { return avPlayer.automaticallyWaitsToMinimizeStalling }
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
avPlayer.currentItem
}
var currentTime: TimeInterval {
@@ -99,49 +85,58 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
else if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
return seconds
}
else if let seconds = currentItem?.loadedTimeRanges.first?.timeRangeValue.duration.seconds,
!seconds.isNaN {
else if let seconds = currentItem?.seekableTimeRanges.last?.timeRangeValue.duration.seconds,
!seconds.isNaN {
return seconds
}
return 0.0
}
var bufferedPosition: TimeInterval {
return currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
}
var reasonForWaitingToPlay: AVPlayer.WaitingReason? {
avPlayer.reasonForWaitingToPlay
}
var rate: Float {
get { avPlayer.rate }
set { avPlayer.rate = newValue }
}
weak var delegate: AVPlayerWrapperDelegate? = nil
var bufferDuration: TimeInterval = 0
var timeEventFrequency: TimeEventFrequency = .everySecond {
didSet {
playerTimeObserver.periodicObserverTimeInterval = timeEventFrequency.getTime()
}
}
var rate: Float {
get { return avPlayer.rate }
set { avPlayer.rate = newValue }
}
var volume: Float {
get { return avPlayer.volume }
get { avPlayer.volume }
set { avPlayer.volume = newValue }
}
var isMuted: Bool {
get { return avPlayer.isMuted }
get { avPlayer.isMuted }
set { avPlayer.isMuted = newValue }
}
var automaticallyWaitsToMinimizeStalling: Bool {
get { avPlayer.automaticallyWaitsToMinimizeStalling }
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
}
func play() {
_playWhenReady = true
playWhenReady = true
avPlayer.play()
}
func pause() {
_playWhenReady = false
playWhenReady = false
avPlayer.pause()
}
@@ -162,42 +157,44 @@ 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 (state == AVPlayerWrapperState.loading) {
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)
}
}
}
func load(from url: URL, playWhenReady: Bool, options: [String: Any]? = nil) {
reset(soft: true)
_playWhenReady = playWhenReady
self.playWhenReady = playWhenReady
if currentItem?.status == .failed {
recreateAVPlayer()
}
pendingAsset = AVURLAsset(url: url, options: options)
self._pendingAsset = AVURLAsset(url: url, options: options)
if let pendingAsset = _pendingAsset {
self._state = .loading
if let pendingAsset = pendingAsset {
state = .loading
pendingAsset.loadValuesAsynchronously(forKeys: [Constants.assetPlayableKey], completionHandler: { [weak self] in
guard let self = self else {
return
}
guard let self = self else { return }
var error: NSError? = nil
let status = pendingAsset.statusOfValue(forKey: Constants.assetPlayableKey, error: &error)
DispatchQueue.main.async {
let isPendingAsset = (self._pendingAsset != nil && pendingAsset.isEqual(self._pendingAsset))
let isPendingAsset = (self.pendingAsset != nil && pendingAsset.isEqual(self.pendingAsset))
switch status {
case .loaded:
if isPendingAsset {
@@ -210,8 +207,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
@@ -219,7 +226,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
case .failed:
if isPendingAsset {
self.delegate?.AVWrapper(failedWithError: error)
self._pendingAsset = nil
self.pendingAsset = nil
}
break
@@ -235,8 +242,11 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
}
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval? = nil, options: [String : Any]? = nil) {
_initialTime = initialTime
self.pause()
self.initialTime = initialTime
pausedForLoad = true
pause()
self.load(from: url, playWhenReady: playWhenReady, options: options)
}
@@ -246,9 +256,9 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
playerItemObserver.stopObservingCurrentItem()
playerTimeObserver.unregisterForBoundaryTimeEvents()
playerItemNotificationObserver.stopObservingCurrentItem()
self._pendingAsset?.cancelLoading()
self._pendingAsset = nil
pendingAsset?.cancelLoading()
pendingAsset = nil
if !soft {
avPlayer.replaceCurrentItem(with: nil)
@@ -275,15 +285,15 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
switch status {
case .paused:
if currentItem == nil {
_state = .idle
state = .idle
}
else {
self._state = .paused
else if pausedForLoad != true {
state = .paused
}
case .waitingToPlayAtSpecifiedRate:
self._state = .buffering
state = .buffering
case .playing:
self._state = .playing
state = .playing
@unknown default:
break
}
@@ -292,17 +302,18 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
func player(statusDidChange status: AVPlayer.Status) {
switch status {
case .readyToPlay:
self._state = .ready
if _playWhenReady && (_initialTime ?? 0) == 0 {
self.play()
state = .ready
pausedForLoad = false
if playWhenReady && (initialTime ?? 0) == 0 {
play()
}
else if let initialTime = _initialTime {
self.seek(to: initialTime)
else if let initialTime = initialTime {
seek(to: initialTime)
}
break
case .failed:
self.delegate?.AVWrapper(failedWithError: avPlayer.error)
delegate?.AVWrapper(failedWithError: avPlayer.error)
break
case .unknown:
@@ -311,7 +322,6 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
break
}
}
}
extension AVPlayerWrapper: AVPlayerTimeObserverDelegate {
@@ -319,11 +329,11 @@ extension AVPlayerWrapper: AVPlayerTimeObserverDelegate {
// MARK: - AVPlayerTimeObserverDelegate
func audioDidStart() {
self._state = .playing
state = .playing
}
func timeEvent(time: CMTime) {
self.delegate?.AVWrapper(secondsElapsed: time.seconds)
delegate?.AVWrapper(secondsElapsed: time.seconds)
}
}
@@ -343,11 +353,11 @@ extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
// MARK: - AVPlayerItemObserverDelegate
func item(didUpdateDuration duration: Double) {
self.delegate?.AVWrapper(didUpdateDuration: duration)
delegate?.AVWrapper(didUpdateDuration: duration)
}
func item(didReceiveMetadata metadata: [AVMetadataItem]) {
self.delegate?.AVWrapper(didReceiveMetadata: metadata)
func item(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
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 playWhenReady: Bool { get }
var currentItem: AVPlayerItem? { get }
@@ -52,5 +54,4 @@ protocol AVPlayerWrapperProtocol: class {
func load(from url: URL, playWhenReady: Bool, options: [String: Any]?)
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?, options: [String: Any]?)
}
+12 -13
View File
@@ -66,23 +66,23 @@ public class DefaultAudioItem: AudioItem {
}
public func getSourceUrl() -> String {
return audioUrl
audioUrl
}
public func getArtist() -> String? {
return artist
artist
}
public func getTitle() -> String? {
return title
title
}
public func getAlbumTitle() -> String? {
return albumTitle
albumTitle
}
public func getSourceType() -> SourceType {
return sourceType
sourceType
}
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
@@ -97,17 +97,17 @@ public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
self.pitchAlgorithmType = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
pitchAlgorithmType = AVAudioTimePitchAlgorithm.timeDomain
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm) {
self.pitchAlgorithmType = audioTimePitchAlgorithm
pitchAlgorithmType = audioTimePitchAlgorithm
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm {
return pitchAlgorithmType
pitchAlgorithmType
}
}
@@ -117,7 +117,7 @@ public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
public var initialTime: TimeInterval
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
self.initialTime = 0.0
initialTime = 0.0
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
@@ -127,7 +127,7 @@ public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
}
public func getInitialTime() -> TimeInterval {
return initialTime
initialTime
}
}
@@ -138,7 +138,7 @@ public class DefaultAudioItemAssetOptionsProviding: DefaultAudioItem, AssetOptio
public var options: [String: Any]
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
self.options = [:]
options = [:]
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
@@ -148,7 +148,6 @@ public class DefaultAudioItemAssetOptionsProviding: DefaultAudioItem, AssetOptio
}
public func getAssetOptions() -> [String: Any] {
return options
options
}
}
+69 -59
View File
@@ -11,22 +11,15 @@ import MediaPlayer
public typealias AudioPlayerState = AVPlayerWrapperState
public class AudioPlayer: AVPlayerWrapperDelegate {
private var _wrapper: AVPlayerWrapperProtocol
/// The wrapper around the underlying AVPlayer
var wrapper: AVPlayerWrapperProtocol {
return _wrapper
}
let wrapper: AVPlayerWrapperProtocol = AVPlayerWrapper()
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
public let remoteCommandController: RemoteCommandController
public let event = EventHolder()
var _currentItem: AudioItem?
public var currentItem: AudioItem? {
return _currentItem
}
private(set) var currentItem: AudioItem?
/**
Set this to false to disable automatic updating of now playing info for control center and lock screen.
@@ -37,42 +30,52 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
Controls the time pitch algorithm applied to each item loaded into the player.
If the loaded `AudioItem` conforms to `TimePitcher`-protocol this will be overriden.
*/
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.timeDomain
/**
Default remote commands to use for each playing item
*/
public var remoteCommands: [RemoteCommand] = []
public var remoteCommands: [RemoteCommand] = [] {
didSet {
if let item = currentItem {
self.enableRemoteCommands(forItem: item)
}
}
}
// MARK: - Getters from AVPlayerWrapper
internal var willPlayWhenReady: Bool {
wrapper.playWhenReady
}
/**
The elapsed playback time of the current item.
*/
public var currentTime: Double {
return wrapper.currentTime
wrapper.currentTime
}
/**
The duration of the current AudioItem.
*/
public var duration: Double {
return wrapper.duration
wrapper.duration
}
/**
The bufferedPosition of the current AudioItem.
*/
public var bufferedPosition: Double {
return wrapper.bufferedPosition
wrapper.bufferedPosition
}
/**
The current state of the underlying `AudioPlayer`.
*/
public var playerState: AudioPlayerState {
return wrapper.state
wrapper.state
}
// MARK: - Setters for AVPlayerWrapper
@@ -85,45 +88,45 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
- Important: This setting will have no effect if `automaticallyWaitsToMinimizeStalling` is set to `true` in the AVPlayer
*/
public var bufferDuration: TimeInterval {
get { return wrapper.bufferDuration }
set { _wrapper.bufferDuration = newValue }
get { wrapper.bufferDuration }
set { wrapper.bufferDuration = newValue }
}
/**
Set this to decide how often the player should call the delegate with time progress events.
*/
public var timeEventFrequency: TimeEventFrequency {
get { return wrapper.timeEventFrequency }
set { _wrapper.timeEventFrequency = newValue }
get { wrapper.timeEventFrequency }
set { wrapper.timeEventFrequency = newValue }
}
/**
Indicates whether the player should automatically delay playback in order to minimize stalling
*/
public var automaticallyWaitsToMinimizeStalling: Bool {
get { return wrapper.automaticallyWaitsToMinimizeStalling }
set { _wrapper.automaticallyWaitsToMinimizeStalling = newValue }
get { wrapper.automaticallyWaitsToMinimizeStalling }
set { wrapper.automaticallyWaitsToMinimizeStalling = newValue }
}
public var volume: Float {
get { return wrapper.volume }
set { _wrapper.volume = newValue }
get { wrapper.volume }
set { wrapper.volume = newValue }
}
public var isMuted: Bool {
get { return wrapper.isMuted }
set { _wrapper.isMuted = newValue }
get { wrapper.isMuted }
set { wrapper.isMuted = newValue }
}
private var _rate: Float = 1.0
public var rate: Float {
get { return _rate }
get { _rate }
set {
_rate = newValue
// Only set the rate on the wrapper if it is already playing.
if _wrapper.rate > 0 {
_wrapper.rate = newValue
if wrapper.rate > 0 {
wrapper.rate = newValue
}
}
}
@@ -137,11 +140,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
*/
public init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
remoteCommandController: RemoteCommandController = RemoteCommandController()) {
self._wrapper = AVPlayerWrapper()
self.nowPlayingInfoController = nowPlayingInfoController
self.remoteCommandController = remoteCommandController
self._wrapper.delegate = self
wrapper.delegate = self
self.remoteCommandController.audioPlayer = self
}
@@ -172,10 +174,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
initialTime: (item as? InitialTiming)?.getInitialTime(),
options:(item as? AssetOptionsProviding)?.getAssetOptions())
self._currentItem = item
currentItem = item
if (automaticallyUpdateNowPlayingInfo) {
self.loadNowPlayingMetaValues()
loadNowPlayingMetaValues()
}
enableRemoteCommands(forItem: item)
}
@@ -184,30 +186,30 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
Toggle playback status.
*/
public func togglePlaying() {
self.wrapper.togglePlaying()
wrapper.togglePlaying()
}
/**
Start playback
*/
public func play() {
self.wrapper.play()
wrapper.play()
}
/**
Pause playback
*/
public func pause() {
self.wrapper.pause()
wrapper.pause()
}
/**
Stop playback, resetting the player.
*/
public func stop() {
self.reset()
self.wrapper.stop()
self.event.playbackEnd.emit(data: .playerStopped)
reset()
wrapper.stop()
event.playbackEnd.emit(data: .playerStopped)
}
/**
@@ -215,15 +217,15 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
*/
public func seek(to seconds: TimeInterval) {
if automaticallyUpdateNowPlayingInfo {
self.updateNowPlayingCurrentTime(seconds)
updateNowPlayingCurrentTime(seconds)
}
self.wrapper.seek(to: seconds)
wrapper.seek(to: seconds)
}
// MARK: - Remote Command Center
func enableRemoteCommands(_ commands: [RemoteCommand]) {
self.remoteCommandController.enable(commands: commands)
remoteCommandController.enable(commands: commands)
}
func enableRemoteCommands(forItem item: AudioItem) {
@@ -234,6 +236,15 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
self.enableRemoteCommands(remoteCommands)
}
}
/**
Syncs the current remoteCommands with the iOS command center.
Can be used to update item states - e.g. like, dislike and bookmark.
*/
@available(*, deprecated, message: "Directly set .remoteCommands instead")
public func syncRemoteCommandsWithCommandCenter() {
self.enableRemoteCommands(remoteCommands)
}
// MARK: - NowPlayingInfo
@@ -267,8 +278,8 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
- Playback rate
*/
public func updateNowPlayingPlaybackValues() {
updateNowPlayingDuration(duration)
updateNowPlayingCurrentTime(currentTime)
updateNowPlayingDuration(duration)
updateNowPlayingRate(rate)
}
@@ -287,10 +298,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
private func loadArtwork(forItem item: AudioItem) {
item.getArtwork { (image) in
if let image = image {
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { (size) -> UIImage in
return image
})
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { _ in image })
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(artwork))
} else {
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(nil))
}
}
}
@@ -298,7 +309,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
// MARK: - Private
func reset() {
self._currentItem = nil
currentItem = nil
}
private func setTimePitchingAlgorithmForCurrentItem() {
@@ -321,7 +332,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
setTimePitchingAlgorithmForCurrentItem()
case .playing:
// When a track starts playing, reset the rate to the stored rate
self.rate = _rate;
rate = _rate;
fallthrough
case .paused:
if (automaticallyUpdateNowPlayingInfo) {
@@ -329,38 +340,37 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
}
default: break
}
self.event.stateChange.emit(data: state)
event.stateChange.emit(data: state)
}
func AVWrapper(secondsElapsed seconds: Double) {
self.event.secondElapse.emit(data: seconds)
event.secondElapse.emit(data: seconds)
}
func AVWrapper(failedWithError error: Error?) {
self.event.fail.emit(data: error)
event.fail.emit(data: error)
}
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
if !didFinish && automaticallyUpdateNowPlayingInfo {
updateNowPlayingCurrentTime(currentTime)
}
self.event.seek.emit(data: (seconds, didFinish))
event.seek.emit(data: (seconds, didFinish))
}
func AVWrapper(didUpdateDuration duration: Double) {
self.event.updateDuration.emit(data: duration)
event.updateDuration.emit(data: duration)
}
func AVWrapper(didReceiveMetadata metadata: [AVMetadataItem]) {
self.event.receiveMetadata.emit(data: metadata)
func AVWrapper(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
event.receiveMetadata.emit(data: metadata)
}
func AVWrapperItemDidPlayToEndTime() {
self.event.playbackEnd.emit(data: .playedUntilEnd)
event.playbackEnd.emit(data: .playedUntilEnd)
}
func AVWrapperDidRecreateAVPlayer() {
self.event.didRecreateAVPlayer.emit(data: ())
event.didRecreateAVPlayer.emit(data: ())
}
}
@@ -21,10 +21,8 @@ protocol AudioSession {
var availableCategories: [AVAudioSession.Category] { get }
@available(iOS 10.0, *)
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws
@available(iOS 11.0, *)
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, policy: AVAudioSession.RouteSharingPolicy, options: AVAudioSession.CategoryOptions) throws
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws
@@ -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.
@@ -30,7 +33,7 @@ public class AudioSessionController {
True if another app is currently playing audio.
*/
public var isOtherAudioPlaying: Bool {
return audioSession.isOtherAudioPlaying
audioSession.isOtherAudioPlaying
}
/**
@@ -46,9 +49,7 @@ public class AudioSessionController {
Set this to false to disable the behaviour.
*/
public var isObservingForInterruptions: Bool {
get {
return _isObservingForInterruptions
}
get { _isObservingForInterruptions }
set {
if newValue == _isObservingForInterruptions {
return
@@ -112,7 +113,19 @@ public class AudioSessionController {
return
}
self.delegate?.handleInterruption(type: type)
switch type {
case .began:
delegate?.handleInterruption(type: .began)
case .ended:
guard let typeValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else {
delegate?.handleInterruption(type: .ended(shouldResume: false))
return
}
let options = AVAudioSession.InterruptionOptions(rawValue: typeValue)
delegate?.handleInterruption(type: .ended(shouldResume: options.contains(.shouldResume)))
@unknown default: return
}
}
}
+8 -10
View File
@@ -10,13 +10,13 @@ import MediaPlayer
extension AudioPlayer {
public typealias StateChangeEventData = (AudioPlayerState)
public typealias PlaybackEndEventData = (PlaybackEndedReason)
public typealias SecondElapseEventData = (TimeInterval)
public typealias FailEventData = (Error?)
public typealias StateChangeEventData = AudioPlayerState
public typealias PlaybackEndEventData = PlaybackEndedReason
public typealias SecondElapseEventData = TimeInterval
public typealias FailEventData = Error?
public typealias SeekEventData = (seconds: Int, didFinish: Bool)
public typealias UpdateDurationEventData = (Double)
public typealias MetadataEventData = ([AVMetadataItem])
public typealias UpdateDurationEventData = Double
public typealias MetadataEventData = [AVTimedMetadataGroup]
public typealias DidRecreateAVPlayerEventData = ()
public typealias QueueIndexEventData = (previousIndex: Int?, newIndex: Int?)
@@ -90,7 +90,7 @@ extension AudioPlayer {
init<Listener: AnyObject>(listener: Listener, closure: @escaping EventClosure<EventData>) {
self.listener = listener
self.invoke = { [weak listener] (data: EventData) in
invoke = { [weak listener] (data: EventData) in
guard let _ = listener else {
return false
}
@@ -133,9 +133,7 @@ extension AudioPlayer {
func emit(data: EventData) {
eventQueue.async {
self.invokersSemaphore.wait()
self.invokers = self.invokers.filter({ (invoker) -> Bool in
return invoker.invoke(data)
})
self.invokers = self.invokers.filter { $0.invoke(data) }
self.invokersSemaphore.signal()
}
}
@@ -11,31 +11,23 @@ import MediaPlayer
public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
private let concurrentInfoQueue: DispatchQueueType
private var _infoCenter: NowPlayingInfoCenter
private var _info: [String: Any] = [:]
var infoCenter: NowPlayingInfoCenter {
return _infoCenter
}
var info: [String: Any] {
return _info
}
private(set) var infoCenter: NowPlayingInfoCenter
private(set) var info: [String: Any] = [:]
public required init() {
self.concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
self._infoCenter = MPNowPlayingInfoCenter.default()
concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
infoCenter = MPNowPlayingInfoCenter.default()
}
/// Used for testing purposes.
public required init(dispatchQueue: DispatchQueueType, infoCenter: NowPlayingInfoCenter) {
self.concurrentInfoQueue = dispatchQueue
self._infoCenter = infoCenter
concurrentInfoQueue = dispatchQueue
self.infoCenter = infoCenter
}
public required init(infoCenter: NowPlayingInfoCenter) {
self.concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
self._infoCenter = infoCenter
concurrentInfoQueue = DispatchQueue(label: "com.doublesymmetry.nowPlayingInfoQueue", attributes: .concurrent)
self.infoCenter = infoCenter
}
public func set(keyValues: [NowPlayingInfoKeyValue]) {
@@ -43,10 +35,10 @@ public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
guard let self = self else { return }
keyValues.forEach { (keyValue) in
self._info[keyValue.getKey()] = keyValue.getValue()
self.info[keyValue.getKey()] = keyValue.getValue()
}
self._infoCenter.nowPlayingInfo = self._info
self.infoCenter.nowPlayingInfo = self.info
}
}
@@ -54,8 +46,8 @@ public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
self._info[keyValue.getKey()] = keyValue.getValue()
self._infoCenter.nowPlayingInfo = self._info
self.info[keyValue.getKey()] = keyValue.getValue()
self.infoCenter.nowPlayingInfo = self.info
}
}
@@ -63,8 +55,8 @@ public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
concurrentInfoQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
self._info = [:]
self._infoCenter.nowPlayingInfo = self._info
self.info = [:]
self.infoCenter.nowPlayingInfo = self.info
}
}
@@ -31,7 +31,6 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
The URL pointing to the now playing item's underlying asset.
This constant is used by the system UI when video thumbnails or audio waveform visualizations are applicable.
*/
@available(iOS 10.3, *)
case assetUrl(URL?)
/**
@@ -116,7 +115,6 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
The service provider associated with the now-playing item.
Value is a unique NSString that identifies the service provider for the now-playing item. If the now-playing item belongs to a channel or subscription service, this key can be used to coordinate various types of now-playing content from the service provider.
*/
@available(iOS 11.0, *)
case serviceIdentifier(String?)
@@ -130,11 +128,7 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
return MPNowPlayingInfoPropertyAvailableLanguageOptions
case .assetUrl(_):
if #available(iOS 10.3, *) {
return MPNowPlayingInfoPropertyAssetURL
} else {
return ""
}
return MPNowPlayingInfoPropertyAssetURL
case .chapterCount(_):
return MPNowPlayingInfoPropertyChapterCount
@@ -175,11 +169,7 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
return MPNowPlayingInfoPropertyPlaybackRate
case .serviceIdentifier(_):
if #available(iOS 11.0, *) {
return MPNowPlayingInfoPropertyServiceIdentifier
} else {
return ""
}
return MPNowPlayingInfoPropertyServiceIdentifier
}
}
@@ -194,10 +184,7 @@ public enum NowPlayingInfoProperty: NowPlayingInfoKeyValue {
return options
case .assetUrl(let url):
if #available(iOS 10.3, *) {
return url
}
return false
return url
case .chapterCount(let count):
return count != nil ? NSNumber(value: count!) : nil
@@ -8,8 +8,7 @@
import Foundation
import AVFoundation
protocol AVPlayerItemNotificationObserverDelegate: class {
protocol AVPlayerItemNotificationObserverDelegate: AnyObject {
func itemDidPlayToEndTime()
}
@@ -51,9 +50,9 @@ class AVPlayerItemNotificationObserver {
guard let observingItem = observingItem, isObserving else {
return
}
self.notificationCenter.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observingItem)
notificationCenter.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: observingItem)
self.observingItem = nil
self.isObserving = false
isObserving = false
}
@objc private func itemDidPlayToEndTime() {
@@ -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)
}
}
@@ -9,7 +9,7 @@
import Foundation
import AVFoundation
protocol AVPlayerObserverDelegate: class {
protocol AVPlayerObserverDelegate: AnyObject {
/**
Called when the AVPlayer.status changes.
@@ -20,37 +20,36 @@ protocol AVPlayerObserverDelegate: class {
Called when the AVPlayer.timeControlStatus changes.
*/
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus)
}
/**
Observing an AVPlayers status changes.
*/
class AVPlayerObserver: NSObject {
private static var context = 0
private let main: DispatchQueue = .main
private struct AVPlayerKeyPath {
static let status = #keyPath(AVPlayer.status)
static let timeControlStatus = #keyPath(AVPlayer.timeControlStatus)
}
private let statusChangeOptions: NSKeyValueObservingOptions = [.new, .initial]
private let timeControlStatusChangeOptions: NSKeyValueObservingOptions = [.new]
private(set) var isObserving: Bool = false
weak var delegate: AVPlayerObserverDelegate?
weak var player: AVPlayer? {
willSet {
self.stopObserving()
stopObserving()
}
}
deinit {
self.stopObserving()
stopObserving()
}
/**
Start receiving events from this observer.
*/
@@ -58,52 +57,49 @@ class AVPlayerObserver: NSObject {
guard let player = player else {
return
}
self.stopObserving()
self.isObserving = true
player.addObserver(self, forKeyPath: AVPlayerKeyPath.status, options: self.statusChangeOptions, context: &AVPlayerObserver.context)
player.addObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, options: self.timeControlStatusChangeOptions, context: &AVPlayerObserver.context)
stopObserving()
isObserving = true
player.addObserver(self, forKeyPath: AVPlayerKeyPath.status, options: statusChangeOptions, context: &AVPlayerObserver.context)
player.addObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, options: timeControlStatusChangeOptions, context: &AVPlayerObserver.context)
}
func stopObserving() {
guard let player = player, isObserving else {
return
}
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context)
player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context)
self.isObserving = false
isObserving = false
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
guard context == &AVPlayerObserver.context, let observedKeyPath = keyPath else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
switch observedKeyPath {
case AVPlayerKeyPath.status:
self.handleStatusChange(change)
handleStatusChange(change)
case AVPlayerKeyPath.timeControlStatus:
self.handleTimeControlStatusChange(change)
handleTimeControlStatusChange(change)
default:
break
}
}
private func handleStatusChange(_ change: [NSKeyValueChangeKey: Any]?) {
let status: AVPlayer.Status
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayer.Status(rawValue: statusNumber.intValue)!
}
else {
} else {
status = .unknown
}
delegate?.player(statusDidChange: status)
}
private func handleTimeControlStatusChange(_ change: [NSKeyValueChangeKey: Any]?) {
let status: AVPlayer.TimeControlStatus
if let statusNumber = change?[.newKey] as? NSNumber {
@@ -111,5 +107,4 @@ class AVPlayerObserver: NSObject {
delegate?.player(didChangeTimeControlStatus: status)
}
}
}
@@ -9,7 +9,7 @@
import Foundation
import AVFoundation
protocol AVPlayerTimeObserverDelegate: class {
protocol AVPlayerTimeObserverDelegate: AnyObject {
func audioDidStart()
func timeEvent(time: CMTime)
}
+56 -64
View File
@@ -16,55 +16,47 @@ class QueueManager<T> {
weak var delegate: QueueManagerDelegate? = nil
private var _items: [T] = [] {
/**
All items held by the queue.
*/
private(set) var items: [T] = [] {
didSet {
if oldValue.count == 0 && _items.count > 0 && _currentIndex == 0 {
if oldValue.count == 0 && items.count > 0 && currentIndex == 0 {
delegate?.onReceivedFirstItem()
}
}
}
/**
All items held by the queue.
*/
public var items: [T] {
return _items
}
public var nextItems: [T] {
guard _currentIndex + 1 < _items.count else {
guard currentIndex + 1 < items.count else {
return []
}
return Array(_items[_currentIndex + 1..<_items.count])
return Array(items[currentIndex + 1..<items.count])
}
public var previousItems: [T] {
if (_currentIndex == 0) {
if (currentIndex == 0) {
return []
}
return Array(_items[0..<_currentIndex])
return Array(items[0..<currentIndex])
}
private var _currentIndex: Int = 0 {
didSet {
delegate?.onCurrentIndexChanged(oldIndex: oldValue, newIndex: _currentIndex)
}
}
/**
The index of the current item.
Will be populated event though there is no current item (When the queue is empty).
*/
public var currentIndex: Int {
return _currentIndex
private(set) var currentIndex: Int = 0 {
didSet {
delegate?.onCurrentIndexChanged(oldIndex: oldValue, newIndex: currentIndex)
}
}
/**
The current item for the queue.
*/
public var current: T? {
if _items.count > _currentIndex {
return _items[_currentIndex]
if items.count > currentIndex {
return items[currentIndex]
}
return nil
}
@@ -75,7 +67,7 @@ class QueueManager<T> {
- parameter item: The `AudioItem` to be added.
*/
public func addItem(_ item: T) {
_items.append(item)
items.append(item)
}
/**
@@ -84,7 +76,7 @@ class QueueManager<T> {
- parameter items: The `AudioItem`s to be added.
*/
public func addItems(_ items: [T]) {
_items.append(contentsOf: items)
self.items.append(contentsOf: items)
}
/**
@@ -94,12 +86,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 && self.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 }
self.items.insert(contentsOf: items, at: index)
if (currentIndex >= index && self.items.count != 1) { currentIndex += items.count }
}
/**
@@ -111,12 +104,12 @@ class QueueManager<T> {
*/
@discardableResult
public func next() throws -> T {
let nextIndex = _currentIndex + 1
guard _items.count > nextIndex else {
let nextIndex = currentIndex + 1
guard items.count > nextIndex else {
throw APError.QueueError.noNextItem
}
_currentIndex = nextIndex
return _items[nextIndex]
currentIndex = nextIndex
return items[nextIndex]
}
/**
@@ -128,12 +121,12 @@ class QueueManager<T> {
*/
@discardableResult
public func previous() throws -> T {
let previousIndex = _currentIndex - 1
let previousIndex = currentIndex - 1
guard previousIndex >= 0 else {
throw APError.QueueError.noPreviousItem
}
_currentIndex = previousIndex
return _items[previousIndex]
currentIndex = previousIndex
return items[previousIndex]
}
/**
@@ -150,12 +143,12 @@ class QueueManager<T> {
throw APError.QueueError.invalidIndex(index: index, message: "Cannot jump to the current item")
}
guard index >= 0 && _items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "The jump index has to be positive and smaller thant the count of current items (\(_items.count))")
guard index >= 0 && items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "The jump index has to be positive and smaller thant the count of current items (\(items.count))")
}
_currentIndex = index
return _items[index]
currentIndex = index
return items[index]
}
/**
@@ -166,17 +159,16 @@ class QueueManager<T> {
- throws: `APError.QueueError`
*/
func moveItem(fromIndex: Int, toIndex: Int) throws {
guard fromIndex != _currentIndex else {
guard fromIndex != currentIndex else {
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex cannot be equal to the current index.")
}
guard fromIndex >= 0 && fromIndex < _items.count else {
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex has to be positive and smaller than the count of current items (\(_items.count)).")
guard fromIndex >= 0 && fromIndex < items.count else {
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex has to be positive and smaller than the count of current items (\(items.count)).")
}
guard toIndex >= 0 && toIndex < _items.count else {
throw APError.QueueError.invalidIndex(index: toIndex, message: "The toIndex has to be positive and smaller than the count of current items (\(_items.count)).")
guard toIndex >= 0 && toIndex < items.count else {
throw APError.QueueError.invalidIndex(index: toIndex, message: "The toIndex has to be positive and smaller than the count of current items (\(items.count)).")
}
let item = try removeItem(at: fromIndex)
@@ -192,19 +184,19 @@ class QueueManager<T> {
*/
@discardableResult
public func removeItem(at index: Int) throws -> T {
guard index != _currentIndex else {
guard index != currentIndex else {
throw APError.QueueError.invalidIndex(index: index, message: "Cannot remove the current item!")
}
guard index >= 0 && _items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "Index for removal 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 for removal has to be positive and smaller than the count of current items (\(items.count)).")
}
if index < _currentIndex {
_currentIndex = _currentIndex - 1
if index < currentIndex {
currentIndex -= 1
}
return _items.remove(at: index)
return items.remove(at: index)
}
/**
@@ -214,10 +206,10 @@ class QueueManager<T> {
*/
public func replaceCurrentItem(with item: T) {
if current == nil {
self.addItem(item)
addItem(item)
}
self._items[_currentIndex] = item
items[currentIndex] = item
}
/**
@@ -226,8 +218,8 @@ class QueueManager<T> {
*/
public func removePreviousItems() {
guard currentIndex > 0 else { return }
_items.removeSubrange(0..<_currentIndex)
_currentIndex = 0
items.removeSubrange(0..<currentIndex)
currentIndex = 0
}
/**
@@ -235,17 +227,17 @@ class QueueManager<T> {
If no upcoming items exist, no action will be taken.
*/
public func removeUpcomingItems() {
let nextIndex = _currentIndex + 1
guard nextIndex < _items.count else { return }
_items.removeSubrange(nextIndex..<_items.count)
let nextIndex = currentIndex + 1
guard nextIndex < items.count else { return }
items.removeSubrange(nextIndex..<items.count)
}
/**
Removes all items for queue
*/
public func clearQueue() {
_currentIndex = 0
_items.removeAll()
currentIndex = 0
items.removeAll()
}
}
+54 -29
View File
@@ -24,14 +24,14 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
public var repeatMode: RepeatMode = .off
public override var currentItem: AudioItem? {
return queueManager.current
queueManager.current
}
/**
The index of the current item.
*/
public var currentIndex: Int {
return queueManager.currentIndex
queueManager.currentIndex
}
/**
@@ -39,7 +39,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
*/
public override func stop() {
super.stop()
self.event.queueIndex.emit(data: (currentIndex, nil))
event.queueIndex.emit(data: (currentIndex, nil))
}
override func reset() {
@@ -51,21 +51,21 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
All items currently in the queue.
*/
public var items: [AudioItem] {
return queueManager.items
queueManager.items
}
/**
The previous items held by the queue.
*/
public var previousItems: [AudioItem] {
return queueManager.previousItems
queueManager.previousItems
}
/**
The upcoming items in the queue.
*/
public var nextItems: [AudioItem] {
return queueManager.nextItems
queueManager.nextItems
}
/**
@@ -89,7 +89,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
public func add(item: AudioItem, playWhenReady: Bool = true) throws {
if currentItem == nil {
queueManager.addItem(item)
try self.load(item: item, playWhenReady: playWhenReady)
try load(item: item, playWhenReady: playWhenReady)
}
else {
queueManager.addItem(item)
@@ -106,7 +106,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
public func add(items: [AudioItem], playWhenReady: Bool = true) throws {
if currentItem == nil {
queueManager.addItems(items)
try self.load(item: currentItem!, playWhenReady: playWhenReady)
try load(item: currentItem!, playWhenReady: playWhenReady)
}
else {
queueManager.addItems(items)
@@ -123,18 +123,33 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
- throws: `APError`
*/
public func next() throws {
event.playbackEnd.emit(data: .skippedToNext)
let nextItem = try queueManager.next()
try self.load(item: nextItem, playWhenReady: true)
let shouldPlayWhenReady = (playerState == .loading) ? willPlayWhenReady : [.buffering, .playing].contains(playerState)
do {
let nextItem = try queueManager.next()
event.playbackEnd.emit(data: .skippedToNext)
try load(item: nextItem, playWhenReady: shouldPlayWhenReady)
} catch APError.QueueError.noNextItem {
if repeatMode == .queue {
event.playbackEnd.emit(data: .skippedToNext)
try jumpToItem(atIndex: 0, playWhenReady: shouldPlayWhenReady)
} else {
throw APError.QueueError.noNextItem
}
} catch {
throw error
}
}
/**
Step to the previous item in the queue.
*/
public func previous() throws {
event.playbackEnd.emit(data: .skippedToPrevious)
let shouldPlayWhenReady = (playerState == .loading) ? willPlayWhenReady : [.buffering, .playing].contains(playerState)
let previousItem = try queueManager.previous()
try self.load(item: previousItem, playWhenReady: true)
event.playbackEnd.emit(data: .skippedToPrevious)
try load(item: previousItem, playWhenReady: shouldPlayWhenReady)
}
/**
@@ -155,9 +170,15 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
- throws: `APError`
*/
public func jumpToItem(atIndex index: Int, playWhenReady: Bool = true) throws {
event.playbackEnd.emit(data: .jumpedToIndex)
let item = try queueManager.jump(to: index)
try self.load(item: item, playWhenReady: playWhenReady)
if (index == currentIndex) {
seek(to: 0)
playWhenReady ? play() : pause()
onCurrentIndexChanged(oldIndex: index, newIndex: index)
} else {
let item = try queueManager.jump(to: index)
event.playbackEnd.emit(data: .jumpedToIndex)
try load(item: item, playWhenReady: playWhenReady)
}
}
/**
@@ -167,7 +188,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)
}
@@ -191,18 +212,22 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
super.AVWrapperItemDidPlayToEndTime()
switch repeatMode {
case .off: try? self.next()
case .off:
do {
let nextItem = try queueManager.next()
try load(item: nextItem, playWhenReady: true)
} catch {
event.queueIndex.emit(data: (currentIndex, nil))
}
case .track:
seek(to: 0)
play()
try? jumpToItem(atIndex: currentIndex, playWhenReady: true)
case .queue:
do {
try self.next()
} catch APError.QueueError.noNextItem {
do {
try jumpToItem(atIndex: 0, playWhenReady: true)
} catch { /* TODO: handle possible errors from load */ }
} catch { /* TODO: handle possible errors from load */ }
let nextItem = try queueManager.next()
try load(item: nextItem, playWhenReady: true)
} catch {
try? jumpToItem(atIndex: 0, playWhenReady: true)
}
}
}
@@ -210,11 +235,11 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
func onCurrentIndexChanged(oldIndex: Int, newIndex: Int) {
// if _currentItem is nil, then this was triggered by a reset. ignore.
if _currentItem == nil { return }
self.event.queueIndex.emit(data: (oldIndex, newIndex))
if currentItem == nil { return }
event.queueIndex.emit(data: (oldIndex, newIndex))
}
func onReceivedFirstItem() {
self.event.queueIndex.emit(data: (nil, 0))
event.queueIndex.emit(data: (nil, 0))
}
}
@@ -103,7 +103,7 @@ public struct FeedbackCommand: RemoteCommandProtocol {
}
}
public enum RemoteCommand {
public enum RemoteCommand: CustomStringConvertible {
case play
@@ -128,6 +128,23 @@ public enum RemoteCommand {
case dislike(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
case bookmark(isActive: Bool, localizedTitle: String, localizedShortTitle: String)
public var description: String {
switch self {
case .play: return "play"
case .pause: return "pause"
case .stop: return "stop"
case .togglePlayPause: return "togglePlayPause"
case .next: return "nextTrack"
case .previous: return "previousTrack"
case .changePlaybackPosition: return "changePlaybackPosition"
case .skipForward(_): return "skipForward"
case .skipBackward(_): return "skipBackward"
case .like(_, _, _): return "like"
case .dislike(_, _, _): return "dislike"
case .bookmark(_, _, _): return "bookmark"
}
}
/**
All values in an array for convenience.
@@ -19,31 +19,34 @@ public class RemoteCommandController {
weak var audioPlayer: AudioPlayer?
var commandTargetPointers: [String: Any] = [:]
private var enabledCommands: [RemoteCommand] = []
/**
Create a new RemoteCommandController.
- parameter remoteCommandCenter: The MPRemoteCommandCenter used. Default is `MPRemoteCommandCenter.shared()`
*/
public init(remoteCommandCenter: MPRemoteCommandCenter = MPRemoteCommandCenter.shared()) {
self.center = remoteCommandCenter
center = remoteCommandCenter
}
internal func enable(commands: [RemoteCommand]) {
self.disable(commands: RemoteCommand.all())
commands.forEach { (command) in
self.enable(command: command)
let commandsToDisable = enabledCommands.filter { command in
!commands.contains(where: { $0.description == command.description })
}
enabledCommands = commands
commands.forEach { self.enable(command: $0) }
disable(commands: commandsToDisable)
}
internal func disable(commands: [RemoteCommand]) {
commands.forEach { (command) in
self.disable(command: command)
}
commands.forEach { self.disable(command: $0) }
}
private func enableCommand<Command: RemoteCommandProtocol>(_ command: Command) {
center[keyPath: command.commandKeyPath].isEnabled = true
center[keyPath: command.commandKeyPath].removeTarget(commandTargetPointers[command.id])
commandTargetPointers[command.id] = center[keyPath: command.commandKeyPath].addTarget(handler: self[keyPath: command.handlerKeyPath])
}
@@ -92,21 +95,21 @@ public class RemoteCommandController {
// MARK: - Handlers
public lazy var handlePlayCommand: RemoteCommandHandler = self.handlePlayCommandDefault
public lazy var handlePauseCommand: RemoteCommandHandler = self.handlePauseCommandDefault
public lazy var handleStopCommand: RemoteCommandHandler = self.handleStopCommandDefault
public lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = self.handleTogglePlayPauseCommandDefault
public lazy var handleSkipForwardCommand: RemoteCommandHandler = self.handleSkipForwardCommandDefault
public lazy var handleSkipBackwardCommand: RemoteCommandHandler = self.handleSkipBackwardDefault
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
public lazy var handlePlayCommand: RemoteCommandHandler = handlePlayCommandDefault
public lazy var handlePauseCommand: RemoteCommandHandler = handlePauseCommandDefault
public lazy var handleStopCommand: RemoteCommandHandler = handleStopCommandDefault
public lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = handleTogglePlayPauseCommandDefault
public lazy var handleSkipForwardCommand: RemoteCommandHandler = handleSkipForwardCommandDefault
public lazy var handleSkipBackwardCommand: RemoteCommandHandler = handleSkipBackwardDefault
public lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = handleChangePlaybackPositionCommandDefault
public lazy var handleNextTrackCommand: RemoteCommandHandler = handleNextTrackCommandDefault
public lazy var handlePreviousTrackCommand: RemoteCommandHandler = handlePreviousTrackCommandDefault
public lazy var handleLikeCommand: RemoteCommandHandler = handleLikeCommandDefault
public lazy var handleDislikeCommand: RemoteCommandHandler = handleDislikeCommandDefault
public lazy var handleBookmarkCommand: RemoteCommandHandler = handleBookmarkCommandDefault
private func handlePlayCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let audioPlayer = self.audioPlayer {
if let audioPlayer = audioPlayer {
audioPlayer.play()
return MPRemoteCommandHandlerStatus.success
}
@@ -114,7 +117,7 @@ public class RemoteCommandController {
}
private func handlePauseCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let audioPlayer = self.audioPlayer {
if let audioPlayer = audioPlayer {
audioPlayer.pause()
return MPRemoteCommandHandlerStatus.success
}
@@ -122,7 +125,7 @@ public class RemoteCommandController {
}
private func handleStopCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let audioPlayer = self.audioPlayer {
if let audioPlayer = audioPlayer {
audioPlayer.stop()
return .success
}
@@ -130,7 +133,7 @@ public class RemoteCommandController {
}
private func handleTogglePlayPauseCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let audioPlayer = self.audioPlayer {
if let audioPlayer = audioPlayer {
audioPlayer.togglePlaying()
return MPRemoteCommandHandlerStatus.success
}
@@ -140,7 +143,7 @@ public class RemoteCommandController {
private func handleSkipForwardCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let command = event.command as? MPSkipIntervalCommand,
let interval = command.preferredIntervals.first,
let audioPlayer = self.audioPlayer {
let audioPlayer = audioPlayer {
audioPlayer.seek(to: audioPlayer.currentTime + Double(truncating: interval))
return MPRemoteCommandHandlerStatus.success
}
@@ -150,7 +153,7 @@ public class RemoteCommandController {
private func handleSkipBackwardDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let command = event.command as? MPSkipIntervalCommand,
let interval = command.preferredIntervals.first,
let audioPlayer = self.audioPlayer {
let audioPlayer = audioPlayer {
audioPlayer.seek(to: audioPlayer.currentTime - Double(truncating: interval))
return MPRemoteCommandHandlerStatus.success
}
@@ -159,7 +162,7 @@ public class RemoteCommandController {
private func handleChangePlaybackPositionCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let event = event as? MPChangePlaybackPositionCommandEvent,
let audioPlayer = self.audioPlayer {
let audioPlayer = audioPlayer {
audioPlayer.seek(to: event.positionTime)
return MPRemoteCommandHandlerStatus.success
}
@@ -167,41 +170,41 @@ public class RemoteCommandController {
}
private func handleNextTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let player = self.audioPlayer as? QueuedAudioPlayer {
if let player = audioPlayer as? QueuedAudioPlayer {
do {
try player.next()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
return getRemoteCommandHandlerStatus(forError: error)
}
}
return MPRemoteCommandHandlerStatus.commandFailed
}
private func handlePreviousTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let player = self.audioPlayer as? QueuedAudioPlayer {
if let player = audioPlayer as? QueuedAudioPlayer {
do {
try player.previous()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
return getRemoteCommandHandlerStatus(forError: error)
}
}
return MPRemoteCommandHandlerStatus.commandFailed
}
private func handleLikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
return MPRemoteCommandHandlerStatus.success
MPRemoteCommandHandlerStatus.success
}
private func handleDislikeCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
return MPRemoteCommandHandlerStatus.success
MPRemoteCommandHandlerStatus.success
}
private func handleBookmarkCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
return MPRemoteCommandHandlerStatus.success
MPRemoteCommandHandlerStatus.success
}
private func getRemoteCommandHandlerStatus(forError error: Error) -> MPRemoteCommandHandlerStatus {
@@ -213,7 +216,7 @@ public class RemoteCommandController {
}
else if let error = error as? APError.QueueError {
switch error {
case .noNextItem, .noPreviousItem, .invalidIndex(_, _):
case .noNextItem, .noPreviousItem, .invalidIndex(_, _), .noNextWhenRepeatModeTrack:
return MPRemoteCommandHandlerStatus.noSuchContent
}
}