Compare commits

..

52 Commits

Author SHA1 Message Date
jorgenhenrichsen 454e036449 Merge pull request #19 from jorgenhenrichsen/dev
Dev
2018-09-27 15:49:55 +02:00
Jørgen Henrichsen 8fb3b8424d Update podspec
Bumped to version 0.3.3
2018-09-27 15:39:40 +02:00
jorgenhenrichsen 731863a900 Merge pull request #18 from jorgenhenrichsen/interruptions
Interruptions
2018-09-27 15:34:03 +02:00
jorgenhenrichsen 45582b3f43 Update README
More info about interruptions
2018-09-27 15:25:15 +02:00
Jørgen Henrichsen 534b62da9e Added tests for interruption notifications 2018-09-27 14:50:33 +02:00
Jørgen Henrichsen e3cb3c4a51 Added possiblity to turn of interruption notifications 2018-09-27 14:50:10 +02:00
Jørgen Henrichsen a0e05a885a Update README
Added part about interruptions.
2018-09-27 09:53:09 +02:00
Jørgen Henrichsen 1a13887a65 Added interruption handler and delegate protocol. 2018-09-27 09:52:52 +02:00
jorgenhenrichsen da6e83f64a Merge pull request #17 from jorgenhenrichsen/testing
Testing
2018-09-25 20:10:14 +02:00
Jørgen Henrichsen 0f2bfb3e79 Use long source when testing SimpleAudioPlayer 2018-09-25 19:52:38 +02:00
Jørgen Henrichsen eabeca93a0 Use shorter buffer duration for audio tests. 2018-09-21 17:35:20 +02:00
Jørgen Henrichsen 6a93840463 More tests
Added for SimpleAudioPlayer and QueuedAudioPlayer.
Made next and previous items in the queue non-optional, instead they will be empty when no items are in the queue.
2018-09-06 23:39:03 +02:00
Jørgen Henrichsen 1261f88803 Removed play to end tests. 2018-08-18 09:29:44 +02:00
Jørgen Henrichsen 6239d6d5e6 Shorter test sound 2018-08-18 09:23:19 +02:00
Jørgen Henrichsen a63771a040 More tests for AVPlayerWrapper and AudioPlayer 2018-08-18 09:03:06 +02:00
Jørgen Henrichsen 7d27ef286f Add the TestSound to the project 2018-08-05 15:28:27 +02:00
Jørgen Henrichsen bf0d3a79dd Use a smaller, silent test sound. 2018-08-05 14:59:31 +02:00
Jørgen Henrichsen 22dec9d6b1 Use low buffer duration when testing audio 2018-08-05 13:48:39 +02:00
jorgenhenrichsen b7be6338c6 Add codecov badge to README 2018-08-05 13:36:38 +02:00
jorgenhenrichsen 97b70d056a Added codecov yml
Ignore Example directory
2018-08-05 13:25:57 +02:00
jorgenhenrichsen c9bce64bc1 Only upload coverage for SwiftAudio 2018-08-05 13:14:57 +02:00
Jørgen Henrichsen dafcbdfe3e Set automaticallyWaitsToMinimizeStalling to false in test case 2018-08-05 13:04:46 +02:00
jorgenhenrichsen 24666c3ab5 Merge pull request #15 from jorgenhenrichsen/master
Master -> Dev
2018-08-05 12:57:18 +02:00
jorgenhenrichsen 8de5d678b8 Use iOS 11.4 on simulator 2018-08-05 12:51:39 +02:00
jorgenhenrichsen 70a55f22e0 Update travis.yml
*Use Xcode 9.4
*Use Swift lang
2018-08-05 12:47:03 +02:00
jorgenhenrichsen e180fc7a72 Add codecov coverage uploading 2018-08-05 12:38:39 +02:00
Jørgen Henrichsen 9008c6f9a0 Renamed the delegate holder 2018-08-05 12:23:44 +02:00
Jørgen Henrichsen d49c522032 Added test case in AudioPlayerSessionControllerTests
Added case for deactivating the audio session.
2018-08-05 12:23:10 +02:00
Jørgen Henrichsen 7681d5d983 Added AudioPlayerTests 2018-08-05 12:22:39 +02:00
Jørgen Henrichsen 8b1e57b3c0 Added a Source file for test audio.
Contains the test audio file path, used in all tests.
2018-08-05 10:32:30 +02:00
Jørgen Henrichsen 1c4f5ec738 AudioSessionController tests.
Also activate audio session on play instead of launch in the example.
2018-08-05 10:02:53 +02:00
Jørgen Henrichsen d2ed064295 Test seeking function of AVPlayerWrapper 2018-08-05 09:07:34 +02:00
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
34 changed files with 1195 additions and 115 deletions
+2
View File
@@ -0,0 +1,2 @@
ignore:
- "Example/.*"
+6 -3
View File
@@ -2,13 +2,16 @@
# * http://www.objc.io/issue-6/travis-ci.html
# * https://github.com/supermarin/xcpretty#usage
osx_image: xcode9.2
language: objective-c
osx_image: xcode9.4
language: swift
cache: cocoapods
podfile: Example/Podfile
before_install:
- gem install cocoapods # Since Travis is not always on latest version
- pod install --project-directory=Example
script:
- set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/SwiftAudio.xcworkspace -scheme SwiftAudio-Example -sdk iphonesimulator11.2 -destination "OS=11.2,name=iPhone X" | xcpretty
- set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/SwiftAudio.xcworkspace -scheme SwiftAudio-Example -sdk iphonesimulator11.4 -destination "OS=11.4,name=iPhone X" | xcpretty
- pod lib lint
after_success:
- bash <(curl -s https://codecov.io/bash) -J 'SwiftAudio'
+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 */,
+48 -2
View File
@@ -13,6 +13,14 @@
0707130B2067F2E000F789B3 /* QueueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0707130A2067F2E000F789B3 /* QueueViewController.swift */; };
0707130F2067F40A00F789B3 /* QueueTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0707130D2067F40A00F789B3 /* QueueTableViewCell.swift */; };
070713102067F40A00F789B3 /* QueueTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0707130E2067F40A00F789B3 /* QueueTableViewCell.xib */; };
0708ED6C2116DA4C00EB29BD /* AudioSessionControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0708ED6B2116DA4B00EB29BD /* AudioSessionControllerTests.swift */; };
0708ED702116E89900EB29BD /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0708ED6F2116E89900EB29BD /* Source.swift */; };
0708ED722116E91D00EB29BD /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0708ED6F2116E89900EB29BD /* Source.swift */; };
0708ED742116EE0100EB29BD /* AudioPlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0708ED732116EE0100EB29BD /* AudioPlayerTests.swift */; };
0708ED79211732F500EB29BD /* TestSound.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 0708ED78211732F500EB29BD /* TestSound.m4a */; };
0708ED7A211732F500EB29BD /* TestSound.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 0708ED78211732F500EB29BD /* TestSound.m4a */; };
07194D212127F6DB002EA8C8 /* ShortTestSound.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 07194D1F2127F283002EA8C8 /* ShortTestSound.m4a */; };
07194D222127F6E9002EA8C8 /* ShortTestSound.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 07194D1F2127F283002EA8C8 /* ShortTestSound.m4a */; };
074A6483205C155E0083D868 /* AVPlayerTimeObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */; };
074A6485205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */; };
074A6487205E59B60083D868 /* AVPlayerWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */; };
@@ -21,6 +29,9 @@
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 */; };
07CC171C213E912E005F880E /* SimpleAudioPlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07CC171A213E912A005F880E /* SimpleAudioPlayerTests.swift */; };
07DBB1E1212C17E600BB4278 /* QueuedAudioPlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.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 */; };
@@ -46,12 +57,20 @@
0707130A2067F2E000F789B3 /* QueueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueViewController.swift; sourceTree = "<group>"; };
0707130D2067F40A00F789B3 /* QueueTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueTableViewCell.swift; sourceTree = "<group>"; };
0707130E2067F40A00F789B3 /* QueueTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QueueTableViewCell.xib; sourceTree = "<group>"; };
0708ED6B2116DA4B00EB29BD /* AudioSessionControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionControllerTests.swift; sourceTree = "<group>"; };
0708ED6F2116E89900EB29BD /* Source.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source.swift; sourceTree = "<group>"; };
0708ED732116EE0100EB29BD /* AudioPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerTests.swift; sourceTree = "<group>"; };
0708ED78211732F500EB29BD /* TestSound.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestSound.m4a; sourceTree = "<group>"; };
07194D1F2127F283002EA8C8 /* ShortTestSound.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = ShortTestSound.m4a; sourceTree = "<group>"; };
074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerTimeObserverTests.swift; sourceTree = "<group>"; };
074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerItemNotificationObserverTests.swift; sourceTree = "<group>"; };
074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerWrapperTests.swift; sourceTree = "<group>"; };
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>"; };
07CC171A213E912A005F880E /* SimpleAudioPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleAudioPlayerTests.swift; sourceTree = "<group>"; };
07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueuedAudioPlayerTests.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>"; };
@@ -93,6 +112,18 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0708ED712116E91300EB29BD /* Source */ = {
isa = PBXGroup;
children = (
07194D1F2127F283002EA8C8 /* ShortTestSound.m4a */,
0708ED6F2116E89900EB29BD /* Source.swift */,
07732650205EACA300C4D1CD /* WAV-MP3.wav */,
07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */,
0708ED78211732F500EB29BD /* TestSound.m4a */,
);
path = Source;
sourceTree = "<group>";
};
607FACC71AFB9204008FA782 = {
isa = PBXGroup;
children = (
@@ -144,13 +175,17 @@
607FACE81AFB9204008FA782 /* Tests */ = {
isa = PBXGroup;
children = (
0708ED732116EE0100EB29BD /* AudioPlayerTests.swift */,
607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */,
074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */,
074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */,
074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */,
0775575820668B020002C6A1 /* QueueManagerTests.swift */,
07732650205EACA300C4D1CD /* WAV-MP3.wav */,
07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */,
078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */,
0708ED6B2116DA4B00EB29BD /* AudioSessionControllerTests.swift */,
07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */,
07CC171A213E912A005F880E /* SimpleAudioPlayerTests.swift */,
0708ED712116E91300EB29BD /* Source */,
607FACE91AFB9204008FA782 /* Supporting Files */,
);
path = Tests;
@@ -293,6 +328,8 @@
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */,
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */,
07732655205ECE1C00C4D1CD /* nasa_throttle_up.mp3 in Resources */,
07194D222127F6E9002EA8C8 /* ShortTestSound.m4a in Resources */,
0708ED79211732F500EB29BD /* TestSound.m4a in Resources */,
070713102067F40A00F789B3 /* QueueTableViewCell.xib in Resources */,
07732654205ECA8B00C4D1CD /* WAV-MP3.wav in Resources */,
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */,
@@ -303,6 +340,8 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
07194D212127F6DB002EA8C8 /* ShortTestSound.m4a in Resources */,
0708ED7A211732F500EB29BD /* TestSound.m4a in Resources */,
07732653205EB1B500C4D1CD /* nasa_throttle_up.mp3 in Resources */,
07732651205EACA300C4D1CD /* WAV-MP3.wav in Resources */,
);
@@ -425,6 +464,7 @@
0707130B2067F2E000F789B3 /* QueueViewController.swift in Sources */,
070713072067EB4F00F789B3 /* Double + Extensions.swift in Sources */,
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */,
0708ED722116E91D00EB29BD /* Source.swift in Sources */,
0707130F2067F40A00F789B3 /* QueueTableViewCell.swift in Sources */,
070713092067EFFB00F789B3 /* AudioController.swift in Sources */,
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
@@ -435,8 +475,14 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0708ED702116E89900EB29BD /* Source.swift in Sources */,
0708ED742116EE0100EB29BD /* AudioPlayerTests.swift in Sources */,
07CC171C213E912E005F880E /* SimpleAudioPlayerTests.swift in Sources */,
0775575920668B020002C6A1 /* QueueManagerTests.swift in Sources */,
074A6483205C155E0083D868 /* AVPlayerTimeObserverTests.swift in Sources */,
078C908F210D263200555E80 /* AVPlayerItemObserverTests.swift in Sources */,
0708ED6C2116DA4C00EB29BD /* AudioSessionControllerTests.swift in Sources */,
07DBB1E1212C17E600BB4278 /* QueuedAudioPlayerTests.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 -3
View File
@@ -27,12 +27,11 @@ class AudioController {
player.remoteCommands = [
.stop,
.togglePlayPause,
.skipForward(preferredIntervals: [30]),
.skipBackward(preferredIntervals: [30]),
.next,
.previous,
.changePlaybackPosition
]
try? audioSessionController.set(category: .playback)
try? audioSessionController.activateSession()
try? player.add(items: sources, playWhenReady: false)
}
+2 -2
View File
@@ -46,7 +46,7 @@ extension QueueViewController: UITableViewDataSource, UITableViewDelegate {
case 0:
return 1
case 1:
return controller.player.nextItems?.count ?? 0
return controller.player.nextItems.count ?? 0
default:
return 0
}
@@ -60,7 +60,7 @@ extension QueueViewController: UITableViewDataSource, UITableViewDelegate {
case 0:
item = controller.player.currentItem
case 1:
item = controller.player.nextItems?[indexPath.row]
item = controller.player.nextItems[indexPath.row]
default:
item = nil
}
+14 -1
View File
@@ -31,6 +31,9 @@ class ViewController: UIViewController {
}
@IBAction func togglePlay(_ sender: Any) {
if (!controller.audioSessionController.audioSessionIsActive) {
try? controller.audioSessionController.activateSession()
}
try? controller.player.togglePlaying()
}
@@ -60,7 +63,6 @@ class ViewController: UIViewController {
extension ViewController: AudioPlayerDelegate {
func audioPlayer(playerDidChangeState state: AVPlayerWrapperState) {
playButton.setTitle(state == .playing ? "Pause" : "Play", for: .normal)
switch state {
@@ -77,6 +79,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 +109,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()
}
}
@@ -15,7 +15,7 @@ class AVPlayerItemNotificationObserverTests: QuickSpec {
var observer: AVPlayerItemNotificationObserver!
beforeEach {
item = AVPlayerItem(asset: AVURLAsset(url: URL(string: "https://p.scdn.co/mp3-preview/4839b070015ab7d6de9fec1756e1f3096d908fba")!))
item = AVPlayerItem(url: URL(fileURLWithPath: Source.path))
observer = AVPlayerItemNotificationObserver()
}
@@ -0,0 +1,56 @@
import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
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.path))
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.path))
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)
}
}
+11 -1
View File
@@ -34,7 +34,7 @@ class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
context("when player has started", {
beforeEach {
player.replaceCurrentItem(with: AVPlayerItem(asset: AVURLAsset(url: URL(string: "https://p.scdn.co/mp3-preview/4839b070015ab7d6de9fec1756e1f3096d908fba")!)))
player.replaceCurrentItem(with: AVPlayerItem(url: URL(fileURLWithPath: Source.path)))
player.play()
}
@@ -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())
})
})
})
}
+97 -19
View File
@@ -6,12 +6,8 @@ import Nimble
class AVPlayerWrapperTests: QuickSpec {
override func spec() {
let source = Bundle.main.path(forResource: "WAV-MP3", ofType: "wav")!
let shortSource = Bundle.main.path(forResource: "nasa_throttle_up", ofType: "mp3")!
describe("An AVPlayerWrapper") {
var wrapper: AVPlayerWrapper!
@@ -19,30 +15,41 @@ class AVPlayerWrapperTests: QuickSpec {
beforeEach {
wrapper = AVPlayerWrapper()
wrapper.automaticallyWaitsToMinimizeStalling = false
wrapper.bufferDuration = 0.0001
wrapper.volume = 0.0
}
describe("its state", {
context("when doing nothing", {
it("should be idle", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
})
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)
try? wrapper.load(fromFilePath: Source.path, 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 {
try? wrapper.load(fromFilePath: source, playWhenReady: true)
try? wrapper.load(fromFilePath: Source.path, playWhenReady: true)
}
it("should eventually be playing", closure: {
@@ -53,7 +60,7 @@ class AVPlayerWrapperTests: QuickSpec {
context("when pausing the source", {
let holder = AudioPlayerDelegateHolder()
let holder = AVPlayerWrapperDelegateHolder()
beforeEach {
wrapper.delegate = holder
@@ -62,22 +69,37 @@ class AVPlayerWrapperTests: QuickSpec {
try? wrapper.pause()
}
}
try? wrapper.load(fromFilePath: source, playWhenReady: true)
try? wrapper.load(fromFilePath: Source.path, playWhenReady: true)
}
it("should eventually be paused", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.paused))
})
})
context("when toggling the source from play", {
let holder = AVPlayerWrapperDelegateHolder()
beforeEach {
wrapper.delegate = holder
holder.stateUpdate = { (state) in
if state == .playing {
try? wrapper.togglePlaying()
}
}
try? wrapper.load(fromFilePath: Source.path, playWhenReady: true)
}
it("should eventually be paused", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.paused))
})
})
context("when stopping the source", {
var holder: AudioPlayerDelegateHolder!
var holder: AVPlayerWrapperDelegateHolder!
var receivedIdleUpdate: Bool = false
beforeEach {
holder = AudioPlayerDelegateHolder()
holder = AVPlayerWrapperDelegateHolder()
wrapper.delegate = holder
holder.stateUpdate = { (state) in
if state == .playing {
@@ -87,7 +109,7 @@ class AVPlayerWrapperTests: QuickSpec {
receivedIdleUpdate = true
}
}
try? wrapper.load(fromFilePath: source, playWhenReady: true)
try? wrapper.load(fromFilePath: Source.path, playWhenReady: true)
}
it("should eventually be 'idle'", closure: {
@@ -95,20 +117,69 @@ 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.path, playWhenReady: false)
}
it("should eventually not be 0", closure: {
expect(wrapper.duration).toEventuallyNot(equal(0))
})
})
})
describe("its current time", {
it("should be 0", closure: {
expect(wrapper.currentTime).to(equal(0))
})
context("when seeking to a time", {
let holder = AVPlayerWrapperDelegateHolder()
let seekTime: TimeInterval = 0.5
beforeEach {
wrapper.delegate = holder
holder.stateUpdate = { (state) in
if state == .ready && wrapper.duration != 0 {
try? wrapper.seek(to: seekTime)
}
}
try? wrapper.load(fromFilePath: Source.path, playWhenReady: false)
}
it("should eventually be equal to the seeked time", closure: {
expect(wrapper.currentTime).toEventually(equal(seekTime))
})
})
})
}
}
}
class AudioPlayerDelegateHolder: AVPlayerWrapperDelegate {
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
var state: AVPlayerWrapperState? {
didSet {
print(state)
if let state = state {
self.stateUpdate?(state)
}
@@ -138,4 +209,11 @@ class AudioPlayerDelegateHolder: AVPlayerWrapperDelegate {
}
func AVWrapper(didUpdateDuration duration: Double) {
if let state = self.state {
self.stateUpdate?(state)
}
}
}
+185
View File
@@ -0,0 +1,185 @@
import Quick
import Nimble
@testable import SwiftAudio
class AudioPlayerTests: QuickSpec {
override func spec() {
describe("An AudioPlayer") {
var audioPlayer: AudioPlayer!
beforeEach {
audioPlayer = AudioPlayer()
audioPlayer.automaticallyWaitsToMinimizeStalling = false
audioPlayer.bufferDuration = 0.0001
audioPlayer.volume = 0
}
describe("its state", {
it("should be idle", closure: {
expect(audioPlayer.playerState).to(equal(AudioPlayerState.idle))
})
context("when audio item is loaded", {
beforeEach {
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: false)
}
it("it should eventually be ready", closure: {
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.ready))
})
})
context("when an item is loaded (playWhenReady=true)", {
beforeEach {
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: true)
}
it("it should eventually be playing", closure: {
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
})
})
context("when playing an item", {
var holder: AudioPlayerDelegateHolder!
beforeEach {
holder = AudioPlayerDelegateHolder()
audioPlayer.delegate = holder
holder.stateUpdate = { state in
print(state.rawValue)
if state == .ready {
try? audioPlayer.play()
}
}
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: false)
}
it("should eventually be playing", closure: {
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.playing))
})
})
context("when pausing an item", {
var holder: AudioPlayerDelegateHolder!
beforeEach {
holder = AudioPlayerDelegateHolder()
audioPlayer.delegate = holder
holder.stateUpdate = { (state) in
if state == .playing {
try? audioPlayer.pause()
}
}
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: true)
}
it("should eventually be paused", closure: {
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.paused))
})
})
context("when stopping an item", {
var holder: AudioPlayerDelegateHolder!
beforeEach {
holder = AudioPlayerDelegateHolder()
audioPlayer.delegate = holder
holder.stateUpdate = { (state) in
if state == .playing {
audioPlayer.stop()
}
}
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: true)
}
it("should eventually be idle", closure: {
expect(audioPlayer.playerState).toEventually(equal(AudioPlayerState.idle))
})
})
})
describe("its current time", {
it("should be 0", closure: {
expect(audioPlayer.currentTime).to(equal(0))
})
context("when seeking to a time", {
let holder = AudioPlayerDelegateHolder()
let seekTime: TimeInterval = 0.5
beforeEach {
audioPlayer.delegate = holder
holder.stateUpdate = { (state) in
if state == .ready && audioPlayer.duration != 0 {
try? audioPlayer.seek(to: seekTime)
}
}
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: false)
}
it("should eventually be equal to the seeked time", closure: {
expect(audioPlayer.currentTime).toEventually(equal(seekTime))
})
})
})
describe("its rate", {
it("should be 0", closure: {
expect(audioPlayer.rate).to(equal(0))
})
context("when playing an item", {
beforeEach {
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: true)
}
it("should eventually be 1.0", closure: {
expect(audioPlayer.rate).toEventually(equal(1.0))
})
})
})
}
}
}
class AudioPlayerDelegateHolder: AudioPlayerDelegate {
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
var state: AudioPlayerState? {
didSet {
if let state = state {
stateUpdate?(state)
}
}
}
func audioPlayer(playerDidChangeState state: AudioPlayerState) {
self.state = state
}
func audioPlayerItemDidComplete() {
}
func audioPlayer(secondsElapsed seconds: Double) {
}
func audioPlayer(failedWithError error: Error?) {
}
func audioPlayer(seekTo seconds: Int, didFinish: Bool) {
}
func audioPlayer(didUpdateDuration duration: Double) {
if let state = self.state {
self.stateUpdate?(state)
}
}
}
@@ -0,0 +1,86 @@
import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
class AudioSessionControllerTests: QuickSpec {
override func spec() {
describe("An AudioSessionController") {
let audioSessionController: AudioSessionController = AudioSessionController.shared
it("should be inactive", closure: {
expect(audioSessionController.audioSessionIsActive).to(beFalse())
})
context("when session is activated", {
beforeEach {
try? audioSessionController.activateSession()
}
it("should be active", closure: {
expect(audioSessionController.audioSessionIsActive).to(beTrue())
})
context("when deactivating session", {
beforeEach {
try? audioSessionController.deactivateSession()
}
it("should be inactive", closure: {
expect(audioSessionController.audioSessionIsActive).to(beFalse())
})
})
})
describe("its isObservingForInterruptions", {
it("should be true", closure: {
expect(audioSessionController.isObservingForInterruptions).to(beTrue())
})
context("when isObservingForInterruptions is set to false", {
beforeEach {
audioSessionController.isObservingForInterruptions = false
}
it("should be false", closure: {
expect(audioSessionController.isObservingForInterruptions).to(beFalse())
})
})
})
describe("its delegate", {
context("when a interruption arrives", {
var delegate: AudioSessionControllerDelegateImplementation!
beforeEach {
let notification = Notification(name: .AVAudioSessionInterruption, object: nil, userInfo: [
AVAudioSessionInterruptionTypeKey: UInt(0)
])
delegate = AudioSessionControllerDelegateImplementation()
audioSessionController.delegate = delegate
audioSessionController.handleInterruption(notification: notification)
}
it("should eventually be updated with the interruption type", closure: {
expect(delegate.interruptionType).toEventuallyNot(beNil())
})
})
})
}
}
}
class AudioSessionControllerDelegateImplementation: AudioSessionControllerDelegate {
var interruptionType: AVAudioSessionInterruptionType? = nil
func handleInterruption(type: AVAudioSessionInterruptionType) {
self.interruptionType = type
}
}
+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).toNot(beNil())
})
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 {
})
})
})
}
}
}
+119
View File
@@ -0,0 +1,119 @@
import Quick
import Nimble
@testable import SwiftAudio
class QueuedAudioPlayerTests: QuickSpec {
override func spec() {
describe("A QueuedAudioPlayer") {
var audioPlayer: QueuedAudioPlayer!
beforeEach {
audioPlayer = QueuedAudioPlayer()
audioPlayer.automaticallyWaitsToMinimizeStalling = false
audioPlayer.bufferDuration = 0.0001
audioPlayer.volume = 0
}
describe("its current item", {
it("should be nil", closure: {
expect(audioPlayer.currentItem).to(beNil())
})
context("when adding one item", {
beforeEach {
try? audioPlayer.add(item: ShortSource.getAudioItem(), playWhenReady: false)
}
it("should not be nil", closure: {
expect(audioPlayer.currentItem).toNot(beNil())
})
})
context("when adding multiple items", {
beforeEach {
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()], playWhenReady: false)
}
it("should not be nil", closure: {
expect(audioPlayer.currentItem).toNot(beNil())
})
})
})
describe("its next items", {
it("should be empty", closure: {
expect(audioPlayer.nextItems.count).to(equal(0))
})
context("when adding 2 items", {
beforeEach {
try? audioPlayer.add(items: [Source.getAudioItem(), Source.getAudioItem()])
}
it("should contain 1 item", closure: {
expect(audioPlayer.nextItems.count).to(equal(1))
})
context("then calling next()", {
beforeEach {
try? audioPlayer.next()
}
it("should contain 0 items", closure: {
expect(audioPlayer.nextItems.count).to(equal(0))
})
context("then calling previous()", {
beforeEach {
try? audioPlayer.previous()
}
it("should contain 1 item", closure: {
expect(audioPlayer.nextItems.count).to(equal(1))
})
})
})
context("then removing one item", {
beforeEach {
try? audioPlayer.removeItem(atIndex: 1)
}
it("should be empty", closure: {
expect(audioPlayer.nextItems.count).to(equal(0))
})
})
context("then jumping to the last item", {
beforeEach {
try? audioPlayer.jumpToItem(atIndex: 1)
}
it("should be empty", closure: {
expect(audioPlayer.nextItems.count).to(equal(0))
})
})
})
})
describe("its previous items", {
it("should be empty", closure: {
expect(audioPlayer.previousItems.count).to(equal(0))
})
context("when adding 2 items", {
beforeEach {
try? audioPlayer.add(items: [ShortSource.getAudioItem(), ShortSource.getAudioItem()])
}
it("should be empty", closure: {
expect(audioPlayer.previousItems.count).to(equal(0))
})
context("then calling next()", {
beforeEach {
try? audioPlayer.next()
}
it("should contain one item", closure: {
expect(audioPlayer.previousItems.count).to(equal(1))
})
})
})
})
}
}
}
@@ -0,0 +1,43 @@
import Quick
import Nimble
@testable import SwiftAudio
class SimpleAudioPlayerTests: QuickSpec {
override func spec() {
describe("A SimpleAudioPlayer") {
var player: SimpleAudioPlayer!
beforeEach {
player = SimpleAudioPlayer()
player.automaticallyWaitsToMinimizeStalling = false
player.bufferDuration = 0.0001
player.volume = 0
}
describe("its state", {
it("should be idle", closure: {
expect(player.playerState).to(equal(AudioPlayerState.idle))
})
context("when loading an item with playeWhenReady: false", {
beforeEach {
try? player.load(item: Source.getAudioItem(), playWhenReady: false)
}
it("should eventually be ready", closure: {
expect(player.playerState).toEventually(equal(AudioPlayerState.ready))
})
})
context("when loading an item with playWhenReady: true", {
beforeEach {
try? player.load(item: Source.getAudioItem(), playWhenReady: true)
}
it("should eventually be playing", closure: {
expect(player.playerState).toEventually(equal(AudioPlayerState.playing))
})
})
})
}
}
}
Binary file not shown.
+26
View File
@@ -0,0 +1,26 @@
//
// Sources.swift
// SwiftAudio_Tests
//
// Created by Jørgen Henrichsen on 05/08/2018.
// Copyright © 2018 CocoaPods. All rights reserved.
//
import Foundation
import SwiftAudio
struct Source {
static let path: String = Bundle.main.path(forResource: "TestSound", ofType: "m4a")!
static func getAudioItem() -> AudioItem {
return DefaultAudioItem(audioUrl: Source.path, sourceType: .file)
}
}
struct ShortSource {
static let path: String = Bundle.main.path(forResource: "ShortTestSound", ofType: "m4a")!
static func getAudioItem() -> AudioItem {
return DefaultAudioItem(audioUrl: ShortSource.path, sourceType: .file)
}
}
Binary file not shown.
+7
View File
@@ -2,6 +2,7 @@
[![Build Status](https://travis-ci.org/jorgenhenrichsen/SwiftAudio.svg?branch=master)](https://travis-ci.org/jorgenhenrichsen/SwiftAudio)
[![Version](https://img.shields.io/cocoapods/v/SwiftAudio.svg?style=flat)](http://cocoapods.org/pods/SwiftAudio)
[![codecov](https://codecov.io/gh/jorgenhenrichsen/SwiftAudio/branch/master/graph/badge.svg)](https://codecov.io/gh/jorgenhenrichsen/SwiftAudio)
[![License](https://img.shields.io/cocoapods/l/SwiftAudio.svg?style=flat)](http://cocoapods.org/pods/SwiftAudio)
[![Platform](https://img.shields.io/cocoapods/p/SwiftAudio.svg?style=flat)](http://cocoapods.org/pods/SwiftAudio)
@@ -76,6 +77,12 @@ try? AudioSessionController.activateSession()
If you want audio to continue playing when the app is inactive, remember to activate background audio:
App Settings -> Capabilities -> Background Modes -> Check 'Audio, AirPlay, and Picture in Picture'.
#### Interruptions
If you are using the AudioSessionController for setting up the audio session, you can use it to handle interruptions too.
Implement `AudioSessionControllerDelegate` and you will be notified by `handleInterruption(type: AVAudioSessionInterruptionType)`.
If you are storing progress for playback time on items when the app quits, it can be a good idea to do it on interruptions as well.
To disable interruption notifcations set `isObservingForInterruptions` to `false`.
### Now Playing Info
The `AudioPlayer` will automatically update the `MPNowPlayingInfoCenter` with artist, title, album, artwork, time if the passed in `AudioItem` supports this.
If you need to set additional properties for some items use `AudioPlayer.add(property:)`. Available properties can be found in `NowPlayingInfoProperty`.
+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.3'
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 -5
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()
@@ -209,7 +210,6 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
func enableRemoteCommands(forItem item: AudioItem) {
if let item = item as? RemoteCommandable {
print("Enabling remote commands for item")
self.enableRemoteCommands(item.getCommands())
}
else {
@@ -273,7 +273,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 +297,8 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
self.delegate?.audioPlayer(seekTo: seconds, didFinish: didFinish)
}
func AVWrapper(didUpdateDuration duration: Double) {
self.delegate?.audioPlayer(didUpdateDuration: duration)
}
}
@@ -52,6 +52,10 @@ public enum AudioSessionCategory {
}
public protocol AudioSessionControllerDelegate: class {
func handleInterruption(type: AVAudioSessionInterruptionType)
}
/**
Simple controller for the `AVAudioSession`. If you need more advanced options, just use the `AVAudioSession` directly.
- warning: Do not combine usage of this and `AVAudioSession` directly, chose one.
@@ -61,6 +65,8 @@ public class AudioSessionController {
public static let shared = AudioSessionController()
private let audioSession: AVAudioSession = AVAudioSession.sharedInstance()
private let notificationCenter: NotificationCenter = NotificationCenter.default
private var _isObservingForInterruptions: Bool = false
/**
True if another app is currently playing audio.
@@ -76,7 +82,34 @@ public class AudioSessionController {
*/
public var audioSessionIsActive: Bool = false
private init() {}
/**
Wheter notifications for interruptions are being observed or not.
This is enabled by default.
Set this to false to disable the behaviour.
*/
public var isObservingForInterruptions: Bool {
get {
return _isObservingForInterruptions
}
set {
if newValue == _isObservingForInterruptions {
return
}
if newValue {
registerForInterruptionNotification()
}
else {
unregisterForInterruptionNotification()
}
}
}
public weak var delegate: AudioSessionControllerDelegate?
private init() {
registerForInterruptionNotification()
}
public func activateSession() throws {
do {
@@ -101,4 +134,29 @@ public class AudioSessionController {
try audioSession.setCategory(category.getValue())
}
// MARK: - Interruptions
private func registerForInterruptionNotification() {
notificationCenter.addObserver(self,
selector: #selector(handleInterruption),
name: .AVAudioSessionInterruption,
object: nil)
_isObservingForInterruptions = true
}
private func unregisterForInterruptionNotification() {
notificationCenter.removeObserver(self, name: .AVAudioSessionInterruption, object: nil)
_isObservingForInterruptions = false
}
@objc func handleInterruption(notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
return
}
self.delegate?.handleInterruption(type: type)
}
}
@@ -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
}
}
}
+13 -11
View File
@@ -19,11 +19,17 @@ class QueueManager<T> {
return _items
}
public var nextItems: [T]? {
public var nextItems: [T] {
guard _currentIndex < _items.count else {
return []
}
return Array(_items[_currentIndex + 1..<items.count])
}
public var previousItems: [T] {
if (_currentIndex == 0) {
return []
}
return Array(_items[0..<_currentIndex])
}
@@ -75,13 +81,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 +98,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]
}
/**
+55 -3
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
}
public var previousItems: [AudioItem]? {
/**
The previous items held by the queue.
*/
public var previousItems: [AudioItem] {
return queueManager.previousItems
}
public var nextItems: [AudioItem]? {
/**
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.