Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 454e036449 | |||
| 8fb3b8424d | |||
| 731863a900 | |||
| 45582b3f43 | |||
| 534b62da9e | |||
| e3cb3c4a51 | |||
| a0e05a885a | |||
| 1a13887a65 | |||
| da6e83f64a | |||
| 0f2bfb3e79 | |||
| eabeca93a0 | |||
| 6a93840463 | |||
| 1261f88803 | |||
| 6239d6d5e6 | |||
| a63771a040 | |||
| 7d27ef286f | |||
| bf0d3a79dd | |||
| 22dec9d6b1 | |||
| b7be6338c6 | |||
| 97b70d056a | |||
| c9bce64bc1 | |||
| dafcbdfe3e | |||
| 24666c3ab5 | |||
| 8de5d678b8 | |||
| 70a55f22e0 | |||
| e180fc7a72 | |||
| 9008c6f9a0 | |||
| d49c522032 | |||
| 7681d5d983 | |||
| 8b1e57b3c0 | |||
| 1c4f5ec738 | |||
| d2ed064295 | |||
| 0d4060eb68 | |||
| 15a8bc4abd | |||
| da5b7702f7 | |||
| 0066a4121c | |||
| 1bf9d695d8 | |||
| 8edc3b0e75 | |||
| 22c780adba | |||
| c8805d55fd | |||
| a6e74efa37 | |||
| 7d525e3129 | |||
| 39d8c55743 | |||
| d56d4c699e | |||
| 04ae9e7986 | |||
| 30503f0ffe | |||
| ba82a46f8b | |||
| 3eef6bf59d | |||
| ea9f632eb6 | |||
| 53aa56348e | |||
| 26c1d2875e | |||
| b2972063dd | |||
| fd232f48ca | |||
| 6ed80f4ff4 | |||
| f3b55b9a3b | |||
| 460908e180 | |||
| 8587b161b2 | |||
| f149ea09ff | |||
| efc04395f9 | |||
| 65bba1eb75 | |||
| 876ed22967 | |||
| b4953f0a73 | |||
| 5ede0f8364 | |||
| 31c3c728f6 | |||
| d6375d996d | |||
| 730629a07a | |||
| 0bb59cde4a | |||
| 661a7755df | |||
| 01702b41f1 | |||
| 47a091bb9a | |||
| f2419282d0 | |||
| 41ac357193 | |||
| 670c3d423c | |||
| ca8f35166d | |||
| e10ea3cd2a | |||
| 5e8bb37c17 | |||
| 74d03d3e13 | |||
| 6af21680c3 | |||
| 3342ba2f30 | |||
| ca292281d2 | |||
| 02b6755362 | |||
| 1cbbf7c686 | |||
| cc329d8d9e | |||
| 2129df3071 | |||
| 12c3f1bcd6 |
@@ -0,0 +1,2 @@
|
||||
ignore:
|
||||
- "Example/.*"
|
||||
+6
-3
@@ -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'
|
||||
|
||||
+28
@@ -7,11 +7,18 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
070713052067E3EA00F789B3 /* APError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070713042067E3EA00F789B3 /* APError.swift */; };
|
||||
0773265A205ED6C400C4D1CD /* AVPlayerObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07732657205ED6C400C4D1CD /* AVPlayerObserver.swift */; };
|
||||
0773265B205ED6C400C4D1CD /* AVPlayerItemNotificationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07732658205ED6C400C4D1CD /* AVPlayerItemNotificationObserver.swift */; };
|
||||
0773265C205ED6C400C4D1CD /* AVPlayerTimeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07732659205ED6C400C4D1CD /* AVPlayerTimeObserver.swift */; };
|
||||
07732660205ED7BF00C4D1CD /* AudioItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0773265F205ED7BF00C4D1CD /* AudioItem.swift */; };
|
||||
0775574B2061C1820002C6A1 /* RemoteCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0775574A2061C1820002C6A1 /* RemoteCommand.swift */; };
|
||||
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, ); }; };
|
||||
0AB1F4B2325838C42320440B1E6BAB1E /* QuickSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 38A82DE4A0130E4C946A904FEDDD74CA /* QuickSpec.m */; };
|
||||
12BBD20E3BEA5D34DCF748EF73904EAF /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEA1DF1C7C22E7B20D2DC30FB9D3EF1 /* Configuration.swift */; };
|
||||
@@ -166,11 +173,18 @@
|
||||
0136A629F86E79FA6B68D9675E08D0AF /* Pods-SwiftAudio_Example-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudio_Example-resources.sh"; sourceTree = "<group>"; };
|
||||
014CC71E572C5CC14ADFA82A8B7B97DC /* Expression.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Expression.swift; path = Sources/Nimble/Expression.swift; sourceTree = "<group>"; };
|
||||
057B1682EF92E71137867F0C259AF2CC /* NSString+C99ExtendedIdentifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSString+C99ExtendedIdentifier.swift"; path = "Sources/Quick/NSString+C99ExtendedIdentifier.swift"; sourceTree = "<group>"; };
|
||||
070713042067E3EA00F789B3 /* APError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = APError.swift; path = SwiftAudio/Classes/APError.swift; sourceTree = "<group>"; };
|
||||
07732657205ED6C400C4D1CD /* AVPlayerObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AVPlayerObserver.swift; sourceTree = "<group>"; };
|
||||
07732658205ED6C400C4D1CD /* AVPlayerItemNotificationObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AVPlayerItemNotificationObserver.swift; sourceTree = "<group>"; };
|
||||
07732659205ED6C400C4D1CD /* AVPlayerTimeObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AVPlayerTimeObserver.swift; sourceTree = "<group>"; };
|
||||
0773265F205ED7BF00C4D1CD /* AudioItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AudioItem.swift; path = SwiftAudio/Classes/AudioItem.swift; sourceTree = "<group>"; };
|
||||
0775574A2061C1820002C6A1 /* RemoteCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RemoteCommand.swift; path = SwiftAudio/Classes/RemoteCommand.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
0AB0094EAAC3C18B000459B228F01637 /* ThrowAssertion.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ThrowAssertion.swift; path = Sources/Nimble/Matchers/ThrowAssertion.swift; sourceTree = "<group>"; };
|
||||
0AC83F8F84F3F843896F4907ACFE3394 /* Pods-SwiftAudio_Tests-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudio_Tests-resources.sh"; sourceTree = "<group>"; };
|
||||
@@ -375,6 +389,7 @@
|
||||
07732657205ED6C400C4D1CD /* AVPlayerObserver.swift */,
|
||||
07732658205ED6C400C4D1CD /* AVPlayerItemNotificationObserver.swift */,
|
||||
07732659205ED6C400C4D1CD /* AVPlayerTimeObserver.swift */,
|
||||
078C908B210CD8B300555E80 /* AVPlayerItemObserver.swift */,
|
||||
);
|
||||
name = Observer;
|
||||
path = SwiftAudio/Classes/Observer;
|
||||
@@ -384,12 +399,18 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E469FE2F4CBA2182A64C31D9B41A936E /* AudioPlayer.swift */,
|
||||
0775575C2066A7DB0002C6A1 /* SimpleAudioPlayer.swift */,
|
||||
077557602066ABAD0002C6A1 /* QueuedAudioPlayer.swift */,
|
||||
0773265F205ED7BF00C4D1CD /* AudioItem.swift */,
|
||||
C455E233BD4071A35296FEDA8D99CEDE /* MediaItemProperty.swift */,
|
||||
32A658905B9E84D827AA64605F28E3F9 /* NowPlayingInfoController.swift */,
|
||||
EEC13E9D5F4FD604F703ACBF80ECA35E /* NowPlayingInfoProperty.swift */,
|
||||
2B276281A286DB68E700474617B8AE3B /* TimeEventFrequency.swift */,
|
||||
07F41B19205FC0B100E25749 /* AudioSessionController.swift */,
|
||||
07F41B2120614BDC00E25749 /* RemoteCommandController.swift */,
|
||||
0775574A2061C1820002C6A1 /* RemoteCommand.swift */,
|
||||
077557562066867F0002C6A1 /* QueueManager.swift */,
|
||||
070713042067E3EA00F789B3 /* APError.swift */,
|
||||
07732656205ED6C400C4D1CD /* Observer */,
|
||||
25961FA3FF989AFD1E08A55F4E464276 /* AVPlayerWrapper */,
|
||||
853A8C0AF6F0B4902930084E06DEA640 /* Support Files */,
|
||||
@@ -975,15 +996,22 @@
|
||||
0773265C205ED6C400C4D1CD /* AVPlayerTimeObserver.swift in Sources */,
|
||||
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 */,
|
||||
5E48B05B2B78E0AD05E2DBA8B1862AE5 /* MediaItemProperty.swift in Sources */,
|
||||
0773265A205ED6C400C4D1CD /* AVPlayerObserver.swift in Sources */,
|
||||
FCE55A287889D68B475EBFBE8AB26E4B /* NowPlayingInfoController.swift in Sources */,
|
||||
0775574B2061C1820002C6A1 /* RemoteCommand.swift in Sources */,
|
||||
07732660205ED7BF00C4D1CD /* AudioItem.swift in Sources */,
|
||||
070713052067E3EA00F789B3 /* APError.swift in Sources */,
|
||||
07F41B2220614BDC00E25749 /* RemoteCommandController.swift in Sources */,
|
||||
6E0C5034AB99CC7E6B0BF002D488D85D /* NowPlayingInfoProperty.swift in Sources */,
|
||||
A45D4CA6BC297DB896B20F391DF60D61 /* SwiftAudio-dummy.m in Sources */,
|
||||
0773265B205ED6C400C4D1CD /* AVPlayerItemNotificationObserver.swift in Sources */,
|
||||
077557612066ABAD0002C6A1 /* QueuedAudioPlayer.swift in Sources */,
|
||||
8577EFB55077C469A41CFFBB5C749995 /* TimeEventFrequency.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
||||
@@ -8,6 +8,19 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
02EAB7FA622CF9CCD4F328C7 /* Pods_SwiftAudio_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9CEE0D1B64E6BEF775F214D /* Pods_SwiftAudio_Tests.framework */; };
|
||||
070713072067EB4F00F789B3 /* Double + Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070713062067EB4F00F789B3 /* Double + Extensions.swift */; };
|
||||
070713092067EFFB00F789B3 /* AudioController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 070713082067EFFB00F789B3 /* AudioController.swift */; };
|
||||
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 */; };
|
||||
@@ -15,6 +28,10 @@
|
||||
07732653205EB1B500C4D1CD /* nasa_throttle_up.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */; };
|
||||
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 */; };
|
||||
@@ -35,11 +52,25 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
070713062067EB4F00F789B3 /* Double + Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double + Extensions.swift"; sourceTree = "<group>"; };
|
||||
070713082067EFFB00F789B3 /* AudioController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioController.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
@@ -81,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 = (
|
||||
@@ -106,7 +149,12 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
607FACD51AFB9204008FA782 /* AppDelegate.swift */,
|
||||
070713082067EFFB00F789B3 /* AudioController.swift */,
|
||||
607FACD71AFB9204008FA782 /* ViewController.swift */,
|
||||
0707130A2067F2E000F789B3 /* QueueViewController.swift */,
|
||||
070713062067EB4F00F789B3 /* Double + Extensions.swift */,
|
||||
0707130D2067F40A00F789B3 /* QueueTableViewCell.swift */,
|
||||
0707130E2067F40A00F789B3 /* QueueTableViewCell.xib */,
|
||||
607FACD91AFB9204008FA782 /* Main.storyboard */,
|
||||
607FACDC1AFB9204008FA782 /* Images.xcassets */,
|
||||
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */,
|
||||
@@ -127,12 +175,17 @@
|
||||
607FACE81AFB9204008FA782 /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0708ED732116EE0100EB29BD /* AudioPlayerTests.swift */,
|
||||
607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */,
|
||||
074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */,
|
||||
074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */,
|
||||
074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */,
|
||||
07732650205EACA300C4D1CD /* WAV-MP3.wav */,
|
||||
07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */,
|
||||
0775575820668B020002C6A1 /* QueueManagerTests.swift */,
|
||||
078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */,
|
||||
0708ED6B2116DA4B00EB29BD /* AudioSessionControllerTests.swift */,
|
||||
07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */,
|
||||
07CC171A213E912A005F880E /* SimpleAudioPlayerTests.swift */,
|
||||
0708ED712116E91300EB29BD /* Source */,
|
||||
607FACE91AFB9204008FA782 /* Supporting Files */,
|
||||
);
|
||||
path = Tests;
|
||||
@@ -275,6 +328,9 @@
|
||||
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 */,
|
||||
);
|
||||
@@ -284,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 */,
|
||||
);
|
||||
@@ -403,7 +461,12 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
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 */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -412,7 +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>
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// AudioController.swift
|
||||
// SwiftAudio_Example
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 25/03/2018.
|
||||
// Copyright © 2018 CocoaPods. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftAudio
|
||||
|
||||
|
||||
class AudioController {
|
||||
|
||||
static let shared = AudioController()
|
||||
let player = QueuedAudioPlayer()
|
||||
let audioSessionController = AudioSessionController.shared
|
||||
|
||||
let sources: [AudioItem] = [
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/67b51d90ffddd6bb3f095059997021b589845f81?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "33 \"GOD\"", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/6f9999d909b017eabef97234dd7a206355720d9d?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "715 - CRΣΣKS", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/bf9bdd403c67fdbe06a582e7b292487c8cfd1f7e?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "____45_____", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI"))
|
||||
]
|
||||
|
||||
init() {
|
||||
player.remoteCommands = [
|
||||
.stop,
|
||||
.togglePlayPause,
|
||||
.next,
|
||||
.previous,
|
||||
.changePlaybackPosition
|
||||
]
|
||||
try? audioSessionController.set(category: .playback)
|
||||
try? player.add(items: sources, playWhenReady: false)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,64 +22,173 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ExO-ir-bnt">
|
||||
<rect key="frame" x="168" y="20" width="38" height="30"/>
|
||||
<state key="normal" title="PlayA"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="RX3-VR-CL6">
|
||||
<rect key="frame" x="32" y="533" width="311" height="34"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9Q1-U9-TUC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="103.5" height="34"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<state key="normal" title="Prev"/>
|
||||
<connections>
|
||||
<action selector="previous:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="fFb-iW-sFr"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="751" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="EOo-zV-6l2">
|
||||
<rect key="frame" x="103.5" y="0.0" width="104" height="34"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
|
||||
<state key="normal" title="Play"/>
|
||||
<connections>
|
||||
<action selector="togglePlay:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="oYu-xi-n6T"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Nhf-qB-91A">
|
||||
<rect key="frame" x="207.5" y="0.0" width="103.5" height="34"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<state key="normal" title="Next"/>
|
||||
<connections>
|
||||
<action selector="next:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="Tha-3J-gVM"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="34" id="T4q-HG-vqM"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="l9B-hM-Ajc">
|
||||
<rect key="frame" x="302" y="20" width="57" height="34"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="18"/>
|
||||
<state key="normal" title="Queue"/>
|
||||
<connections>
|
||||
<action selector="playA:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="KZw-pL-C6H"/>
|
||||
<segue destination="vDz-qW-uY8" kind="presentation" identifier="QueueSegue" id="eke-1c-Fsm"/>
|
||||
</connections>
|
||||
</button>
|
||||
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="RWN-If-dGG">
|
||||
<rect key="frame" x="14" y="318.5" width="347" height="31"/>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="FCd-3e-22D">
|
||||
<rect key="frame" x="67" y="84" width="240" height="240"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="343" id="CD7-DZ-gUR"/>
|
||||
<constraint firstAttribute="width" constant="240" id="5Sj-BZ-sg4"/>
|
||||
<constraint firstAttribute="height" constant="240" id="Hij-Yw-6Lg"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3CL-8o-zYW">
|
||||
<rect key="frame" x="16" y="462" width="39" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RVb-HZ-QCX">
|
||||
<rect key="frame" x="320" y="462" width="39" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="RWN-If-dGG">
|
||||
<rect key="frame" x="14" y="424" width="347" height="31"/>
|
||||
<color key="tintColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="maximumTrackTintColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="thumbTintColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<connections>
|
||||
<action selector="scrubbing:" destination="vXZ-lx-hvc" eventType="touchUpOutside" id="HeH-aB-VXZ"/>
|
||||
<action selector="scrubbing:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="NfP-3T-dnw"/>
|
||||
<action selector="scrubbingValueChanged:" destination="vXZ-lx-hvc" eventType="valueChanged" id="MLD-nW-rXm"/>
|
||||
<action selector="startScrubbing:" destination="vXZ-lx-hvc" eventType="touchDown" id="lD9-dR-QTO"/>
|
||||
</connections>
|
||||
</slider>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="EOo-zV-6l2">
|
||||
<rect key="frame" x="164" y="494" width="46" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="30" id="9sD-FT-FL0"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Play"/>
|
||||
<connections>
|
||||
<action selector="togglePlay:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="oYu-xi-n6T"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="TPr-Wm-k0L">
|
||||
<rect key="frame" x="168" y="58" width="39" height="30"/>
|
||||
<state key="normal" title="PlayB"/>
|
||||
<connections>
|
||||
<action selector="playA:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="LEW-J0-qbI"/>
|
||||
<action selector="playB:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="OhP-ri-ZWx"/>
|
||||
</connections>
|
||||
</button>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dfk-yr-rwm">
|
||||
<rect key="frame" x="16" y="354" width="343" height="21.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
|
||||
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Artist" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="T7Y-1Q-7UU">
|
||||
<rect key="frame" x="16" y="379.5" width="343" height="19.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="thin" pointSize="16"/>
|
||||
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="tintColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="EOo-zV-6l2" secondAttribute="trailing" constant="149" id="71L-Hv-1br"/>
|
||||
<constraint firstItem="RWN-If-dGG" firstAttribute="centerY" secondItem="kh9-bI-dsS" secondAttribute="centerY" id="Ee0-Yx-h5a"/>
|
||||
<constraint firstItem="RWN-If-dGG" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="EzW-Sk-mlN"/>
|
||||
<constraint firstItem="EOo-zV-6l2" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" constant="148" id="OKq-yH-xWk"/>
|
||||
<constraint firstItem="TPr-Wm-k0L" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="OaA-Nd-ZGX"/>
|
||||
<constraint firstItem="ExO-ir-bnt" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" id="VtX-IN-h4a"/>
|
||||
<constraint firstItem="2fi-mo-0CV" firstAttribute="top" secondItem="EOo-zV-6l2" secondAttribute="bottom" constant="143" id="m8i-yM-pER"/>
|
||||
<constraint firstItem="ExO-ir-bnt" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="mc2-i7-c1f"/>
|
||||
<constraint firstItem="TPr-Wm-k0L" firstAttribute="top" secondItem="ExO-ir-bnt" secondAttribute="bottom" constant="8" id="voY-Ue-QrT"/>
|
||||
<constraint firstItem="T7Y-1Q-7UU" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="0eh-sL-186"/>
|
||||
<constraint firstItem="l9B-hM-Ajc" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="54L-0h-0ba"/>
|
||||
<constraint firstItem="l9B-hM-Ajc" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" id="9Uh-K9-988"/>
|
||||
<constraint firstItem="RVb-HZ-QCX" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="BhV-UD-qhh"/>
|
||||
<constraint firstItem="FCd-3e-22D" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="GhI-f1-DkR"/>
|
||||
<constraint firstItem="T7Y-1Q-7UU" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="HoH-i0-yof"/>
|
||||
<constraint firstItem="RWN-If-dGG" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="Nw7-WM-LFd"/>
|
||||
<constraint firstItem="RX3-VR-CL6" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="O0h-NL-iXW"/>
|
||||
<constraint firstItem="dfk-yr-rwm" firstAttribute="top" secondItem="FCd-3e-22D" secondAttribute="bottom" constant="30" id="W4w-6K-AW8"/>
|
||||
<constraint firstItem="RWN-If-dGG" firstAttribute="top" secondItem="T7Y-1Q-7UU" secondAttribute="bottom" constant="25" id="XgV-XL-QCL"/>
|
||||
<constraint firstItem="dfk-yr-rwm" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="YUE-uf-Rp1"/>
|
||||
<constraint firstItem="RVb-HZ-QCX" firstAttribute="top" secondItem="RWN-If-dGG" secondAttribute="bottom" constant="8" id="ZkD-u2-Zbr"/>
|
||||
<constraint firstItem="T7Y-1Q-7UU" firstAttribute="top" secondItem="dfk-yr-rwm" secondAttribute="bottom" constant="4" id="baR-zV-tgo"/>
|
||||
<constraint firstItem="RWN-If-dGG" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="eNt-u9-qot"/>
|
||||
<constraint firstItem="RX3-VR-CL6" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" constant="16" id="hEd-b2-Ggo"/>
|
||||
<constraint firstItem="FCd-3e-22D" firstAttribute="top" secondItem="l9B-hM-Ajc" secondAttribute="bottom" constant="30" id="ikz-ZP-jNM"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="RX3-VR-CL6" secondAttribute="trailing" constant="16" id="kSP-Mq-R5P"/>
|
||||
<constraint firstItem="dfk-yr-rwm" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailingMargin" id="m6u-7a-ffF"/>
|
||||
<constraint firstItem="3CL-8o-zYW" firstAttribute="top" secondItem="RWN-If-dGG" secondAttribute="bottom" constant="8" id="sGK-bn-zxD"/>
|
||||
<constraint firstItem="2fi-mo-0CV" firstAttribute="top" secondItem="RX3-VR-CL6" secondAttribute="bottom" constant="100" id="vd2-dd-hVu"/>
|
||||
<constraint firstItem="3CL-8o-zYW" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" id="wOy-Rx-rvK"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="artistLabel" destination="T7Y-1Q-7UU" id="b5S-lt-PqG"/>
|
||||
<outlet property="elapsedTimeLabel" destination="3CL-8o-zYW" id="7Wg-7X-Vrd"/>
|
||||
<outlet property="imageView" destination="FCd-3e-22D" id="gKL-za-haV"/>
|
||||
<outlet property="playButton" destination="EOo-zV-6l2" id="2d1-ad-s1k"/>
|
||||
<outlet property="remainingTimeLabel" destination="RVb-HZ-QCX" id="8hp-CK-XjF"/>
|
||||
<outlet property="slider" destination="RWN-If-dGG" id="Yxw-Gf-bR3"/>
|
||||
<outlet property="titleLabel" destination="dfk-yr-rwm" id="Hk3-m5-IOi"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="x5A-6p-PRh" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="117.59999999999999" y="118.29085457271366"/>
|
||||
</scene>
|
||||
<!--Queue View Controller-->
|
||||
<scene sceneID="5Fm-oE-9Zc">
|
||||
<objects>
|
||||
<viewController id="vDz-qW-uY8" customClass="QueueViewController" customModule="SwiftAudio_Example" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="kv3-s6-lb0"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="Fhe-7w-8BG"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="y7Y-Gm-oyZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dzA-9p-ejh">
|
||||
<rect key="frame" x="310" y="20" width="49" height="34"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="18"/>
|
||||
<state key="normal" title="Close"/>
|
||||
<connections>
|
||||
<action selector="closeButton:" destination="vDz-qW-uY8" eventType="touchUpInside" id="0TB-bG-he7"/>
|
||||
</connections>
|
||||
</button>
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="HPi-Pd-J9K">
|
||||
<rect key="frame" x="0.0" y="74" width="375" height="593"/>
|
||||
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</tableView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="tintColor" red="1" green="0.1857388616" blue="0.57339501380000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="HPi-Pd-J9K" secondAttribute="trailing" id="CdI-lT-19N"/>
|
||||
<constraint firstItem="Fhe-7w-8BG" firstAttribute="top" secondItem="HPi-Pd-J9K" secondAttribute="bottom" id="Gb9-C1-ajx"/>
|
||||
<constraint firstItem="HPi-Pd-J9K" firstAttribute="leading" secondItem="y7Y-Gm-oyZ" secondAttribute="leading" id="aN2-LD-yxR"/>
|
||||
<constraint firstItem="HPi-Pd-J9K" firstAttribute="top" secondItem="dzA-9p-ejh" secondAttribute="bottom" constant="20" id="aSx-t1-T3e"/>
|
||||
<constraint firstItem="dzA-9p-ejh" firstAttribute="top" secondItem="kv3-s6-lb0" secondAttribute="bottom" id="nAL-i2-VQS"/>
|
||||
<constraint firstAttribute="trailing" secondItem="dzA-9p-ejh" secondAttribute="trailing" constant="16" id="qrg-S3-JJ2"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="tableView" destination="HPi-Pd-J9K" id="P8P-at-xLc"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="zk4-9r-5Oh" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="917.60000000000002" y="117.39130434782609"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// Double + Extensions.swift
|
||||
// SwiftAudio_Example
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 25/03/2018.
|
||||
// Copyright © 2018 CocoaPods. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Double {
|
||||
|
||||
private var formatter: DateComponentsFormatter {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.minute, .second]
|
||||
formatter.unitsStyle = .positional
|
||||
formatter.zeroFormattingBehavior = .pad
|
||||
return formatter
|
||||
}
|
||||
|
||||
func secondsToString() -> String {
|
||||
return formatter.string(from: self) ?? ""
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "22AMillion.jpg",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -35,10 +35,14 @@
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleLightContent</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// QueueTableViewCell.swift
|
||||
// SwiftAudio_Example
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 25/03/2018.
|
||||
// Copyright © 2018 CocoaPods. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class QueueTableViewCell: UITableViewCell {
|
||||
|
||||
@IBOutlet weak var titleLabel: UILabel!
|
||||
@IBOutlet weak var artistLabel: UILabel!
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
|
||||
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="QueueTableViewCell" customModule="SwiftAudio_Example" customModuleProvider="target"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="QueueTableViewCell" customModule="SwiftAudio_Example" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="79.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="R0I-g7-ETn">
|
||||
<rect key="frame" x="16" y="16" width="343" height="19.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
|
||||
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Artist" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jRU-3B-2pA">
|
||||
<rect key="frame" x="16" y="43.5" width="343" height="19.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="16"/>
|
||||
<color key="textColor" red="0.99999600649999998" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.12984204290000001" green="0.12984612579999999" blue="0.12984395030000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="R0I-g7-ETn" firstAttribute="trailing" secondItem="H2p-sc-9uM" secondAttribute="trailingMargin" id="8gl-XI-iAW"/>
|
||||
<constraint firstItem="jRU-3B-2pA" firstAttribute="trailing" secondItem="H2p-sc-9uM" secondAttribute="trailingMargin" id="A7F-XO-H0i"/>
|
||||
<constraint firstItem="jRU-3B-2pA" firstAttribute="top" secondItem="R0I-g7-ETn" secondAttribute="bottom" constant="8" id="Jdu-e3-Oeq"/>
|
||||
<constraint firstItem="R0I-g7-ETn" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="VNU-d7-G4N"/>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="jRU-3B-2pA" secondAttribute="bottom" constant="6" id="nBr-J4-PUM"/>
|
||||
<constraint firstItem="R0I-g7-ETn" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" constant="5" id="tE6-pp-JML"/>
|
||||
<constraint firstItem="jRU-3B-2pA" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="z3F-hI-GcC"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<connections>
|
||||
<outlet property="artistLabel" destination="jRU-3B-2pA" id="IVV-n5-wmt"/>
|
||||
<outlet property="titleLabel" destination="R0I-g7-ETn" id="ICg-6a-6vz"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="34.5" y="54"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// QueueViewController.swift
|
||||
// SwiftAudio_Example
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 25/03/2018.
|
||||
// Copyright © 2018 CocoaPods. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftAudio
|
||||
|
||||
|
||||
class QueueViewController: UIViewController {
|
||||
|
||||
let controller = AudioController.shared
|
||||
@IBOutlet weak var tableView: UITableView!
|
||||
|
||||
let cellReuseId: String = "QueueCell"
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.register(UINib.init(nibName: "QueueTableViewCell", bundle: Bundle.main), forCellReuseIdentifier: cellReuseId)
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
}
|
||||
|
||||
override func didReceiveMemoryWarning() {
|
||||
super.didReceiveMemoryWarning()
|
||||
}
|
||||
|
||||
@IBAction func closeButton(_ sender: UIButton) {
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension QueueViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch section {
|
||||
case 0:
|
||||
return 1
|
||||
case 1:
|
||||
return controller.player.nextItems.count ?? 0
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath) as! QueueTableViewCell
|
||||
|
||||
let item: AudioItem?
|
||||
switch indexPath.section {
|
||||
case 0:
|
||||
item = controller.player.currentItem
|
||||
case 1:
|
||||
item = controller.player.nextItems[indexPath.row]
|
||||
default:
|
||||
item = nil
|
||||
}
|
||||
|
||||
if let item = item {
|
||||
cell.titleLabel.text = item.getTitle()
|
||||
cell.artistLabel.text = item.getArtist()
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch section {
|
||||
case 0: return "Playing Now"
|
||||
case 1: return "Up Next"
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,36 +16,33 @@ class ViewController: UIViewController {
|
||||
|
||||
@IBOutlet weak var playButton: UIButton!
|
||||
@IBOutlet weak var slider: UISlider!
|
||||
@IBOutlet weak var imageView: UIImageView!
|
||||
@IBOutlet weak var remainingTimeLabel: UILabel!
|
||||
@IBOutlet weak var elapsedTimeLabel: UILabel!
|
||||
@IBOutlet weak var titleLabel: UILabel!
|
||||
@IBOutlet weak var artistLabel: UILabel!
|
||||
|
||||
var isScrubbing: Bool = false
|
||||
var audioPlayer: AudioPlayer = AudioPlayer()
|
||||
let audioSessionController: AudioSessionController = AudioSessionController.shared
|
||||
let localSource = DefaultAudioItem(audioUrl: Bundle.main.path(forResource: "WAV-MP3", ofType: "wav")!, artist: "Artist", title: "Title", albumTitle: "Album", sourceType: .file, artwork: #imageLiteral(resourceName: "cover"))
|
||||
let streamSource = DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/4839b070015ab7d6de9fec1756e1f3096d908fba", artist: "Artist", title: "Title", albumTitle: "Album", sourceType: .stream, artwork: #imageLiteral(resourceName: "cover"))
|
||||
|
||||
var artwork: MPMediaItemArtwork!
|
||||
let controller = AudioController.shared
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
audioPlayer.delegate = self
|
||||
try? audioSessionController.set(category: .playback)
|
||||
try? audioSessionController.activateSession()
|
||||
let image = #imageLiteral(resourceName: "cover")
|
||||
artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { (size) -> UIImage in
|
||||
return image
|
||||
})
|
||||
}
|
||||
|
||||
@IBAction func playA(_ sender: Any) {
|
||||
audioPlayer.load(item: localSource)
|
||||
}
|
||||
|
||||
@IBAction func playB(_ sender: Any) {
|
||||
audioPlayer.load(item: streamSource)
|
||||
controller.player.delegate = self
|
||||
}
|
||||
|
||||
@IBAction func togglePlay(_ sender: Any) {
|
||||
audioPlayer.togglePlaying()
|
||||
if (!controller.audioSessionController.audioSessionIsActive) {
|
||||
try? controller.audioSessionController.activateSession()
|
||||
}
|
||||
try? controller.player.togglePlaying()
|
||||
}
|
||||
|
||||
@IBAction func previous(_ sender: Any) {
|
||||
try? controller.player.previous()
|
||||
}
|
||||
|
||||
@IBAction func next(_ sender: Any) {
|
||||
try? controller.player.next()
|
||||
}
|
||||
|
||||
@IBAction func startScrubbing(_ sender: UISlider) {
|
||||
@@ -53,27 +50,41 @@ class ViewController: UIViewController {
|
||||
}
|
||||
|
||||
@IBAction func scrubbing(_ sender: UISlider) {
|
||||
audioPlayer.seek(to: Double(slider.value))
|
||||
try? controller.player.seek(to: Double(slider.value))
|
||||
}
|
||||
|
||||
func update() {
|
||||
slider.maximumValue = Float(audioPlayer.duration)
|
||||
slider.setValue(Float(audioPlayer.currentTime), animated: true)
|
||||
@IBAction func scrubbingValueChanged(_ sender: UISlider) {
|
||||
let value = Double(slider.value)
|
||||
elapsedTimeLabel.text = value.secondsToString()
|
||||
remainingTimeLabel.text = (controller.player.duration - value).secondsToString()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ViewController: AudioPlayerDelegate {
|
||||
|
||||
func audioPlayer(playerDidChangeState state: AVPlayerWrapperState) {
|
||||
print("AudioPlayer state: ", state.rawValue)
|
||||
self.update()
|
||||
playButton.setTitle(state == .playing ? "Pause" : "Play", for: .normal)
|
||||
|
||||
if state == .playing {
|
||||
playButton.setTitle("Pause", for: .normal)
|
||||
}
|
||||
else {
|
||||
playButton.setTitle("Play", for: .normal)
|
||||
switch state {
|
||||
case .ready:
|
||||
|
||||
if let item = controller.player.currentItem {
|
||||
titleLabel.text = item.getTitle()
|
||||
artistLabel.text = item.getArtist()
|
||||
item.getArtwork({ (image) in
|
||||
self.imageView.image = image
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -85,6 +96,8 @@ extension ViewController: AudioPlayerDelegate {
|
||||
func audioPlayer(secondsElapsed seconds: Double) {
|
||||
if !isScrubbing {
|
||||
slider.setValue(Float(seconds), animated: false)
|
||||
elapsedTimeLabel.text = controller.player.currentTime.secondsToString()
|
||||
remainingTimeLabel.text = (controller.player.duration - controller.player.currentTime).secondsToString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
@testable import SwiftAudio
|
||||
|
||||
|
||||
class QueueManagerTests: QuickSpec {
|
||||
|
||||
let dummyItem = 0
|
||||
|
||||
let dummyItems: [Int] = [0, 1, 2, 3, 4, 5, 6]
|
||||
|
||||
override func spec() {
|
||||
|
||||
describe("A QueueManager") {
|
||||
|
||||
var manager: QueueManager<Int>!
|
||||
|
||||
beforeEach {
|
||||
manager = QueueManager()
|
||||
}
|
||||
|
||||
context("when adding one item", {
|
||||
|
||||
beforeEach {
|
||||
manager.addItem(self.dummyItem)
|
||||
}
|
||||
|
||||
it("should have an item in the queue", closure: {
|
||||
expect(manager.items).notTo(beEmpty())
|
||||
})
|
||||
|
||||
it("should set it as the current item", closure: {
|
||||
expect(manager.current).toNot(beNil())
|
||||
expect(manager.current).to(equal(self.dummyItem))
|
||||
})
|
||||
|
||||
context("then calling next", {
|
||||
|
||||
var nextItem: Int?
|
||||
|
||||
beforeEach {
|
||||
nextItem = try? manager.next()
|
||||
}
|
||||
|
||||
it("should not return", closure: {
|
||||
expect(nextItem).to(beNil())
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
context("then calling previous", {
|
||||
var previousItem: Int?
|
||||
|
||||
beforeEach {
|
||||
previousItem = try? manager.previous()
|
||||
}
|
||||
|
||||
it("should not return", closure: {
|
||||
expect(previousItem).to(beNil())
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
context("when adding multiple items", {
|
||||
|
||||
beforeEach {
|
||||
manager.addItems(self.dummyItems)
|
||||
}
|
||||
|
||||
it("should have items in the queue", closure: {
|
||||
expect(manager.items.count).to(equal(self.dummyItems.count))
|
||||
})
|
||||
|
||||
it("should have the first item as a current item", closure: {
|
||||
expect(manager.current).toNot(beNil())
|
||||
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 {
|
||||
nextItem = try? manager.next()
|
||||
}
|
||||
|
||||
it("should return the next item", closure: {
|
||||
expect(nextItem).toNot(beNil())
|
||||
expect(nextItem).to(equal(self.dummyItems[1]))
|
||||
})
|
||||
|
||||
it("should have next current item", closure: {
|
||||
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 {
|
||||
previousItem = try? manager.previous()
|
||||
}
|
||||
it("should return the first item", closure: {
|
||||
expect(previousItem).toNot(beNil())
|
||||
expect(previousItem).to(equal(self.dummyItems.first))
|
||||
})
|
||||
it("should have the previous current item", closure: {
|
||||
expect(manager.current).to(equal(self.dummyItems.first))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// MARK: - Removal
|
||||
|
||||
context("then removing the second item", {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
removed = try? manager.remove(atIndex: 1)
|
||||
}
|
||||
|
||||
it("should have one less item", closure: {
|
||||
expect(removed).toNot(beNil())
|
||||
expect(manager.items.count).to(equal(self.dummyItems.count - 1))
|
||||
})
|
||||
})
|
||||
|
||||
context("then removing the last item", {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
removed = try? manager.remove(atIndex: self.dummyItems.count - 1)
|
||||
}
|
||||
|
||||
it("should have one less item", closure: {
|
||||
expect(removed).toNot(beNil())
|
||||
expect(manager.items.count).to(equal(self.dummyItems.count - 1))
|
||||
})
|
||||
})
|
||||
|
||||
context("then removing the current item", {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
removed = try? manager.remove(atIndex: manager.currentIndex)
|
||||
}
|
||||
it("should not remove any items", closure: {
|
||||
expect(removed).to(beNil())
|
||||
expect(manager.items.count).to(equal(self.dummyItems.count))
|
||||
})
|
||||
})
|
||||
|
||||
context("then removing with too large index", {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
removed = try? manager.remove(atIndex: self.dummyItems.count)
|
||||
}
|
||||
|
||||
it("should not remove any items", closure: {
|
||||
expect(removed).to(beNil())
|
||||
expect(manager.items.count).to(equal(self.dummyItems.count))
|
||||
})
|
||||
})
|
||||
|
||||
context("then removing with too small index", {
|
||||
var removed: Int?
|
||||
beforeEach {
|
||||
removed = try? manager.remove(atIndex: -1)
|
||||
}
|
||||
|
||||
it("should not remove any items", closure: {
|
||||
expect(removed).to(beNil())
|
||||
expect(manager.items.count).to(equal(self.dummyItems.count))
|
||||
})
|
||||
})
|
||||
|
||||
// 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 {
|
||||
try? jumped = manager.jump(to: 1)
|
||||
}
|
||||
|
||||
it("should return the current item", closure: {
|
||||
expect(jumped).toNot(beNil())
|
||||
expect(jumped).to(equal(manager.current))
|
||||
})
|
||||
|
||||
it("should move the current index", closure: {
|
||||
expect(manager.currentIndex).to(equal(1))
|
||||
})
|
||||
})
|
||||
|
||||
context("then jumping to last item", closure: {
|
||||
var jumped: Int?
|
||||
beforeEach {
|
||||
try? jumped = manager.jump(to: manager.items.count - 1)
|
||||
}
|
||||
it("should return the current item", closure: {
|
||||
expect(jumped).toNot(beNil())
|
||||
expect(jumped).to(equal(manager.current))
|
||||
})
|
||||
|
||||
it("should move the current index", closure: {
|
||||
expect(manager.currentIndex).to(equal(manager.items.count - 1))
|
||||
})
|
||||
})
|
||||
|
||||
context("then jumping to a negative index", closure: {
|
||||
var jumped: Int?
|
||||
beforeEach {
|
||||
jumped = try? manager.jump(to: -1)
|
||||
}
|
||||
|
||||
it("should not return", closure: {
|
||||
expect(jumped).to(beNil())
|
||||
})
|
||||
|
||||
it("should not move the current index", closure: {
|
||||
expect(manager.currentIndex).to(equal(0))
|
||||
})
|
||||
})
|
||||
|
||||
context("then jumping with too large index", closure: {
|
||||
var jumped: Int?
|
||||
beforeEach {
|
||||
jumped = try? manager.jump(to: manager.items.count)
|
||||
}
|
||||
it("should not return", closure: {
|
||||
expect(jumped).to(beNil())
|
||||
})
|
||||
|
||||
it("should not move the current index", closure: {
|
||||
expect(manager.currentIndex).to(equal(0))
|
||||
})
|
||||
})
|
||||
|
||||
// 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 {
|
||||
try? manager.moveItem(fromIndex: 1, toIndex: 3)
|
||||
}
|
||||
|
||||
it("should move the item", closure: {
|
||||
expect(manager.items).to(equal(afterMoving))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
@@ -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.
@@ -1,14 +1,16 @@
|
||||
# SwiftAudio
|
||||
|
||||
[](https://travis-ci.com/jorgenhenrichsen/SwiftAudio)
|
||||
[](https://travis-ci.org/jorgenhenrichsen/SwiftAudio)
|
||||
[](http://cocoapods.org/pods/SwiftAudio)
|
||||
[](https://codecov.io/gh/jorgenhenrichsen/SwiftAudio)
|
||||
[](http://cocoapods.org/pods/SwiftAudio)
|
||||
[](http://cocoapods.org/pods/SwiftAudio)
|
||||
|
||||
SwiftAudio aims to make audio playback easier on iOS. No more boundaryTimeObserver, periodicTimeObserver, KVO and NotificationCenter to get state update from the player. It also updates NowPlayingInfo for you and handles remote events.
|
||||
SwiftAudio is an audio player written in Swift, making it simpler to work with audio playback from streams and files.
|
||||
|
||||
## Example
|
||||
|
||||
To see the audio player in action clone the repo and run the example project!
|
||||
To run the example project, clone the repo, and run `pod install` from the Example directory first.
|
||||
|
||||
## Requirements
|
||||
@@ -25,16 +27,45 @@ pod 'SwiftAudio'
|
||||
|
||||
## Usage
|
||||
|
||||
Using the AudioPlayer:
|
||||
### AudioPlayer
|
||||
```swift
|
||||
let player = AudioPlayer()
|
||||
let player = QueuedAudioPlayer()
|
||||
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
|
||||
player.add(item: audioItem)
|
||||
```
|
||||
|
||||
The player will load the track and start playing when ready. To disable this behaviour use `add(item:playWhenReady:)` and pass in `false`. This is `true` by default. To get notified of events during playback and loading, implement `AudioPlayerDelegate` and the player will notify you with changes.
|
||||
|
||||
If you want a simpler audio player without queue functionality, use:
|
||||
```swift
|
||||
let player = SimpleAudioPlayer()
|
||||
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
|
||||
player.load(item: audioItem)
|
||||
```
|
||||
The player will load the track and start playing when ready.
|
||||
The `AudioPlayer` will automatically update the `MPNowPlayingInfoCenter` with artist, title, album, artwork, time etc.
|
||||
To enable this behaviour the AudioItems supplied to the player must supply these values. You can either use the `DefaultAudioItem` or make your playback objects implement the `AudioItem` protocol.
|
||||
You must also remember to set a AudioSessionCategory that supports this behaviour, and activate the session:
|
||||
|
||||
**NOTE**: Do not use `AudioPlayer` directly. Use one of the above types.
|
||||
|
||||
#### States
|
||||
The `AudioPlayer` has a `state` property, to make it easier to determine appropriate actions. The different states:
|
||||
+ **idle**: The player is doing nothing, no item is set as current. This is the default state.
|
||||
+ **ready**: The player has its current item set and is ready to start loading for playback. This is when you can call `play()` if you supplied `playWhenReady=false` when calling `load(item:playWhenReady)`.
|
||||
+ **loading**: The player is loading the track and will start playback soon.
|
||||
+ **playing**: The player is playing.
|
||||
+ **paused**: The player is paused.
|
||||
|
||||
#### Queue
|
||||
The `QueuedAudioPlayer` maintains a queue of audio tracks.
|
||||
The arrangement of the tracks are: [Previous]-[Current]-[Next].
|
||||
|
||||
When a track is done playing, the player will load the next track and update the queue, as long as `automaticallyPlayNextSong` is `true` (This is by default).
|
||||
|
||||
Items can be added to the queue by calling `player.add(item:)` or `player.add(items:)`.
|
||||
Use `removeItem(atIndex:)` and `moveItem(fromIndex:toIndex:)` to manipulate the queue.
|
||||
|
||||
The queue can be navigated by using `next()`, `previous()` and `jumpToItem(atIndex:)`
|
||||
|
||||
### Audio Session
|
||||
Remember to activate an audio session with an appropriate category for your app. This can be done with `AudioSessionCategory`:
|
||||
```swift
|
||||
try? AudioSessionController.set(category: .playback)
|
||||
//...
|
||||
@@ -42,21 +73,36 @@ try? AudioSessionController.set(category: .playback)
|
||||
// This is to avoid interrupting other audio without the need to do it.
|
||||
try? AudioSessionController.activateSession()
|
||||
```
|
||||
If you want audio to continue playing when the app is closed or phone locked remember to activate background audio:
|
||||
App Settings -> Capabilities -> Background Modes -> Check 'Audio, AirPlay, and Picture in Picture'
|
||||
|
||||
The player will also handle remote events received from `MPRemoteCommandCenter`'s shared instance. To enable this, you have to go to App Settings -> Capabilites -> Background Modes -> Check 'Remote notifications'
|
||||
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'.
|
||||
|
||||
To get notified of events during playback and loading, implement `AudioPlayerDelegate`
|
||||
The player will notify you with changes.
|
||||
#### 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`.
|
||||
|
||||
### Remote Commands
|
||||
|
||||
The player will handle remote commands received from `MPRemoteCommandCenter`'s shared instance, enabled by:
|
||||
```swift
|
||||
audioPlayer.remoteCommands = [
|
||||
.play,
|
||||
.pause,
|
||||
.skipForward(intervals: [30]),
|
||||
.skipBackward(intervals: [30]),
|
||||
]
|
||||
```
|
||||
|
||||
These commands will be activated for each `AudioItem`. If you need some audio items to have different commands, implement `RemoteCommandable`. These commands will override the commands found in `AudioPlayer.remoteCommands` so make sure to supply all commands you need for that particular `AudioItem`.
|
||||
|
||||
**Remember** to go to App Settings -> Capabilites -> Background Modes -> Check 'Remote notifications'
|
||||
|
||||
### States
|
||||
The `AudioPlayer` has a `state` property, to make it easier to determine appropriate actions. The delegate will be called when the state is updated.
|
||||
+ **idle**: The player is doing nothing, no item is set as current. This is the default state.
|
||||
+ **ready**: The player has its current item set and is ready to start loading for playback. This is when you can call `play()` if you supplied `playWhenReady=false` when calling `load(item:playWhenReady)`.
|
||||
+ **loading**: The player is loading the track and will start playback soon.
|
||||
+ **playing**: The player is playing.
|
||||
+ **paused**: The player is paused.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -67,10 +113,6 @@ Currently some configuration options are supported:
|
||||
+ `volume`: The volume of the player. From 0.0 to 1.0.
|
||||
+ `automaticallyUpdateNowPlayingInfo`: If you want to handle updating of the `MPNowPlayingInfoCenter` yourself, set this to `false`. Default is `true`.
|
||||
|
||||
## Plans
|
||||
* More configuration on the RemoteHandlerEvents
|
||||
* Ability to queue items
|
||||
|
||||
## Author
|
||||
|
||||
Jørgen Henrichsen
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudio'
|
||||
s.version = '0.1.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,8 +18,8 @@ Pod::Spec.new do |s|
|
||||
# * Finally, don't worry about the indent, CocoaPods strips it!
|
||||
|
||||
s.description = <<-DESC
|
||||
SwiftAudio aims to make audio playback easier on iOS. No more boundaryTimeObserver, periodicTimeObserver, KVO and NotificationCenter to get state update from the player. It also updates NowPlayingInfo for you and handles remote events.
|
||||
DESC
|
||||
SwiftAudio is an audio player written in Swift, making it simpler to work with audio playback from streams and files.
|
||||
DESC
|
||||
|
||||
s.homepage = 'https://github.com/jorgenhenrichsen/SwiftAudio'
|
||||
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// APError.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 25/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
public struct APError {
|
||||
|
||||
enum LoadError: Error {
|
||||
case invalidSourceUrl(String)
|
||||
}
|
||||
|
||||
enum PlaybackError: Error {
|
||||
case noLoadedItem
|
||||
}
|
||||
|
||||
enum QueueError: Error {
|
||||
case noPreviousItem
|
||||
case noNextItem
|
||||
case invalidIndex(index: Int, message: String)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,18 +18,7 @@ protocol AVPlayerWrapperDelegate: class {
|
||||
func AVWrapper(secondsElapsed seconds: Double)
|
||||
func AVWrapper(failedWithError error: Error?)
|
||||
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
|
||||
|
||||
}
|
||||
|
||||
public struct APError {
|
||||
|
||||
enum LoadError: Error {
|
||||
case invalidSourceUrl(String)
|
||||
}
|
||||
|
||||
enum PlaybackError: Error {
|
||||
case noLoadedItem
|
||||
}
|
||||
func AVWrapper(didUpdateDuration duration: Double)
|
||||
|
||||
}
|
||||
|
||||
@@ -45,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.
|
||||
@@ -61,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,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
|
||||
@@ -164,6 +157,7 @@ class AVPlayerWrapper {
|
||||
self.playerObserver.delegate = self
|
||||
self.playerTimeObserver.delegate = self
|
||||
self.playerItemNotificationObserver.delegate = self
|
||||
self.playerItemObserver.delegate = self
|
||||
|
||||
playerTimeObserver.registerForPeriodicTimeEvents()
|
||||
}
|
||||
@@ -215,7 +209,7 @@ class AVPlayerWrapper {
|
||||
*/
|
||||
func stop() {
|
||||
try? pause()
|
||||
reset()
|
||||
reset(soft: false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,8 +259,9 @@ class AVPlayerWrapper {
|
||||
|
||||
private func load(from url: URL, playWhenReady: Bool) {
|
||||
|
||||
reset()
|
||||
reset(soft: true)
|
||||
_playWhenReady = playWhenReady
|
||||
_state = .loading
|
||||
|
||||
// Set item
|
||||
let currentAsset = AVURLAsset(url: url)
|
||||
@@ -278,13 +273,16 @@ class AVPlayerWrapper {
|
||||
playerTimeObserver.registerForBoundaryTimeEvents()
|
||||
playerObserver.startObserving()
|
||||
playerItemNotificationObserver.startObserving(item: currentItem)
|
||||
playerItemObserver.startObserving(item: currentItem)
|
||||
}
|
||||
|
||||
/**
|
||||
Reset to get ready for playing from a different source.
|
||||
*/
|
||||
private func reset() {
|
||||
avPlayer.replaceCurrentItem(with: nil)
|
||||
private func reset(soft: Bool) {
|
||||
if !soft {
|
||||
avPlayer.replaceCurrentItem(with: nil)
|
||||
}
|
||||
playerTimeObserver.unregisterForBoundaryTimeEvents()
|
||||
playerItemNotificationObserver.stopObservingCurrentItem()
|
||||
}
|
||||
@@ -351,8 +349,17 @@ extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
|
||||
// MARK: - AVPlayerItemNotificationObserverDelegate
|
||||
|
||||
func itemDidPlayToEndTime() {
|
||||
self.reset()
|
||||
delegate?.AVWrapperItemDidComplete()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVPlayerWrapper: AVPlayerItemObserverDelegate {
|
||||
|
||||
// MARK: - AVPlayerItemObserverDelegate
|
||||
|
||||
func item(didUpdateDuration duration: Double) {
|
||||
self.delegate?.AVWrapper(didUpdateDuration: duration)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
public enum SourceType {
|
||||
case stream
|
||||
case file
|
||||
@@ -15,22 +14,18 @@ public enum SourceType {
|
||||
|
||||
public protocol AudioItem {
|
||||
|
||||
var audioUrl: String { get }
|
||||
|
||||
var artist: String? { get }
|
||||
|
||||
var title: String? { get }
|
||||
|
||||
var albumTitle: String? { get }
|
||||
|
||||
var sourceType: SourceType { get }
|
||||
|
||||
func getArtwork(_ handler: (UIImage?) -> Void)
|
||||
func getSourceUrl() -> String
|
||||
func getArtist() -> String?
|
||||
func getTitle() -> String?
|
||||
func getAlbumTitle() -> String?
|
||||
func getSourceType() -> SourceType
|
||||
func getArtwork(_ handler: @escaping (UIImage?) -> Void)
|
||||
|
||||
}
|
||||
|
||||
public struct DefaultAudioItem: AudioItem {
|
||||
|
||||
|
||||
public var audioUrl: String
|
||||
|
||||
public var artist: String?
|
||||
@@ -52,7 +47,29 @@ public struct DefaultAudioItem: AudioItem {
|
||||
self.artwork = artwork
|
||||
}
|
||||
|
||||
public func getArtwork(_ handler: (UIImage?) -> Void) {
|
||||
public func getSourceUrl() -> String {
|
||||
return audioUrl
|
||||
}
|
||||
|
||||
public func getArtist() -> String? {
|
||||
return artist
|
||||
}
|
||||
|
||||
public func getTitle() -> String? {
|
||||
return title
|
||||
}
|
||||
|
||||
public func getAlbumTitle() -> String? {
|
||||
return albumTitle
|
||||
}
|
||||
|
||||
public func getSourceType() -> SourceType {
|
||||
return sourceType
|
||||
}
|
||||
|
||||
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
|
||||
handler(artwork)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -22,22 +22,37 @@ public protocol AudioPlayerDelegate: class {
|
||||
|
||||
func audioPlayer(seekTo seconds: Int, didFinish: Bool)
|
||||
|
||||
func audioPlayer(didUpdateDuration duration: Double)
|
||||
|
||||
}
|
||||
|
||||
public class AudioPlayer {
|
||||
/**
|
||||
The main AudioPlayer.
|
||||
- warning: DO NOT USE THIS CLASS, use `SimpleAudioPlayer` or `QueuedAudioPlayer`
|
||||
*/
|
||||
public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
|
||||
let wrapper: AVPlayerWrapper
|
||||
let nowPlayingInfoController: NowPlayingInfoController
|
||||
let remoteCommandCenter: MPRemoteCommandCenter = MPRemoteCommandCenter.shared()
|
||||
let remoteCommandController: RemoteCommandController
|
||||
|
||||
var _currentItem: AudioItem?
|
||||
|
||||
public weak var delegate: AudioPlayerDelegate?
|
||||
public var currentItem: AudioItem?
|
||||
public var currentItem: AudioItem? {
|
||||
return _currentItem
|
||||
}
|
||||
|
||||
/**
|
||||
Set this to false to disable automatic updating of now playing info for control center and lock screen.
|
||||
*/
|
||||
public var automaticallyUpdateNowPlayingInfo: Bool = true
|
||||
|
||||
/**
|
||||
Default remote commands to use for each playing item
|
||||
*/
|
||||
public var remoteCommands: [RemoteCommand] = []
|
||||
|
||||
// MARK: - Getters from AVPlayerWrapper
|
||||
|
||||
/**
|
||||
@@ -74,7 +89,7 @@ public class AudioPlayer {
|
||||
Indicates wether the player should automatically delay playback in order to minimize stalling.
|
||||
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayer/1643482-automaticallywaitstominimizestal)
|
||||
*/
|
||||
var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
public var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { return wrapper.automaticallyWaitsToMinimizeStalling }
|
||||
set { wrapper.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
}
|
||||
@@ -86,7 +101,7 @@ public class AudioPlayer {
|
||||
|
||||
- Important: This setting will have no effect if `automaticallyWaitsToMinimizeStalling` is set to `true`
|
||||
*/
|
||||
var bufferDuration: TimeInterval {
|
||||
public var bufferDuration: TimeInterval {
|
||||
get { return wrapper.bufferDuration }
|
||||
set { wrapper.bufferDuration = newValue }
|
||||
}
|
||||
@@ -94,7 +109,7 @@ public class AudioPlayer {
|
||||
/**
|
||||
Set this to decide how often the player should call the delegate with time progress events.
|
||||
*/
|
||||
var timeEventFrquency: TimeEventFrequency {
|
||||
public var timeEventFrquency: TimeEventFrequency {
|
||||
get { return wrapper.timeEventFrequency }
|
||||
set { wrapper.timeEventFrequency = newValue }
|
||||
}
|
||||
@@ -103,85 +118,128 @@ public class AudioPlayer {
|
||||
The player volume, from 0.0 to 1.0
|
||||
Default is 1.0
|
||||
*/
|
||||
var volume: Float {
|
||||
public var volume: Float {
|
||||
get { return wrapper.volume }
|
||||
set { wrapper.volume = newValue }
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
// 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()
|
||||
|
||||
self.wrapper.delegate = self
|
||||
|
||||
connectToCommandCenter()
|
||||
self.remoteCommandController.audioPlayer = self
|
||||
}
|
||||
|
||||
// MARK: - Player Actions
|
||||
|
||||
/**
|
||||
Load an AudioItem into the manager.
|
||||
|
||||
- parameter item: The AudioItem to load. The info given in this item is the one used for the InfoCenter.
|
||||
- parameter playWhenReady: Immediately start playback when the item is ready. Default is `true`. If you disable this you have to call play() or togglePlay() when the `state` switches to `ready`.
|
||||
*/
|
||||
public func load(item: AudioItem, playWhenReady: Bool = true) {
|
||||
|
||||
switch item.sourceType {
|
||||
func loadItem(_ item: AudioItem, playWhenReady: Bool = true) throws {
|
||||
print("Loading: \(item)")
|
||||
switch item.getSourceType() {
|
||||
case .stream:
|
||||
try? self.wrapper.load(fromUrlString: item.audioUrl, playWhenReady: playWhenReady)
|
||||
try self.wrapper.load(fromUrlString: item.getSourceUrl(), playWhenReady: playWhenReady)
|
||||
case .file:
|
||||
print(item.audioUrl)
|
||||
try? self.wrapper.load(fromFilePath: item.audioUrl, playWhenReady: playWhenReady)
|
||||
try self.wrapper.load(fromFilePath: item.getSourceUrl(), playWhenReady: playWhenReady)
|
||||
}
|
||||
|
||||
self.currentItem = item
|
||||
self._currentItem = item
|
||||
set(item: item)
|
||||
setArtwork(forItem: item)
|
||||
enableRemoteCommands(forItem: item)
|
||||
}
|
||||
|
||||
/**
|
||||
Toggle playback status.
|
||||
*/
|
||||
public func togglePlaying() {
|
||||
try? self.wrapper.togglePlaying()
|
||||
public func togglePlaying() throws {
|
||||
try self.wrapper.togglePlaying()
|
||||
}
|
||||
|
||||
/**
|
||||
Start playback
|
||||
*/
|
||||
public func play() {
|
||||
try? self.wrapper.play()
|
||||
public func play() throws {
|
||||
try self.wrapper.play()
|
||||
}
|
||||
|
||||
/**
|
||||
Pause playback
|
||||
*/
|
||||
public func pause() {
|
||||
try? self.wrapper.pause()
|
||||
public func pause() throws {
|
||||
try self.wrapper.pause()
|
||||
}
|
||||
|
||||
/**
|
||||
Stop playback, resetting the player.
|
||||
*/
|
||||
public func stop() {
|
||||
self.reset()
|
||||
self.wrapper.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
Seek to a specific time in the item.
|
||||
*/
|
||||
public func seek(to seconds: TimeInterval) {
|
||||
try? self.wrapper.seek(to: seconds)
|
||||
public func seek(to seconds: TimeInterval) throws {
|
||||
try self.wrapper.seek(to: seconds)
|
||||
}
|
||||
|
||||
// MARK: - Remote Command Center
|
||||
|
||||
/**
|
||||
Set the remote commands that should be activated and handled.
|
||||
Calling this will disable all earlier enabled commands, so include all commands you need.
|
||||
*/
|
||||
func enableRemoteCommands(_ commands: [RemoteCommand]) {
|
||||
self.remoteCommandController.enable(commands: commands)
|
||||
}
|
||||
|
||||
func enableRemoteCommands(forItem item: AudioItem) {
|
||||
if let item = item as? RemoteCommandable {
|
||||
self.enableRemoteCommands(item.getCommands())
|
||||
}
|
||||
else {
|
||||
self.enableRemoteCommands(remoteCommands)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NowPlayingInfo
|
||||
|
||||
/**
|
||||
Reloads the NowPlayingInfo from the current AudioItem.
|
||||
*/
|
||||
public func reloadNowPlayingInfo() {
|
||||
guard let item = currentItem else { return }
|
||||
set(item: item)
|
||||
setArtwork(forItem: item)
|
||||
updatePlaybackValues()
|
||||
}
|
||||
|
||||
public func add(property: NowPlayingInfoKeyValue) {
|
||||
self.nowPlayingInfoController.set(keyValue: property)
|
||||
}
|
||||
|
||||
func set(item: AudioItem) {
|
||||
guard automaticallyUpdateNowPlayingInfo else { return }
|
||||
|
||||
nowPlayingInfoController.set(keyValues: [
|
||||
MediaItemProperty.artist(item.artist),
|
||||
MediaItemProperty.title(item.title),
|
||||
MediaItemProperty.albumTitle(item.albumTitle),
|
||||
MediaItemProperty.artist(item.getArtist()),
|
||||
MediaItemProperty.title(item.getTitle()),
|
||||
MediaItemProperty.albumTitle(item.getAlbumTitle()),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -189,11 +247,12 @@ public class AudioPlayer {
|
||||
guard automaticallyUpdateNowPlayingInfo else { return }
|
||||
item.getArtwork { (image) in
|
||||
if let image = image {
|
||||
|
||||
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { (size) -> UIImage in
|
||||
return image
|
||||
})
|
||||
|
||||
nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(artwork))
|
||||
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(artwork))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,105 +264,19 @@ public class AudioPlayer {
|
||||
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.playbackRate(Double(wrapper.rate)))
|
||||
}
|
||||
|
||||
// MARK: - Remote Commands Handlers
|
||||
// MARK: - Private
|
||||
|
||||
func connectToCommandCenter() {
|
||||
self.remoteCommandCenter.playCommand.addTarget(handler: handlePlayCommand(event:))
|
||||
self.remoteCommandCenter.pauseCommand.addTarget(handler: handlePauseCommand(event:))
|
||||
self.remoteCommandCenter.togglePlayPauseCommand.addTarget(handler: handleTogglePlaybackCommand(event:))
|
||||
self.remoteCommandCenter.stopCommand.addTarget(handler: handleStopCommand(event:))
|
||||
self.remoteCommandCenter.skipForwardCommand.addTarget(handler: handleSkipForwardCommand(event:))
|
||||
remoteCommandCenter.skipForwardCommand.preferredIntervals = [15]
|
||||
self.remoteCommandCenter.skipBackwardCommand.addTarget(handler: handleSkipBackwardCommand(event:))
|
||||
remoteCommandCenter.skipBackwardCommand.preferredIntervals = [15]
|
||||
|
||||
self.remoteCommandCenter.changePlaybackPositionCommand.addTarget(handler: handleChangePlaybackPositionCommand(event:))
|
||||
private func reset() {
|
||||
self._currentItem = nil
|
||||
}
|
||||
|
||||
func getRemoteCommandHandlerStatus(forError error: Error) -> MPRemoteCommandHandlerStatus {
|
||||
if let error = error as? APError.PlaybackError {
|
||||
switch error {
|
||||
case .noLoadedItem:
|
||||
return MPRemoteCommandHandlerStatus.noActionableNowPlayingItem
|
||||
}
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
func handlePlayCommand(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
do {
|
||||
try self.wrapper.play()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
}
|
||||
|
||||
func handlePauseCommand(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
do {
|
||||
try self.wrapper.pause()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
}
|
||||
|
||||
func handleTogglePlaybackCommand(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
do {
|
||||
try self.wrapper.togglePlaying()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
}
|
||||
|
||||
func handleStopCommand(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
self.wrapper.stop()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
|
||||
func handleSkipForwardCommand(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let command = event.command as? MPSkipIntervalCommand {
|
||||
if let interval = command.preferredIntervals.first {
|
||||
self.seek(to: currentTime + interval.doubleValue)
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
}
|
||||
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
func handleSkipBackwardCommand(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let command = event.command as? MPSkipIntervalCommand {
|
||||
if let interval = command.preferredIntervals.first {
|
||||
self.seek(to: currentTime - interval.doubleValue)
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
func handleChangePlaybackPositionCommand(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
|
||||
if let event = event as? MPChangePlaybackPositionCommandEvent {
|
||||
do {
|
||||
try wrapper.seek(to: event.positionTime)
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
}
|
||||
|
||||
extension AudioPlayer: AVPlayerWrapperDelegate {
|
||||
// MARK: - AVPlayerWrapperDelegate
|
||||
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
|
||||
updatePlaybackValues()
|
||||
switch state {
|
||||
case .playing, .paused: updatePlaybackValues()
|
||||
default: break
|
||||
}
|
||||
self.delegate?.audioPlayer(playerDidChangeState: state)
|
||||
}
|
||||
|
||||
@@ -324,4 +297,8 @@ extension 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,11 +65,13 @@ 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.
|
||||
*/
|
||||
var isOtherAudioPlaying: Bool {
|
||||
public var isOtherAudioPlaying: Bool {
|
||||
return audioSession.isOtherAudioPlaying
|
||||
}
|
||||
|
||||
@@ -74,9 +80,36 @@ public class AudioSessionController {
|
||||
|
||||
- warning: This will only be correct if the audiosession is activated through this class!
|
||||
*/
|
||||
var audioSessionIsActive: Bool = false
|
||||
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
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
//
|
||||
// QueueManager.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 24/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
class QueueManager<T> {
|
||||
|
||||
private var _items: [T] = []
|
||||
|
||||
/**
|
||||
All items held by the queue.
|
||||
*/
|
||||
public var items: [T] {
|
||||
return _items
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
private var _currentIndex: Int = 0
|
||||
|
||||
/**
|
||||
The index of the current item.
|
||||
Will be populated event though there is no current item (When the queue is empty).
|
||||
*/
|
||||
public var currentIndex: Int {
|
||||
return _currentIndex
|
||||
}
|
||||
|
||||
/**
|
||||
The current item for the queue.
|
||||
*/
|
||||
public var current: T? {
|
||||
if _items.count > _currentIndex {
|
||||
return _items[_currentIndex]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Add a single item to the queue.
|
||||
|
||||
- parameter item: The `AudioItem` to be added.
|
||||
*/
|
||||
public func addItem(_ item: T) {
|
||||
_items.append(item)
|
||||
}
|
||||
|
||||
/**
|
||||
Add an array of items to the queue.
|
||||
|
||||
- parameter items: The `AudioItem`s to be added.
|
||||
*/
|
||||
public func addItems(_ items: [T]) {
|
||||
_items.append(contentsOf: items)
|
||||
}
|
||||
|
||||
/**
|
||||
Get the next item in the queue, if there are any.
|
||||
Will update the current item.
|
||||
|
||||
- throws: `APError.QueueError`
|
||||
- returns: The next item.
|
||||
*/
|
||||
@discardableResult
|
||||
public func next() throws -> T {
|
||||
let nextIndex = _currentIndex + 1
|
||||
guard _items.count > nextIndex else {
|
||||
throw APError.QueueError.noNextItem
|
||||
}
|
||||
_currentIndex = nextIndex
|
||||
return _items[nextIndex]
|
||||
}
|
||||
|
||||
/**
|
||||
Get the previous item in the queue, if there are any.
|
||||
Will update the current item.
|
||||
|
||||
- throws: `APError.QueueError`
|
||||
- returns: The previous item.
|
||||
*/
|
||||
@discardableResult
|
||||
public func previous() throws -> T {
|
||||
let previousIndex = _currentIndex - 1
|
||||
guard previousIndex >= 0 else {
|
||||
throw APError.QueueError.noPreviousItem
|
||||
}
|
||||
_currentIndex = previousIndex
|
||||
return _items[previousIndex]
|
||||
}
|
||||
|
||||
/**
|
||||
Jump to a position in the queue.
|
||||
Will update the current item.
|
||||
|
||||
- parameter index: The index to jump to.
|
||||
- throws: `APError.QueueError`
|
||||
- returns: The item at the index.
|
||||
*/
|
||||
@discardableResult
|
||||
func jump(to index: Int) throws -> T {
|
||||
guard index != currentIndex else {
|
||||
throw APError.QueueError.invalidIndex(index: index, message: "Cannot jump to the current item")
|
||||
}
|
||||
|
||||
guard index >= 0 && items.count > index else {
|
||||
throw APError.QueueError.invalidIndex(index: index, message: "The jump index has to be positive and smaller thant the count of current items (\(items.count))")
|
||||
}
|
||||
_currentIndex = index
|
||||
return _items[index]
|
||||
}
|
||||
|
||||
/**
|
||||
Move an item in the queue.
|
||||
|
||||
- parameter fromIndex: The index of the item to be moved.
|
||||
- parameter toIndex: The index to move the item to.
|
||||
- throws: `APError.QueueError`
|
||||
*/
|
||||
func moveItem(fromIndex: Int, toIndex: Int) throws {
|
||||
|
||||
guard fromIndex != _currentIndex else {
|
||||
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex cannot be equal to the current index.")
|
||||
}
|
||||
|
||||
guard fromIndex >= 0 && fromIndex < _items.count else {
|
||||
throw APError.QueueError.invalidIndex(index: fromIndex, message: "The fromIndex has to be positive and smaller than the count of current items (\(items.count)).")
|
||||
}
|
||||
|
||||
guard toIndex >= 0 && toIndex < _items.count else {
|
||||
throw APError.QueueError.invalidIndex(index: toIndex, message: "The toIndex has to be positive and smaller than the count of current items (\(items.count)).")
|
||||
}
|
||||
|
||||
_items.insert(_items.remove(at: fromIndex), at: toIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
Remove an item.
|
||||
|
||||
- parameter index: The index of the item to remove.
|
||||
- throws: APError.QueueError
|
||||
- returns: The removed item.
|
||||
*/
|
||||
@discardableResult
|
||||
public func remove(atIndex index: Int) throws -> T {
|
||||
guard index != _currentIndex else {
|
||||
throw APError.QueueError.invalidIndex(index: index, message: "Cannot remove the current item!")
|
||||
}
|
||||
|
||||
guard index >= 0 && _items.count > index else {
|
||||
throw APError.QueueError.invalidIndex(index: index, message: "Index for removal has to be postivie and smaller than the count of current items (\(items.count)).")
|
||||
}
|
||||
return _items.remove(at: index)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
//
|
||||
// QueuedAudioPlayer.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 24/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
/**
|
||||
An audio player that can keep track of a queue of AudioItems.
|
||||
*/
|
||||
public class QueuedAudioPlayer: AudioPlayer {
|
||||
|
||||
let queueManager: QueueManager = QueueManager<AudioItem>()
|
||||
|
||||
/**
|
||||
Set wether the player should automatically play the next song when a song is finished.
|
||||
Default is `true`.
|
||||
*/
|
||||
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)
|
||||
try self.loadItem(item, playWhenReady: playWhenReady)
|
||||
}
|
||||
else {
|
||||
queueManager.addItem(item)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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)
|
||||
try self.loadItem(currentItem!, playWhenReady: playWhenReady)
|
||||
}
|
||||
else {
|
||||
queueManager.addItems(items)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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)
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerWrapperDelegate
|
||||
|
||||
override func AVWrapperItemDidComplete() {
|
||||
super.AVWrapperItemDidComplete()
|
||||
if automaticallyPlayNextSong {
|
||||
try? self.next()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// RemoteCommand.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 20/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
|
||||
public typealias RemoteCommandHandler = (MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus
|
||||
|
||||
public protocol RemoteCommandProtocol {
|
||||
associatedtype Command: MPRemoteCommand
|
||||
|
||||
var id: String { get }
|
||||
var commandKeyPath: KeyPath<MPRemoteCommandCenter, Command> { get }
|
||||
var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler> { get }
|
||||
}
|
||||
|
||||
public struct PlayBackCommand: RemoteCommandProtocol {
|
||||
|
||||
public static let play = PlayBackCommand(id: "Play", commandKeyPath: \MPRemoteCommandCenter.playCommand, handlerKeyPath: \RemoteCommandController.handlePlayCommand)
|
||||
|
||||
public static let pause = PlayBackCommand(id: "Pause", commandKeyPath: \MPRemoteCommandCenter.pauseCommand, handlerKeyPath: \RemoteCommandController.handlePauseCommand)
|
||||
|
||||
public static let stop = PlayBackCommand(id: "Stop", commandKeyPath: \MPRemoteCommandCenter.stopCommand, handlerKeyPath: \RemoteCommandController.handleStopCommand)
|
||||
|
||||
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>
|
||||
|
||||
}
|
||||
|
||||
public struct ChangePlaybackPositionCommand: RemoteCommandProtocol {
|
||||
|
||||
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>
|
||||
|
||||
}
|
||||
|
||||
public struct SkipIntervalCommand: RemoteCommandProtocol {
|
||||
|
||||
public static let skipForward = SkipIntervalCommand(id: "SkipForward", commandKeyPath: \MPRemoteCommandCenter.skipForwardCommand, handlerKeyPath: \RemoteCommandController.handleSkipForwardCommand)
|
||||
|
||||
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>
|
||||
|
||||
func set(preferredIntervals: [NSNumber]) -> SkipIntervalCommand {
|
||||
MPRemoteCommandCenter.shared()[keyPath: commandKeyPath].preferredIntervals = preferredIntervals
|
||||
return self
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum RemoteCommand {
|
||||
|
||||
case play
|
||||
|
||||
case pause
|
||||
|
||||
case stop
|
||||
|
||||
case togglePlayPause
|
||||
|
||||
case next
|
||||
|
||||
case previous
|
||||
|
||||
case changePlaybackPosition
|
||||
|
||||
case skipForward(preferredIntervals: [NSNumber])
|
||||
|
||||
case skipBackward(preferredIntervals: [NSNumber])
|
||||
|
||||
/**
|
||||
All values in an array for convenience.
|
||||
Don't use for associated values.
|
||||
*/
|
||||
static func all() -> [RemoteCommand] {
|
||||
return [
|
||||
.play,
|
||||
.pause,
|
||||
.stop,
|
||||
.togglePlayPause,
|
||||
.next,
|
||||
.previous,
|
||||
.changePlaybackPosition,
|
||||
.skipForward(preferredIntervals: []),
|
||||
.skipBackward(preferredIntervals: []),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
//
|
||||
// File.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 20/03/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
public protocol RemoteCommandable {
|
||||
func getCommands() -> [RemoteCommand]
|
||||
}
|
||||
|
||||
public class RemoteCommandController {
|
||||
|
||||
private let center = MPRemoteCommandCenter.shared()
|
||||
|
||||
weak var audioPlayer: AudioPlayer?
|
||||
|
||||
var commandTargetPointers: [String: Any] = [:]
|
||||
|
||||
init() {}
|
||||
|
||||
/**
|
||||
Enable a set of RemoteCommands. Calling this will disable all earlier set commands, so include all commands that needs to be active.
|
||||
|
||||
- parameter commands: The RemoteCommands that is to be enabled.
|
||||
*/
|
||||
public func enable(commands: [RemoteCommand]) {
|
||||
self.disable(commands: RemoteCommand.all())
|
||||
commands.forEach { (command) in
|
||||
self.enable(command: command)
|
||||
}
|
||||
}
|
||||
|
||||
private func disable(commands: [RemoteCommand]) {
|
||||
commands.forEach { (command) in
|
||||
self.disable(command: command)
|
||||
}
|
||||
}
|
||||
|
||||
private func enableCommand<Command: RemoteCommandProtocol>(_ command: Command) {
|
||||
center[keyPath: command.commandKeyPath].isEnabled = true
|
||||
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(commandTargetPointers[command.id])
|
||||
commandTargetPointers.removeValue(forKey: command.id)
|
||||
}
|
||||
|
||||
private func enable(command: RemoteCommand) {
|
||||
switch command {
|
||||
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))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private func disable(command: RemoteCommand) {
|
||||
switch command {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Handlers
|
||||
|
||||
lazy var handlePlayCommand: RemoteCommandHandler = { (event) in
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
do {
|
||||
try audioPlayer.play()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
lazy var handlePauseCommand: RemoteCommandHandler = { (event) in
|
||||
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
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
audioPlayer.stop()
|
||||
return .success
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = { (event) in
|
||||
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 {
|
||||
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 handleSkipBackwardCommand: RemoteCommandHandler = { (event) in
|
||||
if let command = event.command as? MPSkipIntervalCommand,
|
||||
let interval = command.preferredIntervals.first,
|
||||
let audioPlayer = self.audioPlayer {
|
||||
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,
|
||||
let audioPlayer = self.audioPlayer {
|
||||
do {
|
||||
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 {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
private func getRemoteCommandHandlerStatus(forError error: Error) -> MPRemoteCommandHandlerStatus {
|
||||
if let error = error as? APError.PlaybackError {
|
||||
switch error {
|
||||
case .noLoadedItem:
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// SimpleAudioPlayer.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 24/03/2018.
|
||||
//
|
||||
|
||||
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.
|
||||
|
||||
- parameter item: The AudioItem to load. The info given in this item is the one used for the InfoCenter.
|
||||
- parameter playWhenReady: Immediately start playback when the item is ready. Default is `true`. If you disable this you have to call play() or togglePlay() when the `state` switches to `ready`.
|
||||
*/
|
||||
public func load(item: AudioItem, playWhenReady: Bool = true) throws {
|
||||
try self.loadItem(item, playWhenReady: playWhenReady)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user