Compare commits

..

20 Commits

Author SHA1 Message Date
jorgenhenrichsen 0d4060eb68 Merge pull request #14 from jorgenhenrichsen/remote-commands
Remote commands
2018-08-04 19:59:41 +02:00
Jørgen Henrichsen 15a8bc4abd Bumped pod version 2018-08-04 19:53:59 +02:00
Jørgen Henrichsen da5b7702f7 Use next/prev commands in the example. 2018-08-04 08:55:44 +02:00
Jørgen Henrichsen 0066a4121c Added next and previous remote commands.
Also improved error handling in the RemoteCommandController.
2018-08-04 08:55:04 +02:00
jorgenhenrichsen 1bf9d695d8 Merge pull request #12 from jorgenhenrichsen/dev
Dev
2018-07-29 15:21:27 +02:00
Jørgen Henrichsen 8edc3b0e75 Update podspec
Bump version
2018-07-29 15:14:40 +02:00
jorgenhenrichsen 22c780adba Merge pull request #11 from jorgenhenrichsen/master
Merge pull request #7 from jorgenhenrichsen/dev
2018-07-29 15:10:31 +02:00
jorgenhenrichsen c8805d55fd Merge pull request #10 from jorgenhenrichsen/fix/remote-command-handlers
Pass in the correct value in removeTarget(_:) when disabling remote c…
2018-07-29 15:07:56 +02:00
Jørgen Henrichsen a6e74efa37 Pass in the correct value in removeTarget(_:) when disabling remote commands.
Use the actual pointer returned from addTarget(handler:).
https://developer.apple.com/documentation/mediaplayer/mpremotecommand/1622910-addtarget
2018-07-29 14:33:26 +02:00
Jørgen Henrichsen 7d525e3129 Made initializer of AudioPlayer internal.
AudioPlayer is not intended to be used. SimpleAudioPlayer and QueuedAudioPLayer now have public inits.
2018-07-29 10:17:18 +02:00
Jørgen Henrichsen 39d8c55743 Removed delegate test.
The duration of the item did not load without a player.
2018-07-29 09:53:53 +02:00
Jørgen Henrichsen d56d4c699e Added some more testing to the QueueManager and AVPlayerWrapper 2018-07-29 01:10:30 +02:00
Jørgen Henrichsen 04ae9e7986 Made and AVplayerItemObserver
Observes the state of an AVPlayerItem. Just duration for now.
Observing the duration is usefull for updating the UI.
2018-07-29 01:09:08 +02:00
Jørgen Henrichsen 30503f0ffe Use guard statements for error checking 2018-07-28 18:24:16 +02:00
jorgenhenrichsen ba82a46f8b Merge pull request #8 from jorgenhenrichsen/fix/now-playing-rate
The rate is set correctly.
2018-07-28 18:22:28 +02:00
Jørgen Henrichsen 3eef6bf59d Set state to loading on source loading 2018-07-28 18:10:34 +02:00
Jørgen Henrichsen ea9f632eb6 Added a testcase for AVPlayerObserver 2018-07-28 16:13:25 +02:00
Jørgen Henrichsen 53aa56348e Documentation for QueuedAudioPlayer. 2018-03-31 18:16:44 +02:00
Jørgen Henrichsen 26c1d2875e Less updates to the AudioPlayerDelegate.
Only update the AudioPlayerDelegate when the state actually changes.
2018-03-25 22:16:39 +02:00
Jørgen Henrichsen 5ede0f8364 The rate is set correctly.
The rate was set to 1.0 to early, resulting in unsynced elapsed time in the now playing infocenter.
2018-03-22 14:34:36 +01:00
19 changed files with 568 additions and 89 deletions
+4
View File
@@ -16,6 +16,7 @@
077557572066867F0002C6A1 /* QueueManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077557562066867F0002C6A1 /* QueueManager.swift */; };
0775575D2066A7DB0002C6A1 /* SimpleAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0775575C2066A7DB0002C6A1 /* SimpleAudioPlayer.swift */; };
077557612066ABAD0002C6A1 /* QueuedAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077557602066ABAD0002C6A1 /* QueuedAudioPlayer.swift */; };
078C908C210CD8B300555E80 /* AVPlayerItemObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078C908B210CD8B300555E80 /* AVPlayerItemObserver.swift */; };
07F41B1A205FC0B100E25749 /* AudioSessionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F41B19205FC0B100E25749 /* AudioSessionController.swift */; };
07F41B2220614BDC00E25749 /* RemoteCommandController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F41B2120614BDC00E25749 /* RemoteCommandController.swift */; };
0A2CA8B0DD7E300ABFCF1956E6B58248 /* Nimble.h in Headers */ = {isa = PBXBuildFile; fileRef = 2727DBDF3F41A52F002F6B1992165881 /* Nimble.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -181,6 +182,7 @@
077557562066867F0002C6A1 /* QueueManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = QueueManager.swift; path = SwiftAudio/Classes/QueueManager.swift; sourceTree = "<group>"; };
0775575C2066A7DB0002C6A1 /* SimpleAudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SimpleAudioPlayer.swift; path = SwiftAudio/Classes/SimpleAudioPlayer.swift; sourceTree = "<group>"; };
077557602066ABAD0002C6A1 /* QueuedAudioPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = QueuedAudioPlayer.swift; path = SwiftAudio/Classes/QueuedAudioPlayer.swift; sourceTree = "<group>"; };
078C908B210CD8B300555E80 /* AVPlayerItemObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerItemObserver.swift; sourceTree = "<group>"; };
07F41B19205FC0B100E25749 /* AudioSessionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AudioSessionController.swift; path = SwiftAudio/Classes/AudioSessionController.swift; sourceTree = "<group>"; };
07F41B2120614BDC00E25749 /* RemoteCommandController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RemoteCommandController.swift; path = SwiftAudio/Classes/RemoteCommandController.swift; sourceTree = "<group>"; };
0A9D7EA20C39A55A1EF0C23094895A75 /* Quick-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Quick-dummy.m"; sourceTree = "<group>"; };
@@ -387,6 +389,7 @@
07732657205ED6C400C4D1CD /* AVPlayerObserver.swift */,
07732658205ED6C400C4D1CD /* AVPlayerItemNotificationObserver.swift */,
07732659205ED6C400C4D1CD /* AVPlayerTimeObserver.swift */,
078C908B210CD8B300555E80 /* AVPlayerItemObserver.swift */,
);
name = Observer;
path = SwiftAudio/Classes/Observer;
@@ -994,6 +997,7 @@
07F41B1A205FC0B100E25749 /* AudioSessionController.swift in Sources */,
A1A245A1D2A54FF00AACE521F153D281 /* AudioPlayer.swift in Sources */,
0775575D2066A7DB0002C6A1 /* SimpleAudioPlayer.swift in Sources */,
078C908C210CD8B300555E80 /* AVPlayerItemObserver.swift in Sources */,
077557572066867F0002C6A1 /* QueueManager.swift in Sources */,
C525C159B05D6FEEE0FC3D16910C934B /* AVPlayerWrapper.swift in Sources */,
C226CFBDCE851BDA9CD1DA26894BF272 /* AVPlayerWrapperState.swift in Sources */,
@@ -21,6 +21,7 @@
07732654205ECA8B00C4D1CD /* WAV-MP3.wav in Resources */ = {isa = PBXBuildFile; fileRef = 07732650205EACA300C4D1CD /* WAV-MP3.wav */; };
07732655205ECE1C00C4D1CD /* nasa_throttle_up.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */; };
0775575920668B020002C6A1 /* QueueManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0775575820668B020002C6A1 /* QueueManagerTests.swift */; };
078C908F210D263200555E80 /* AVPlayerItemObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */; };
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; };
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
@@ -52,6 +53,7 @@
07732650205EACA300C4D1CD /* WAV-MP3.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = "WAV-MP3.wav"; sourceTree = "<group>"; };
07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = nasa_throttle_up.mp3; sourceTree = "<group>"; };
0775575820668B020002C6A1 /* QueueManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueManagerTests.swift; sourceTree = "<group>"; };
078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerItemObserverTests.swift; sourceTree = "<group>"; };
521F3AEC1228A2FA2637355F /* Pods-SwiftAudio_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftAudio_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftAudio_Tests/Pods-SwiftAudio_Tests.debug.xcconfig"; sourceTree = "<group>"; };
607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAudio_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -149,6 +151,7 @@
074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */,
074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */,
0775575820668B020002C6A1 /* QueueManagerTests.swift */,
078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */,
07732650205EACA300C4D1CD /* WAV-MP3.wav */,
07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */,
607FACE91AFB9204008FA782 /* Supporting Files */,
@@ -437,6 +440,7 @@
files = (
0775575920668B020002C6A1 /* QueueManagerTests.swift in Sources */,
074A6483205C155E0083D868 /* AVPlayerTimeObserverTests.swift in Sources */,
078C908F210D263200555E80 /* AVPlayerItemObserverTests.swift in Sources */,
074A6485205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift in Sources */,
607FACEC1AFB9204008FA782 /* AVPlayerObserverTests.swift in Sources */,
074A6487205E59B60083D868 /* AVPlayerWrapperTests.swift in Sources */,
@@ -40,7 +40,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
codeCoverageEnabled = "YES"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
@@ -70,7 +70,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
+2 -2
View File
@@ -27,8 +27,8 @@ class AudioController {
player.remoteCommands = [
.stop,
.togglePlayPause,
.skipForward(preferredIntervals: [30]),
.skipBackward(preferredIntervals: [30]),
.next,
.previous,
.changePlaybackPosition
]
try? audioSessionController.set(category: .playback)
+11 -1
View File
@@ -60,7 +60,6 @@ class ViewController: UIViewController {
extension ViewController: AudioPlayerDelegate {
func audioPlayer(playerDidChangeState state: AVPlayerWrapperState) {
playButton.setTitle(state == .playing ? "Pause" : "Play", for: .normal)
switch state {
@@ -77,6 +76,9 @@ extension ViewController: AudioPlayerDelegate {
slider.maximumValue = Float(controller.player.duration)
slider.setValue(Float(controller.player.currentTime), animated: true)
elapsedTimeLabel.text = controller.player.currentTime.secondsToString()
remainingTimeLabel.text = (controller.player.duration - controller.player.currentTime).secondsToString()
case .loading, .playing, .paused, .idle:
slider.maximumValue = Float(controller.player.duration)
slider.setValue(Float(controller.player.currentTime), animated: true)
@@ -104,4 +106,12 @@ extension ViewController: AudioPlayerDelegate {
isScrubbing = false
}
func audioPlayer(didUpdateDuration duration: Double) {
slider.maximumValue = Float(controller.player.duration)
slider.setValue(Float(controller.player.currentTime), animated: true)
elapsedTimeLabel.text = controller.player.currentTime.secondsToString()
remainingTimeLabel.text = (controller.player.duration - controller.player.currentTime).secondsToString()
}
}
@@ -0,0 +1,58 @@
import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
let source = Bundle.main.path(forResource: "WAV-MP3", ofType: "wav")!
class AVPlayerItemObserverTests: QuickSpec {
override func spec() {
describe("An AVPlayerItemObserver") {
var observer: AVPlayerItemObserver!
beforeEach {
observer = AVPlayerItemObserver()
}
describe("observed item", {
context("when observing", {
var item: AVPlayerItem!
beforeEach {
item = AVPlayerItem(url: URL(fileURLWithPath: source))
observer.startObserving(item: item)
}
it("should exist", closure: {
expect(observer.observingItem).toEventuallyNot(beNil())
})
})
})
describe("observing status", {
it("should not be observing", closure: {
expect(observer.isObserving).toEventuallyNot(beTrue())
})
context("when observing", {
var item: AVPlayerItem!
beforeEach {
item = AVPlayerItem(url: URL(fileURLWithPath: source))
observer.startObserving(item: item)
}
it("should be observing", closure: {
expect(observer.isObserving).toEventually(beTrue())
})
})
})
}
}
}
class AVPlayerItemObserverDelegateHolder: AVPlayerItemObserverDelegate {
var updateDuration: ((_ duration: Double) -> Void)?
func item(didUpdateDuration duration: Double) {
updateDuration?(duration)
}
}
+10
View File
@@ -43,6 +43,16 @@ class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
expect(self.timeControlStatus).toEventuallyNot(beNil())
})
})
context("when observing again", {
beforeEach {
observer.startObserving()
}
it("should be observing", closure: {
expect(observer.isObserving).toEventually(beTrue())
})
})
})
}
+61 -8
View File
@@ -22,23 +22,33 @@ class AVPlayerWrapperTests: QuickSpec {
wrapper.volume = 0.0
}
describe("its state", {
context("when doing nothing", {
it("should be idle", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
})
describe("state", {
it("should be idle", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
})
context("when loading a source", {
beforeEach {
try? wrapper.load(fromFilePath: source, playWhenReady: false)
}
it("should be loading", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.loading))
})
it("should eventually be ready", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.ready))
})
})
context("when playing with no source", {
beforeEach {
try? wrapper.play()
}
it("should be idle", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
})
})
context("when playing a source", {
beforeEach {
@@ -68,7 +78,22 @@ class AVPlayerWrapperTests: QuickSpec {
it("should eventually be paused", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.paused))
})
})
context("when toggling the source from play", {
let holder = AudioPlayerDelegateHolder()
beforeEach {
wrapper.delegate = holder
holder.stateUpdate = { (state) in
if state == .playing {
try? wrapper.togglePlaying()
}
}
try? wrapper.load(fromFilePath: source, playWhenReady: true)
}
it("should eventually be playing", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.paused))
})
})
context("when stopping the source", {
@@ -95,7 +120,30 @@ class AVPlayerWrapperTests: QuickSpec {
})
})
context("when seeking before loading", {
beforeEach {
try? wrapper.seek(to: 10)
}
it("should be idle", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
})
})
})
describe("its duration", {
it("should be 0", closure: {
expect(wrapper.duration).to(equal(0))
})
context("when loading source", {
beforeEach {
try? wrapper.load(fromFilePath: source, playWhenReady: false)
}
it("should eventually not be 0", closure: {
expect(wrapper.duration).toEventuallyNot(equal(0))
})
})
})
}
@@ -109,6 +157,7 @@ class AudioPlayerDelegateHolder: AVPlayerWrapperDelegate {
var state: AVPlayerWrapperState? {
didSet {
print(state)
if let state = state {
self.stateUpdate?(state)
}
@@ -138,4 +187,8 @@ class AudioPlayerDelegateHolder: AVPlayerWrapperDelegate {
}
func AVWrapper(didUpdateDuration duration: Double) {
}
}
+100 -5
View File
@@ -78,6 +78,11 @@ class QueueManagerTests: QuickSpec {
expect(manager.current).to(equal(self.dummyItems.first))
})
it("should have next items", closure: {
expect(manager.nextItems).toNot(beNil())
expect(manager.nextItems?.count).to(equal(self.dummyItems.count - 1))
})
context("then calling next", {
var nextItem: Int?
beforeEach {
@@ -93,6 +98,10 @@ class QueueManagerTests: QuickSpec {
expect(manager.current).to(equal(self.dummyItems[1]))
})
it("should have previous items", closure: {
expect(manager.previousItems.count).to(equal(1))
})
context("then calling previous", {
var previousItem: Int?
beforeEach {
@@ -171,6 +180,27 @@ class QueueManagerTests: QuickSpec {
// MARK: - Jumping
context("then jumping to the current item", {
var error: Error?
var item: Int?
beforeEach {
do {
item = try manager.jump(to: manager.currentIndex)
}
catch let err {
error = err
}
}
it("should not return an item", closure: {
expect(item).to(beNil())
})
it("should throw an error", closure: {
expect(error).toNot(beNil())
})
})
context("then jumping to the second item", {
var jumped: Int?
beforeEach {
@@ -233,6 +263,76 @@ class QueueManagerTests: QuickSpec {
// MARK: - Moving
context("moving from current index", {
var error: Error?
beforeEach {
do {
try manager.moveItem(fromIndex: manager.currentIndex, toIndex: manager.currentIndex + 1)
}
catch let err { error = err }
}
it("throw an error", closure: {
expect(error).toNot(beNil())
})
})
context("moving from a negative index", {
var error: Error?
beforeEach {
do {
try manager.moveItem(fromIndex: -1, toIndex: manager.currentIndex + 1)
}
catch let err { error = err }
}
it("should throw an error", closure: {
expect(error).toNot(beNil())
})
})
context("moving from a too large index", {
var error: Error?
beforeEach {
do {
try manager.moveItem(fromIndex: manager.items.count, toIndex: manager.currentIndex + 1)
}
catch let err { error = err }
}
it("should throw an error", closure: {
expect(error).toNot(beNil())
})
})
context("moving to a negative index", {
var error: Error?
beforeEach {
do {
try manager.moveItem(fromIndex: manager.currentIndex + 1, toIndex: -1)
}
catch let err { error = err }
}
it("should throw an error", closure: {
expect(error).toNot(beNil())
})
})
context("moving to a too large index", {
var error: Error?
beforeEach {
do {
try manager.moveItem(fromIndex: manager.currentIndex + 1, toIndex: manager.items.count)
}
catch let err { error = err }
}
it("should throw an error", closure: {
expect(error).toNot(beNil())
})
})
context("then moving 2nd to 4th", closure: {
let afterMoving: [Int] = [0, 2, 3, 1, 4, 5, 6]
beforeEach {
@@ -244,11 +344,6 @@ class QueueManagerTests: QuickSpec {
})
})
})
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudio'
s.version = '0.3.0'
s.version = '0.3.2'
s.summary = 'Easy audio streaming for iOS'
# This description is used to generate tags and improve search results.
@@ -18,6 +18,7 @@ protocol AVPlayerWrapperDelegate: class {
func AVWrapper(secondsElapsed seconds: Double)
func AVWrapper(failedWithError error: Error?)
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
func AVWrapper(didUpdateDuration duration: Double)
}
@@ -33,6 +34,7 @@ class AVPlayerWrapper {
let playerObserver: AVPlayerObserver
let playerTimeObserver: AVPlayerTimeObserver
let playerItemNotificationObserver: AVPlayerItemNotificationObserver
let playerItemObserver: AVPlayerItemObserver
/**
True if the last call to load(from:playWhenReady) had playWhenReady=true.
@@ -49,7 +51,9 @@ class AVPlayerWrapper {
fileprivate var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
didSet {
self.delegate?.AVWrapper(didChangeState: _state)
if oldValue != _state {
self.delegate?.AVWrapper(didChangeState: _state)
}
}
}
@@ -145,6 +149,7 @@ class AVPlayerWrapper {
self.playerObserver = AVPlayerObserver(player: avPlayer)
self.playerTimeObserver = AVPlayerTimeObserver(player: avPlayer, periodicObserverTimeInterval: timeEventFrequency.getTime())
self.playerItemNotificationObserver = AVPlayerItemNotificationObserver()
self.playerItemObserver = AVPlayerItemObserver()
self.bufferDuration = 0
self.timeEventFrequency = timeEventFrequency
@@ -152,6 +157,7 @@ class AVPlayerWrapper {
self.playerObserver.delegate = self
self.playerTimeObserver.delegate = self
self.playerItemNotificationObserver.delegate = self
self.playerItemObserver.delegate = self
playerTimeObserver.registerForPeriodicTimeEvents()
}
@@ -255,6 +261,7 @@ class AVPlayerWrapper {
reset(soft: true)
_playWhenReady = playWhenReady
_state = .loading
// Set item
let currentAsset = AVURLAsset(url: url)
@@ -266,6 +273,7 @@ class AVPlayerWrapper {
playerTimeObserver.registerForBoundaryTimeEvents()
playerObserver.startObserving()
playerItemNotificationObserver.startObserving(item: currentItem)
playerItemObserver.startObserving(item: currentItem)
}
/**
@@ -345,3 +353,13 @@ extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
}
}
extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
// MARK: - AVPlayerItemObserverDelegate
func item(didUpdateDuration duration: Double) {
self.delegate?.AVWrapper(didUpdateDuration: duration)
}
}
+12 -4
View File
@@ -22,6 +22,8 @@ public protocol AudioPlayerDelegate: class {
func audioPlayer(seekTo seconds: Int, didFinish: Bool)
func audioPlayer(didUpdateDuration duration: Double)
}
/**
@@ -124,12 +126,11 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
// MARK: - Init
/**
Create a new AudioManager.
Create a new AudioPlayer.
- parameter audioPlayer: The underlying AudioPlayer instance for the Manager. If you need to configure the behaviour of the player, create an instance, configure it and pass it in here.
- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
*/
public init(infoCenter: MPNowPlayingInfoCenter = MPNowPlayingInfoCenter.default()) {
init(infoCenter: MPNowPlayingInfoCenter = MPNowPlayingInfoCenter.default()) {
self.wrapper = AVPlayerWrapper()
self.nowPlayingInfoController = NowPlayingInfoController(infoCenter: infoCenter)
self.remoteCommandController = RemoteCommandController()
@@ -273,7 +274,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
// MARK: - AVPlayerWrapperDelegate
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
updatePlaybackValues()
switch state {
case .playing, .paused: updatePlaybackValues()
default: break
}
self.delegate?.audioPlayer(playerDidChangeState: state)
}
@@ -294,4 +298,8 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
self.delegate?.audioPlayer(seekTo: seconds, didFinish: didFinish)
}
func AVWrapper(didUpdateDuration duration: Double) {
self.delegate?.audioPlayer(didUpdateDuration: duration)
}
}
@@ -0,0 +1,82 @@
//
// AVPlayerItemObserver.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 28/07/2018.
//
import Foundation
import AVFoundation
protocol AVPlayerItemObserverDelegate: class {
/**
Called when the observed item updates the duration.
*/
func item(didUpdateDuration duration: Double)
}
/**
Observing an AVPlayers status changes.
*/
class AVPlayerItemObserver: NSObject {
private static var context = 0
private let main: DispatchQueue = .main
private struct AVPlayerItemKeyPath {
static let duration = #keyPath(AVPlayerItem.duration)
}
var isObserving: Bool = false
weak var observingItem: AVPlayerItem?
weak var delegate: AVPlayerItemObserverDelegate?
deinit {
if self.isObserving {
stopObservingCurrentItem()
}
}
/**
Start observing an item. Will remove self as observer from old item.
- parameter item: The player item to observe.
*/
func startObserving(item: AVPlayerItem) {
main.async {
if self.isObserving {
self.stopObservingCurrentItem()
}
self.isObserving = true
self.observingItem = item
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
}
}
private func stopObservingCurrentItem() {
observingItem?.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
self.isObserving = false
self.observingItem = nil
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard context == &AVPlayerItemObserver.context, let observedKeyPath = keyPath else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
switch observedKeyPath {
case AVPlayerItemKeyPath.duration:
if let duration = change?[.newKey] as? CMTime {
self.delegate?.item(didUpdateDuration: duration.seconds)
}
default:
break
}
}
}
+6 -10
View File
@@ -75,13 +75,11 @@ class QueueManager<T> {
@discardableResult
public func next() throws -> T {
let nextIndex = _currentIndex + 1
if _items.count > nextIndex {
_currentIndex = nextIndex
return _items[nextIndex]
}
else {
guard _items.count > nextIndex else {
throw APError.QueueError.noNextItem
}
_currentIndex = nextIndex
return _items[nextIndex]
}
/**
@@ -94,13 +92,11 @@ class QueueManager<T> {
@discardableResult
public func previous() throws -> T {
let previousIndex = _currentIndex - 1
if previousIndex >= 0 {
_currentIndex = previousIndex
return _items[previousIndex]
}
else {
guard previousIndex >= 0 else {
throw APError.QueueError.noPreviousItem
}
_currentIndex = previousIndex
return _items[previousIndex]
}
/**
+53 -1
View File
@@ -6,7 +6,7 @@
//
import Foundation
import MediaPlayer
/**
An audio player that can keep track of a queue of AudioItems.
@@ -21,18 +21,35 @@ public class QueuedAudioPlayer: AudioPlayer {
*/
public var automaticallyPlayNextSong: Bool = true
public override init(infoCenter: MPNowPlayingInfoCenter = MPNowPlayingInfoCenter.default()) {
super.init(infoCenter: infoCenter)
}
public override var currentItem: AudioItem? {
return queueManager.current
}
/**
The previous items held by the queue.
*/
public var previousItems: [AudioItem]? {
return queueManager.previousItems
}
/**
The upcoming items in the queue.
*/
public var nextItems: [AudioItem]? {
return queueManager.nextItems
}
/**
Add a single item to the queue.
- parameter item: The item to add.
- parameter playWhenReady: If the AudioPlayer has no item loaded, it will load the `item`. If this is `true` it will automatically start playback. Default is `true`.
- throws: `APError`
*/
public func add(item: AudioItem, playWhenReady: Bool = true) throws {
if currentItem == nil {
queueManager.addItem(item)
@@ -43,6 +60,13 @@ public class QueuedAudioPlayer: AudioPlayer {
}
}
/**
Add items to the queue.
- parameter items: The items to add to the queue.
- parameter playWhenReady: If the AudioPlayer has no item loaded, it will load the first item in the list. If this is `true` it will automatically start playback. Default is `true`.
- throws: `APError`
*/
public func add(items: [AudioItem], playWhenReady: Bool = true) throws {
if currentItem == nil {
queueManager.addItems(items)
@@ -53,25 +77,53 @@ public class QueuedAudioPlayer: AudioPlayer {
}
}
/**
Step to the next item in the queue.
- throws: `APError`
*/
public func next() throws {
let nextItem = try queueManager.next()
try self.loadItem(nextItem, playWhenReady: true)
}
/**
Step to the previous item in the queue.
*/
public func previous() throws {
let previousItem = try queueManager.previous()
try self.loadItem(previousItem, playWhenReady: true)
}
/**
Remove an item from the queue.
- parameter index: The index of the item to remove.
- throws: `APError.QueueError`
*/
public func removeItem(atIndex index: Int) throws {
try queueManager.remove(atIndex: index)
}
/**
Jump to a certain item in the queue.
- parameter index: The index of the item to jump to.
- parameter playWhenReady: Wether the item should start playing when ready. Default is `true`.
- throws: `APError`
*/
public func jumpToItem(atIndex index: Int, playWhenReady: Bool = true) throws {
let item = try queueManager.jump(to: index)
try self.loadItem(item, playWhenReady: playWhenReady)
}
/**
Move an item in the queue from one position to another.
- parameter fromIndex: The index of the item to move.
- parameter toIndex: The index to move the item to.
- throws: `APError.QueueError`
*/
func moveItem(fromIndex: Int, toIndex: Int) throws {
try queueManager.moveItem(fromIndex: fromIndex, toIndex: toIndex)
}
+25 -8
View File
@@ -14,23 +14,30 @@ public typealias RemoteCommandHandler = (MPRemoteCommandEvent) -> MPRemoteComman
public protocol RemoteCommandProtocol {
associatedtype Command: MPRemoteCommand
var id: String { get }
var commandKeyPath: KeyPath<MPRemoteCommandCenter, Command> { get }
var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler> { get }
}
public struct BaseRemoteCommand: RemoteCommandProtocol {
public struct PlayBackCommand: RemoteCommandProtocol {
public static let play = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.playCommand, handlerKeyPath: \RemoteCommandController.handlePlayCommand)
public static let play = PlayBackCommand(id: "Play", commandKeyPath: \MPRemoteCommandCenter.playCommand, handlerKeyPath: \RemoteCommandController.handlePlayCommand)
public static let pause = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.pauseCommand, handlerKeyPath: \RemoteCommandController.handlePauseCommand)
public static let pause = PlayBackCommand(id: "Pause", commandKeyPath: \MPRemoteCommandCenter.pauseCommand, handlerKeyPath: \RemoteCommandController.handlePauseCommand)
public static let stop = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.stopCommand, handlerKeyPath: \RemoteCommandController.handleStopCommand)
public static let stop = PlayBackCommand(id: "Stop", commandKeyPath: \MPRemoteCommandCenter.stopCommand, handlerKeyPath: \RemoteCommandController.handleStopCommand)
public static let togglePlayPause = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.togglePlayPauseCommand, handlerKeyPath: \RemoteCommandController.handleTogglePlayPauseCommand)
public static let togglePlayPause = PlayBackCommand(id: "TogglePlayPause", commandKeyPath: \MPRemoteCommandCenter.togglePlayPauseCommand, handlerKeyPath: \RemoteCommandController.handleTogglePlayPauseCommand)
public static let nextTrack = PlayBackCommand(id: "NextTrackCommand", commandKeyPath: \MPRemoteCommandCenter.nextTrackCommand, handlerKeyPath: \RemoteCommandController.handleNextTrackCommand)
public static let previousTrack = PlayBackCommand(id: "PreviousTrack", commandKeyPath: \MPRemoteCommandCenter.previousTrackCommand, handlerKeyPath: \RemoteCommandController.handlePreviousTrackCommand)
public typealias Command = MPRemoteCommand
public let id: String
public var commandKeyPath: KeyPath<MPRemoteCommandCenter, MPRemoteCommand>
public var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler>
@@ -39,10 +46,12 @@ public struct BaseRemoteCommand: RemoteCommandProtocol {
public struct ChangePlaybackPositionCommand: RemoteCommandProtocol {
public static let changePlaybackPosition = ChangePlaybackPositionCommand(commandKeyPath: \MPRemoteCommandCenter.changePlaybackPositionCommand, handlerKeyPath: \RemoteCommandController.handleChangePlaybackPositionCommand)
public static let changePlaybackPosition = ChangePlaybackPositionCommand(id: "ChangePlaybackPosition", commandKeyPath: \MPRemoteCommandCenter.changePlaybackPositionCommand, handlerKeyPath: \RemoteCommandController.handleChangePlaybackPositionCommand)
public typealias Command = MPChangePlaybackPositionCommand
public let id: String
public var commandKeyPath: KeyPath<MPRemoteCommandCenter, MPChangePlaybackPositionCommand>
public var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler>
@@ -51,12 +60,14 @@ public struct ChangePlaybackPositionCommand: RemoteCommandProtocol {
public struct SkipIntervalCommand: RemoteCommandProtocol {
public static let skipForward = SkipIntervalCommand(commandKeyPath: \MPRemoteCommandCenter.skipForwardCommand, handlerKeyPath: \RemoteCommandController.handleSkipForwardCommand)
public static let skipForward = SkipIntervalCommand(id: "SkipForward", commandKeyPath: \MPRemoteCommandCenter.skipForwardCommand, handlerKeyPath: \RemoteCommandController.handleSkipForwardCommand)
public static let skipBackward = SkipIntervalCommand(commandKeyPath: \MPRemoteCommandCenter.skipBackwardCommand, handlerKeyPath: \RemoteCommandController.handleSkipBackwardCommand)
public static let skipBackward = SkipIntervalCommand(id: "SkipBackward", commandKeyPath: \MPRemoteCommandCenter.skipBackwardCommand, handlerKeyPath: \RemoteCommandController.handleSkipBackwardCommand)
public typealias Command = MPSkipIntervalCommand
public let id: String
public var commandKeyPath: KeyPath<MPRemoteCommandCenter, MPSkipIntervalCommand>
public var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler>
@@ -78,6 +89,10 @@ public enum RemoteCommand {
case togglePlayPause
case next
case previous
case changePlaybackPosition
case skipForward(preferredIntervals: [NSNumber])
@@ -94,6 +109,8 @@ public enum RemoteCommand {
.pause,
.stop,
.togglePlayPause,
.next,
.previous,
.changePlaybackPosition,
.skipForward(preferredIntervals: []),
.skipBackward(preferredIntervals: []),
+106 -46
View File
@@ -18,6 +18,8 @@ public class RemoteCommandController {
weak var audioPlayer: AudioPlayer?
var commandTargetPointers: [String: Any] = [:]
init() {}
/**
@@ -40,37 +42,38 @@ public class RemoteCommandController {
private func enableCommand<Command: RemoteCommandProtocol>(_ command: Command) {
center[keyPath: command.commandKeyPath].isEnabled = true
center[keyPath: command.commandKeyPath].addTarget(handler: self[keyPath: command.handlerKeyPath])
commandTargetPointers[command.id] = center[keyPath: command.commandKeyPath].addTarget(handler: self[keyPath: command.handlerKeyPath])
}
private func disableCommand<Command: RemoteCommandProtocol>(_ command: Command) {
center[keyPath: command.commandKeyPath].isEnabled = false
center[keyPath: command.commandKeyPath].removeTarget(self[keyPath: command.handlerKeyPath])
center[keyPath: command.commandKeyPath].removeTarget(commandTargetPointers[command.id])
commandTargetPointers.removeValue(forKey: command.id)
}
private func enable(command: RemoteCommand) {
switch command {
case .play: self.enableCommand(BaseRemoteCommand.play)
case .pause: self.enableCommand(BaseRemoteCommand.pause)
case .stop: self.enableCommand(BaseRemoteCommand.stop)
case .togglePlayPause: self.enableCommand(BaseRemoteCommand.togglePlayPause)
case .play: self.enableCommand(PlayBackCommand.play)
case .pause: self.enableCommand(PlayBackCommand.pause)
case .stop: self.enableCommand(PlayBackCommand.stop)
case .togglePlayPause: self.enableCommand(PlayBackCommand.togglePlayPause)
case .next: self.enableCommand(PlayBackCommand.nextTrack)
case .previous: self.enableCommand(PlayBackCommand.previousTrack)
case .changePlaybackPosition: self.enableCommand(ChangePlaybackPositionCommand.changePlaybackPosition)
case .skipForward(let preferredIntervals):
self.enableCommand(SkipIntervalCommand.skipForward.set(preferredIntervals: preferredIntervals))
case .skipBackward(let preferredIntervals):
self.enableCommand(SkipIntervalCommand.skipBackward.set(preferredIntervals: preferredIntervals))
case .skipForward(let preferredIntervals): self.enableCommand(SkipIntervalCommand.skipForward.set(preferredIntervals: preferredIntervals))
case .skipBackward(let preferredIntervals): self.enableCommand(SkipIntervalCommand.skipBackward.set(preferredIntervals: preferredIntervals))
}
}
private func disable(command: RemoteCommand) {
switch command {
case .play: self.disableCommand(BaseRemoteCommand.play)
case .pause: self.disableCommand(BaseRemoteCommand.pause)
case .stop: self.disableCommand(BaseRemoteCommand.stop)
case .togglePlayPause: self.disableCommand(BaseRemoteCommand.togglePlayPause)
case .play: self.disableCommand(PlayBackCommand.play)
case .pause: self.disableCommand(PlayBackCommand.pause)
case .stop: self.disableCommand(PlayBackCommand.stop)
case .togglePlayPause: self.disableCommand(PlayBackCommand.togglePlayPause)
case .next: self.disableCommand(PlayBackCommand.nextTrack)
case .previous: self.disableCommand(PlayBackCommand.previousTrack)
case .changePlaybackPosition: self.disableCommand(ChangePlaybackPositionCommand.changePlaybackPosition)
case .skipForward(_): self.disableCommand(SkipIntervalCommand.skipForward)
case .skipBackward(_): self.disableCommand(SkipIntervalCommand.skipBackward)
@@ -80,49 +83,64 @@ public class RemoteCommandController {
// MARK: - Handlers
lazy var handlePlayCommand: RemoteCommandHandler = { (event) in
do {
try self.audioPlayer?.play()
return MPRemoteCommandHandlerStatus.success
if let audioPlayer = self.audioPlayer {
do {
try audioPlayer.play()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handlePauseCommand: RemoteCommandHandler = { (event) in
do {
try self.audioPlayer?.pause()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
if let audioPlayer = self.audioPlayer {
do {
try audioPlayer.pause()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
}
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handleStopCommand: RemoteCommandHandler = { (event) in
self.audioPlayer?.stop()
return .success
if let audioPlayer = self.audioPlayer {
audioPlayer.stop()
return .success
}
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = { (event) in
do {
try self.audioPlayer?.togglePlaying()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
if let audioPlayer = self.audioPlayer {
do {
try audioPlayer.togglePlaying()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
}
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handleSkipForwardCommand: RemoteCommandHandler = { (event) in
if let command = event.command as? MPSkipIntervalCommand,
let interval = command.preferredIntervals.first,
let audioPlayer = self.audioPlayer {
try? audioPlayer.seek(to: audioPlayer.currentTime + Double(truncating: interval))
return MPRemoteCommandHandlerStatus.success
do {
try audioPlayer.seek(to: audioPlayer.currentTime + Double(truncating: interval))
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
}
return MPRemoteCommandHandlerStatus.commandFailed
}
@@ -130,17 +148,48 @@ public class RemoteCommandController {
if let command = event.command as? MPSkipIntervalCommand,
let interval = command.preferredIntervals.first,
let audioPlayer = self.audioPlayer {
try? audioPlayer.seek(to: audioPlayer.currentTime - Double(truncating: interval))
return MPRemoteCommandHandlerStatus.success
do {
try audioPlayer.seek(to: audioPlayer.currentTime - Double(truncating: interval))
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
}
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = { (event) in
if let event = event as? MPChangePlaybackPositionCommandEvent {
if let event = event as? MPChangePlaybackPositionCommandEvent,
let audioPlayer = self.audioPlayer {
do {
try self.audioPlayer?.seek(to: event.positionTime)
try audioPlayer.seek(to: event.positionTime)
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
}
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handleNextTrackCommand: RemoteCommandHandler = { (event) in
if let player = self.audioPlayer as? QueuedAudioPlayer {
do {
try player.next()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
}
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handlePreviousTrackCommand: RemoteCommandHandler = { (event) in
if let player = self.audioPlayer as? QueuedAudioPlayer {
do {
try player.previous()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
@@ -157,8 +206,19 @@ public class RemoteCommandController {
return MPRemoteCommandHandlerStatus.noActionableNowPlayingItem
}
}
else if let error = error as? APError.LoadError {
switch error {
case .invalidSourceUrl(_):
return MPRemoteCommandHandlerStatus.commandFailed
}
}
else if let error = error as? APError.QueueError {
switch error {
case .noNextItem, .noPreviousItem, .invalidIndex(_, _):
return MPRemoteCommandHandlerStatus.noSuchContent
}
}
return MPRemoteCommandHandlerStatus.commandFailed
}
}
@@ -6,12 +6,17 @@
//
import Foundation
import MediaPlayer
/**
A simple audio player that keeps on item at a time.
*/
public class SimpleAudioPlayer: AudioPlayer {
public override init(infoCenter: MPNowPlayingInfoCenter = MPNowPlayingInfoCenter.default()) {
super.init(infoCenter: infoCenter)
}
/**
Load an AudioItem into the manager.