Compare commits

..

27 Commits

Author SHA1 Message Date
jorgenhenrichsen 1bf9d695d8 Merge pull request #12 from jorgenhenrichsen/dev
Dev
2018-07-29 15:21:27 +02:00
Jørgen Henrichsen 8edc3b0e75 Update podspec
Bump version
2018-07-29 15:14:40 +02:00
jorgenhenrichsen 22c780adba Merge pull request #11 from jorgenhenrichsen/master
Merge pull request #7 from jorgenhenrichsen/dev
2018-07-29 15:10:31 +02:00
jorgenhenrichsen c8805d55fd Merge pull request #10 from jorgenhenrichsen/fix/remote-command-handlers
Pass in the correct value in removeTarget(_:) when disabling remote c…
2018-07-29 15:07:56 +02:00
Jørgen Henrichsen a6e74efa37 Pass in the correct value in removeTarget(_:) when disabling remote commands.
Use the actual pointer returned from addTarget(handler:).
https://developer.apple.com/documentation/mediaplayer/mpremotecommand/1622910-addtarget
2018-07-29 14:33:26 +02:00
Jørgen Henrichsen 7d525e3129 Made initializer of AudioPlayer internal.
AudioPlayer is not intended to be used. SimpleAudioPlayer and QueuedAudioPLayer now have public inits.
2018-07-29 10:17:18 +02:00
Jørgen Henrichsen 39d8c55743 Removed delegate test.
The duration of the item did not load without a player.
2018-07-29 09:53:53 +02:00
Jørgen Henrichsen d56d4c699e Added some more testing to the QueueManager and AVPlayerWrapper 2018-07-29 01:10:30 +02:00
Jørgen Henrichsen 04ae9e7986 Made and AVplayerItemObserver
Observes the state of an AVPlayerItem. Just duration for now.
Observing the duration is usefull for updating the UI.
2018-07-29 01:09:08 +02:00
Jørgen Henrichsen 30503f0ffe Use guard statements for error checking 2018-07-28 18:24:16 +02:00
jorgenhenrichsen ba82a46f8b Merge pull request #8 from jorgenhenrichsen/fix/now-playing-rate
The rate is set correctly.
2018-07-28 18:22:28 +02:00
Jørgen Henrichsen 3eef6bf59d Set state to loading on source loading 2018-07-28 18:10:34 +02:00
Jørgen Henrichsen ea9f632eb6 Added a testcase for AVPlayerObserver 2018-07-28 16:13:25 +02:00
Jørgen Henrichsen 53aa56348e Documentation for QueuedAudioPlayer. 2018-03-31 18:16:44 +02:00
Jørgen Henrichsen 26c1d2875e Less updates to the AudioPlayerDelegate.
Only update the AudioPlayerDelegate when the state actually changes.
2018-03-25 22:16:39 +02:00
jorgenhenrichsen b2972063dd Merge pull request #7 from jorgenhenrichsen/dev
Dev
2018-03-25 18:46:23 +02:00
jorgenhenrichsen fd232f48ca Update podspec
Bump version
2018-03-25 18:36:51 +02:00
jorgenhenrichsen 6ed80f4ff4 Merge pull request #6 from jorgenhenrichsen/feature/queue
Queue
2018-03-25 18:35:33 +02:00
Jørgen Henrichsen f3b55b9a3b Update README
Added bit about the queue
2018-03-25 18:20:12 +02:00
Jørgen Henrichsen 460908e180 Updated example app
Added title, artist, image, timelabels and a queue.
2018-03-25 17:58:56 +02:00
Jørgen Henrichsen 8587b161b2 Added getters for next and previous items. 2018-03-25 17:58:45 +02:00
Jørgen Henrichsen f149ea09ff Moved APError to seperate file 2018-03-25 16:04:49 +02:00
Jørgen Henrichsen efc04395f9 More functions for the queuemanager and QueuedAudioPlayer 2018-03-25 16:03:24 +02:00
Jørgen Henrichsen 65bba1eb75 Next, and previous methods. 2018-03-24 17:44:57 +01:00
Jørgen Henrichsen 876ed22967 Do not remove current item on item complete. 2018-03-24 17:42:48 +01:00
Jørgen Henrichsen b4953f0a73 QueueManager
Two subclasses of AudioPlayer. Simple and Queued.
2018-03-24 16:53:11 +01:00
Jørgen Henrichsen 5ede0f8364 The rate is set correctly.
The rate was set to 1.0 to early, resulting in unsynced elapsed time in the now playing infocenter.
2018-03-22 14:34:36 +01:00
28 changed files with 1506 additions and 133 deletions
+20
View File
@@ -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,11 @@
/* 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 */; };
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 +20,8 @@
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 */; };
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 +42,18 @@
/* 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>"; };
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>"; };
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>"; };
@@ -106,7 +120,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 */,
@@ -131,6 +150,8 @@
074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */,
074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */,
074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */,
0775575820668B020002C6A1 /* QueueManagerTests.swift */,
078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */,
07732650205EACA300C4D1CD /* WAV-MP3.wav */,
07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */,
607FACE91AFB9204008FA782 /* Supporting Files */,
@@ -275,6 +296,7 @@
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */,
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */,
07732655205ECE1C00C4D1CD /* nasa_throttle_up.mp3 in Resources */,
070713102067F40A00F789B3 /* QueueTableViewCell.xib in Resources */,
07732654205ECA8B00C4D1CD /* WAV-MP3.wav in Resources */,
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */,
);
@@ -403,7 +425,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0707130B2067F2E000F789B3 /* QueueViewController.swift in Sources */,
070713072067EB4F00F789B3 /* Double + Extensions.swift in Sources */,
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */,
0707130F2067F40A00F789B3 /* QueueTableViewCell.swift in Sources */,
070713092067EFFB00F789B3 /* AudioController.swift in Sources */,
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -412,7 +438,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0775575920668B020002C6A1 /* QueueManagerTests.swift in Sources */,
074A6483205C155E0083D868 /* AVPlayerTimeObserverTests.swift in Sources */,
078C908F210D263200555E80 /* AVPlayerItemObserverTests.swift in Sources */,
074A6485205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift in Sources */,
607FACEC1AFB9204008FA782 /* AVPlayerObserverTests.swift in Sources */,
074A6487205E59B60083D868 /* AVPlayerWrapperTests.swift in Sources */,
@@ -40,7 +40,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
codeCoverageEnabled = "YES"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
@@ -70,7 +70,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
+39
View File
@@ -0,0 +1,39 @@
//
// 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,
.skipForward(preferredIntervals: [30]),
.skipBackward(preferredIntervals: [30]),
.changePlaybackPosition
]
try? audioSessionController.set(category: .playback)
try? audioSessionController.activateSession()
try? player.add(items: sources, playWhenReady: false)
}
}
+144 -35
View File
@@ -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) ?? ""
}
}
+4
View File
@@ -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)
}
}
+55
View File
@@ -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
}
}
}
+52 -41
View File
@@ -16,43 +16,30 @@ 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()
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 +47,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 +93,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 +106,12 @@ extension ViewController: AudioPlayerDelegate {
isScrubbing = false
}
func audioPlayer(didUpdateDuration duration: Double) {
slider.maximumValue = Float(controller.player.duration)
slider.setValue(Float(controller.player.currentTime), animated: true)
elapsedTimeLabel.text = controller.player.currentTime.secondsToString()
remainingTimeLabel.text = (controller.player.duration - controller.player.currentTime).secondsToString()
}
}
@@ -0,0 +1,58 @@
import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
let source = Bundle.main.path(forResource: "WAV-MP3", ofType: "wav")!
class AVPlayerItemObserverTests: QuickSpec {
override func spec() {
describe("An AVPlayerItemObserver") {
var observer: AVPlayerItemObserver!
beforeEach {
observer = AVPlayerItemObserver()
}
describe("observed item", {
context("when observing", {
var item: AVPlayerItem!
beforeEach {
item = AVPlayerItem(url: URL(fileURLWithPath: source))
observer.startObserving(item: item)
}
it("should exist", closure: {
expect(observer.observingItem).toEventuallyNot(beNil())
})
})
})
describe("observing status", {
it("should not be observing", closure: {
expect(observer.isObserving).toEventuallyNot(beTrue())
})
context("when observing", {
var item: AVPlayerItem!
beforeEach {
item = AVPlayerItem(url: URL(fileURLWithPath: source))
observer.startObserving(item: item)
}
it("should be observing", closure: {
expect(observer.isObserving).toEventually(beTrue())
})
})
})
}
}
}
class AVPlayerItemObserverDelegateHolder: AVPlayerItemObserverDelegate {
var updateDuration: ((_ duration: Double) -> Void)?
func item(didUpdateDuration duration: Double) {
updateDuration?(duration)
}
}
+10
View File
@@ -43,6 +43,16 @@ class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
expect(self.timeControlStatus).toEventuallyNot(beNil())
})
})
context("when observing again", {
beforeEach {
observer.startObserving()
}
it("should be observing", closure: {
expect(observer.isObserving).toEventually(beTrue())
})
})
})
}
+61 -8
View File
@@ -22,23 +22,33 @@ class AVPlayerWrapperTests: QuickSpec {
wrapper.volume = 0.0
}
describe("its state", {
context("when doing nothing", {
it("should be idle", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
})
describe("state", {
it("should be idle", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
})
context("when loading a source", {
beforeEach {
try? wrapper.load(fromFilePath: source, playWhenReady: false)
}
it("should be loading", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.loading))
})
it("should eventually be ready", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.ready))
})
})
context("when playing with no source", {
beforeEach {
try? wrapper.play()
}
it("should be idle", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
})
})
context("when playing a source", {
beforeEach {
@@ -68,7 +78,22 @@ class AVPlayerWrapperTests: QuickSpec {
it("should eventually be paused", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.paused))
})
})
context("when toggling the source from play", {
let holder = AudioPlayerDelegateHolder()
beforeEach {
wrapper.delegate = holder
holder.stateUpdate = { (state) in
if state == .playing {
try? wrapper.togglePlaying()
}
}
try? wrapper.load(fromFilePath: source, playWhenReady: true)
}
it("should eventually be playing", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.paused))
})
})
context("when stopping the source", {
@@ -95,7 +120,30 @@ class AVPlayerWrapperTests: QuickSpec {
})
})
context("when seeking before loading", {
beforeEach {
try? wrapper.seek(to: 10)
}
it("should be idle", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
})
})
})
describe("its duration", {
it("should be 0", closure: {
expect(wrapper.duration).to(equal(0))
})
context("when loading source", {
beforeEach {
try? wrapper.load(fromFilePath: source, playWhenReady: false)
}
it("should eventually not be 0", closure: {
expect(wrapper.duration).toEventuallyNot(equal(0))
})
})
})
}
@@ -109,6 +157,7 @@ class AudioPlayerDelegateHolder: AVPlayerWrapperDelegate {
var state: AVPlayerWrapperState? {
didSet {
print(state)
if let state = state {
self.stateUpdate?(state)
}
@@ -138,4 +187,8 @@ class AudioPlayerDelegateHolder: AVPlayerWrapperDelegate {
}
func AVWrapper(didUpdateDuration duration: Double) {
}
}
+349
View File
@@ -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.count).to(equal(1))
})
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))
})
})
})
}
}
}
+23 -5
View File
@@ -9,6 +9,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 +28,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 +52,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
@@ -85,9 +106,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
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudio'
s.version = '0.2.1'
s.version = '0.3.1'
s.summary = 'Easy audio streaming for iOS'
# This description is used to generate tags and improve search results.
+27
View File
@@ -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)
}
}
+3
View File
@@ -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)
}
}
+27 -14
View File
@@ -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)
@@ -260,15 +268,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 +298,8 @@ extension AudioPlayer: AVPlayerWrapperDelegate {
self.delegate?.audioPlayer(seekTo: seconds, didFinish: didFinish)
}
func AVWrapper(didUpdateDuration duration: Double) {
self.delegate?.audioPlayer(didUpdateDuration: duration)
}
}
@@ -0,0 +1,82 @@
//
// AVPlayerItemObserver.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 28/07/2018.
//
import Foundation
import AVFoundation
protocol AVPlayerItemObserverDelegate: class {
/**
Called when the observed item updates the duration.
*/
func item(didUpdateDuration duration: Double)
}
/**
Observing an AVPlayers status changes.
*/
class AVPlayerItemObserver: NSObject {
private static var context = 0
private let main: DispatchQueue = .main
private struct AVPlayerItemKeyPath {
static let duration = #keyPath(AVPlayerItem.duration)
}
var isObserving: Bool = false
weak var observingItem: AVPlayerItem?
weak var delegate: AVPlayerItemObserverDelegate?
deinit {
if self.isObserving {
stopObservingCurrentItem()
}
}
/**
Start observing an item. Will remove self as observer from old item.
- parameter item: The player item to observe.
*/
func startObserving(item: AVPlayerItem) {
main.async {
if self.isObserving {
self.stopObservingCurrentItem()
}
self.isObserving = true
self.observingItem = item
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
}
}
private func stopObservingCurrentItem() {
observingItem?.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
self.isObserving = false
self.observingItem = nil
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard context == &AVPlayerItemObserver.context, let observedKeyPath = keyPath else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
switch observedKeyPath {
case AVPlayerItemKeyPath.duration:
if let duration = change?[.newKey] as? CMTime {
self.delegate?.item(didUpdateDuration: duration.seconds)
}
default:
break
}
}
}
+167
View File
@@ -0,0 +1,167 @@
//
// 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]? {
return Array(_items[_currentIndex + 1..<items.count])
}
public var previousItems: [T] {
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)
}
}
+140
View File
@@ -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 -7
View File
@@ -14,23 +14,26 @@ 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 static let play = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.playCommand, handlerKeyPath: \RemoteCommandController.handlePlayCommand)
public static let play = BaseRemoteCommand(id: "Play", commandKeyPath: \MPRemoteCommandCenter.playCommand, handlerKeyPath: \RemoteCommandController.handlePlayCommand)
public static let pause = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.pauseCommand, handlerKeyPath: \RemoteCommandController.handlePauseCommand)
public static let pause = BaseRemoteCommand(id: "Pause", commandKeyPath: \MPRemoteCommandCenter.pauseCommand, handlerKeyPath: \RemoteCommandController.handlePauseCommand)
public static let stop = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.stopCommand, handlerKeyPath: \RemoteCommandController.handleStopCommand)
public static let stop = BaseRemoteCommand(id: "Stop", commandKeyPath: \MPRemoteCommandCenter.stopCommand, handlerKeyPath: \RemoteCommandController.handleStopCommand)
public static let togglePlayPause = BaseRemoteCommand(commandKeyPath: \MPRemoteCommandCenter.togglePlayPauseCommand, handlerKeyPath: \RemoteCommandController.handleTogglePlayPauseCommand)
public static let togglePlayPause = BaseRemoteCommand(id: "TogglePlayPause", commandKeyPath: \MPRemoteCommandCenter.togglePlayPauseCommand, handlerKeyPath: \RemoteCommandController.handleTogglePlayPauseCommand)
public typealias Command = MPRemoteCommand
public let id: String
public var commandKeyPath: KeyPath<MPRemoteCommandCenter, MPRemoteCommand>
public var handlerKeyPath: KeyPath<RemoteCommandController, RemoteCommandHandler>
@@ -39,10 +42,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 +56,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>
@@ -18,6 +18,8 @@ public class RemoteCommandController {
weak var audioPlayer: AudioPlayer?
var commandTargetPointers: [String: Any] = [:]
init() {}
/**
@@ -40,12 +42,13 @@ 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) {
@@ -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)
}
}