Compare commits

...

78 Commits

Author SHA1 Message Date
Jørgen Henrichsen 04296fa681 Update podspec. Update readme.
Bumpe versions to 0.5.0.
2018-11-18 23:42:40 +01:00
Jørgen Henrichsen 252ed947d2 Merge pull request #34 from jorgenhenrichsen/optional-time-pitch-algo
Optional time pitch algo
2018-11-18 23:35:11 +01:00
Jørgen Henrichsen b5fdb5c54e Merge branch 'master' into optional-time-pitch-algo 2018-11-18 22:59:00 +01:00
Jørgen Henrichsen 1b4c0b0d3b Update Readme. Update podspec.
Verision 0.4.4
2018-11-16 11:54:49 +01:00
Jørgen Henrichsen e0aa2a09a9 Merge pull request #35 from Alex601t/issue26
Fixes #26
2018-11-16 11:52:44 +01:00
Alexander ab0eb4f8eb Merge pull request #1 from jorgenhenrichsen/Alex601t-issue26
Observe loadedTimeRanges for AVPlayerItem
Use items duration.seconds if available.
2018-11-16 10:58:49 +03:00
Jørgen Henrichsen 99e7c65bbc Use items duration.seconds if available. 2018-11-15 20:35:07 +01:00
Jørgen Henrichsen 9072259631 Observe loadedTimeRanges for AVPlayerItem 2018-11-14 16:38:55 +01:00
Alexander eb9af1007a Issue 26:
Handling player current item duration from loaded time ranges
2018-11-14 16:34:58 +03:00
Jørgen Henrichsen 69d3a9c0c0 Test TimePitchAlgorithms in the AudioPlayer. 2018-11-14 10:06:14 +01:00
Jørgen Henrichsen 43821c68a9 Made DefaultAudioItem not conform to TimePitching.
New DefaultAudioItemTimePitching can be used instead.
2018-11-14 10:05:20 +01:00
Jørgen Henrichsen 610ff4c7f3 Made getter for wrapper internal.
Makes testing easier.
2018-11-14 10:04:12 +01:00
Jørgen Henrichsen 1fc533214f Update Readme.
- Remove timePitch algorithm from DefaultAudioItem inits
- Add audioTimePitchAlgorithm in the list of configurations
2018-11-09 13:50:53 +01:00
Jørgen Henrichsen 71f22c3e25 Time pitch algorithm is no longer required from AudioItems.
New protocol TimePitcher can be conformed to if AudioItems need to give their own Time Pitch Algorithm.
2018-11-09 13:45:48 +01:00
Jørgen Henrichsen 8a5e6d18cc Update Readme
Bump carthage install version
2018-11-08 23:13:02 +01:00
Jørgen Henrichsen a7f53bfec9 Update Readme. Update podspec.
Bump versions to 0.4.3
2018-11-08 23:11:49 +01:00
Jørgen Henrichsen 7b10f0476b Merge pull request #33 from jorgenhenrichsen/unsubscribe-observers
Unregister observer before removing AVPlayerItem.
2018-11-08 23:09:07 +01:00
Jørgen Henrichsen 618da75339 Unregister observer before removing AVPlayerItem.
Should fix a problem where the AVPlayerWrapper's item was deinitialized while observers where observing the item.
2018-11-08 17:59:24 +01:00
Jørgen Henrichsen 0694f5d8a0 Update Readme. Update podspec.
Bumping versions to 0.4.2
2018-11-06 22:41:13 +01:00
Jørgen Henrichsen 61268b45eb Merge pull request #32 from jorgenhenrichsen/queue-clearing
Queue functions
2018-11-06 22:40:07 +01:00
Jørgen Henrichsen 962e64fe24 Merge branch 'queue-clearing' of github.com:jorgenhenrichsen/SwiftAudio into queue-clearing 2018-11-06 22:31:05 +01:00
Jørgen Henrichsen 0f9b2656a1 Test remove upcoming/next items and clear queue on stop. 2018-11-06 22:30:15 +01:00
Jørgen Henrichsen 5e871fc7e2 Merge branch 'master' into queue-clearing 2018-11-06 21:30:27 +01:00
Jørgen Henrichsen fdf8fc9482 Added tests for removePreviousItems() 2018-11-06 21:27:07 +01:00
Jørgen Henrichsen a9eb713964 Remove upcoming items. Clear queue on reset 2018-11-04 15:21:24 +01:00
Jørgen Henrichsen a0efa5f408 Update README
Bump Carthage install release version
2018-11-02 19:26:03 +01:00
Jørgen Henrichsen 74cafd4c42 Merge pull request #31 from jorgenhenrichsen/feature/support-carthage
Feature/support carthage
2018-11-02 19:18:23 +01:00
Jørgen Henrichsen 53780ac03e Update Readme
Added info about installing via Carthage.
2018-11-02 18:24:31 +01:00
Jørgen Henrichsen ba438c8ede Set SwiftAudio scheme as shared. 2018-11-02 17:53:37 +01:00
Jørgen Henrichsen 06cb6577c1 Merge pull request #30 from jorgenhenrichsen/dev
Dev into master
2018-11-02 15:33:25 +01:00
Jørgen Henrichsen 5a0f379275 Update podspec. Update README.
- Bumped version to 0.4.0
- Updated the readme iwth new content and clarifications
2018-11-02 15:12:22 +01:00
Jørgen Henrichsen 11e793f963 Fixed typo. Added docs.
- Fixed type in the `timeEventFrequency`-property
- Added doc comment to `automaticallyWaitsToMinimizeStalling`
2018-11-02 15:11:01 +01:00
Jørgen Henrichsen d094067ac7 Use old syntax for AVAudioSession Options and Category 2018-11-02 13:54:07 +01:00
Jørgen Henrichsen 4bacd5f1ff Fixed problem in addItems(). Added more tests.
- QueueManager's addItems() incremented the currentIndex with the managers item count. Corrected to only increment by the added item count.
- Added tests for the QueueManager.
2018-11-02 13:13:50 +01:00
Jørgen Henrichsen 73089bbe8b Use AudioSession protocol instead of AVAudioSession.
- Makes the AudioSessionController easier to test.
- Updated tests
- Fixes a problem where the AudioSessionController tests would not succeed on device.
2018-11-02 08:01:15 +01:00
Jørgen Henrichsen b4f919e8f4 Merge branch 'master' into dev 2018-10-29 20:56:14 +01:00
Jørgen Henrichsen 7e33789644 Update README
Change install instructions to use 0.3.6
2018-10-29 19:10:19 +01:00
Jørgen Henrichsen 843ba9f450 Bumped version to 0.3.6 2018-10-29 18:53:26 +01:00
Jørgen Henrichsen 4305110867 Merge pull request #28 from dcvz/master
Add a Couple of Features and Fixes
2018-10-29 18:50:48 +01:00
David Chavez cd43ecc6f9 Update example and fix tests 2018-10-29 15:44:06 +01:00
David Chavez 274377af3f Address review comments 2018-10-29 14:21:09 +01:00
Jørgen Henrichsen ef41885eaf Merge pull request #29 from jorgenhenrichsen/SwiftAudio-1
Restructuring and simplifying
2018-10-28 22:54:06 +01:00
Jørgen Henrichsen 21da2da43b Update README
Added part about AudioPlayerDelegate and adding additional NowPlayingInfo
2018-10-28 22:35:02 +01:00
Jørgen Henrichsen 336d5586bf Added a couple of tests to the AVPlayerWrapper 2018-10-28 22:29:29 +01:00
Jørgen Henrichsen e917867220 Updated tests 2018-10-28 19:24:20 +01:00
Jørgen Henrichsen 7897a79a79 Added options for volume, muting and automaticallyWaitingToMinimizeStalling 2018-10-28 19:20:14 +01:00
David Chavez 43824a9700 Add granularity for a track finishing playback between simple/queued 2018-10-28 13:37:02 +01:00
Jørgen Henrichsen 460af7ab1e Added tests for the currentItem in the AudioPlayer 2018-10-28 12:44:52 +01:00
Jørgen Henrichsen 670bf0e09e Made enable(disable commands internal 2018-10-28 12:42:21 +01:00
Jørgen Henrichsen 238c02db12 Removed unneccessary comments.
Made the enable commands method private, as there is no need for it to be public.
2018-10-28 12:38:43 +01:00
Jørgen Henrichsen 20190152eb Remove the add(propert:) for adding new NowPlayingInfo properties.
Made the nowPlayingInfoController public.
2018-10-28 12:30:47 +01:00
David Chavez 9e6683674b Fix issues with updating index upon queue mutations 2018-10-28 12:02:45 +01:00
David Chavez b27332aafb Add a couple of features and fix some issues 2018-10-28 10:35:44 +01:00
Jørgen Henrichsen 18688ab766 QueuedAudioPlayer load(item:) will replace the current item in the queue. 2018-10-26 14:07:47 +02:00
Jørgen Henrichsen 6835738754 Update README 2018-10-26 13:23:07 +02:00
Jørgen Henrichsen 9f05915993 Removed testing code snippet breaking the AVPlayerWrapper 2018-10-26 13:12:38 +02:00
Jørgen Henrichsen 2f9a5481ff Removed warnings 2018-10-26 13:09:49 +02:00
Jørgen Henrichsen 079f8c8ce1 Update travis.yml
Change back to Xcode 9
2018-10-26 12:57:38 +02:00
Jørgen Henrichsen 720b4739b4 Update travis.yml
Roll back to iOS 11.4 simulator for testing, as there is an issue with loading duration on the player with iOS 12 simulators #26
2018-10-26 12:52:42 +02:00
Jørgen Henrichsen 2b150b5652 Merge branch 'SwiftAudio-1' of github.com:jorgenhenrichsen/SwiftAudio into SwiftAudio-1 2018-10-26 12:25:14 +02:00
Jørgen Henrichsen 4adc84aaf3 Moved files to correct folder. 2018-10-26 12:24:48 +02:00
Jørgen Henrichsen 76f2d22e7a Update travis.yml
Set SDK version to iOS 12.0
2018-10-26 12:18:40 +02:00
Jørgen Henrichsen 3c409200ee Update travis.yml
Change osx_image to Xcode 10.
Change iOS version to iOS 12.
2018-10-26 12:13:36 +02:00
Jørgen Henrichsen b44777fcd7 Changed method signature for loading items. 2018-10-26 11:28:55 +02:00
Jørgen Henrichsen d682fd9468 Removed outdated comment. 2018-10-26 11:22:30 +02:00
Jørgen Henrichsen 10e6c46c18 Updated AudioPlayer tests 2018-10-26 11:18:26 +02:00
Jørgen Henrichsen 89715d9d38 Made it possible to supply custom AVPlayer, NowPlayingInfoController and RemoteCommandController in the AudioPlayer init(). 2018-10-26 11:11:58 +02:00
Jørgen Henrichsen 99bd43769c Folder structure in the project 2018-10-26 11:00:15 +02:00
Jørgen Henrichsen ce5e5e886f Removed SimpleAudioPlayer, will use AudioPlayer from now on.
Removed the tests.
2018-10-26 10:57:31 +02:00
Jørgen Henrichsen 521141ba0d Moved the AVPlayerWrapperDelegate to a seperate file. 2018-10-26 10:54:08 +02:00
Jørgen Henrichsen d3d354d5bd Simplified AVPlayerWrapper.
Created a protocol for the wrapper to follow.
Removed config options that where just getters/setters to the underlying AVPlayer.
Made it possible to pass in custom AVPlayer that can be customized with these options.
Updated AVPlayerWrapperTests and added a couple tests for the rate property.
2018-10-26 10:50:55 +02:00
Jørgen Henrichsen 664c56b79c Update README
Add version to pod file instruction
2018-10-25 10:49:53 +02:00
Jørgen Henrichsen 7bb83e87e0 Merge pull request #25 from jorgenhenrichsen/dev
Update master
2018-10-23 10:12:46 +02:00
Jørgen Henrichsen 6cad96b4f8 Update podscpec
Bump version to 0.3.5
2018-10-22 14:20:40 +02:00
Jørgen Henrichsen 50a58c2306 Merge pull request #24 from jorgenhenrichsen/handle-remote-commands
Handle remote commands
2018-10-22 14:18:17 +02:00
Jørgen Henrichsen 2dbaf3d4dc Update Readme
Add description of how to override remote command handlers.
2018-10-22 10:11:04 +02:00
Jørgen Henrichsen 851d8704d9 Made remotecommandcontroller public. 2018-10-21 19:44:24 +02:00
Jørgen Henrichsen 7c4dc27868 Made it possible to override remote command handlers 2018-10-21 10:42:29 +02:00
34 changed files with 1184 additions and 689 deletions
+77 -40
View File
@@ -12,6 +12,16 @@
0262F9E0C329E749D1B3480EBF568E48 /* Quick-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 25DBE2FD6322DB92AF73F714DBCDAC39 /* Quick-dummy.m */; };
03D7EA5C17FCEB40D10C3C73F9472F4E /* NimbleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194F331624F24ECBAEF52731D29FAD3E /* NimbleEnvironment.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
04AA9A137A0D38056D1081B4B304CFB6 /* Predicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9753520784E15A0CF31F3233F37520 /* Predicate.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
075131AD2182FF1600D3BFB9 /* AVPlayerWrapperProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 075131AC2182FF1600D3BFB9 /* AVPlayerWrapperProtocol.swift */; };
075131AF21830D9500D3BFB9 /* AVPlayerWrapperDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 075131AE21830D9500D3BFB9 /* AVPlayerWrapperDelegate.swift */; };
075131B9218322E000D3BFB9 /* MediaItemProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 075131B3218322E000D3BFB9 /* MediaItemProperty.swift */; };
075131BA218322E000D3BFB9 /* NowPlayingInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 075131B4218322E000D3BFB9 /* NowPlayingInfoController.swift */; };
075131BB218322E000D3BFB9 /* NowPlayingInfoProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 075131B5218322E000D3BFB9 /* NowPlayingInfoProperty.swift */; };
075131BC218322E000D3BFB9 /* RemoteCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 075131B7218322E000D3BFB9 /* RemoteCommand.swift */; };
075131BD218322E000D3BFB9 /* RemoteCommandController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 075131B8218322E000D3BFB9 /* RemoteCommandController.swift */; };
07756B73218C2D590023935E /* AudioSessionCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07756B70218C2D590023935E /* AudioSessionCategory.swift */; };
07756B74218C2D590023935E /* AudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07756B71218C2D590023935E /* AudioSession.swift */; };
07756B75218C2D590023935E /* AudioSessionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07756B72218C2D590023935E /* AudioSessionController.swift */; };
089FC967E3D8DD6212509D9C6AC7185B /* CwlCatchException.m in Sources */ = {isa = PBXBuildFile; fileRef = 25045F70284E802FFFF0F6EFD40A01C7 /* CwlCatchException.m */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
08EA01BACA4194902667F1FF29DC6265 /* MatcherProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945BF3F9EA32D635660DD7902B1A3D03 /* MatcherProtocols.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
095BD8D416719E425189E8E4963A8D4E /* TimeEventFrequency.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF4534C7E47CA5B502A19513117CBFC3 /* TimeEventFrequency.swift */; };
@@ -25,12 +35,10 @@
15FA146C0756A295AE40D1FBD77ABB6B /* AllPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD70D6C69732CC5E9DE6DC55873B188 /* AllPass.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
19FB07979AE2C3531B7F249AA237B30D /* BeGreaterThanOrEqualTo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC94BD467B9F8E22E297008546EB659 /* BeGreaterThanOrEqualTo.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
1B1A26FD4363A19DEFED48BF5E5341BE /* NMBExceptionCapture.h in Headers */ = {isa = PBXBuildFile; fileRef = D73E015835BBDFB245780DE9E43ED9FE /* NMBExceptionCapture.h */; settings = {ATTRIBUTES = (Public, ); }; };
1CAA71BDD1BCB7A8EBD8A890526774C6 /* RemoteCommandController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 689190491F4225B281ABC68343A24D61 /* RemoteCommandController.swift */; };
1CFDB94DFF57F98BA7A40829DDB3CFC0 /* CwlPreconditionTesting.h in Headers */ = {isa = PBXBuildFile; fileRef = 20E6435A404A70F289B347C5815ABC10 /* CwlPreconditionTesting.h */; settings = {ATTRIBUTES = (Public, ); }; };
1D230EC73A17C8494F279447636B6D51 /* Nimble.h in Headers */ = {isa = PBXBuildFile; fileRef = 572C9B621320D273AC417E7C04066AEA /* Nimble.h */; settings = {ATTRIBUTES = (Public, ); }; };
1DC86A0AEBBA8A5B9353C4F7B2D4649A /* SwiftAudio-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = B6541C48548E76AC573D95227051A3E8 /* SwiftAudio-dummy.m */; };
1F626496F8B9ACBEA1C48D85D371790E /* ExampleMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D0BC856D7B86412A484A3DD582F9841 /* ExampleMetadata.swift */; };
213745669894F37CD02BED396AC2EFFA /* AudioSessionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 006367118F9218EE3420DCEF343E7B4C /* AudioSessionController.swift */; };
2722196E68C16CB6AE1C739A6632DDDD /* ToSucceed.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08DE2F262516A80F255EE2310F0EDD0 /* ToSucceed.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
275B03F03D701555A341DEFDA586D3F2 /* Nimble-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ADDE4EA63584E9BF3933026B32B66E2 /* Nimble-dummy.m */; };
2A561B51439F9724CF97415032FFB649 /* NMBObjCMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDAE77638703F05830D6AF66FBCAE75 /* NMBObjCMatcher.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
@@ -79,17 +87,13 @@
838A64480DA58965581A1750F3FED016 /* World+DSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB337D9B97833B31DBEAB7D4FB2AE0E /* World+DSL.swift */; };
83C035F188A4F80F9696E6AD261DF00B /* Pods-SwiftAudio_Tests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 3350F0F9B5DDF05AE230A91E5FD38686 /* Pods-SwiftAudio_Tests-dummy.m */; };
83F282C7E57F327D439604EB838FAA92 /* URL+FileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F09E00015A51CF7CCAC20DF7144AE44 /* URL+FileName.swift */; };
8A77D5CE0D8380C1FD2FE859015C7DAA /* NowPlayingInfoProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4609750B9014770E50A7FF8F13C08BE9 /* NowPlayingInfoProperty.swift */; };
8AA1573A6E580F689A48C3237CC36268 /* Contain.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49C5B1205F3B26219A5F9471156353B /* Contain.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
8DECE3E28E934801E17DE5C0ACA24ABA /* RemoteCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C4339512935F0311EE3E2406CC855F /* RemoteCommand.swift */; };
8F69EDCC09739459EB61749494F1E002 /* CwlCatchException.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7DB1CD8C2C3735948E12DC35B0BED7 /* CwlCatchException.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
907EBF43A4A37DD92C61F0AEA6ED1330 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CF45228F79391196CE7D3A18D70519B /* Filter.swift */; };
9108613C28FC0D1A7948C6B4FE64932F /* Nimble-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = E4D833ED9179EEF01C24DCA76DD19097 /* Nimble-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
95ABBFEBA3AF22853E345BAAC8867FF3 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80C82283F763D9ABC7DD3BB91787CA8D /* XCTest.framework */; };
95C261DC185368D41443743CBA636141 /* APError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011EE192D3B1D2A686C8C5D93E3AB0D0 /* APError.swift */; };
9676A05220B174FC299049577162F1C8 /* MediaItemProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F815B046C467FFE21F47EEF4F76CBE2 /* MediaItemProperty.swift */; };
9AADA2573B8098C022E8A390155DFD90 /* Functional.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3EF3195721B0195371824114C2A4BEE /* Functional.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
9B94816B4EC1BBEC4E7971F04EA5192A /* SimpleAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C7830817928E0FBBAE48F19F730C2C /* SimpleAudioPlayer.swift */; };
9D321DC100B95C35B6FF4D01BFCD6C07 /* ThrowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292FBFB0070F2E0C4399EAD76B169D24 /* ThrowError.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
9DEECB22E5869BB2ECD155D53946F939 /* NimbleXCTestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA9613DD13D6685DDEE70B45F0ADFB6 /* NimbleXCTestHandler.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
A0871080516F7088835670597EB06674 /* CwlDarwinDefinitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF5CEB6ECB6C3C4793E3828927021A57 /* CwlDarwinDefinitions.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
@@ -123,7 +127,6 @@
C74928AC19FB27AA9D7AC4FC94A2D7F1 /* AVPlayerTimeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7481CD97B89126BA4A1C6FCC80366BD /* AVPlayerTimeObserver.swift */; };
CCEC9E4FCE42FF652C0563E4F6BB0459 /* Example.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54476EB21A4C89C927FF42E858D9E3F /* Example.swift */; };
D10E1F8FA9C618DFE1DE13BBF1A1B683 /* AudioItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C9AEEB071CD4A301C5C9CC7DB3CD15 /* AudioItem.swift */; };
D1CC79EBF2498C6E01C4234330E33DA4 /* NowPlayingInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E72935CCE60D79BE7ECCC2A6645B9A6 /* NowPlayingInfoController.swift */; };
D1F6601FD7B12B4DD24F04BB553F0B6D /* BeLessThan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70B5E02942CB72732D6947AFD0D263FA /* BeLessThan.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
D5CEB8D0B830BAE216347870F154079D /* DSL.m in Sources */ = {isa = PBXBuildFile; fileRef = 63BAD0DA551276454D9ACE719DEFCEB1 /* DSL.m */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
DCD90D9E9A79C53BC7CD0FA8FA02961D /* BeEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A6074FF9C29583B7D7FA989EDEDB550 /* BeEmpty.swift */; settings = {COMPILER_FLAGS = "-DPRODUCT_NAME=Nimble/Nimble"; }; };
@@ -175,23 +178,31 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
006367118F9218EE3420DCEF343E7B4C /* AudioSessionController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AudioSessionController.swift; path = SwiftAudio/Classes/AudioSessionController.swift; sourceTree = "<group>"; };
01056F9C4DEDD08119B7F98ECE8B2845 /* HaveCount.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HaveCount.swift; path = Sources/Nimble/Matchers/HaveCount.swift; sourceTree = "<group>"; };
011EE192D3B1D2A686C8C5D93E3AB0D0 /* APError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = APError.swift; path = SwiftAudio/Classes/APError.swift; sourceTree = "<group>"; };
01DCEA2B70C5FE3C749141DED60AA3A1 /* AVPlayerObserver.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AVPlayerObserver.swift; sourceTree = "<group>"; };
048A74934A0826237E15D12BBFA56C98 /* Quick-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Quick-umbrella.h"; sourceTree = "<group>"; };
049D65258659A381E53BD1E11B7345E8 /* BeVoid.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BeVoid.swift; path = Sources/Nimble/Matchers/BeVoid.swift; sourceTree = "<group>"; };
04C9AEEB071CD4A301C5C9CC7DB3CD15 /* AudioItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AudioItem.swift; path = SwiftAudio/Classes/AudioItem.swift; sourceTree = "<group>"; };
075131AC2182FF1600D3BFB9 /* AVPlayerWrapperProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerWrapperProtocol.swift; sourceTree = "<group>"; };
075131AE21830D9500D3BFB9 /* AVPlayerWrapperDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerWrapperDelegate.swift; sourceTree = "<group>"; };
075131B3218322E000D3BFB9 /* MediaItemProperty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaItemProperty.swift; sourceTree = "<group>"; };
075131B4218322E000D3BFB9 /* NowPlayingInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoController.swift; sourceTree = "<group>"; };
075131B5218322E000D3BFB9 /* NowPlayingInfoProperty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoProperty.swift; sourceTree = "<group>"; };
075131B7218322E000D3BFB9 /* RemoteCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteCommand.swift; sourceTree = "<group>"; };
075131B8218322E000D3BFB9 /* RemoteCommandController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteCommandController.swift; sourceTree = "<group>"; };
07756B70218C2D590023935E /* AudioSessionCategory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioSessionCategory.swift; sourceTree = "<group>"; };
07756B71218C2D590023935E /* AudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioSession.swift; sourceTree = "<group>"; };
07756B72218C2D590023935E /* AudioSessionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioSessionController.swift; sourceTree = "<group>"; };
0A362FBD9BA860D03408EC59B8DAD715 /* QueuedAudioPlayer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = QueuedAudioPlayer.swift; path = SwiftAudio/Classes/QueuedAudioPlayer.swift; sourceTree = "<group>"; };
0B2E2D47B69C1FB3AC152895C90935DE /* NMBStringify.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = NMBStringify.m; path = Sources/NimbleObjectiveC/NMBStringify.m; sourceTree = "<group>"; };
0B83D7847D2E148325682799F8D762FB /* AdapterProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AdapterProtocols.swift; path = Sources/Nimble/Adapters/AdapterProtocols.swift; sourceTree = "<group>"; };
0C8C8954498E96A380A9E019FA368062 /* Pods_SwiftAudio_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_SwiftAudio_Tests.framework; path = "Pods-SwiftAudio_Tests.framework"; sourceTree = BUILT_PRODUCTS_DIR; };
0C8C8954498E96A380A9E019FA368062 /* Pods_SwiftAudio_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftAudio_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
0CFE2240B90DBC44697F16996798ADC0 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0D9BE9AED5D0D4A233A5F28CDAF64E5F /* NMBExceptionCapture.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = NMBExceptionCapture.m; path = Sources/NimbleObjectiveC/NMBExceptionCapture.m; sourceTree = "<group>"; };
0E696C2BC0CF5F4035F7B6A9613C5608 /* BeAnInstanceOf.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BeAnInstanceOf.swift; path = Sources/Nimble/Matchers/BeAnInstanceOf.swift; sourceTree = "<group>"; };
0E72935CCE60D79BE7ECCC2A6645B9A6 /* NowPlayingInfoController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NowPlayingInfoController.swift; path = SwiftAudio/Classes/NowPlayingInfoController.swift; sourceTree = "<group>"; };
0F8B70224FF4BAA3EF856694DAEFEEEA /* BeGreaterThan.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BeGreaterThan.swift; path = Sources/Nimble/Matchers/BeGreaterThan.swift; sourceTree = "<group>"; };
1038701997A03A7D55AAAC5A3E1D42F8 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; path = README.md; sourceTree = "<group>"; };
1038701997A03A7D55AAAC5A3E1D42F8 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
1093B46C09355EB4CED5CA7DE7FDBFEE /* mach_excServer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = mach_excServer.h; path = Carthage/Checkouts/CwlPreconditionTesting/Sources/CwlMachBadInstructionHandler/mach_excServer.h; sourceTree = "<group>"; };
10D81A32DD08157E729CDA25855D4178 /* QCKDSL.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = QCKDSL.h; path = Sources/QuickObjectiveC/DSL/QCKDSL.h; sourceTree = "<group>"; };
160931E5B7C58FBCE27C6F77CD31065A /* Expectation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Expectation.swift; path = Sources/Nimble/Expectation.swift; sourceTree = "<group>"; };
@@ -211,7 +222,7 @@
2DEC6727EE6F37FE78F0B0879E3FC89C /* ExpectationMessage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpectationMessage.swift; path = Sources/Nimble/ExpectationMessage.swift; sourceTree = "<group>"; };
2EE57213362ECA66BB3B1C08B2DB0C57 /* CwlCatchBadInstruction.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CwlCatchBadInstruction.swift; path = Carthage/Checkouts/CwlPreconditionTesting/Sources/CwlPreconditionTesting/CwlCatchBadInstruction.swift; sourceTree = "<group>"; };
2F45FD38D2C696978A840807E542F9DA /* ExampleGroup.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExampleGroup.swift; path = Sources/Quick/ExampleGroup.swift; sourceTree = "<group>"; };
2FD31C5F451C5A3BD436B937CE169BAD /* SwiftAudio.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = SwiftAudio.framework; path = SwiftAudio.framework; sourceTree = BUILT_PRODUCTS_DIR; };
2FD31C5F451C5A3BD436B937CE169BAD /* SwiftAudio.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftAudio.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3350F0F9B5DDF05AE230A91E5FD38686 /* Pods-SwiftAudio_Tests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-SwiftAudio_Tests-dummy.m"; sourceTree = "<group>"; };
336E11B7F8854A9118FB070A9E34CF78 /* Await.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Await.swift; path = Sources/Nimble/Utils/Await.swift; sourceTree = "<group>"; };
354FCCFF316C6B11FFA6BB86B5B58D43 /* NSBundle+CurrentTestBundle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSBundle+CurrentTestBundle.swift"; path = "Sources/Quick/NSBundle+CurrentTestBundle.swift"; sourceTree = "<group>"; };
@@ -220,7 +231,6 @@
3D0BC856D7B86412A484A3DD582F9841 /* ExampleMetadata.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExampleMetadata.swift; path = Sources/Quick/ExampleMetadata.swift; sourceTree = "<group>"; };
3EB72EE755307D2502E70A6B529B73EC /* QuickSpec.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = QuickSpec.h; path = Sources/QuickObjectiveC/QuickSpec.h; sourceTree = "<group>"; };
42CE60C662D9A1382B4BF26914AC82D0 /* DSL.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DSL.h; path = Sources/NimbleObjectiveC/DSL.h; sourceTree = "<group>"; };
4609750B9014770E50A7FF8F13C08BE9 /* NowPlayingInfoProperty.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NowPlayingInfoProperty.swift; path = SwiftAudio/Classes/NowPlayingInfoProperty.swift; sourceTree = "<group>"; };
4760419BE7E8BC5915D5CFF14109A979 /* Pods-SwiftAudio_Tests-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudio_Tests-resources.sh"; sourceTree = "<group>"; };
48FB816DE4B6A4BFDD305F58A6472925 /* ThrowAssertion.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ThrowAssertion.swift; path = Sources/Nimble/Matchers/ThrowAssertion.swift; sourceTree = "<group>"; };
4ADDE4EA63584E9BF3933026B32B66E2 /* Nimble-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Nimble-dummy.m"; sourceTree = "<group>"; };
@@ -243,19 +253,18 @@
5F4FB27486313C6A081388FCCB02EC16 /* QuickSpecBase.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = QuickSpecBase.m; path = Sources/QuickSpecBase/QuickSpecBase.m; sourceTree = "<group>"; };
5FF5498ACD8246B3B1EC6CBFCAD4C8D7 /* Quick-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Quick-prefix.pch"; sourceTree = "<group>"; };
60B070F60A08FF5EF7CF3D1A69143000 /* NMBExpectation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NMBExpectation.swift; path = Sources/Nimble/Adapters/NMBExpectation.swift; sourceTree = "<group>"; };
60D239C6021277D481ED86E2E29B1670 /* SwiftAudio.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; lastKnownFileType = text; path = SwiftAudio.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
60D239C6021277D481ED86E2E29B1670 /* SwiftAudio.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; path = SwiftAudio.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
63BAD0DA551276454D9ACE719DEFCEB1 /* DSL.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DSL.m; path = Sources/NimbleObjectiveC/DSL.m; sourceTree = "<group>"; };
660D7D4C3B82FFD945DFA23D0C1C6A9E /* SatisfyAnyOf.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SatisfyAnyOf.swift; path = Sources/Nimble/Matchers/SatisfyAnyOf.swift; sourceTree = "<group>"; };
66323C82A0A7CB5FB54B5F774BEC72DC /* Quick.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Quick.framework; path = Quick.framework; sourceTree = BUILT_PRODUCTS_DIR; };
66323C82A0A7CB5FB54B5F774BEC72DC /* Quick.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Quick.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6708E82D864D86CC9A66E3A038B6FDB9 /* ExampleHooks.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExampleHooks.swift; path = Sources/Quick/Hooks/ExampleHooks.swift; sourceTree = "<group>"; };
689190491F4225B281ABC68343A24D61 /* RemoteCommandController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RemoteCommandController.swift; path = SwiftAudio/Classes/RemoteCommandController.swift; sourceTree = "<group>"; };
6CCBA4A43D738FAE7BC1910666153FD0 /* SwiftAudio.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SwiftAudio.xcconfig; sourceTree = "<group>"; };
6DEE0ABD257E7A562F3A8FAFE03E9FBC /* Pods-SwiftAudio_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-SwiftAudio_Tests.release.xcconfig"; sourceTree = "<group>"; };
6F09E00015A51CF7CCAC20DF7144AE44 /* URL+FileName.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "URL+FileName.swift"; path = "Sources/Quick/URL+FileName.swift"; sourceTree = "<group>"; };
70B5E02942CB72732D6947AFD0D263FA /* BeLessThan.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BeLessThan.swift; path = Sources/Nimble/Matchers/BeLessThan.swift; sourceTree = "<group>"; };
731667DB8FCC21C2D4FEAE6CC1FED184 /* Errors.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Errors.swift; path = Sources/Nimble/Utils/Errors.swift; sourceTree = "<group>"; };
74320E6F1A8A9105DC422E9C6DFE820C /* AssertionDispatcher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AssertionDispatcher.swift; path = Sources/Nimble/Adapters/AssertionDispatcher.swift; sourceTree = "<group>"; };
747BC4EDBF186DED5F6ECE69B29801A9 /* Nimble.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Nimble.framework; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; };
747BC4EDBF186DED5F6ECE69B29801A9 /* Nimble.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7AB337D9B97833B31DBEAB7D4FB2AE0E /* World+DSL.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "World+DSL.swift"; path = "Sources/Quick/DSL/World+DSL.swift"; sourceTree = "<group>"; };
7C47506434F8C86E58A02928CC65CC10 /* Pods-SwiftAudio_Tests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-SwiftAudio_Tests-acknowledgements.plist"; sourceTree = "<group>"; };
7CF92AE58937B3AD4BA6DA7134DA9F52 /* AudioPlayer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AudioPlayer.swift; path = SwiftAudio/Classes/AudioPlayer.swift; sourceTree = "<group>"; };
@@ -265,17 +274,15 @@
81B10C2CFD2B4D759CF041A45CF58FF2 /* CwlBadInstructionException.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CwlBadInstructionException.swift; path = Carthage/Checkouts/CwlPreconditionTesting/Sources/CwlPreconditionTesting/CwlBadInstructionException.swift; sourceTree = "<group>"; };
823F6A12075FF0A33C70898ECC8A145B /* NSString+C99ExtendedIdentifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSString+C99ExtendedIdentifier.swift"; path = "Sources/Quick/NSString+C99ExtendedIdentifier.swift"; sourceTree = "<group>"; };
8350374D2D2D105674580DABB85F37FF /* AVPlayerWrapper.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AVPlayerWrapper.swift; sourceTree = "<group>"; };
84A291A79F9C752A91C561C0AFB7B957 /* Pods_SwiftAudio_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_SwiftAudio_Example.framework; path = "Pods-SwiftAudio_Example.framework"; sourceTree = BUILT_PRODUCTS_DIR; };
84A291A79F9C752A91C561C0AFB7B957 /* Pods_SwiftAudio_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftAudio_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
85A12CC592518974E5AF8CC73C019039 /* Quick.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Quick.modulemap; sourceTree = "<group>"; };
879D4131C2851B0A039CD43D72FA9AF5 /* Stringers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Stringers.swift; path = Sources/Nimble/Utils/Stringers.swift; sourceTree = "<group>"; };
87C7830817928E0FBBAE48F19F730C2C /* SimpleAudioPlayer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SimpleAudioPlayer.swift; path = SwiftAudio/Classes/SimpleAudioPlayer.swift; sourceTree = "<group>"; };
8967DA4BBBE5E17E301D3C56BE293A95 /* QuickSpec.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = QuickSpec.m; path = Sources/QuickObjectiveC/QuickSpec.m; sourceTree = "<group>"; };
8CA0648396C81EDE0A9B7C711638FA8B /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8D87AE93CC56AA3DD5FA80EF8564C9C2 /* EndWith.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = EndWith.swift; path = Sources/Nimble/Matchers/EndWith.swift; sourceTree = "<group>"; };
8DA9613DD13D6685DDEE70B45F0ADFB6 /* NimbleXCTestHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NimbleXCTestHandler.swift; path = Sources/Nimble/Adapters/NimbleXCTestHandler.swift; sourceTree = "<group>"; };
8DBDFCD12FE89ED7977BAEC1F0EC7101 /* XCTestSuite+QuickTestSuiteBuilder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "XCTestSuite+QuickTestSuiteBuilder.m"; path = "Sources/QuickObjectiveC/XCTestSuite+QuickTestSuiteBuilder.m"; sourceTree = "<group>"; };
8F815B046C467FFE21F47EEF4F76CBE2 /* MediaItemProperty.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaItemProperty.swift; path = SwiftAudio/Classes/MediaItemProperty.swift; sourceTree = "<group>"; };
93A4A3777CF96A4AAC1D13BA6DCCEA73 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
93A4A3777CF96A4AAC1D13BA6DCCEA73 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
942F4368934964E307D71356A565E34F /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
945BF3F9EA32D635660DD7902B1A3D03 /* MatcherProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MatcherProtocols.swift; path = Sources/Nimble/Matchers/MatcherProtocols.swift; sourceTree = "<group>"; };
993D3D81BF52A44266E4840EF7D5D995 /* AssertionRecorder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AssertionRecorder.swift; path = Sources/Nimble/Adapters/AssertionRecorder.swift; sourceTree = "<group>"; };
@@ -286,7 +293,6 @@
9FC94BD467B9F8E22E297008546EB659 /* BeGreaterThanOrEqualTo.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BeGreaterThanOrEqualTo.swift; path = Sources/Nimble/Matchers/BeGreaterThanOrEqualTo.swift; sourceTree = "<group>"; };
A474607214D558C1FDF0E33F10A70CC2 /* PostNotification.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PostNotification.swift; path = Sources/Nimble/Matchers/PostNotification.swift; sourceTree = "<group>"; };
A54476EB21A4C89C927FF42E858D9E3F /* Example.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Example.swift; path = Sources/Quick/Example.swift; sourceTree = "<group>"; };
A9C4339512935F0311EE3E2406CC855F /* RemoteCommand.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RemoteCommand.swift; path = SwiftAudio/Classes/RemoteCommand.swift; sourceTree = "<group>"; };
AF2AA3686E2E5C992300580BE170EF0B /* Quick.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Quick.xcconfig; sourceTree = "<group>"; };
AFE4BE7D9282807F0AEB8591BD9031E6 /* Nimble.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Nimble.xcconfig; sourceTree = "<group>"; };
B110998EFA9DEB0A41CF492290C3CC9E /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -306,7 +312,7 @@
C965EB7D2ABC715652F428F0755CC37C /* Pods-SwiftAudio_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-SwiftAudio_Tests.debug.xcconfig"; sourceTree = "<group>"; };
CA2B83B52F99EEA836F6F0669A3B9B3A /* DSL.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DSL.swift; path = Sources/Quick/DSL/DSL.swift; sourceTree = "<group>"; };
CA4741D70CAC2E64FB39C437CE5CF9AF /* SwiftAudio.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SwiftAudio.modulemap; sourceTree = "<group>"; };
CD78111E631F1A9FBB37A70A9EF3B187 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; path = LICENSE; sourceTree = "<group>"; };
CD78111E631F1A9FBB37A70A9EF3B187 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
CFCDA081DA07D782D75DA9C33E476214 /* SourceLocation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SourceLocation.swift; path = Sources/Nimble/Utils/SourceLocation.swift; sourceTree = "<group>"; };
D0463AFE8D72243A6EB984085BE04A65 /* BeLessThanOrEqual.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BeLessThanOrEqual.swift; path = Sources/Nimble/Matchers/BeLessThanOrEqual.swift; sourceTree = "<group>"; };
D08DE2F262516A80F255EE2310F0EDD0 /* ToSucceed.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ToSucceed.swift; path = Sources/Nimble/Matchers/ToSucceed.swift; sourceTree = "<group>"; };
@@ -328,7 +334,7 @@
EA5E86ABAB681E9B91D7D7CD0FCD5EB2 /* Pods-SwiftAudio_Example-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudio_Example-frameworks.sh"; sourceTree = "<group>"; };
EDE8144940E39AB93895BF39F2E9FEBC /* Pods-SwiftAudio_Tests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-SwiftAudio_Tests-umbrella.h"; sourceTree = "<group>"; };
EE8BB96B10E6B8555DD8E78B0E39FEBE /* World.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = World.swift; path = Sources/Quick/World.swift; sourceTree = "<group>"; };
EED52EE491F66BC008AB993C58365EBE /* mach_excServer.c */ = {isa = PBXFileReference; includeInIndex = 1; name = mach_excServer.c; path = Carthage/Checkouts/CwlPreconditionTesting/Sources/CwlMachBadInstructionHandler/mach_excServer.c; sourceTree = "<group>"; };
EED52EE491F66BC008AB993C58365EBE /* mach_excServer.c */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.c; name = mach_excServer.c; path = Carthage/Checkouts/CwlPreconditionTesting/Sources/CwlMachBadInstructionHandler/mach_excServer.c; sourceTree = "<group>"; };
EF4534C7E47CA5B502A19513117CBFC3 /* TimeEventFrequency.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TimeEventFrequency.swift; path = SwiftAudio/Classes/TimeEventFrequency.swift; sourceTree = "<group>"; };
EF5CEB6ECB6C3C4793E3828927021A57 /* CwlDarwinDefinitions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CwlDarwinDefinitions.swift; path = Carthage/Checkouts/CwlPreconditionTesting/Sources/CwlPreconditionTesting/CwlDarwinDefinitions.swift; sourceTree = "<group>"; };
F031D2A41C0E2D94B334BC31754F2F09 /* AVPlayerWrapperState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AVPlayerWrapperState.swift; sourceTree = "<group>"; };
@@ -389,6 +395,38 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
075131B2218322E000D3BFB9 /* NowPlayingInfoController */ = {
isa = PBXGroup;
children = (
075131B3218322E000D3BFB9 /* MediaItemProperty.swift */,
075131B4218322E000D3BFB9 /* NowPlayingInfoController.swift */,
075131B5218322E000D3BFB9 /* NowPlayingInfoProperty.swift */,
);
name = NowPlayingInfoController;
path = SwiftAudio/Classes/NowPlayingInfoController;
sourceTree = "<group>";
};
075131B6218322E000D3BFB9 /* RemoteCommandController */ = {
isa = PBXGroup;
children = (
075131B7218322E000D3BFB9 /* RemoteCommand.swift */,
075131B8218322E000D3BFB9 /* RemoteCommandController.swift */,
);
name = RemoteCommandController;
path = SwiftAudio/Classes/RemoteCommandController;
sourceTree = "<group>";
};
07756B6F218C2D590023935E /* AudioSessionController */ = {
isa = PBXGroup;
children = (
07756B72218C2D590023935E /* AudioSessionController.swift */,
07756B70218C2D590023935E /* AudioSessionCategory.swift */,
07756B71218C2D590023935E /* AudioSession.swift */,
);
name = AudioSessionController;
path = SwiftAudio/Classes/AudioSessionController;
sourceTree = "<group>";
};
1E85F0F44277212282918D8D5DDB3588 /* iOS */ = {
isa = PBXGroup;
children = (
@@ -464,7 +502,6 @@
8DBDFCD12FE89ED7977BAEC1F0EC7101 /* XCTestSuite+QuickTestSuiteBuilder.m */,
3E8F7D7BA6F7BD122DBB624992079C91 /* Support Files */,
);
name = Quick;
path = Quick;
sourceTree = "<group>";
};
@@ -481,6 +518,8 @@
children = (
8350374D2D2D105674580DABB85F37FF /* AVPlayerWrapper.swift */,
F031D2A41C0E2D94B334BC31754F2F09 /* AVPlayerWrapperState.swift */,
075131AC2182FF1600D3BFB9 /* AVPlayerWrapperProtocol.swift */,
075131AE21830D9500D3BFB9 /* AVPlayerWrapperDelegate.swift */,
);
name = AVPlayerWrapper;
path = SwiftAudio/Classes/AVPlayerWrapper;
@@ -661,7 +700,6 @@
56AED7BB8EBC87A7394B1B4309775DC2 /* XCTestObservationCenter+Register.m */,
5F94F4B7D93544A44CDA76AFC8704CB7 /* Support Files */,
);
name = Nimble;
path = Nimble;
sourceTree = "<group>";
};
@@ -671,16 +709,12 @@
011EE192D3B1D2A686C8C5D93E3AB0D0 /* APError.swift */,
04C9AEEB071CD4A301C5C9CC7DB3CD15 /* AudioItem.swift */,
7CF92AE58937B3AD4BA6DA7134DA9F52 /* AudioPlayer.swift */,
006367118F9218EE3420DCEF343E7B4C /* AudioSessionController.swift */,
8F815B046C467FFE21F47EEF4F76CBE2 /* MediaItemProperty.swift */,
0E72935CCE60D79BE7ECCC2A6645B9A6 /* NowPlayingInfoController.swift */,
4609750B9014770E50A7FF8F13C08BE9 /* NowPlayingInfoProperty.swift */,
0A362FBD9BA860D03408EC59B8DAD715 /* QueuedAudioPlayer.swift */,
7D4763A422F83644D194E97981775831 /* QueueManager.swift */,
A9C4339512935F0311EE3E2406CC855F /* RemoteCommand.swift */,
689190491F4225B281ABC68343A24D61 /* RemoteCommandController.swift */,
87C7830817928E0FBBAE48F19F730C2C /* SimpleAudioPlayer.swift */,
EF4534C7E47CA5B502A19513117CBFC3 /* TimeEventFrequency.swift */,
07756B6F218C2D590023935E /* AudioSessionController */,
075131B2218322E000D3BFB9 /* NowPlayingInfoController */,
075131B6218322E000D3BFB9 /* RemoteCommandController */,
581526FDBE4DC5C1C3F143696A8DE42E /* AVPlayerWrapper */,
A6AF9F48F4B25035E227BFDAE847B384 /* Observer */,
5D68DC904FAAE9CFC1059C6B3F4BB87D /* Pod */,
@@ -907,24 +941,27 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
075131BD218322E000D3BFB9 /* RemoteCommandController.swift in Sources */,
95C261DC185368D41443743CBA636141 /* APError.swift in Sources */,
075131BB218322E000D3BFB9 /* NowPlayingInfoProperty.swift in Sources */,
D10E1F8FA9C618DFE1DE13BBF1A1B683 /* AudioItem.swift in Sources */,
42249CE127313A81E6DF9F492BC3FD60 /* AudioPlayer.swift in Sources */,
213745669894F37CD02BED396AC2EFFA /* AudioSessionController.swift in Sources */,
075131B9218322E000D3BFB9 /* MediaItemProperty.swift in Sources */,
07756B73218C2D590023935E /* AudioSessionCategory.swift in Sources */,
121F6F4C3C3FBD49FBFF3BC91205C11B /* AVPlayerItemNotificationObserver.swift in Sources */,
E10CAB33DDA27C101EF36E8923046511 /* AVPlayerItemObserver.swift in Sources */,
13AFC3FFA6B2175A542C1279345CAAA3 /* AVPlayerObserver.swift in Sources */,
C74928AC19FB27AA9D7AC4FC94A2D7F1 /* AVPlayerTimeObserver.swift in Sources */,
07756B74218C2D590023935E /* AudioSession.swift in Sources */,
315B8C7DDF47918E6F996F8AC45C6BB7 /* AVPlayerWrapper.swift in Sources */,
F462AF5848880C2C1791D0C7F943879A /* AVPlayerWrapperState.swift in Sources */,
9676A05220B174FC299049577162F1C8 /* MediaItemProperty.swift in Sources */,
D1CC79EBF2498C6E01C4234330E33DA4 /* NowPlayingInfoController.swift in Sources */,
8A77D5CE0D8380C1FD2FE859015C7DAA /* NowPlayingInfoProperty.swift in Sources */,
075131AD2182FF1600D3BFB9 /* AVPlayerWrapperProtocol.swift in Sources */,
075131BC218322E000D3BFB9 /* RemoteCommand.swift in Sources */,
36DAEF0CCEB4AEFA64DFAF8197FD5C68 /* QueuedAudioPlayer.swift in Sources */,
075131BA218322E000D3BFB9 /* NowPlayingInfoController.swift in Sources */,
47B306C3AA1AA9EB82EE2FDE5267B13C /* QueueManager.swift in Sources */,
8DECE3E28E934801E17DE5C0ACA24ABA /* RemoteCommand.swift in Sources */,
1CAA71BDD1BCB7A8EBD8A890526774C6 /* RemoteCommandController.swift in Sources */,
9B94816B4EC1BBEC4E7971F04EA5192A /* SimpleAudioPlayer.swift in Sources */,
07756B75218C2D590023935E /* AudioSessionController.swift in Sources */,
075131AF21830D9500D3BFB9 /* AVPlayerWrapperDelegate.swift in Sources */,
1DC86A0AEBBA8A5B9353C4F7B2D4649A /* SwiftAudio-dummy.m in Sources */,
095BD8D416719E425189E8E4963A8D4E /* TimeEventFrequency.swift in Sources */,
);
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0930"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1AB61EB02FDF0033DCB1F8416419F110"
BuildableName = "SwiftAudio.framework"
BlueprintName = "SwiftAudio"
ReferencedContainer = "container:Pods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1AB61EB02FDF0033DCB1F8416419F110"
BuildableName = "SwiftAudio.framework"
BlueprintName = "SwiftAudio"
ReferencedContainer = "container:Pods.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+14 -4
View File
@@ -29,8 +29,8 @@
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 */; };
07756B69218A4E870023935E /* AudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07756B68218A4E870023935E /* AudioSession.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 */; };
@@ -68,8 +68,8 @@
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>"; };
07756B68218A4E870023935E /* AudioSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSession.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; };
@@ -124,6 +124,14 @@
path = Source;
sourceTree = "<group>";
};
07756B67218A4E640023935E /* Mocks */ = {
isa = PBXGroup;
children = (
07756B68218A4E870023935E /* AudioSession.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
607FACC71AFB9204008FA782 = {
isa = PBXGroup;
children = (
@@ -175,6 +183,7 @@
607FACE81AFB9204008FA782 /* Tests */ = {
isa = PBXGroup;
children = (
07756B67218A4E640023935E /* Mocks */,
0708ED732116EE0100EB29BD /* AudioPlayerTests.swift */,
607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */,
074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */,
@@ -184,7 +193,6 @@
078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */,
0708ED6B2116DA4B00EB29BD /* AudioSessionControllerTests.swift */,
07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */,
07CC171A213E912A005F880E /* SimpleAudioPlayerTests.swift */,
0708ED712116E91300EB29BD /* Source */,
607FACE91AFB9204008FA782 /* Supporting Files */,
);
@@ -443,9 +451,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
07756B69218A4E870023935E /* AudioSession.swift in Sources */,
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 */,
@@ -600,6 +608,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
@@ -617,6 +626,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
@@ -1,5 +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/>
<dict>
<key>BuildSystemType</key>
<string>Original</string>
</dict>
</plist>
+5 -1
View File
@@ -13,7 +13,7 @@ import SwiftAudio
class AudioController {
static let shared = AudioController()
let player = QueuedAudioPlayer()
let player: QueuedAudioPlayer
let audioSessionController = AudioSessionController.shared
let sources: [AudioItem] = [
@@ -24,8 +24,12 @@ class AudioController {
]
init() {
let controller = RemoteCommandController()
player = QueuedAudioPlayer(remoteCommandController: controller)
player.remoteCommands = [
.stop,
.play,
.pause,
.togglePlayPause,
.next,
.previous,
+4 -4
View File
@@ -61,6 +61,10 @@ class ViewController: UIViewController {
}
extension ViewController: AudioPlayerDelegate {
func audioPlayer(itemPlaybackEndedWithReason reason: PlaybackEndedReason) {
}
func audioPlayer(playerDidChangeState state: AVPlayerWrapperState) {
playButton.setTitle(state == .playing ? "Pause" : "Play", for: .normal)
@@ -89,10 +93,6 @@ extension ViewController: AudioPlayerDelegate {
}
}
func audioPlayerItemDidComplete() {
}
func audioPlayer(secondsElapsed seconds: Double) {
if !isScrubbing {
slider.setValue(Float(seconds), animated: false)
+70 -68
View File
@@ -1,5 +1,6 @@
import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
@@ -13,51 +14,12 @@ class AVPlayerWrapperTests: QuickSpec {
var wrapper: AVPlayerWrapper!
beforeEach {
wrapper = AVPlayerWrapper()
wrapper.automaticallyWaitsToMinimizeStalling = false
let player = AVPlayer()
player.automaticallyWaitsToMinimizeStalling = false
player.volume = 0.0
wrapper = AVPlayerWrapper(avPlayer: player)
wrapper.bufferDuration = 0.0001
wrapper.volume = 0.0
}
context("when calling play() with no item", {
var err: APError.PlaybackError?
beforeEach {
do {
try wrapper.play()
}
catch {
if let error = error as? APError.PlaybackError {
err = error
}
}
}
it("should throw a noItemLoaded error", closure: {
expect(err).toNot(beNil())
expect(err).to(equal(APError.PlaybackError.noLoadedItem))
})
})
context("when calling pause() with no item", {
var err: APError.PlaybackError?
beforeEach {
do {
try wrapper.pause()
}
catch {
if let error = error as? APError.PlaybackError {
err = error
}
}
}
it("should throw a noItemLoaded error", closure: {
expect(err).toNot(beNil())
expect(err).to(equal(APError.PlaybackError.noLoadedItem))
})
})
describe("its state", {
it("should be idle", closure: {
@@ -66,7 +28,7 @@ class AVPlayerWrapperTests: QuickSpec {
context("when loading a source", {
beforeEach {
try? wrapper.load(fromFilePath: Source.path, playWhenReady: false)
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: false)
}
it("should be loading", closure: {
@@ -80,7 +42,7 @@ class AVPlayerWrapperTests: QuickSpec {
context("when playing with no source", {
beforeEach {
try? wrapper.play()
wrapper.play()
}
it("should be idle", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
@@ -89,7 +51,7 @@ class AVPlayerWrapperTests: QuickSpec {
context("when playing a source", {
beforeEach {
try? wrapper.load(fromFilePath: Source.path, playWhenReady: true)
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: true)
}
it("should eventually be playing", closure: {
@@ -106,10 +68,10 @@ class AVPlayerWrapperTests: QuickSpec {
wrapper.delegate = holder
holder.stateUpdate = { (state) in
if state == .playing {
try? wrapper.pause()
wrapper.pause()
}
}
try? wrapper.load(fromFilePath: Source.path, playWhenReady: true)
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: true)
}
it("should eventually be paused", closure: {
@@ -123,10 +85,10 @@ class AVPlayerWrapperTests: QuickSpec {
wrapper.delegate = holder
holder.stateUpdate = { (state) in
if state == .playing {
try? wrapper.togglePlaying()
wrapper.togglePlaying()
}
}
try? wrapper.load(fromFilePath: Source.path, playWhenReady: true)
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: true)
}
it("should eventually be paused", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.paused))
@@ -149,7 +111,7 @@ class AVPlayerWrapperTests: QuickSpec {
receivedIdleUpdate = true
}
}
try? wrapper.load(fromFilePath: Source.path, playWhenReady: true)
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: true)
}
it("should eventually be 'idle'", closure: {
@@ -160,7 +122,7 @@ class AVPlayerWrapperTests: QuickSpec {
context("when seeking before loading", {
beforeEach {
try? wrapper.seek(to: 10)
wrapper.seek(to: 10)
}
it("should be idle", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
@@ -175,7 +137,7 @@ class AVPlayerWrapperTests: QuickSpec {
context("when loading source", {
beforeEach {
try? wrapper.load(fromFilePath: Source.path, playWhenReady: false)
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: false)
}
it("should eventually not be 0", closure: {
expect(wrapper.duration).toEventuallyNot(equal(0))
@@ -189,20 +151,63 @@ class AVPlayerWrapperTests: QuickSpec {
})
context("when seeking to a time", {
var passed = false
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)
holder.seekCompletion = { passed = true }
wrapper.load(from: Source.url, playWhenReady: false)
wrapper.seek(to: seekTime)
}
it("should eventually be equal to the seeked time", closure: {
expect(wrapper.currentTime).toEventually(equal(seekTime))
expect(passed).toEventually(beTrue())
})
})
})
describe("its rate", {
it("should be 0", closure: {
expect(wrapper.rate).to(equal(0.0))
})
context("when playing a source", {
beforeEach {
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: true)
}
it("should eventually be 1.0", closure: {
expect(wrapper.rate).toEventually(equal(1.0))
})
})
})
describe("its automaticallyWaitsToMinimizeStalling option", {
it("should be false", closure: {
expect(wrapper.automaticallyWaitsToMinimizeStalling).to(beFalse())
})
context("when setting it to true", {
beforeEach {
wrapper.automaticallyWaitsToMinimizeStalling = true
}
it("should be true", closure: {
expect(wrapper.automaticallyWaitsToMinimizeStalling).to(beTrue())
})
})
})
describe("its timeEventFrequency", {
context("when updated", {
beforeEach {
wrapper.timeEventFrequency = .everyHalfSecond
}
it("should update the playerTimeObservers periodicObserverTimeInterval", closure: {
expect(wrapper.playerTimeObserver.periodicObserverTimeInterval).to(equal(TimeEventFrequency.everyHalfSecond.getTime()))
})
})
})
@@ -214,12 +219,12 @@ class AVPlayerWrapperTests: QuickSpec {
}
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
func AVWrapper(itemPlaybackDoneWithReason reason: PlaybackEndedReason) {
}
var state: AVPlayerWrapperState? {
didSet {
print(state)
if let state = state {
self.stateUpdate?(state)
}
@@ -233,10 +238,6 @@ class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
self.state = state
}
func AVWrapperItemDidComplete() {
}
func AVWrapper(secondsElapsed seconds: Double) {
}
@@ -245,8 +246,9 @@ class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
}
var seekCompletion: (() -> Void)?
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
seekCompletion?()
}
func AVWrapper(didUpdateDuration duration: Double) {
+64 -20
View File
@@ -1,5 +1,6 @@
import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
@@ -11,9 +12,9 @@ class AudioPlayerTests: QuickSpec {
beforeEach {
audioPlayer = AudioPlayer()
audioPlayer.automaticallyWaitsToMinimizeStalling = false
audioPlayer.bufferDuration = 0.0001
audioPlayer.volume = 0
audioPlayer.automaticallyWaitsToMinimizeStalling = false
audioPlayer.volume = 0.0
}
describe("its state", {
@@ -24,7 +25,7 @@ class AudioPlayerTests: QuickSpec {
context("when audio item is loaded", {
beforeEach {
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: false)
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
}
it("it should eventually be ready", closure: {
@@ -34,7 +35,7 @@ class AudioPlayerTests: QuickSpec {
context("when an item is loaded (playWhenReady=true)", {
beforeEach {
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: true)
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
}
it("it should eventually be playing", closure: {
@@ -53,7 +54,7 @@ class AudioPlayerTests: QuickSpec {
try? audioPlayer.play()
}
}
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: false)
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
}
it("should eventually be playing", closure: {
@@ -71,7 +72,7 @@ class AudioPlayerTests: QuickSpec {
try? audioPlayer.pause()
}
}
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: true)
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
}
it("should eventually be paused", closure: {
@@ -89,7 +90,7 @@ class AudioPlayerTests: QuickSpec {
audioPlayer.stop()
}
}
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: true)
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
}
it("should eventually be idle", closure: {
@@ -105,20 +106,18 @@ class AudioPlayerTests: QuickSpec {
})
context("when seeking to a time", {
var passed = false
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)
holder.seekCompletion = { passed = true }
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
audioPlayer.seek(to: seekTime)
}
it("should eventually be equal to the seeked time", closure: {
expect(audioPlayer.currentTime).toEventually(equal(seekTime))
expect(passed).toEventually(beTrue())
})
})
})
@@ -130,7 +129,7 @@ class AudioPlayerTests: QuickSpec {
context("when playing an item", {
beforeEach {
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: true)
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
}
it("should eventually be 1.0", closure: {
@@ -138,12 +137,60 @@ class AudioPlayerTests: QuickSpec {
})
})
})
describe("its currentItem", {
it("should be nil", closure: {
expect(audioPlayer.currentItem).to(beNil())
})
context("when loading an item", {
beforeEach {
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
}
it("should not be nil", closure: {
expect(audioPlayer.currentItem).toNot(beNil())
})
})
context("when setting the timePitchAlgorithm", {
beforeEach {
audioPlayer.audioTimePitchAlgorithm = .timeDomain
}
context("then loading an item", {
beforeEach {
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
}
it("should have the applied timePitchAlgorithm", closure: {
expect(audioPlayer.wrapper.currentItem?.audioTimePitchAlgorithm).to(equal(AVAudioTimePitchAlgorithm.timeDomain))
})
})
context("then loading a timepitching item", {
beforeEach {
let item = DefaultAudioItemTimePitching(audioUrl: Source.path, artist: nil, title: nil, albumTitle: nil, sourceType: .file, artwork: nil, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm.spectral)
try? audioPlayer.load(item: item, playWhenReady: false)
}
it("should have the applied timePitchAlgorithm", closure: {
expect(audioPlayer.wrapper.currentItem?.audioTimePitchAlgorithm).to(equal(AVAudioTimePitchAlgorithm.spectral))
})
})
})
})
}
}
}
class AudioPlayerDelegateHolder: AudioPlayerDelegate {
func audioPlayer(itemPlaybackEndedWithReason reason: PlaybackEndedReason) {
}
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
var state: AudioPlayerState? {
@@ -158,10 +205,6 @@ class AudioPlayerDelegateHolder: AudioPlayerDelegate {
self.state = state
}
func audioPlayerItemDidComplete() {
}
func audioPlayer(secondsElapsed seconds: Double) {
}
@@ -170,8 +213,9 @@ class AudioPlayerDelegateHolder: AudioPlayerDelegate {
}
var seekCompletion: (() -> Void)?
func audioPlayer(seekTo seconds: Int, didFinish: Bool) {
seekCompletion?()
}
func audioPlayer(didUpdateDuration duration: Double) {
@@ -9,7 +9,7 @@ class AudioSessionControllerTests: QuickSpec {
override func spec() {
describe("An AudioSessionController") {
let audioSessionController: AudioSessionController = AudioSessionController.shared
let audioSessionController: AudioSessionController = AudioSessionController(audioSession: NonFailingAudioSession())
it("should be inactive", closure: {
expect(audioSessionController.audioSessionIsActive).to(beFalse())
@@ -69,11 +69,25 @@ class AudioSessionControllerTests: QuickSpec {
})
})
}
describe("An AudioSessionController with a failing AudioSession") {
var audioSessionController: AudioSessionController!
beforeEach {
audioSessionController = AudioSessionController(audioSession: FailingAudioSession())
}
context("when activated", {
beforeEach {
try? audioSessionController.activateSession()
}
it("should be inactive", closure: {
expect(audioSessionController.audioSessionIsActive).to(beFalse())
})
})
}
}
}
class AudioSessionControllerDelegateImplementation: AudioSessionControllerDelegate {
+54
View File
@@ -0,0 +1,54 @@
//
// AudioSession.swift
// SwiftAudio_Tests
//
// Created by Jørgen Henrichsen on 31/10/2018.
// Copyright © 2018 CocoaPods. All rights reserved.
//
import Foundation
import AVFoundation
@testable import SwiftAudio
class NonFailingAudioSession: AudioSession {
var isOtherAudioPlaying: Bool = false
var availableCategories: [String] = []
func setCategory(_ category: String) throws {}
func setCategory(_ category: String, mode: String, options: AVAudioSessionCategoryOptions) throws {}
func setActive(_ active: Bool) throws {}
func setActive(_ active: Bool, with options: AVAudioSessionSetActiveOptions) throws {}
}
class FailingAudioSession: AudioSession {
var isOtherAudioPlaying: Bool = false
var availableCategories: [String] = []
func setCategory(_ category: String) throws {
throw AVError(AVError.unknown)
}
func setCategory(_ category: String, mode: String, options: AVAudioSessionCategoryOptions) throws {
throw AVError(AVError.unknown)
}
func setActive(_ active: Bool) throws {
throw AVError(AVError.unknown)
}
func setActive(_ active: Bool, with options: AVAudioSessionSetActiveOptions) throws {
throw AVError(AVError.unknown)
}
}
+147 -8
View File
@@ -20,6 +20,57 @@ class QueueManagerTests: QuickSpec {
manager = QueueManager()
}
describe("its current item", {
it("should be nil", closure: {
expect(manager.current).to(beNil())
})
context("when one item is added", closure: {
beforeEach {
manager.addItem(self.dummyItem)
}
it("should not be nil", closure: {
expect(manager.current).toNot(beNil())
})
it("should be the added item", closure: {
expect(manager.current).to(equal(self.dummyItem))
})
context("then replaced", closure: {
beforeEach {
manager.replaceCurrentItem(with: 1)
}
it("should be the new item", closure: {
expect(manager.current).to(equal(1))
})
})
})
context("when replaced", closure: {
beforeEach {
manager.replaceCurrentItem(with: 1)
}
it("should not be nil", closure: {
expect(manager.current).toNot(beNil())
})
})
context("when mulitple items are added", {
beforeEach {
manager.addItems(self.dummyItems)
}
it("should not be nil", closure: {
expect(manager.current).toNot(beNil())
})
})
})
context("when adding one item", {
beforeEach {
@@ -30,9 +81,13 @@ class QueueManagerTests: QuickSpec {
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 replacing the item", closure: {
beforeEach {
try? manager.replaceCurrentItem(with: 1)
}
it("should have replaced the current item", closure: {
expect(manager.current).to(equal(1))
})
})
context("then calling next", {
@@ -115,14 +170,72 @@ class QueueManagerTests: QuickSpec {
expect(manager.current).to(equal(self.dummyItems.first))
})
})
context("then removing previous items", {
beforeEach {
manager.removePreviousItems()
}
it("should have no previous items", closure: {
expect(manager.previousItems.count).to(equal(0))
})
it("should have current index zero", closure: {
expect(manager.currentIndex).to(equal(0))
})
})
})
context("adding more items", {
var initialItemCount: Int!
let newItems: [Int] = [10, 11, 12, 13]
beforeEach {
initialItemCount = manager.items.count
try? manager.addItems(newItems, at: manager.items.endIndex - 1)
}
it("should have more items", closure: {
expect(manager.items.count).to(equal(initialItemCount + newItems.count))
})
})
context("adding more items at a smaller index than currentIndex", {
var initialCurrentIndex: Int!
let newItems: [Int] = [10, 11, 12, 13]
beforeEach {
initialCurrentIndex = manager.currentIndex
try? manager.addItems(newItems, at: initialCurrentIndex)
}
it("currentIndex should increase by number of new items", closure: {
expect(manager.currentIndex).to(equal(initialCurrentIndex + newItems.count))
})
})
// MARK: - Removal
context("then removing a item with index less than currentIndex", {
beforeEach {
var removed: Int?
var initialCurrentIndex: Int!
beforeEach {
try? manager.jump(to: 3)
initialCurrentIndex = manager.currentIndex
removed = try? manager.removeItem(at: initialCurrentIndex - 1)
}
it("should remove an item", closure: {
expect(removed).toNot(beNil())
})
it("should decrement the currentIndex", closure: {
expect(manager.currentIndex).to(equal(initialCurrentIndex - 1))
})
}
})
context("then removing the second item", {
var removed: Int?
beforeEach {
removed = try? manager.remove(atIndex: 1)
removed = try? manager.removeItem(at: 1)
}
it("should have one less item", closure: {
@@ -134,7 +247,7 @@ class QueueManagerTests: QuickSpec {
context("then removing the last item", {
var removed: Int?
beforeEach {
removed = try? manager.remove(atIndex: self.dummyItems.count - 1)
removed = try? manager.removeItem(at: self.dummyItems.count - 1)
}
it("should have one less item", closure: {
@@ -146,7 +259,7 @@ class QueueManagerTests: QuickSpec {
context("then removing the current item", {
var removed: Int?
beforeEach {
removed = try? manager.remove(atIndex: manager.currentIndex)
removed = try? manager.removeItem(at: manager.currentIndex)
}
it("should not remove any items", closure: {
expect(removed).to(beNil())
@@ -157,7 +270,7 @@ class QueueManagerTests: QuickSpec {
context("then removing with too large index", {
var removed: Int?
beforeEach {
removed = try? manager.remove(atIndex: self.dummyItems.count)
removed = try? manager.removeItem(at: self.dummyItems.count)
}
it("should not remove any items", closure: {
@@ -169,7 +282,7 @@ class QueueManagerTests: QuickSpec {
context("then removing with too small index", {
var removed: Int?
beforeEach {
removed = try? manager.remove(atIndex: -1)
removed = try? manager.removeItem(at: -1)
}
it("should not remove any items", closure: {
@@ -178,6 +291,16 @@ class QueueManagerTests: QuickSpec {
})
})
context("then removing upcoming items", {
beforeEach {
manager.removeUpcomingItems()
}
it("should have no next items", closure: {
expect(manager.nextItems.count).to(equal(0))
})
})
// MARK: - Jumping
context("then jumping to the current item", {
@@ -343,6 +466,22 @@ class QueueManagerTests: QuickSpec {
expect(manager.items).to(equal(afterMoving))
})
})
// MARK: - Clear
context("when queue is cleared", {
beforeEach {
manager.clearQueue()
}
it("should have currentIndex 0", closure: {
expect(manager.currentIndex).to(equal(0))
})
it("should have no items", closure: {
expect(manager.items.count).to(equal(0))
})
})
})
}
}
+56 -4
View File
@@ -9,9 +9,9 @@ class QueuedAudioPlayerTests: QuickSpec {
var audioPlayer: QueuedAudioPlayer!
beforeEach {
audioPlayer = QueuedAudioPlayer()
audioPlayer.automaticallyWaitsToMinimizeStalling = false
audioPlayer.bufferDuration = 0.0001
audioPlayer.volume = 0
audioPlayer.automaticallyWaitsToMinimizeStalling = false
audioPlayer.volume = 0.0
}
describe("its current item", {
it("should be nil", closure: {
@@ -19,12 +19,24 @@ class QueuedAudioPlayerTests: QuickSpec {
})
context("when adding one item", {
var item: AudioItem!
beforeEach {
try? audioPlayer.add(item: ShortSource.getAudioItem(), playWhenReady: false)
item = ShortSource.getAudioItem()
try? audioPlayer.add(item: item, playWhenReady: false)
}
it("should not be nil", closure: {
expect(audioPlayer.currentItem).toNot(beNil())
})
context("then loading a new item", closure: {
beforeEach {
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
}
it("should have replaced the item", closure: {
expect(audioPlayer.currentItem?.getSourceUrl()).toNot(equal(item.getSourceUrl()))
})
})
})
context("when adding multiple items", {
@@ -70,7 +82,7 @@ class QueuedAudioPlayerTests: QuickSpec {
context("then removing one item", {
beforeEach {
try? audioPlayer.removeItem(atIndex: 1)
try? audioPlayer.removeItem(at: 1)
}
it("should be empty", closure: {
@@ -86,6 +98,26 @@ class QueuedAudioPlayerTests: QuickSpec {
expect(audioPlayer.nextItems.count).to(equal(0))
})
})
context("then removing upcoming items", {
beforeEach {
audioPlayer.removeUpcomingItems()
}
it("should be empty", closure: {
expect(audioPlayer.nextItems.count).to(equal(0))
})
})
context("then stopping", {
beforeEach {
audioPlayer.stop()
}
it("should be empty", closure: {
expect(audioPlayer.nextItems.count).to(equal(0))
})
})
})
})
@@ -112,6 +144,26 @@ class QueuedAudioPlayerTests: QuickSpec {
})
})
context("then removing all previous items", {
beforeEach {
audioPlayer.removePreviousItems()
}
it("should be empty", closure: {
expect(audioPlayer.previousItems.count).to(equal(0))
})
})
context("then stopping", {
beforeEach {
audioPlayer.stop()
}
it("should be empty", closure: {
expect(audioPlayer.previousItems.count).to(equal(0))
})
})
})
})
}
@@ -1,43 +0,0 @@
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))
})
})
})
}
}
}
+2
View File
@@ -11,6 +11,7 @@ import SwiftAudio
struct Source {
static let path: String = Bundle.main.path(forResource: "TestSound", ofType: "m4a")!
static let url: URL = URL(fileURLWithPath: Source.path)
static func getAudioItem() -> AudioItem {
return DefaultAudioItem(audioUrl: Source.path, sourceType: .file)
@@ -19,6 +20,7 @@ struct Source {
struct ShortSource {
static let path: String = Bundle.main.path(forResource: "ShortTestSound", ofType: "m4a")!
static let url: URL = URL(fileURLWithPath: Source.path)
static func getAudioItem() -> AudioItem {
return DefaultAudioItem(audioUrl: ShortSource.path, sourceType: .file)
+59 -46
View File
@@ -10,7 +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 see the audio player in action, run the example project!
To run the example project, clone the repo, and run `pod install` from the Example directory first.
## Requirements
@@ -18,54 +18,69 @@ iOS 10.0+
## Installation
### CocoaPods
SwiftAudio is available through [CocoaPods](http://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod 'SwiftAudio'
pod 'SwiftAudio', '~> 0.5.0'
```
### Carthage
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
```ruby
github "jorgenhenrichsen/SwiftAudio" ~> 0.5.0
```
Then follow the rest of Carthage instructions on [adding a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
## Usage
### AudioPlayer
To get started playing some audio:
```swift
let player = AudioPlayer()
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
player.load(item: audioItem, playWhenReady: true) // Load the item and start playing when the player is ready.
```
Implement `AudioPlayerDelegate` to get notified about useful events and updates to the state of the `AudioPlayer`.
#### QueuedAudioPlayer
The `QueuedAudioPlayer` is asubclass of `AudioPlayer` that maintains a queue of audio tracks.
```swift
let player = QueuedAudioPlayer()
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
player.add(item: audioItem)
player.add(item: audioItem, playWhenReady: true) // Since this is the first item, we can supply playWhenReady: true to immedietaly start playing when the item is loaded.
```
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.
When a track is done playing, the player will load the next track and update the queue, as long as `automaticallyPlayNextSong` is `true` (default).
If you want a simpler audio player without queue functionality, use:
##### Navigating the queue
All `AudioItem`s are stored in either `previousItems` or `nextItems`, which refers to items that come prior to the `currentItem` and after, respectively. The queue is navigated with:
```swift
let player = SimpleAudioPlayer()
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
player.load(item: audioItem)
player.next() // Increments the queue, and loads the next item.
player.previous() // Decrements the queue, and loads the previous item.
player.jumpToItem(atIndex:) // Jumps to a certain item and loads that item.
```
**NOTE**: Do not use `AudioPlayer` directly. Use one of the above types.
##### Manipulating the queue
```swift
player.removeItem(at:) // Remove a specific item from the queue.
player.removeUpcomingItems() // Remove all items in nextItems.
```
#### States
The `AudioPlayer` has a `state` property, to make it easier to determine appropriate actions. The different states:
+ **idle**: The player is doing nothing, no item is set as current. This is the default state.
+ **ready**: The player has its current item set and is ready to start loading for playback. This is when you can call `play()` if you supplied `playWhenReady=false` when calling `load(item:playWhenReady)`.
+ **loading**: The player is loading the track and will start playback soon.
+ **playing**: The player is playing.
+ **paused**: The player is paused.
#### Queue
The `QueuedAudioPlayer` maintains a queue of audio tracks.
The arrangement of the tracks are: [Previous]-[Current]-[Next].
When a track is done playing, the player will load the next track and update the queue, as long as `automaticallyPlayNextSong` is `true` (This is by default).
Items can be added to the queue by calling `player.add(item:)` or `player.add(items:)`.
Use `removeItem(atIndex:)` and `moveItem(fromIndex:toIndex:)` to manipulate the queue.
The queue can be navigated by using `next()`, `previous()` and `jumpToItem(atIndex:)`
### Configuring the AudioPlayer
Current options for configuring the `AudioPlayer`:
- `bufferDuration`: The amount of seconds to be buffered by the player.
- `timeEventFrequency`: How often the player should call the delegate with time progress events.
- `automaticallyWaitsToMinimizeStalling`: Indicates whether the player should automatically delay playback in order to minimize stalling.
- `volume`
- `isMuted`
- `rate`
- `audioTimePitchAlgorithm`: This value decides the `AVAudioTimePitchAlgorithm` used for each `AudioItem`. Implement `TimePitching` in your `AudioItem`-subclass to override individually for each `AudioItem`.
### Audio Session
Remember to activate an audio session with an appropriate category for your app. This can be done with `AudioSessionCategory`:
Remember to activate an audio session with an appropriate category for your app. This can be done with `AudioSessionController`:
```swift
try? AudioSessionController.set(category: .playback)
//...
@@ -74,22 +89,23 @@ try? AudioSessionController.set(category: .playback)
try? AudioSessionController.activateSession()
```
If you want audio to continue playing when the app is inactive, remember to activate background audio:
**Important**: 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.
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`.
The `AudioPlayer` will automatically update the `MPNowPlayingInfoCenter` with artist, title, album, artwork and time if the passed in `AudioItem` supports this. This functionality can be turned off by setting `automaticallyUpdateNowPlayingInfo` to `false`.
If you need to set additional properties for some items, access the player's `NowPlayingInfoController` and call `set(keyValue:)`. Available properties can be found in `NowPlayingInfoProperty`.
### Remote Commands
**First** go to App Settings -> Capabilites -> Background Modes -> Check 'Remote notifications'
The player will handle remote commands received from `MPRemoteCommandCenter`'s shared instance, enabled by:
To enable remote commands for the player you need to populate the RemoteCommands array for the player:
```swift
audioPlayer.remoteCommands = [
.play,
@@ -98,20 +114,17 @@ audioPlayer.remoteCommands = [
.skipBackward(intervals: [30]),
]
```
These commands will be activated for each `AudioItem`. If you need some audio items to have different commands, implement `RemoteCommandable` in a custom `AudioItem`-subclass. These commands will override the commands found in `AudioPlayer.remoteCommands` so make sure to supply all commands you need for that particular `AudioItem`.
These commands will be activated for each `AudioItem`. If you need some audio items to have different commands, implement `RemoteCommandable`. These commands will override the commands found in `AudioPlayer.remoteCommands` so make sure to supply all commands you need for that particular `AudioItem`.
**Remember** to go to App Settings -> Capabilites -> Background Modes -> Check 'Remote notifications'
## Configuration
Currently some configuration options are supported:
+ `automaticallyWaitsToMinimizeStalling`: Whether the player should delay playback start to minimize stalling. If you are streaming large audio files and playback start is slow, it can help to set this to `false`. Default is `true`.
+ `bufferDuration`: The amount of seconds to be buffered by the player. Does not have any effect if `automaticallyWaitsToMinimizeStalling` is set to `true`.
+ `timeEventFrequency`: This decides how ofen the delegate should be notified that a time unit elapsed in the playback.
+ `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`.
#### Custom handlers for remote commands
To supply custom handlers for your remote commands, just override the handlers contained in the player's `RemoteCommandController`:
```swift
let player = QueuedAudioPlayer()
player.remoteCommandController.handlePlayCommand = { (event) in
// Handle remote command here.
}
```
All available overrides can be found by looking at `RemoteCommandController`.
## Author
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudio'
s.version = '0.3.4'
s.version = '0.5.0'
s.summary = 'Easy audio streaming for iOS'
# This description is used to generate tags and improve search results.
+89 -202
View File
@@ -10,19 +10,15 @@ import Foundation
import AVFoundation
import MediaPlayer
protocol AVPlayerWrapperDelegate: class {
func AVWrapper(didChangeState state: AVPlayerWrapperState)
func AVWrapperItemDidComplete()
func AVWrapper(secondsElapsed seconds: Double)
func AVWrapper(failedWithError error: Error?)
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
func AVWrapper(didUpdateDuration duration: Double)
public enum PlaybackEndedReason: String {
case playedUntilEnd
case playerStopped
case skippedToNext
case skippedToPrevious
case jumpedToIndex
}
class AVPlayerWrapper {
class AVPlayerWrapper: AVPlayerWrapperProtocol {
struct Constants {
static let assetPlayableKey = "playable"
@@ -35,19 +31,11 @@ class AVPlayerWrapper {
let playerTimeObserver: AVPlayerTimeObserver
let playerItemNotificationObserver: AVPlayerItemNotificationObserver
let playerItemObserver: AVPlayerItemObserver
/**
True if the last call to load(from:playWhenReady) had playWhenReady=true.
Cannot be set directly.
*/
var playWhenReady: Bool { return _playWhenReady }
private var _playWhenReady: Bool = true
/**
The current `AudioPlayerState` of the player.
*/
var state: AVPlayerWrapperState { return _state }
fileprivate var _playWhenReady: Bool = true
fileprivate var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
didSet {
@@ -57,102 +45,12 @@ class AVPlayerWrapper {
}
}
/**
The delegate receiving events.
*/
weak var delegate: AVPlayerWrapperDelegate?
// MARK: - AVPlayer Get Properties
/**
The AVAsset for the currentItem.
*/
var currentAsset: AVAsset? {
return currentItem?.asset
}
/**
The current item of the AVPlayer.
*/
var currentItem: AVPlayerItem? {
return avPlayer.currentItem
}
/**
The duration of the current item.
*/
var duration: Double {
if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
return seconds
}
return 0
}
/**
The current time of the item in the player.
*/
var currentTime: Double {
let seconds = avPlayer.currentTime().seconds
return seconds.isNaN ? 0 : seconds
}
/**
The rate of the AVPlayer
*/
var rate: Float {
return avPlayer.rate
}
// MARK: - AVPlayer Config Properties
/**
Indicates wether the player should automatically delay playback in order to minimize stalling.
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayer/1643482-automaticallywaitstominimizestal)
*/
var automaticallyWaitsToMinimizeStalling: Bool {
get { return avPlayer.automaticallyWaitsToMinimizeStalling }
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
}
/**
The amount of seconds to be buffered by the player. Default value is 0 seconds, this means the AVPlayer will choose an appropriate level of buffering.
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration)
- Important: This setting will have no effect if `automaticallyWaitsToMinimizeStalling` is set to `true`
*/
var bufferDuration: TimeInterval
/**
Set this to decide how often the player should call the delegate with time progress events.
*/
var timeEventFrequency: TimeEventFrequency {
didSet {
playerTimeObserver.periodicObserverTimeInterval = timeEventFrequency.getTime()
}
}
/**
The player volume, from 0.0 to 1.0
Default is 1.0
*/
public var volume: Float {
get { return avPlayer.volume }
set { avPlayer.volume = newValue }
}
// MARK: - Public Methods
public init(timeEventFrequency: TimeEventFrequency = .everySecond) {
self.avPlayer = AVPlayer()
public init(avPlayer: AVPlayer = AVPlayer()) {
self.avPlayer = avPlayer
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
self.playerObserver.delegate = self
self.playerTimeObserver.delegate = self
@@ -162,117 +60,105 @@ class AVPlayerWrapper {
playerTimeObserver.registerForPeriodicTimeEvents()
}
/**
Start playback.
- throws: APError.PlaybackError
*/
func play() throws {
guard currentItem != nil else {
throw APError.PlaybackError.noLoadedItem
// MARK: - AVPlayerWrapperProtocol
var state: AVPlayerWrapperState {
return _state
}
var reasonForWaitingToPlay: AVPlayer.WaitingReason? {
return avPlayer.reasonForWaitingToPlay
}
var currentItem: AVPlayerItem? {
return avPlayer.currentItem
}
var automaticallyWaitsToMinimizeStalling: Bool {
get { return avPlayer.automaticallyWaitsToMinimizeStalling }
set { avPlayer.automaticallyWaitsToMinimizeStalling = newValue }
}
var currentTime: TimeInterval {
let seconds = avPlayer.currentTime().seconds
return seconds.isNaN ? 0 : seconds
}
var duration: TimeInterval {
if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
return seconds
}
guard avPlayer.timeControlStatus == .paused else {
return
else if let seconds = currentItem?.loadedTimeRanges.first?.timeRangeValue.duration.seconds,
!seconds.isNaN {
return seconds
}
return 0.0
}
weak var delegate: AVPlayerWrapperDelegate? = nil
var bufferDuration: TimeInterval = 0
var timeEventFrequency: TimeEventFrequency = .everySecond {
didSet {
playerTimeObserver.periodicObserverTimeInterval = timeEventFrequency.getTime()
}
}
var rate: Float {
get { return avPlayer.rate }
set { avPlayer.rate = newValue }
}
var volume: Float {
get { return avPlayer.volume }
set { avPlayer.volume = newValue }
}
var isMuted: Bool {
get { return avPlayer.isMuted }
set { avPlayer.isMuted = newValue }
}
func play() {
avPlayer.play()
}
/**
Will pause playback.
- throws: APError.PlaybackError
*/
func pause() throws {
guard currentItem != nil else {
throw APError.PlaybackError.noLoadedItem
}
guard avPlayer.timeControlStatus == .playing || avPlayer.timeControlStatus == .waitingToPlayAtSpecifiedRate else {
return
}
func pause() {
avPlayer.pause()
}
/**
Will toggle playback.
*/
func togglePlaying() throws {
func togglePlaying() {
switch avPlayer.timeControlStatus {
case .playing, .waitingToPlayAtSpecifiedRate:
try pause()
pause()
case .paused:
try play()
play()
}
}
/**
Stop the player and remove the currently playing item.
*/
func stop() {
try? pause()
pause()
reset(soft: false)
}
/**
Seek to a point in the item.
- parameter seconds: The point to move the player head, in seconds. If the given value is less than 0, 0 is used. If the value is larger than the duration, the duration is used.
- throws: `APError.PlaybackError`
*/
func seek(to seconds: TimeInterval) throws {
guard currentItem != nil else {
throw APError.PlaybackError.noLoadedItem
}
let millis = Int64(max(min(seconds, duration), 0) * 1000)
let time = CMTime(value: millis, timescale: 1000)
avPlayer.seek(to: time) { (finished) in
func seek(to seconds: TimeInterval) {
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, 1)) { (finished) in
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
}
}
/**
Load an item from a URL string. Use this when streaming sound.
- parameter urlString: The AudioSource to load the item from.
- parameter playWhenReady: Whether playback should start immediately when the item is ready. Default is `true`
*/
func load(fromUrlString urlString: String, playWhenReady: Bool = true) throws {
guard let url = URL(string: urlString) else {
throw APError.LoadError.invalidSourceUrl(urlString)
}
self.load(from: url, playWhenReady: playWhenReady)
}
/**
Load an item from a file. Use this when playing local.
- parameter filePath: The path to the sound file.
- parameter playWhenReady: Whether playback should start immediately when the item is ready. Default is `true`
*/
func load(fromFilePath filePath: String, playWhenReady: Bool = true) throws {
let url = URL(fileURLWithPath: filePath)
self.load(from: url, playWhenReady: playWhenReady)
}
// MARK: - Private
private func load(from url: URL, playWhenReady: Bool) {
func load(from url: URL, playWhenReady: Bool) {
reset(soft: true)
_playWhenReady = playWhenReady
_state = .loading
// Set item
let currentAsset = AVURLAsset(url: url)
let currentItem = AVPlayerItem(asset: currentAsset, automaticallyLoadedAssetKeys: [Constants.assetPlayableKey])
currentItem.preferredForwardBufferDuration = bufferDuration
avPlayer.replaceCurrentItem(with: currentItem)
// Register for events
playerTimeObserver.registerForBoundaryTimeEvents()
playerObserver.startObserving()
@@ -280,15 +166,16 @@ class AVPlayerWrapper {
playerItemObserver.startObserving(item: currentItem)
}
/**
Reset to get ready for playing from a different source.
*/
// MARK: - Util
private func reset(soft: Bool) {
playerItemObserver.stopObservingCurrentItem()
playerTimeObserver.unregisterForBoundaryTimeEvents()
playerItemNotificationObserver.stopObservingCurrentItem()
if !soft {
avPlayer.replaceCurrentItem(with: nil)
}
playerTimeObserver.unregisterForBoundaryTimeEvents()
playerItemNotificationObserver.stopObservingCurrentItem()
}
}
@@ -319,7 +206,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
case .readyToPlay:
self._state = .ready
if _playWhenReady {
try? self.play()
self.play()
}
break
@@ -353,7 +240,7 @@ extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
// MARK: - AVPlayerItemNotificationObserverDelegate
func itemDidPlayToEndTime() {
delegate?.AVWrapperItemDidComplete()
delegate?.AVWrapper(itemPlaybackDoneWithReason: .playedUntilEnd)
}
}
@@ -0,0 +1,20 @@
//
// AVPlayerWrapperDelegate.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 26/10/2018.
//
import Foundation
protocol AVPlayerWrapperDelegate: class {
func AVWrapper(didChangeState state: AVPlayerWrapperState)
func AVWrapper(itemPlaybackDoneWithReason: PlaybackEndedReason)
func AVWrapper(secondsElapsed seconds: Double)
func AVWrapper(failedWithError error: Error?)
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
func AVWrapper(didUpdateDuration duration: Double)
}
@@ -0,0 +1,52 @@
//
// AVPlayerWrapperProtocol.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 26/10/2018.
//
import Foundation
import AVFoundation
protocol AVPlayerWrapperProtocol {
var state: AVPlayerWrapperState { get }
var currentItem: AVPlayerItem? { get }
var currentTime: TimeInterval { get }
var duration: TimeInterval { get }
var reasonForWaitingToPlay: AVPlayer.WaitingReason? { get }
var rate: Float { get set }
var delegate: AVPlayerWrapperDelegate? { get set }
var bufferDuration: TimeInterval { get set }
var timeEventFrequency: TimeEventFrequency { get set }
var volume: Float { get set }
var isMuted: Bool { get set }
var automaticallyWaitsToMinimizeStalling: Bool { get set }
func play()
func pause()
func togglePlaying()
func stop()
func seek(to seconds: TimeInterval)
func load(from url: URL, playWhenReady: Bool)
}
@@ -26,7 +26,7 @@ public enum AVPlayerWrapperState: String {
/// The player is playing.
case playing
/// No item loaded, the player is stopped. Call play(from:) to start loading.
/// No item loaded, the player is stopped.
case idle
}
+29 -3
View File
@@ -6,6 +6,7 @@
//
import Foundation
import AVFoundation
public enum SourceType {
case stream
@@ -23,9 +24,15 @@ public protocol AudioItem {
}
public struct DefaultAudioItem: AudioItem {
/// Make your `AudioItem`-subclass conform to this protocol to control which AVAudioTimePitchAlgorithm is used for each item.
public protocol TimePitching {
func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm
}
public class DefaultAudioItem: AudioItem {
public var audioUrl: String
public var artist: String?
@@ -66,10 +73,29 @@ public struct DefaultAudioItem: AudioItem {
public func getSourceType() -> SourceType {
return sourceType
}
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
handler(artwork)
}
}
/// An AudioItem that also conforms to the `TimePitching`-protocol
public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
self.pitchAlgorithmType = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm) {
self.pitchAlgorithmType = audioTimePitchAlgorithm
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm {
return pitchAlgorithmType
}
}
+78 -69
View File
@@ -14,7 +14,7 @@ public protocol AudioPlayerDelegate: class {
func audioPlayer(playerDidChangeState state: AudioPlayerState)
func audioPlayerItemDidComplete()
func audioPlayer(itemPlaybackEndedWithReason reason: PlaybackEndedReason)
func audioPlayer(secondsElapsed seconds: Double)
@@ -23,22 +23,23 @@ public protocol AudioPlayerDelegate: class {
func audioPlayer(seekTo seconds: Int, didFinish: Bool)
func audioPlayer(didUpdateDuration duration: Double)
}
/**
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
private var _wrapper: AVPlayerWrapperProtocol
/// The wrapper around the underlying AVPlayer
var wrapper: AVPlayerWrapperProtocol {
return _wrapper
}
public let nowPlayingInfoController: NowPlayingInfoController
public let remoteCommandController: RemoteCommandController
public weak var delegate: AudioPlayerDelegate?
var _currentItem: AudioItem?
public weak var delegate: AudioPlayerDelegate?
public var currentItem: AudioItem? {
return _currentItem
}
@@ -48,11 +49,18 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
*/
public var automaticallyUpdateNowPlayingInfo: Bool = true
/**
Controls the time pitch algorithm applied to each item loaded into the player.
If the loaded `AudioItem` conforms to `TimePitcher`-protocol this will be overriden.
*/
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
/**
Default remote commands to use for each playing item
*/
public var remoteCommands: [RemoteCommand] = []
// MARK: - Getters from AVPlayerWrapper
/**
@@ -69,13 +77,6 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
return wrapper.duration
}
/**
The current rate of the underlying `AudioPlayer`.
*/
public var rate: Float {
return wrapper.rate
}
/**
The current state of the underlying `AudioPlayer`.
*/
@@ -85,42 +86,47 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
// MARK: - Setters for AVPlayerWrapper
/**
Indicates wether the player should automatically delay playback in order to minimize stalling.
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayer/1643482-automaticallywaitstominimizestal)
*/
public var automaticallyWaitsToMinimizeStalling: Bool {
get { return wrapper.automaticallyWaitsToMinimizeStalling }
set { wrapper.automaticallyWaitsToMinimizeStalling = newValue }
}
/**
The amount of seconds to be buffered by the player. Default value is 0 seconds, this means the AVPlayer will choose an appropriate level of buffering.
[Read more from Apple Documentation](https://developer.apple.com/documentation/avfoundation/avplayeritem/1643630-preferredforwardbufferduration)
- Important: This setting will have no effect if `automaticallyWaitsToMinimizeStalling` is set to `true`
- Important: This setting will have no effect if `automaticallyWaitsToMinimizeStalling` is set to `true` in the AVPlayer
*/
public var bufferDuration: TimeInterval {
get { return wrapper.bufferDuration }
set { wrapper.bufferDuration = newValue }
set { _wrapper.bufferDuration = newValue }
}
/**
Set this to decide how often the player should call the delegate with time progress events.
*/
public var timeEventFrquency: TimeEventFrequency {
public var timeEventFrequency: TimeEventFrequency {
get { return wrapper.timeEventFrequency }
set { wrapper.timeEventFrequency = newValue }
set { _wrapper.timeEventFrequency = newValue }
}
/**
The player volume, from 0.0 to 1.0
Default is 1.0
Indicates whether the player should automatically delay playback in order to minimize stalling
*/
public var automaticallyWaitsToMinimizeStalling: Bool {
get { return wrapper.automaticallyWaitsToMinimizeStalling }
set { _wrapper.automaticallyWaitsToMinimizeStalling = newValue }
}
public var volume: Float {
get { return wrapper.volume }
set { wrapper.volume = newValue }
set { _wrapper.volume = newValue }
}
public var isMuted: Bool {
get { return wrapper.isMuted }
set { _wrapper.isMuted = newValue }
}
public var rate: Float {
get { return wrapper.rate }
set { _wrapper.rate = newValue }
}
// MARK: - Init
@@ -130,12 +136,14 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
*/
init(infoCenter: MPNowPlayingInfoCenter = MPNowPlayingInfoCenter.default()) {
self.wrapper = AVPlayerWrapper()
self.nowPlayingInfoController = NowPlayingInfoController(infoCenter: infoCenter)
self.remoteCommandController = RemoteCommandController()
public init(avPlayer: AVPlayer = AVPlayer(),
nowPlayingInfoController: NowPlayingInfoController = NowPlayingInfoController(),
remoteCommandController: RemoteCommandController = RemoteCommandController()) {
self._wrapper = AVPlayerWrapper(avPlayer: avPlayer)
self.nowPlayingInfoController = nowPlayingInfoController
self.remoteCommandController = remoteCommandController
self.wrapper.delegate = self
self._wrapper.delegate = self
self.remoteCommandController.audioPlayer = self
}
@@ -147,17 +155,29 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
- 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`.
*/
func loadItem(_ item: AudioItem, playWhenReady: Bool = true) throws {
public func load(item: AudioItem, playWhenReady: Bool = true) throws {
print("Loading: \(item)")
switch item.getSourceType() {
case .stream:
try self.wrapper.load(fromUrlString: item.getSourceUrl(), playWhenReady: playWhenReady)
if let url = URL(string: item.getSourceUrl()) {
wrapper.load(from: url, playWhenReady: playWhenReady)
}
else {
throw APError.LoadError.invalidSourceUrl(item.getSourceUrl())
}
case .file:
try self.wrapper.load(fromFilePath: item.getSourceUrl(), playWhenReady: playWhenReady)
wrapper.load(from: URL(fileURLWithPath: item.getSourceUrl()), playWhenReady: playWhenReady)
}
if let item = item as? TimePitching {
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
}
else {
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
}
self._currentItem = item
set(item: item)
self.updateMetaValues(item: item)
setArtwork(forItem: item)
enableRemoteCommands(forItem: item)
}
@@ -165,28 +185,29 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
/**
Toggle playback status.
*/
public func togglePlaying() throws {
try self.wrapper.togglePlaying()
public func togglePlaying() {
self.wrapper.togglePlaying()
}
/**
Start playback
*/
public func play() throws {
try self.wrapper.play()
public func play() {
self.wrapper.play()
}
/**
Pause playback
*/
public func pause() throws {
try self.wrapper.pause()
public func pause() {
self.wrapper.pause()
}
/**
Stop playback, resetting the player.
*/
public func stop() {
AVWrapper(itemPlaybackDoneWithReason: .playerStopped)
self.reset()
self.wrapper.stop()
}
@@ -194,16 +215,12 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
/**
Seek to a specific time in the item.
*/
public func seek(to seconds: TimeInterval) throws {
try self.wrapper.seek(to: seconds)
public func seek(to seconds: TimeInterval) {
self.wrapper.seek(to: seconds)
}
// MARK: - Remote Command Center
/**
Set the remote commands that should be activated and handled.
Calling this will disable all earlier enabled commands, so include all commands you need.
*/
func enableRemoteCommands(_ commands: [RemoteCommand]) {
self.remoteCommandController.enable(commands: commands)
}
@@ -219,21 +236,15 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
// MARK: - NowPlayingInfo
/**
Reloads the NowPlayingInfo from the current AudioItem.
*/
/// Reload all NowPlayingInfo for the playing item.
public func reloadNowPlayingInfo() {
guard let item = currentItem else { return }
set(item: item)
updateMetaValues(item: item)
setArtwork(forItem: item)
updatePlaybackValues()
}
public func add(property: NowPlayingInfoKeyValue) {
self.nowPlayingInfoController.set(keyValue: property)
}
func set(item: AudioItem) {
func updateMetaValues(item: AudioItem) {
guard automaticallyUpdateNowPlayingInfo else { return }
nowPlayingInfoController.set(keyValues: [
@@ -247,11 +258,9 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
guard automaticallyUpdateNowPlayingInfo else { return }
item.getArtwork { (image) in
if let image = image {
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { (size) -> UIImage in
return image
})
self.nowPlayingInfoController.set(keyValue: MediaItemProperty.artwork(artwork))
}
}
@@ -266,7 +275,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
// MARK: - Private
private func reset() {
func reset() {
self._currentItem = nil
}
@@ -280,8 +289,8 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
self.delegate?.audioPlayer(playerDidChangeState: state)
}
func AVWrapperItemDidComplete() {
self.delegate?.audioPlayerItemDidComplete()
func AVWrapper(itemPlaybackDoneWithReason reason: PlaybackEndedReason) {
self.delegate?.audioPlayer(itemPlaybackEndedWithReason: reason)
}
func AVWrapper(secondsElapsed seconds: Double) {
@@ -0,0 +1,29 @@
//
// AudioSession.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 02/11/2018.
//
import Foundation
import AVFoundation
protocol AudioSession {
var isOtherAudioPlaying: Bool { get }
var availableCategories: [String] { get }
func setCategory(_ category: String) throws
func setCategory(_ category: String, mode: String, options: AVAudioSessionCategoryOptions) throws
func setActive(_ active: Bool) throws
func setActive(_ active: Bool, with options: AVAudioSessionSetActiveOptions) throws
}
extension AVAudioSession: AudioSession {}
@@ -0,0 +1,53 @@
//
// AudioSessionCategory.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 02/11/2018.
//
import Foundation
import AVFoundation
/**
An enum wrapper around the AVAudioSessionCategories.
For detailed info about the categories, see: [AudioSession Programming Guide](https://developer.apple.com/library/content/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioSessionCategoriesandModes/AudioSessionCategoriesandModes.html#//apple_ref/doc/uid/TP40007875-CH10)
*/
public enum AudioSessionCategory {
case ambient
case soloAmbient
case playback
case record
case playAndRecord
case multiRoute
func getValue() -> String {
switch self {
case .ambient:
return AVAudioSessionCategoryAmbient
case .soloAmbient:
return AVAudioSessionCategorySoloAmbient
case .playback:
return AVAudioSessionCategoryPlayback
case .record:
return AVAudioSessionCategoryRecord
case .playAndRecord:
return AVAudioSessionCategoryPlayAndRecord
case .multiRoute:
return AVAudioSessionCategoryMultiRoute
}
}
}
@@ -8,54 +8,12 @@
import Foundation
import AVFoundation
/**
An enum wrapper around the AVAudioSessionCategories.
For detailed info about the categories, see: [AudioSession Programming Guide](https://developer.apple.com/library/content/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioSessionCategoriesandModes/AudioSessionCategoriesandModes.html#//apple_ref/doc/uid/TP40007875-CH10)
*/
public enum AudioSessionCategory {
case ambient
case soloAmbient
case playback
case record
case playAndRecord
case multiRoute
func getValue() -> String {
switch self {
case .ambient:
return AVAudioSessionCategoryAmbient
case .soloAmbient:
return AVAudioSessionCategorySoloAmbient
case .playback:
return AVAudioSessionCategoryPlayback
case .record:
return AVAudioSessionCategoryRecord
case .playAndRecord:
return AVAudioSessionCategoryPlayAndRecord
case .multiRoute:
return AVAudioSessionCategoryMultiRoute
}
}
}
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.
@@ -64,7 +22,7 @@ public class AudioSessionController {
public static let shared = AudioSessionController()
private let audioSession: AVAudioSession = AVAudioSession.sharedInstance()
private let audioSession: AudioSession
private let notificationCenter: NotificationCenter = NotificationCenter.default
private var _isObservingForInterruptions: Bool = false
@@ -107,7 +65,8 @@ public class AudioSessionController {
public weak var delegate: AudioSessionControllerDelegate?
private init() {
init(audioSession: AudioSession = AVAudioSession.sharedInstance()) {
self.audioSession = audioSession
registerForInterruptionNotification()
}
@@ -14,16 +14,18 @@ public protocol NowPlayingInfoKeyValue {
func getValue() -> Any?
}
/**
Wrapper class to control the NowPlayingInfoCenter
*/
public class NowPlayingInfoController {
let infoCenter: MPNowPlayingInfoCenter
var info: [String: Any]
public init(infoCenter: MPNowPlayingInfoCenter) {
/**
Create a new NowPlayingInfoController.
- parameter infoCenter: The MPNowPlayingInfoCenter to use. Default is `MPNowPlayingInfoCenter.default()`
*/
public init(infoCenter: MPNowPlayingInfoCenter = MPNowPlayingInfoCenter.default()) {
self.infoCenter = infoCenter
self.info = [:]
}
@@ -27,6 +27,7 @@ class AVPlayerItemObserver: NSObject {
private struct AVPlayerItemKeyPath {
static let duration = #keyPath(AVPlayerItem.duration)
static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges)
}
var isObserving: Bool = false
@@ -53,11 +54,13 @@ class AVPlayerItemObserver: NSObject {
self.isObserving = true
self.observingItem = item
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context)
}
}
private func stopObservingCurrentItem() {
func stopObservingCurrentItem() {
observingItem?.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
observingItem?.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context)
self.isObserving = false
self.observingItem = nil
}
@@ -73,8 +76,12 @@ class AVPlayerItemObserver: NSObject {
if let duration = change?[.newKey] as? CMTime {
self.delegate?.item(didUpdateDuration: duration.seconds)
}
default:
break
case AVPlayerItemKeyPath.loadedTimeRanges:
if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration {
self.delegate?.item(didUpdateDuration: duration.seconds)
}
default: break
}
}
+72 -10
View File
@@ -20,10 +20,10 @@ class QueueManager<T> {
}
public var nextItems: [T] {
guard _currentIndex < _items.count else {
guard _currentIndex + 1 < _items.count else {
return []
}
return Array(_items[_currentIndex + 1..<items.count])
return Array(_items[_currentIndex + 1..<_items.count])
}
public var previousItems: [T] {
@@ -71,6 +71,21 @@ class QueueManager<T> {
_items.append(contentsOf: items)
}
/**
Add an array of items to the queue at a given index.
- parameter items: The `AudioItem`s to be added.
- parameter at: The index to insert the items at.
*/
public func addItems(_ items: [T], at index: Int) throws {
guard index >= 0 && _items.count > index else {
throw APError.QueueError.invalidIndex(index: index, message: "Index for addition has to be positive and smaller than the count of current items (\(_items.count))")
}
_items.insert(contentsOf: items, at: index)
if (_currentIndex >= index) { _currentIndex = _currentIndex + items.count }
}
/**
Get the next item in the queue, if there are any.
Will update the current item.
@@ -119,9 +134,10 @@ class QueueManager<T> {
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))")
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]
}
@@ -140,14 +156,15 @@ class QueueManager<T> {
}
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)).")
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)).")
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)
let item = try removeItem(at: fromIndex)
try addItems([item], at: toIndex)
}
/**
@@ -158,16 +175,61 @@ class QueueManager<T> {
- returns: The removed item.
*/
@discardableResult
public func remove(atIndex index: Int) throws -> T {
public func removeItem(at 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)).")
throw APError.QueueError.invalidIndex(index: index, message: "Index for removal has to be positive and smaller than the count of current items (\(_items.count)).")
}
if index < _currentIndex {
_currentIndex = _currentIndex - 1
}
return _items.remove(at: index)
}
/**
Replace the current item with a new one. If there is no current item, it is equivalent to calling add(item:).
- parameter item: The item to set as the new current item.
*/
public func replaceCurrentItem(with item: T) {
if current == nil {
self.addItem(item)
}
self._items[_currentIndex] = item
}
/**
Remove all previous items in the queue.
If no previous items exist, no action will be taken.
*/
public func removePreviousItems() {
guard currentIndex > 0 else { return }
_items.removeSubrange(0..<_currentIndex)
_currentIndex = 0
}
/**
Remove upcoming items.
If no upcoming items exist, no action will be taken.
*/
public func removeUpcomingItems() {
let nextIndex = _currentIndex + 1
guard nextIndex < _items.count else { return }
_items.removeSubrange(nextIndex..<_items.count)
}
/**
Removes all items for queue
*/
public func clearQueue() {
_currentIndex = 0
_items.removeAll()
}
}
+55 -13
View File
@@ -21,14 +21,21 @@ public class QueuedAudioPlayer: AudioPlayer {
*/
public var automaticallyPlayNextSong: Bool = true
public override init(infoCenter: MPNowPlayingInfoCenter = MPNowPlayingInfoCenter.default()) {
super.init(infoCenter: infoCenter)
}
public override var currentItem: AudioItem? {
return queueManager.current
}
/**
Stops the player and clears the queue.
*/
public override func stop() {
super.stop()
}
override func reset() {
queueManager.clearQueue()
}
/**
The previous items held by the queue.
*/
@@ -43,6 +50,17 @@ public class QueuedAudioPlayer: AudioPlayer {
return queueManager.nextItems
}
/**
Will replace the current item with a new one and load it into the player.
- parameter item: The AudioItem to replace the current item.
- throws: APError.LoadError
*/
public override func load(item: AudioItem, playWhenReady: Bool) throws {
try super.load(item: item, playWhenReady: playWhenReady)
queueManager.replaceCurrentItem(with: item)
}
/**
Add a single item to the queue.
@@ -53,7 +71,7 @@ public class QueuedAudioPlayer: AudioPlayer {
public func add(item: AudioItem, playWhenReady: Bool = true) throws {
if currentItem == nil {
queueManager.addItem(item)
try self.loadItem(item, playWhenReady: playWhenReady)
try self.load(item: item, playWhenReady: playWhenReady)
}
else {
queueManager.addItem(item)
@@ -70,29 +88,35 @@ public class QueuedAudioPlayer: AudioPlayer {
public func add(items: [AudioItem], playWhenReady: Bool = true) throws {
if currentItem == nil {
queueManager.addItems(items)
try self.loadItem(currentItem!, playWhenReady: playWhenReady)
try self.load(item: currentItem!, playWhenReady: playWhenReady)
}
else {
queueManager.addItems(items)
}
}
public func add(items: [AudioItem], at index: Int) throws {
try queueManager.addItems(items, at: index)
}
/**
Step to the next item in the queue.
- throws: `APError`
*/
public func next() throws {
AVWrapper(itemPlaybackDoneWithReason: .skippedToNext)
let nextItem = try queueManager.next()
try self.loadItem(nextItem, playWhenReady: true)
try self.load(item: nextItem, playWhenReady: true)
}
/**
Step to the previous item in the queue.
*/
public func previous() throws {
AVWrapper(itemPlaybackDoneWithReason: .skippedToPrevious)
let previousItem = try queueManager.previous()
try self.loadItem(previousItem, playWhenReady: true)
try self.load(item: previousItem, playWhenReady: true)
}
/**
@@ -101,8 +125,8 @@ public class QueuedAudioPlayer: AudioPlayer {
- parameter index: The index of the item to remove.
- throws: `APError.QueueError`
*/
public func removeItem(atIndex index: Int) throws {
try queueManager.remove(atIndex: index)
public func removeItem(at index: Int) throws {
try queueManager.removeItem(at: index)
}
/**
@@ -113,8 +137,10 @@ public class QueuedAudioPlayer: AudioPlayer {
- throws: `APError`
*/
public func jumpToItem(atIndex index: Int, playWhenReady: Bool = true) throws {
AVWrapper(itemPlaybackDoneWithReason: .jumpedToIndex)
let item = try queueManager.jump(to: index)
try self.loadItem(item, playWhenReady: playWhenReady)
try self.load(item: item, playWhenReady: playWhenReady)
}
/**
@@ -128,10 +154,26 @@ public class QueuedAudioPlayer: AudioPlayer {
try queueManager.moveItem(fromIndex: fromIndex, toIndex: toIndex)
}
/**
Remove all upcoming items, those returned by `next()`
*/
public func removeUpcomingItems() {
queueManager.removeUpcomingItems()
}
/**
Remove all previous items, those returned by `previous()`
*/
public func removePreviousItems() {
queueManager.removePreviousItems()
}
// MARK: - AVPlayerWrapperDelegate
override func AVWrapperItemDidComplete() {
super.AVWrapperItemDidComplete()
override func AVWrapper(itemPlaybackDoneWithReason reason: PlaybackEndedReason) {
super.AVWrapper(itemPlaybackDoneWithReason: reason)
guard reason == .playedUntilEnd else { return }
if automaticallyPlayNextSong {
try? self.next()
}
@@ -14,27 +14,29 @@ public protocol RemoteCommandable {
public class RemoteCommandController {
private let center = MPRemoteCommandCenter.shared()
private let center: MPRemoteCommandCenter
weak var audioPlayer: AudioPlayer?
var commandTargetPointers: [String: Any] = [:]
init() {}
/**
Enable a set of RemoteCommands. Calling this will disable all earlier set commands, so include all commands that needs to be active.
Create a new RemoteCommandController.
- parameter commands: The RemoteCommands that is to be enabled.
- parameter remoteCommandCenter: The MPRemoteCommandCenter used. Default is `MPRemoteCommandCenter.shared()`
*/
public func enable(commands: [RemoteCommand]) {
public init(remoteCommandCenter: MPRemoteCommandCenter = MPRemoteCommandCenter.shared()) {
self.center = remoteCommandCenter
}
internal func enable(commands: [RemoteCommand]) {
self.disable(commands: RemoteCommand.all())
commands.forEach { (command) in
self.enable(command: command)
}
}
private func disable(commands: [RemoteCommand]) {
internal func disable(commands: [RemoteCommand]) {
commands.forEach { (command) in
self.disable(command: command)
}
@@ -62,7 +64,6 @@ public class RemoteCommandController {
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))
}
}
@@ -82,33 +83,33 @@ public class RemoteCommandController {
// MARK: - Handlers
lazy var handlePlayCommand: RemoteCommandHandler = { (event) in
public lazy var handlePlayCommand: RemoteCommandHandler = self.handlePlayCommandDefault
public lazy var handlePauseCommand: RemoteCommandHandler = self.handlePauseCommandDefault
public lazy var handleStopCommand: RemoteCommandHandler = self.handleStopCommandDefault
public lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = self.handleTogglePlayPauseCommandDefault
public lazy var handleSkipForwardCommand: RemoteCommandHandler = self.handleSkipForwardCommandDefault
public lazy var handleSkipBackwardCommand: RemoteCommandHandler = self.handleSkipBackwardDefault
public lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = self.handleChangePlaybackPositionCommandDefault
public lazy var handleNextTrackCommand: RemoteCommandHandler = self.handleNextTrackCommandDefault
public lazy var handlePreviousTrackCommand: RemoteCommandHandler = self.handlePreviousTrackCommandDefault
private func handlePlayCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let audioPlayer = self.audioPlayer {
do {
try audioPlayer.play()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
audioPlayer.play()
return MPRemoteCommandHandlerStatus.success
}
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handlePauseCommand: RemoteCommandHandler = { (event) in
private func handlePauseCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let audioPlayer = self.audioPlayer {
do {
try audioPlayer.pause()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
audioPlayer.pause()
return MPRemoteCommandHandlerStatus.success
}
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handleStopCommand: RemoteCommandHandler = { (event) in
private func handleStopCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let audioPlayer = self.audioPlayer {
audioPlayer.stop()
return .success
@@ -116,64 +117,44 @@ public class RemoteCommandController {
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = { (event) in
private func handleTogglePlayPauseCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let audioPlayer = self.audioPlayer {
do {
try audioPlayer.togglePlaying()
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
audioPlayer.togglePlaying()
return MPRemoteCommandHandlerStatus.success
}
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handleSkipForwardCommand: RemoteCommandHandler = { (event) in
private func handleSkipForwardCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let command = event.command as? MPSkipIntervalCommand,
let interval = command.preferredIntervals.first,
let audioPlayer = self.audioPlayer {
do {
try audioPlayer.seek(to: audioPlayer.currentTime + Double(truncating: interval))
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
audioPlayer.seek(to: audioPlayer.currentTime + Double(truncating: interval))
return MPRemoteCommandHandlerStatus.success
}
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handleSkipBackwardCommand: RemoteCommandHandler = { (event) in
private func handleSkipBackwardDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let command = event.command as? MPSkipIntervalCommand,
let interval = command.preferredIntervals.first,
let audioPlayer = self.audioPlayer {
do {
try audioPlayer.seek(to: audioPlayer.currentTime - Double(truncating: interval))
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
audioPlayer.seek(to: audioPlayer.currentTime - Double(truncating: interval))
return MPRemoteCommandHandlerStatus.success
}
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = { (event) in
private func handleChangePlaybackPositionCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let event = event as? MPChangePlaybackPositionCommandEvent,
let audioPlayer = self.audioPlayer {
do {
try audioPlayer.seek(to: event.positionTime)
return MPRemoteCommandHandlerStatus.success
}
catch let error {
return self.getRemoteCommandHandlerStatus(forError: error)
}
audioPlayer.seek(to: event.positionTime)
return MPRemoteCommandHandlerStatus.success
}
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handleNextTrackCommand: RemoteCommandHandler = { (event) in
private func handleNextTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let player = self.audioPlayer as? QueuedAudioPlayer {
do {
try player.next()
@@ -186,7 +167,7 @@ public class RemoteCommandController {
return MPRemoteCommandHandlerStatus.commandFailed
}
lazy var handlePreviousTrackCommand: RemoteCommandHandler = { (event) in
private func handlePreviousTrackCommandDefault(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
if let player = self.audioPlayer as? QueuedAudioPlayer {
do {
try player.previous()
@@ -200,13 +181,7 @@ public class RemoteCommandController {
}
private func getRemoteCommandHandlerStatus(forError error: Error) -> MPRemoteCommandHandlerStatus {
if let error = error as? APError.PlaybackError {
switch error {
case .noLoadedItem:
return MPRemoteCommandHandlerStatus.noActionableNowPlayingItem
}
}
else if let error = error as? APError.LoadError {
if let error = error as? APError.LoadError {
switch error {
case .invalidSourceUrl(_):
return MPRemoteCommandHandlerStatus.commandFailed
@@ -1,30 +0,0 @@
//
// 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)
}
}