Compare commits
63 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 |
@@ -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'
|
||||
|
||||
+20
@@ -7,11 +7,16 @@
|
||||
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, ); }; };
|
||||
@@ -168,11 +173,16 @@
|
||||
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>"; };
|
||||
@@ -379,6 +389,7 @@
|
||||
07732657205ED6C400C4D1CD /* AVPlayerObserver.swift */,
|
||||
07732658205ED6C400C4D1CD /* AVPlayerItemNotificationObserver.swift */,
|
||||
07732659205ED6C400C4D1CD /* AVPlayerTimeObserver.swift */,
|
||||
078C908B210CD8B300555E80 /* AVPlayerItemObserver.swift */,
|
||||
);
|
||||
name = Observer;
|
||||
path = SwiftAudio/Classes/Observer;
|
||||
@@ -388,6 +399,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E469FE2F4CBA2182A64C31D9B41A936E /* AudioPlayer.swift */,
|
||||
0775575C2066A7DB0002C6A1 /* SimpleAudioPlayer.swift */,
|
||||
077557602066ABAD0002C6A1 /* QueuedAudioPlayer.swift */,
|
||||
0773265F205ED7BF00C4D1CD /* AudioItem.swift */,
|
||||
C455E233BD4071A35296FEDA8D99CEDE /* MediaItemProperty.swift */,
|
||||
32A658905B9E84D827AA64605F28E3F9 /* NowPlayingInfoController.swift */,
|
||||
@@ -396,6 +409,8 @@
|
||||
07F41B19205FC0B100E25749 /* AudioSessionController.swift */,
|
||||
07F41B2120614BDC00E25749 /* RemoteCommandController.swift */,
|
||||
0775574A2061C1820002C6A1 /* RemoteCommand.swift */,
|
||||
077557562066867F0002C6A1 /* QueueManager.swift */,
|
||||
070713042067E3EA00F789B3 /* APError.swift */,
|
||||
07732656205ED6C400C4D1CD /* Observer */,
|
||||
25961FA3FF989AFD1E08A55F4E464276 /* AVPlayerWrapper */,
|
||||
853A8C0AF6F0B4902930084E06DEA640 /* Support Files */,
|
||||
@@ -981,6 +996,9 @@
|
||||
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 */,
|
||||
@@ -988,10 +1006,12 @@
|
||||
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) ?? ""
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,43 +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/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI"))
|
||||
|
||||
var artwork: MPMediaItemArtwork!
|
||||
let controller = AudioController.shared
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
audioPlayer.delegate = self
|
||||
audioPlayer.remoteCommands = [
|
||||
.stop,
|
||||
.togglePlayPause,
|
||||
.skipForward(preferredIntervals: [30]),
|
||||
.skipBackward(preferredIntervals: [30]),
|
||||
.changePlaybackPosition
|
||||
]
|
||||
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) {
|
||||
try? audioPlayer.load(item: localSource)
|
||||
}
|
||||
|
||||
@IBAction func playB(_ sender: Any) {
|
||||
try? audioPlayer.load(item: streamSource)
|
||||
controller.player.delegate = self
|
||||
}
|
||||
|
||||
@IBAction func togglePlay(_ sender: Any) {
|
||||
try? 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) {
|
||||
@@ -60,27 +50,41 @@ class ViewController: UIViewController {
|
||||
}
|
||||
|
||||
@IBAction func scrubbing(_ sender: UISlider) {
|
||||
try? 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)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -92,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,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.
@@ -2,6 +2,7 @@
|
||||
|
||||
[](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)
|
||||
|
||||
@@ -9,6 +10,7 @@ SwiftAudio is an audio player written in Swift, making it simpler to work with a
|
||||
|
||||
## 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
|
||||
@@ -27,12 +29,21 @@ pod 'SwiftAudio'
|
||||
|
||||
### 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. To disable this behaviour use `load(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.
|
||||
**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:
|
||||
@@ -42,6 +53,17 @@ The `AudioPlayer` has a `state` property, to make it easier to determine appropr
|
||||
+ **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
|
||||
@@ -55,6 +77,12 @@ try? AudioSessionController.activateSession()
|
||||
If you want audio to continue playing when the app is inactive, remember to activate background audio:
|
||||
App Settings -> Capabilities -> Background Modes -> Check 'Audio, AirPlay, and Picture in Picture'.
|
||||
|
||||
#### Interruptions
|
||||
If you are using the AudioSessionController for setting up the audio session, you can use it to handle interruptions too.
|
||||
Implement `AudioSessionControllerDelegate` and you will be notified by `handleInterruption(type: AVAudioSessionInterruptionType)`.
|
||||
If you are storing progress for playback time on items when the app quits, it can be a good idea to do it on interruptions as well.
|
||||
To disable interruption notifcations set `isObservingForInterruptions` to `false`.
|
||||
|
||||
### Now Playing Info
|
||||
The `AudioPlayer` will automatically update the `MPNowPlayingInfoCenter` with artist, title, album, artwork, time if the passed in `AudioItem` supports this.
|
||||
If you need to set additional properties for some items use `AudioPlayer.add(property:)`. Available properties can be found in `NowPlayingInfoProperty`.
|
||||
@@ -85,9 +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
|
||||
* Ability to queue items
|
||||
|
||||
## Author
|
||||
|
||||
Jørgen Henrichsen
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudio'
|
||||
s.version = '0.2.1'
|
||||
s.version = '0.3.3'
|
||||
s.summary = 'Easy audio streaming for iOS'
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public protocol AudioItem {
|
||||
}
|
||||
|
||||
public struct DefaultAudioItem: AudioItem {
|
||||
|
||||
|
||||
public var audioUrl: String
|
||||
|
||||
@@ -69,4 +70,6 @@ public struct DefaultAudioItem: AudioItem {
|
||||
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
|
||||
handler(artwork)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -22,17 +22,26 @@ 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 remoteCommandController: RemoteCommandController
|
||||
|
||||
public weak var delegate: AudioPlayerDelegate?
|
||||
public var currentItem: AudioItem?
|
||||
var _currentItem: AudioItem?
|
||||
|
||||
public weak var delegate: AudioPlayerDelegate?
|
||||
public var currentItem: AudioItem? {
|
||||
return _currentItem
|
||||
}
|
||||
|
||||
/**
|
||||
Set this to false to disable automatic updating of now playing info for control center and lock screen.
|
||||
@@ -117,12 +126,11 @@ public class AudioPlayer {
|
||||
// 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()
|
||||
@@ -139,8 +147,8 @@ public class AudioPlayer {
|
||||
- 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 {
|
||||
|
||||
func loadItem(_ item: AudioItem, playWhenReady: Bool = true) throws {
|
||||
print("Loading: \(item)")
|
||||
switch item.getSourceType() {
|
||||
case .stream:
|
||||
try self.wrapper.load(fromUrlString: item.getSourceUrl(), playWhenReady: playWhenReady)
|
||||
@@ -148,7 +156,7 @@ public class AudioPlayer {
|
||||
try self.wrapper.load(fromFilePath: item.getSourceUrl(), playWhenReady: playWhenReady)
|
||||
}
|
||||
|
||||
self.currentItem = item
|
||||
self._currentItem = item
|
||||
set(item: item)
|
||||
setArtwork(forItem: item)
|
||||
enableRemoteCommands(forItem: item)
|
||||
@@ -202,7 +210,6 @@ public class AudioPlayer {
|
||||
|
||||
func enableRemoteCommands(forItem item: AudioItem) {
|
||||
if let item = item as? RemoteCommandable {
|
||||
print("Enabling remote commands for item")
|
||||
self.enableRemoteCommands(item.getCommands())
|
||||
}
|
||||
else {
|
||||
@@ -260,15 +267,16 @@ public class AudioPlayer {
|
||||
// MARK: - Private
|
||||
|
||||
private func reset() {
|
||||
self.currentItem = nil
|
||||
self._currentItem = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AudioPlayer: AVPlayerWrapperDelegate {
|
||||
// MARK: - AVPlayerWrapperDelegate
|
||||
|
||||
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
|
||||
updatePlaybackValues()
|
||||
switch state {
|
||||
case .playing, .paused: updatePlaybackValues()
|
||||
default: break
|
||||
}
|
||||
self.delegate?.audioPlayer(playerDidChangeState: state)
|
||||
}
|
||||
|
||||
@@ -289,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,6 +65,8 @@ public class AudioSessionController {
|
||||
public static let shared = AudioSessionController()
|
||||
|
||||
private let audioSession: AVAudioSession = AVAudioSession.sharedInstance()
|
||||
private let notificationCenter: NotificationCenter = NotificationCenter.default
|
||||
private var _isObservingForInterruptions: Bool = false
|
||||
|
||||
/**
|
||||
True if another app is currently playing audio.
|
||||
@@ -76,7 +82,34 @@ public class AudioSessionController {
|
||||
*/
|
||||
public var audioSessionIsActive: Bool = false
|
||||
|
||||
private init() {}
|
||||
/**
|
||||
Wheter notifications for interruptions are being observed or not.
|
||||
This is enabled by default.
|
||||
Set this to false to disable the behaviour.
|
||||
*/
|
||||
public var isObservingForInterruptions: Bool {
|
||||
get {
|
||||
return _isObservingForInterruptions
|
||||
}
|
||||
set {
|
||||
if newValue == _isObservingForInterruptions {
|
||||
return
|
||||
}
|
||||
|
||||
if newValue {
|
||||
registerForInterruptionNotification()
|
||||
}
|
||||
else {
|
||||
unregisterForInterruptionNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public weak var delegate: AudioSessionControllerDelegate?
|
||||
|
||||
private init() {
|
||||
registerForInterruptionNotification()
|
||||
}
|
||||
|
||||
public func activateSession() throws {
|
||||
do {
|
||||
@@ -101,4 +134,29 @@ public class AudioSessionController {
|
||||
try audioSession.setCategory(category.getValue())
|
||||
}
|
||||
|
||||
// MARK: - Interruptions
|
||||
|
||||
private func registerForInterruptionNotification() {
|
||||
notificationCenter.addObserver(self,
|
||||
selector: #selector(handleInterruption),
|
||||
name: .AVAudioSessionInterruption,
|
||||
object: nil)
|
||||
_isObservingForInterruptions = true
|
||||
}
|
||||
|
||||
private func unregisterForInterruptionNotification() {
|
||||
notificationCenter.removeObserver(self, name: .AVAudioSessionInterruption, object: nil)
|
||||
_isObservingForInterruptions = false
|
||||
}
|
||||
|
||||
@objc func handleInterruption(notification: Notification) {
|
||||
guard let userInfo = notification.userInfo,
|
||||
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
|
||||
let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.delegate?.handleInterruption(type: type)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// AVPlayerItemObserver.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 28/07/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
protocol AVPlayerItemObserverDelegate: class {
|
||||
|
||||
/**
|
||||
Called when the observed item updates the duration.
|
||||
*/
|
||||
func item(didUpdateDuration duration: Double)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
Observing an AVPlayers status changes.
|
||||
*/
|
||||
class AVPlayerItemObserver: NSObject {
|
||||
|
||||
private static var context = 0
|
||||
private let main: DispatchQueue = .main
|
||||
|
||||
private struct AVPlayerItemKeyPath {
|
||||
static let duration = #keyPath(AVPlayerItem.duration)
|
||||
}
|
||||
|
||||
var isObserving: Bool = false
|
||||
|
||||
weak var observingItem: AVPlayerItem?
|
||||
weak var delegate: AVPlayerItemObserverDelegate?
|
||||
|
||||
deinit {
|
||||
if self.isObserving {
|
||||
stopObservingCurrentItem()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Start observing an item. Will remove self as observer from old item.
|
||||
|
||||
- parameter item: The player item to observe.
|
||||
*/
|
||||
func startObserving(item: AVPlayerItem) {
|
||||
main.async {
|
||||
if self.isObserving {
|
||||
self.stopObservingCurrentItem()
|
||||
}
|
||||
self.isObserving = true
|
||||
self.observingItem = item
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
}
|
||||
}
|
||||
|
||||
private func stopObservingCurrentItem() {
|
||||
observingItem?.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
|
||||
self.isObserving = false
|
||||
self.observingItem = nil
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
guard context == &AVPlayerItemObserver.context, let observedKeyPath = keyPath else {
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
return
|
||||
}
|
||||
|
||||
switch observedKeyPath {
|
||||
case AVPlayerItemKeyPath.duration:
|
||||
if let duration = change?[.newKey] as? CMTime {
|
||||
self.delegate?.item(didUpdateDuration: duration.seconds)
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,23 +14,30 @@ public typealias RemoteCommandHandler = (MPRemoteCommandEvent) -> MPRemoteComman
|
||||
public protocol RemoteCommandProtocol {
|
||||
associatedtype Command: MPRemoteCommand
|
||||
|
||||
var id: String { get }
|
||||
var commandKeyPath: KeyPath<MPRemoteCommandCenter, Command> { get }
|
||||
var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler> { get }
|
||||
}
|
||||
|
||||
public struct BaseRemoteCommand: RemoteCommandProtocol {
|
||||
public struct PlayBackCommand: RemoteCommandProtocol {
|
||||
|
||||
public static let play = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.playCommand, handlerKeyPath: \RemoteCommandController.handlePlayCommand)
|
||||
public static let play = PlayBackCommand(id: "Play", commandKeyPath: \MPRemoteCommandCenter.playCommand, handlerKeyPath: \RemoteCommandController.handlePlayCommand)
|
||||
|
||||
public static let pause = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.pauseCommand, handlerKeyPath: \RemoteCommandController.handlePauseCommand)
|
||||
public static let pause = PlayBackCommand(id: "Pause", commandKeyPath: \MPRemoteCommandCenter.pauseCommand, handlerKeyPath: \RemoteCommandController.handlePauseCommand)
|
||||
|
||||
public static let stop = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.stopCommand, handlerKeyPath: \RemoteCommandController.handleStopCommand)
|
||||
public static let stop = PlayBackCommand(id: "Stop", commandKeyPath: \MPRemoteCommandCenter.stopCommand, handlerKeyPath: \RemoteCommandController.handleStopCommand)
|
||||
|
||||
public static let togglePlayPause = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.togglePlayPauseCommand, handlerKeyPath: \RemoteCommandController.handleTogglePlayPauseCommand)
|
||||
public static let togglePlayPause = PlayBackCommand(id: "TogglePlayPause", commandKeyPath: \MPRemoteCommandCenter.togglePlayPauseCommand, handlerKeyPath: \RemoteCommandController.handleTogglePlayPauseCommand)
|
||||
|
||||
public static let nextTrack = PlayBackCommand(id: "NextTrackCommand", commandKeyPath: \MPRemoteCommandCenter.nextTrackCommand, handlerKeyPath: \RemoteCommandController.handleNextTrackCommand)
|
||||
|
||||
public static let previousTrack = PlayBackCommand(id: "PreviousTrack", commandKeyPath: \MPRemoteCommandCenter.previousTrackCommand, handlerKeyPath: \RemoteCommandController.handlePreviousTrackCommand)
|
||||
|
||||
|
||||
public typealias Command = MPRemoteCommand
|
||||
|
||||
public let id: String
|
||||
|
||||
public var commandKeyPath: KeyPath<MPRemoteCommandCenter, MPRemoteCommand>
|
||||
|
||||
public var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler>
|
||||
@@ -39,10 +46,12 @@ public struct BaseRemoteCommand: RemoteCommandProtocol {
|
||||
|
||||
public struct ChangePlaybackPositionCommand: RemoteCommandProtocol {
|
||||
|
||||
public static let changePlaybackPosition = ChangePlaybackPositionCommand(commandKeyPath: \MPRemoteCommandCenter.changePlaybackPositionCommand, handlerKeyPath: \RemoteCommandController.handleChangePlaybackPositionCommand)
|
||||
public static let changePlaybackPosition = ChangePlaybackPositionCommand(id: "ChangePlaybackPosition", commandKeyPath: \MPRemoteCommandCenter.changePlaybackPositionCommand, handlerKeyPath: \RemoteCommandController.handleChangePlaybackPositionCommand)
|
||||
|
||||
public typealias Command = MPChangePlaybackPositionCommand
|
||||
|
||||
public let id: String
|
||||
|
||||
public var commandKeyPath: KeyPath<MPRemoteCommandCenter, MPChangePlaybackPositionCommand>
|
||||
|
||||
public var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler>
|
||||
@@ -51,12 +60,14 @@ public struct ChangePlaybackPositionCommand: RemoteCommandProtocol {
|
||||
|
||||
public struct SkipIntervalCommand: RemoteCommandProtocol {
|
||||
|
||||
public static let skipForward = SkipIntervalCommand(commandKeyPath: \MPRemoteCommandCenter.skipForwardCommand, handlerKeyPath: \RemoteCommandController.handleSkipForwardCommand)
|
||||
public static let skipForward = SkipIntervalCommand(id: "SkipForward", commandKeyPath: \MPRemoteCommandCenter.skipForwardCommand, handlerKeyPath: \RemoteCommandController.handleSkipForwardCommand)
|
||||
|
||||
public static let skipBackward = SkipIntervalCommand(commandKeyPath: \MPRemoteCommandCenter.skipBackwardCommand, handlerKeyPath: \RemoteCommandController.handleSkipBackwardCommand)
|
||||
public static let skipBackward = SkipIntervalCommand(id: "SkipBackward", commandKeyPath: \MPRemoteCommandCenter.skipBackwardCommand, handlerKeyPath: \RemoteCommandController.handleSkipBackwardCommand)
|
||||
|
||||
public typealias Command = MPSkipIntervalCommand
|
||||
|
||||
public let id: String
|
||||
|
||||
public var commandKeyPath: KeyPath<MPRemoteCommandCenter, MPSkipIntervalCommand>
|
||||
|
||||
public var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler>
|
||||
@@ -78,6 +89,10 @@ public enum RemoteCommand {
|
||||
|
||||
case togglePlayPause
|
||||
|
||||
case next
|
||||
|
||||
case previous
|
||||
|
||||
case changePlaybackPosition
|
||||
|
||||
case skipForward(preferredIntervals: [NSNumber])
|
||||
@@ -94,6 +109,8 @@ public enum RemoteCommand {
|
||||
.pause,
|
||||
.stop,
|
||||
.togglePlayPause,
|
||||
.next,
|
||||
.previous,
|
||||
.changePlaybackPosition,
|
||||
.skipForward(preferredIntervals: []),
|
||||
.skipBackward(preferredIntervals: []),
|
||||
|
||||
@@ -18,6 +18,8 @@ public class RemoteCommandController {
|
||||
|
||||
weak var audioPlayer: AudioPlayer?
|
||||
|
||||
var commandTargetPointers: [String: Any] = [:]
|
||||
|
||||
init() {}
|
||||
|
||||
/**
|
||||
@@ -40,37 +42,38 @@ public class RemoteCommandController {
|
||||
|
||||
private func enableCommand<Command: RemoteCommandProtocol>(_ command: Command) {
|
||||
center[keyPath: command.commandKeyPath].isEnabled = true
|
||||
center[keyPath: command.commandKeyPath].addTarget(handler: self[keyPath: command.handlerKeyPath])
|
||||
commandTargetPointers[command.id] = center[keyPath: command.commandKeyPath].addTarget(handler: self[keyPath: command.handlerKeyPath])
|
||||
}
|
||||
|
||||
private func disableCommand<Command: RemoteCommandProtocol>(_ command: Command) {
|
||||
center[keyPath: command.commandKeyPath].isEnabled = false
|
||||
center[keyPath: command.commandKeyPath].removeTarget(self[keyPath: command.handlerKeyPath])
|
||||
center[keyPath: command.commandKeyPath].removeTarget(commandTargetPointers[command.id])
|
||||
commandTargetPointers.removeValue(forKey: command.id)
|
||||
}
|
||||
|
||||
private func enable(command: RemoteCommand) {
|
||||
switch command {
|
||||
case .play: self.enableCommand(BaseRemoteCommand.play)
|
||||
case .pause: self.enableCommand(BaseRemoteCommand.pause)
|
||||
case .stop: self.enableCommand(BaseRemoteCommand.stop)
|
||||
case .togglePlayPause: self.enableCommand(BaseRemoteCommand.togglePlayPause)
|
||||
case .play: self.enableCommand(PlayBackCommand.play)
|
||||
case .pause: self.enableCommand(PlayBackCommand.pause)
|
||||
case .stop: self.enableCommand(PlayBackCommand.stop)
|
||||
case .togglePlayPause: self.enableCommand(PlayBackCommand.togglePlayPause)
|
||||
case .next: self.enableCommand(PlayBackCommand.nextTrack)
|
||||
case .previous: self.enableCommand(PlayBackCommand.previousTrack)
|
||||
case .changePlaybackPosition: self.enableCommand(ChangePlaybackPositionCommand.changePlaybackPosition)
|
||||
|
||||
case .skipForward(let preferredIntervals):
|
||||
self.enableCommand(SkipIntervalCommand.skipForward.set(preferredIntervals: preferredIntervals))
|
||||
|
||||
case .skipBackward(let preferredIntervals):
|
||||
self.enableCommand(SkipIntervalCommand.skipBackward.set(preferredIntervals: preferredIntervals))
|
||||
case .skipForward(let preferredIntervals): self.enableCommand(SkipIntervalCommand.skipForward.set(preferredIntervals: preferredIntervals))
|
||||
case .skipBackward(let preferredIntervals): self.enableCommand(SkipIntervalCommand.skipBackward.set(preferredIntervals: preferredIntervals))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private func disable(command: RemoteCommand) {
|
||||
switch command {
|
||||
case .play: self.disableCommand(BaseRemoteCommand.play)
|
||||
case .pause: self.disableCommand(BaseRemoteCommand.pause)
|
||||
case .stop: self.disableCommand(BaseRemoteCommand.stop)
|
||||
case .togglePlayPause: self.disableCommand(BaseRemoteCommand.togglePlayPause)
|
||||
case .play: self.disableCommand(PlayBackCommand.play)
|
||||
case .pause: self.disableCommand(PlayBackCommand.pause)
|
||||
case .stop: self.disableCommand(PlayBackCommand.stop)
|
||||
case .togglePlayPause: self.disableCommand(PlayBackCommand.togglePlayPause)
|
||||
case .next: self.disableCommand(PlayBackCommand.nextTrack)
|
||||
case .previous: self.disableCommand(PlayBackCommand.previousTrack)
|
||||
case .changePlaybackPosition: self.disableCommand(ChangePlaybackPositionCommand.changePlaybackPosition)
|
||||
case .skipForward(_): self.disableCommand(SkipIntervalCommand.skipForward)
|
||||
case .skipBackward(_): self.disableCommand(SkipIntervalCommand.skipBackward)
|
||||
@@ -80,49 +83,64 @@ public class RemoteCommandController {
|
||||
// MARK: - Handlers
|
||||
|
||||
lazy var handlePlayCommand: RemoteCommandHandler = { (event) in
|
||||
do {
|
||||
try self.audioPlayer?.play()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
do {
|
||||
try audioPlayer.play()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
lazy var handlePauseCommand: RemoteCommandHandler = { (event) in
|
||||
do {
|
||||
try self.audioPlayer?.pause()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
do {
|
||||
try audioPlayer.pause()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
lazy var handleStopCommand: RemoteCommandHandler = { (event) in
|
||||
self.audioPlayer?.stop()
|
||||
return .success
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
audioPlayer.stop()
|
||||
return .success
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = { (event) in
|
||||
do {
|
||||
try self.audioPlayer?.togglePlaying()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
if let audioPlayer = self.audioPlayer {
|
||||
do {
|
||||
try audioPlayer.togglePlaying()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
lazy var handleSkipForwardCommand: RemoteCommandHandler = { (event) in
|
||||
if let command = event.command as? MPSkipIntervalCommand,
|
||||
let interval = command.preferredIntervals.first,
|
||||
let audioPlayer = self.audioPlayer {
|
||||
try? audioPlayer.seek(to: audioPlayer.currentTime + Double(truncating: interval))
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
do {
|
||||
try audioPlayer.seek(to: audioPlayer.currentTime + Double(truncating: interval))
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
}
|
||||
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
@@ -130,17 +148,48 @@ public class RemoteCommandController {
|
||||
if let command = event.command as? MPSkipIntervalCommand,
|
||||
let interval = command.preferredIntervals.first,
|
||||
let audioPlayer = self.audioPlayer {
|
||||
try? audioPlayer.seek(to: audioPlayer.currentTime - Double(truncating: interval))
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
do {
|
||||
try audioPlayer.seek(to: audioPlayer.currentTime - Double(truncating: interval))
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
}
|
||||
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = { (event) in
|
||||
if let event = event as? MPChangePlaybackPositionCommandEvent {
|
||||
if let event = event as? MPChangePlaybackPositionCommandEvent,
|
||||
let audioPlayer = self.audioPlayer {
|
||||
do {
|
||||
try self.audioPlayer?.seek(to: event.positionTime)
|
||||
try audioPlayer.seek(to: event.positionTime)
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
lazy var handleNextTrackCommand: RemoteCommandHandler = { (event) in
|
||||
if let player = self.audioPlayer as? QueuedAudioPlayer {
|
||||
do {
|
||||
try player.next()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
return self.getRemoteCommandHandlerStatus(forError: error)
|
||||
}
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
lazy var handlePreviousTrackCommand: RemoteCommandHandler = { (event) in
|
||||
if let player = self.audioPlayer as? QueuedAudioPlayer {
|
||||
do {
|
||||
try player.previous()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
}
|
||||
catch let error {
|
||||
@@ -157,8 +206,19 @@ public class RemoteCommandController {
|
||||
return MPRemoteCommandHandlerStatus.noActionableNowPlayingItem
|
||||
}
|
||||
}
|
||||
else if let error = error as? APError.LoadError {
|
||||
switch error {
|
||||
case .invalidSourceUrl(_):
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
}
|
||||
else if let error = error as? APError.QueueError {
|
||||
switch error {
|
||||
case .noNextItem, .noPreviousItem, .invalidIndex(_, _):
|
||||
return MPRemoteCommandHandlerStatus.noSuchContent
|
||||
}
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.commandFailed
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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