Compare commits

..

34 Commits

Author SHA1 Message Date
David Chavez 92554a187c Release 0.15.0 2022-04-01 23:54:08 +02:00
David Chavez 473651f357 Support mp3 embedded chapters 2022-04-01 23:47:46 +02:00
David Chavez db2f3e9af7 Remove obsolete code 2022-04-01 23:22:26 +02:00
David Chavez a9f831a258 Fix bug in addItems at index and add tests 2022-04-01 21:18:52 +02:00
David Chavez cc3840d81e Fix next/previous with repeat modes 2022-04-01 20:47:54 +02:00
David Chavez 5307090ea3 Replace deprecated “timedMetadata" KVO 2022-04-01 17:47:57 +02:00
David Chavez bdaee8b18f Extract more information from interruptions 2022-04-01 00:14:47 +02:00
David Chavez 84d359bc4f Update README.md 2022-02-24 09:14:36 +01:00
David Chavez 40ea7ad2f9 Release 0.14.7 2022-02-24 08:49:31 +01:00
David Chavez f2f1c1236c Add tests for new seek improvements 2022-02-24 08:48:54 +01:00
Terkel a75f0d0201 fix: make moveItem public and accessible from outside the class (#9) 2022-02-23 21:40:39 +01:00
Jacob Spizziri 9e4e7f6807 fix(seek): fix an issue causing seek to fail if called immediatly after load (#11) 2022-02-23 21:27:38 +01:00
David Chavez dbd3b03989 Release 0.14.6 2021-11-06 14:38:13 +01:00
David Chavez 7e19604df7 Create LICENSE (#5)
* Create LICENSE

* Update LICENSE
2021-11-06 14:29:06 +01:00
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
David Chavez acab6473b2 Release 0.13.2 2021-08-12 11:40:50 +02:00
David Chavez 57b6fb08f3 Fix tests 2021-08-12 11:40:21 +02:00
40 changed files with 600 additions and 142 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']
+16 -14
View File
@@ -44,8 +44,8 @@
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 */; };
9B05AA3A266028E200C7A389 /* SwiftAudio in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA39266028E200C7A389 /* SwiftAudio */; };
9B05AA3C26602C0E00C7A389 /* SwiftAudio in Frameworks */ = {isa = PBXBuildFile; productRef = 9B05AA3B26602C0E00C7A389 /* SwiftAudio */; };
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 */; };
/* End PBXBuildFile 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 = (
9B05AA3A266028E200C7A389 /* SwiftAudio in Frameworks */,
9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -111,7 +111,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9B05AA3C26602C0E00C7A389 /* SwiftAudio 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 = (
9B05AA39266028E200C7A389 /* SwiftAudio */,
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */,
);
productName = SwiftAudio;
productReference = 607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */;
@@ -267,7 +267,7 @@
packageProductDependencies = (
9B05AA302660276400C7A389 /* Quick */,
9B05AA322660276400C7A389 /* Nimble */,
9B05AA3B26602C0E00C7A389 /* SwiftAudio */,
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,13 +674,13 @@
package = 9B05AA2C2660274F00C7A389 /* XCRemoteSwiftPackageReference "Nimble" */;
productName = Nimble;
};
9B05AA39266028E200C7A389 /* SwiftAudio */ = {
9B1D5E1D27C76F5C004CA883 /* SwiftAudioEx */ = {
isa = XCSwiftPackageProductDependency;
productName = SwiftAudio;
productName = SwiftAudioEx;
};
9B05AA3B26602C0E00C7A389 /* SwiftAudio */ = {
9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */ = {
isa = XCSwiftPackageProductDependency;
productName = SwiftAudio;
productName = SwiftAudioEx;
};
/* End XCSwiftPackageProductDependency section */
};
+1 -1
View File
@@ -7,7 +7,7 @@
//
import Foundation
import SwiftAudio
import SwiftAudioEx
class AudioController {
+1 -1
View File
@@ -7,7 +7,7 @@
//
import UIKit
import SwiftAudio
import SwiftAudioEx
class QueueViewController: UIViewController {
+7 -2
View File
@@ -7,7 +7,7 @@
//
import UIKit
import SwiftAudio
import SwiftAudioEx
import AVFoundation
import MediaPlayer
@@ -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 {
@@ -2,7 +2,7 @@ import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
@testable import SwiftAudioEx
class AVPlayerItemNotificationObserverTests: QuickSpec {
@@ -2,7 +2,7 @@ import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
@testable import SwiftAudioEx
class AVPlayerItemObserverTests: QuickSpec {
@@ -47,9 +47,9 @@ class AVPlayerItemObserverTests: QuickSpec {
}
class AVPlayerItemObserverDelegateHolder: AVPlayerItemObserverDelegate {
var receivedMetadata: ((_ metadata: [AVMetadataItem]) -> Void)?
func item(didReceiveMetadata metadata: [AVMetadataItem]) {
var receivedMetadata: ((_ metadata: [AVTimedMetadataGroup]) -> Void)?
func item(didReceiveMetadata metadata: [AVTimedMetadataGroup]) {
receivedMetadata?(metadata)
}
+1 -1
View File
@@ -2,7 +2,7 @@ import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
@testable import SwiftAudioEx
class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
@@ -2,7 +2,7 @@ import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
@testable import SwiftAudioEx
class AVPlayerTimeObserverTests: QuickSpec {
+13 -2
View File
@@ -1,7 +1,7 @@
import AVFoundation
import XCTest
@testable import SwiftAudio
@testable import SwiftAudioEx
class AVPlayerWrapperTests: XCTestCase {
@@ -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]) {
}
+1 -1
View File
@@ -2,7 +2,7 @@ import Quick
import Nimble
import MediaPlayer
@testable import SwiftAudio
@testable import SwiftAudioEx
class AudioPlayerEventTests: QuickSpec {
+3 -3
View File
@@ -3,7 +3,7 @@ import Nimble
import AVFoundation
import XCTest
@testable import SwiftAudio
@testable import SwiftAudioEx
class AudioPlayerTests: XCTestCase {
@@ -124,8 +124,8 @@ class AudioPlayerTests: XCTestCase {
// MARK: - Rate
func test_AudioPlayer__rate__should_be_0() {
XCTAssert(audioPlayer.rate == 0.0)
func test_AudioPlayer__rate__should_be_1() {
XCTAssert(audioPlayer.rate == 1.0)
}
func test_AudioPlayer__rate__playing_source__should_be_1() {
@@ -2,7 +2,7 @@ import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
@testable import SwiftAudioEx
class AudioSessionControllerTests: QuickSpec {
@@ -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
}
}
+1 -1
View File
@@ -9,7 +9,7 @@
import Foundation
import AVFoundation
@testable import SwiftAudio
@testable import SwiftAudioEx
class NonFailingAudioSession: AudioSession {
+1 -1
View File
@@ -8,7 +8,7 @@
import Foundation
@testable import SwiftAudio
@testable import SwiftAudioEx
final class MockDispatchQueue: DispatchQueueType {
func async(flags: DispatchWorkItemFlags, execute work: @escaping @convention(block) () -> Void) {
@@ -9,7 +9,7 @@
import Foundation
import AVFoundation
@testable import SwiftAudio
@testable import SwiftAudioEx
class NowPlayingInfoCenter_Mock: NowPlayingInfoCenter {
@@ -9,7 +9,7 @@
import Foundation
import MediaPlayer
@testable import SwiftAudio
@testable import SwiftAudioEx
class NowPlayingInfoController_Mock: NowPlayingInfoControllerProtocol {
@@ -2,7 +2,7 @@ import Quick
import Nimble
import MediaPlayer
@testable import SwiftAudio
@testable import SwiftAudioEx
class NowPlayingInfoControllerTests: QuickSpec {
+1 -1
View File
@@ -2,7 +2,7 @@ import Quick
import Nimble
import MediaPlayer
@testable import SwiftAudio
@testable import SwiftAudioEx
/// Tests that the AudioPlayer is automatically updating the values it should update in the NowPlayingInfoController.
class NowPlayingInfoTests: QuickSpec {
+48 -1
View File
@@ -1,7 +1,7 @@
import Quick
import Nimble
@testable import SwiftAudio
@testable import SwiftAudioEx
class QueueManagerTests: QuickSpec {
@@ -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") {
+221 -1
View File
@@ -1,7 +1,7 @@
import Quick
import Nimble
@testable import SwiftAudio
@testable import SwiftAudioEx
class QueuedAudioPlayerTests: QuickSpec {
override func spec() {
@@ -166,6 +166,226 @@ 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()], playWhenReady: true)
}
context("then setting repeat mode off") {
beforeEach {
audioPlayer.repeatMode = .off
}
context("allow playback to end") {
beforeEach {
audioPlayer.seek(to: 0.0682)
}
it("should move to next item") {
expect(audioPlayer.nextItems.count).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
context("allow playback to end again") {
beforeEach {
audioPlayer.seek(to: 0.0682)
}
it("should stop playback normally") {
expect(audioPlayer.nextItems.count).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.paused))
}
}
}
context("then calling next()") {
beforeEach {
try? audioPlayer.next()
}
it("should move to next item") {
expect(audioPlayer.nextItems.count).to(equal(0))
expect(audioPlayer.currentIndex).to(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
context("then calling next() again") {
it("should fail") {
expect(try audioPlayer.next()).to(throwError())
}
}
}
}
context("then setting repeat mode track") {
beforeEach {
audioPlayer.repeatMode = .track
}
context("allow playback to end") {
beforeEach {
audioPlayer.seek(to: 0.0682)
}
it("should restart current item") {
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))
}
}
context("then calling next()") {
beforeEach {
try? audioPlayer.next()
}
it("should move to next item and should play") {
expect(audioPlayer.nextItems.count).to(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
}
}
context("then setting repeat mode queue") {
beforeEach {
audioPlayer.repeatMode = .queue
}
context("allow playback to end") {
beforeEach {
audioPlayer.seek(to: 0.0682)
}
it("should move to next item and should play") {
expect(audioPlayer.nextItems.count).toEventually(equal(0))
expect(audioPlayer.currentIndex).toEventually(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
context("allow playback to end again") {
beforeEach {
audioPlayer.seek(to: 0.0682)
}
it("should move to first track and should play") {
expect(audioPlayer.nextItems.count).toEventually(equal(1))
expect(audioPlayer.currentIndex).toEventually(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
}
}
context("then calling next()") {
beforeEach {
try? audioPlayer.next()
}
it("should move to next item and should play") {
expect(audioPlayer.nextItems.count).to(equal(0))
expect(audioPlayer.currentIndex).to(equal(1))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
context("then calling next() again") {
beforeEach {
try? audioPlayer.next()
}
it("should move to first track and should play") {
expect(audioPlayer.nextItems.count).to(equal(1))
expect(audioPlayer.currentIndex).to(equal(0))
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
}
}
}
}
}
}
}
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
//
import Foundation
import SwiftAudio
import SwiftAudioEx
import UIKit
struct Source {
+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.
+7 -7
View File
@@ -2,18 +2,18 @@
import PackageDescription
let package = Package(
name: "SwiftAudio",
platforms: [.iOS(.v10)],
name: "SwiftAudioEx",
platforms: [.iOS(.v11)],
products: [
.library(
name: "SwiftAudio",
targets: ["SwiftAudio"]),
name: "SwiftAudioEx",
targets: ["SwiftAudioEx"]),
],
dependencies: [],
targets: [
.target(
name: "SwiftAudio",
name: "SwiftAudioEx",
dependencies: [],
path: "SwiftAudio/Classes")
path: "SwiftAudioEx/Classes")
]
)
)
+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.1'
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.
@@ -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
}
}
@@ -37,6 +37,9 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
*/
fileprivate var _playWhenReady: Bool = true
fileprivate var _initialTime: TimeInterval?
/// True when the track was paused for the purpose of switching tracks
fileprivate var _pausedForLoad: Bool = false
fileprivate var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
didSet {
@@ -86,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
@@ -100,7 +107,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
return seconds
}
else if let seconds = currentItem?.loadedTimeRanges.first?.timeRangeValue.duration.seconds,
!seconds.isNaN {
!seconds.isNaN {
return seconds
}
return 0.0
@@ -162,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)
}
}
}
@@ -210,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
@@ -236,7 +258,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval? = nil, options: [String : Any]? = nil) {
_initialTime = initialTime
_pausedForLoad = true
self.pause()
self.load(from: url, playWhenReady: playWhenReady, options: options)
}
@@ -277,6 +302,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
if currentItem == nil {
_state = .idle
}
else if _pausedForLoad == true {}
else {
self._state = .paused
}
@@ -293,6 +319,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
switch status {
case .readyToPlay:
self._state = .ready
self._pausedForLoad = false
if _playWhenReady && (_initialTime ?? 0) == 0 {
self.play()
}
@@ -345,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 }
+23 -4
View File
@@ -42,10 +42,20 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
/**
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 {
return wrapper.willPlayWhenReady
}
/**
The elapsed playback time of the current item.
@@ -234,6 +244,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 +286,8 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
- Playback rate
*/
public func updateNowPlayingPlaybackValues() {
updateNowPlayingDuration(duration)
updateNowPlayingCurrentTime(currentTime)
updateNowPlayingDuration(duration)
updateNowPlayingRate(rate)
}
@@ -350,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)
}
@@ -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.
@@ -112,7 +115,19 @@ public class AudioSessionController {
return
}
self.delegate?.handleInterruption(type: type)
switch type {
case .began:
self.delegate?.handleInterruption(type: .began)
case .ended:
guard let typeValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else {
self.delegate?.handleInterruption(type: .ended(shouldResume: false))
return
}
let options = AVAudioSession.InterruptionOptions(rawValue: typeValue)
self.delegate?.handleInterruption(type: .ended(shouldResume: options.contains(.shouldResume)))
@unknown default: return
}
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ extension AudioPlayer {
public typealias FailEventData = (Error?)
public typealias SeekEventData = (seconds: Int, didFinish: Bool)
public typealias UpdateDurationEventData = (Double)
public typealias MetadataEventData = ([AVMetadataItem])
public typealias MetadataEventData = ([AVTimedMetadataGroup])
public typealias DidRecreateAVPlayerEventData = ()
public typealias QueueIndexEventData = (previousIndex: Int?, newIndex: Int?)
@@ -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,7 +8,7 @@
import Foundation
import AVFoundation
protocol AVPlayerItemObserverDelegate: class {
protocol AVPlayerItemObserverDelegate: AnyObject {
/**
Called when the observed item updates the duration.
@@ -18,7 +18,7 @@ protocol AVPlayerItemObserverDelegate: class {
/**
Called when the observed item receives metadata
*/
func item(didReceiveMetadata metadata: [AVMetadataItem])
func item(didReceiveMetadata metadata: [AVTimedMetadataGroup])
}
@@ -29,11 +29,11 @@ class AVPlayerItemObserver: NSObject {
private static var context = 0
private let main: DispatchQueue = .main
private let metadataOutput: AVPlayerItemMetadataOutput
private struct AVPlayerItemKeyPath {
static let duration = #keyPath(AVPlayerItem.duration)
static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges)
static let timedMetadata = #keyPath(AVPlayerItem.timedMetadata)
}
private(set) var isObserving: Bool = false
@@ -41,6 +41,13 @@ class AVPlayerItemObserver: NSObject {
private(set) weak var observingItem: AVPlayerItem?
weak var delegate: AVPlayerItemObserverDelegate?
override init() {
metadataOutput = AVPlayerItemMetadataOutput()
super.init()
metadataOutput.setDelegate(self, queue: main)
}
deinit {
stopObservingCurrentItem()
}
@@ -51,12 +58,12 @@ class AVPlayerItemObserver: NSObject {
- parameter item: The player item to observe.
*/
func startObserving(item: AVPlayerItem) {
self.stopObservingCurrentItem()
self.isObserving = true
self.observingItem = item
stopObservingCurrentItem()
isObserving = true
observingItem = item
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.timedMetadata, options: [.new], context: &AVPlayerItemObserver.context)
item.add(metadataOutput)
}
func stopObservingCurrentItem() {
@@ -65,8 +72,8 @@ class AVPlayerItemObserver: NSObject {
}
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context)
observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.timedMetadata, context: &AVPlayerItemObserver.context)
self.isObserving = false
observingItem.remove(metadataOutput)
isObserving = false
self.observingItem = nil
}
@@ -79,21 +86,22 @@ class AVPlayerItemObserver: NSObject {
switch observedKeyPath {
case AVPlayerItemKeyPath.duration:
if let duration = change?[.newKey] as? CMTime {
self.delegate?.item(didUpdateDuration: duration.seconds)
delegate?.item(didUpdateDuration: duration.seconds)
}
case AVPlayerItemKeyPath.loadedTimeRanges:
if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration {
self.delegate?.item(didUpdateDuration: duration.seconds)
delegate?.item(didUpdateDuration: duration.seconds)
}
case AVPlayerItemKeyPath.timedMetadata:
if let metadata = change?[.newKey] as? [AVMetadataItem] {
self.delegate?.item(didReceiveMetadata: metadata)
}
default: break
}
}
}
extension AVPlayerItemObserver: AVPlayerItemMetadataOutputPushDelegate {
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
delegate?.item(didReceiveMetadata: groups)
}
}
+5 -4
View File
@@ -94,12 +94,13 @@ class QueueManager<T> {
- parameter at: The index to insert the items at.
*/
public func addItems(_ items: [T], at index: Int) throws {
guard index >= 0 && _items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "Index for addition has to be positive and smaller than the count of current items (\(_items.count))")
guard index >= 0 && _items.count >= index else {
throw APError.QueueError.invalidIndex(index: index, message: "Index to insert at has to be non-negative and equal to or smaller than the number of items: (\(_items.count))")
}
_items.insert(contentsOf: items, at: index)
if (_currentIndex >= index) { _currentIndex = _currentIndex + items.count }
if (_currentIndex >= index && _items.count != 1) { _currentIndex = _currentIndex + items.count }
}
/**
+34 -16
View File
@@ -15,8 +15,8 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
let queueManager: QueueManager = QueueManager<AudioItem>()
public init() {
super.init()
public override init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(), remoteCommandController: RemoteCommandController = RemoteCommandController()) {
super.init(nowPlayingInfoController: nowPlayingInfoController, remoteCommandController: remoteCommandController)
queueManager.delegate = self
}
@@ -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 self.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 self.load(item: previousItem, playWhenReady: shouldPlayWhenReady)
}
/**
@@ -155,8 +170,8 @@ 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)
event.playbackEnd.emit(data: .jumpedToIndex)
try self.load(item: item, playWhenReady: playWhenReady)
}
@@ -167,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)
}
@@ -191,18 +206,21 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate {
super.AVWrapperItemDidPlayToEndTime()
switch repeatMode {
case .off: try? self.next()
case .off:
do {
let nextItem = try queueManager.next()
try self.load(item: nextItem, playWhenReady: true)
} catch { /* playback finished */ }
case .track:
seek(to: 0)
play()
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 self.load(item: nextItem, playWhenReady: true)
} catch {
try? jumpToItem(atIndex: 0, playWhenReady: true)
}
}
}
@@ -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,7 +19,8 @@ public class RemoteCommandController {
weak var audioPlayer: AudioPlayer?
var commandTargetPointers: [String: Any] = [:]
private var enabledCommands: [RemoteCommand] = []
/**
Create a new RemoteCommandController.
@@ -30,10 +31,18 @@ public class RemoteCommandController {
}
internal func enable(commands: [RemoteCommand]) {
self.disable(commands: RemoteCommand.all())
let commandsToDisable = enabledCommands.filter { command in
!commands.contains(where: { $0.description == command.description })
}
self.enabledCommands = commands
commands.forEach { (command) in
self.enable(command: command)
}
commandsToDisable.forEach { (command) in
self.disable(command: command)
}
}
internal func disable(commands: [RemoteCommand]) {
@@ -44,6 +53,7 @@ public class RemoteCommandController {
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])
}
@@ -213,7 +223,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
}
}