Compare commits

...

123 Commits

Author SHA1 Message Date
Jørgen Henrichsen 852578c914 Merge pull request #47 from jorgenhenrichsen/queue-access
Queue access
2019-04-07 21:47:09 +02:00
Jørgen Henrichsen 0ae0427556 Update README. Update podspec.
Bump versions to 0.7.1.
2019-04-07 21:40:38 +02:00
Jørgen Henrichsen 9170be5e36 More information available in the QueuedAudioPlayer.
Can get the currentIndex and all items in the queue.
2019-04-07 20:58:27 +02:00
Jørgen Henrichsen 3930096357 Merge pull request #44 from jorgenhenrichsen/v0.7.0
V0.7.0
2019-03-10 12:22:24 +01:00
Jørgen Henrichsen 9e8ebdfca1 Update README. Update podspec.
Bump versions to 0.7.0.
2019-03-10 10:53:14 +01:00
Jørgen Henrichsen 31f4745bae Update README.
Updated description of events.
2019-03-10 10:47:46 +01:00
Jørgen Henrichsen 4a8af1bce3 Removed unneeded mark. 2019-03-10 10:39:32 +01:00
Jørgen Henrichsen 8e6b72613d Set volume of player in tests to 0. 2019-03-10 10:36:30 +01:00
Jørgen Henrichsen ae760c4cac Put events in seperate struct.
Makes it easier to discover which events are available.
2019-03-10 09:37:00 +01:00
Jørgen Henrichsen b3856e0a7a Tests for AudioPlayer events. 2019-03-10 01:02:24 +01:00
Jørgen Henrichsen 757e5f476c Use direct closure handlers for events. 2019-03-10 01:02:04 +01:00
Jørgen Henrichsen 1f5eae407d Make event calls async.
- Events will be emitted on the utility class.
- Use a semaphore to avoid race conditions in the Event class.
- Update example to dispatch ui updates to the main queue.
2019-03-09 21:30:50 +01:00
Jørgen Henrichsen 29660371c9 Updated tests to use events. 2019-03-09 21:30:02 +01:00
Jørgen Henrichsen c2530a0193 Update README.
Add details about the event handlers.
2019-03-09 20:31:27 +01:00
Jørgen Henrichsen 6804cf5163 Add events as an alternative to delegate. Updated example app.
This allows for selective listening to events, and no need to implement empty uneeded delegate methods.
Added a deprecation message to the delegate class, will most likely remove AudioPlayerDelegate in a future update.
2019-03-09 20:18:07 +01:00
Jørgen Henrichsen 6ea2e082b0 Call func audioPlayer(itemPlaybackEndedWithReason:) directly, instead of calling the AVWrapperDelegate implementation method.
Also remove the corresponding delegate method in the AVWrapper, and add a `AVWrapperItemDidPlayToEndTime` method instead.
2019-03-03 20:04:27 +01:00
Jørgen Henrichsen b6b4599ef5 Update nowPlayingInfo elapsed time immediately when seeking.
Causes less jumpy slider when seeking from the lockscreen.
Will rewind to the currentTime if the seeking fails.
2019-03-03 19:46:10 +01:00
Jørgen Henrichsen 3ccf9ed20e Update README
Add a missing `shared` call for the AudioSessionController.
2019-03-03 16:26:36 +01:00
Jørgen Henrichsen d6eb187788 Update README
Add more descriptions on setting the nowPlayingInfo
2019-03-03 16:25:46 +01:00
Jørgen Henrichsen 1febb782d8 Add tests for NowPlayingInfoController. 2019-03-03 16:10:30 +01:00
Jørgen Henrichsen c4c6f42ac0 Added clear() method. Use NowPlayingInfoCenter protocol.
`NowPlayingInfoCenter` makes the info center mockable, so the controller can be easily tested.
2019-03-03 16:09:29 +01:00
Jørgen Henrichsen d8b4466629 Add test for NowPlayingInfo artwork. 2019-03-01 12:21:00 +01:00
Jørgen Henrichsen cb9dec49b2 Test updating of NowPlayingInfo.
* Create protocol NowPlayingInfoControllerProtocol to make it more testable (so it can be mocked).
2019-02-28 23:06:03 +01:00
Jørgen Henrichsen 7e46a91e73 Don't reset info dict on set(keyValues:). Actually apply new values to infocenter.
The set(keyValues:) function did not actually apply the new values to the nowPlayingInfo, but does now.
2019-02-27 22:35:14 +01:00
Jørgen Henrichsen 8ca4a873a5 Remove reloadNowPlayingInfo() and replace it with two seperate functions for meta values and playbackvalues.
- loadNowPlayingMetaValues() loads the meta data from the AudioItem into the NowPlayingInfoController.
- updateNowPlayingPlaybackValues() updates the playback related values in the NowPlayingInfoController.
- Earlier, reloadNowPlayingInfo would have no effect if `automaticallyUpdateNowPlayingInfo`  was set to false. This is no longer the case, and both playbackValues and meta data can be forced to update without the player loading it automatically.
- Playback values are no longer updated each second in the NowPlayingInfoController. It should be sufficient to keep the rate, currentTime and duration in sync when they change.
2019-02-27 17:03:17 +01:00
Jørgen Henrichsen 3f54de70bc Merge pull request #42 from jorgenhenrichsen/bitrise
Bitrise
2019-02-24 14:21:28 +01:00
Jørgen Henrichsen cae549a401 Merge branch 'bitrise' of github.com:jorgenhenrichsen/SwiftAudio into bitrise 2019-02-24 14:07:41 +01:00
Jørgen Henrichsen 85fecef45e Deleted travis.yml. 2019-02-24 14:06:38 +01:00
Jørgen Henrichsen e2375f7a82 Update README
Use Bitrise for build status
2019-02-24 13:49:39 +01:00
Jørgen Henrichsen 3c8402503b Update podspec.
Bump version to 0.6.2
2019-02-24 13:19:08 +01:00
Jørgen Henrichsen 4c46137498 Update README
Bump version to 0.6.2
2019-02-24 13:18:20 +01:00
Jørgen Henrichsen a4e023d75f Merge pull request #41 from jorgenhenrichsen/initial-time
Add possiblity to start AudioItem from certain time.
2019-02-24 13:17:26 +01:00
Jørgen Henrichsen fe549a522a Update README
Add info about `InitialTiming`
2019-02-24 12:30:43 +01:00
Jørgen Henrichsen 6cc0638a70 Add possiblity to start AudioItem from certain time. 2019-02-24 11:58:03 +01:00
Jørgen Henrichsen 34c1f493ae Update podspec. Update Readme.
Bump versions to 0.6.1.
2019-01-31 22:08:16 +01:00
Jørgen Henrichsen 30750a0c81 Merge pull request #40 from jorgenhenrichsen/fix/seekto-imprecision
Increase timescale in seekto(time:) method.
2019-01-31 21:59:45 +01:00
Jørgen Henrichsen e57cf9d2e5 Increase timescale in seekto(time:) method.
Stops truncating of the seeked to time.
Also updated seekTo test to actually test the seeked value.
2019-01-31 18:31:34 +01:00
Jørgen Henrichsen 0eeedc6467 Merge pull request #38 from dcvz/patch-1
Get duration directly from asset
2019-01-31 18:26:51 +01:00
David Chavez e96cbf6337 Get duration directly from asset
It is more reliable because the current item directly will return NaN depending on what's loaded.
2019-01-30 20:39:19 +01:00
Jørgen Henrichsen debc1c519f Merge pull request #36 from jorgenhenrichsen/swift4.2
- Update to Swift 4.2
- Use Xcode 10.1 in CI
- Fix some compiler warnings in the project
- Removes custom `AudioSessionCategory`-enum as `AudioSession.Category` is introduced with Swift 4.2
2018-12-25 18:11:20 +01:00
Jørgen Henrichsen 09bec023a9 Update project version to 0.6.0. 2018-12-25 18:01:40 +01:00
Jørgen Henrichsen 44e022389c Merge branch 'swift4.2' of github.com:jorgenhenrichsen/SwiftAudio into swift4.2 2018-12-25 17:39:52 +01:00
Jørgen Henrichsen 05ca97b8eb Deleted AudioSessionCategory file. 2018-12-25 17:39:32 +01:00
Jørgen Henrichsen f6ff2b4cc0 Update travis
Test on iOS 11.4
2018-12-25 11:39:45 +01:00
Jørgen Henrichsen 9e3f5c0291 Use longer source for duration testing. 2018-12-23 14:36:39 +01:00
Jørgen Henrichsen 8ba40ca45f Updated project settings. 2018-12-23 14:30:23 +01:00
Jørgen Henrichsen 9919bde9fe Fixed compiler warnings. 2018-12-23 14:28:44 +01:00
Jørgen Henrichsen 3ba62d8657 Update travis config
Update to Xcode 10.1 and iOS 12.1
2018-12-23 12:52:27 +01:00
Jørgen Henrichsen 0f283b171f Update swift-version 2018-12-23 12:43:48 +01:00
Jørgen Henrichsen f6b5e30e85 Converted SwiftAudio and example to Swift 4.2 2018-12-23 12:38:39 +01:00
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
49 changed files with 1994 additions and 895 deletions
+1 -1
View File
@@ -1 +1 @@
4.0
4.2
-17
View File
@@ -1,17 +0,0 @@
# references:
# * http://www.objc.io/issue-6/travis-ci.html
# * https://github.com/supermarin/xcpretty#usage
osx_image: xcode9.4
language: swift
cache: cocoapods
podfile: Example/Podfile
before_install:
- gem install cocoapods # Since Travis is not always on latest version
- pod install --project-directory=Example --repo-update
script:
- set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/SwiftAudio.xcworkspace -scheme SwiftAudio-Example -sdk iphonesimulator11.4 -destination "OS=11.4,name=iPhone X" | xcpretty
- pod lib lint
after_success:
- bash <(curl -s https://codecov.io/bash) -J 'SwiftAudio'
+96 -42
View File
@@ -12,6 +12,19 @@
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"; }; };
074B0D6322289268001A45A9 /* NowPlayingInfoControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D6222289268001A45A9 /* NowPlayingInfoControllerProtocol.swift */; };
074B0D652228929B001A45A9 /* NowPlayingInfoKeyValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D642228929B001A45A9 /* NowPlayingInfoKeyValue.swift */; };
074B0D69222C23BB001A45A9 /* NowPlayingInfoCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D68222C23BB001A45A9 /* NowPlayingInfoCenter.swift */; };
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 */; };
076DFC5D2233CB1800A8D163 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 076DFC5C2233CB1700A8D163 /* Event.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 +38,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 +90,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 +130,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 +181,34 @@
/* 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>"; };
074B0D6222289268001A45A9 /* NowPlayingInfoControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoControllerProtocol.swift; sourceTree = "<group>"; };
074B0D642228929B001A45A9 /* NowPlayingInfoKeyValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoKeyValue.swift; sourceTree = "<group>"; };
074B0D68222C23BB001A45A9 /* NowPlayingInfoCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoCenter.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>"; };
076DFC5C2233CB1700A8D163 /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Event.swift; path = SwiftAudio/Classes/Event.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 +228,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 +237,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 +259,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 +280,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 +299,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 +318,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 +340,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 +401,40 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
075131B2218322E000D3BFB9 /* NowPlayingInfoController */ = {
isa = PBXGroup;
children = (
075131B3218322E000D3BFB9 /* MediaItemProperty.swift */,
075131B4218322E000D3BFB9 /* NowPlayingInfoController.swift */,
075131B5218322E000D3BFB9 /* NowPlayingInfoProperty.swift */,
074B0D6222289268001A45A9 /* NowPlayingInfoControllerProtocol.swift */,
074B0D642228929B001A45A9 /* NowPlayingInfoKeyValue.swift */,
074B0D68222C23BB001A45A9 /* NowPlayingInfoCenter.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 */,
07756B71218C2D590023935E /* AudioSession.swift */,
);
name = AudioSessionController;
path = SwiftAudio/Classes/AudioSessionController;
sourceTree = "<group>";
};
1E85F0F44277212282918D8D5DDB3588 /* iOS */ = {
isa = PBXGroup;
children = (
@@ -464,7 +510,6 @@
8DBDFCD12FE89ED7977BAEC1F0EC7101 /* XCTestSuite+QuickTestSuiteBuilder.m */,
3E8F7D7BA6F7BD122DBB624992079C91 /* Support Files */,
);
name = Quick;
path = Quick;
sourceTree = "<group>";
};
@@ -481,6 +526,8 @@
children = (
8350374D2D2D105674580DABB85F37FF /* AVPlayerWrapper.swift */,
F031D2A41C0E2D94B334BC31754F2F09 /* AVPlayerWrapperState.swift */,
075131AC2182FF1600D3BFB9 /* AVPlayerWrapperProtocol.swift */,
075131AE21830D9500D3BFB9 /* AVPlayerWrapperDelegate.swift */,
);
name = AVPlayerWrapper;
path = SwiftAudio/Classes/AVPlayerWrapper;
@@ -661,7 +708,6 @@
56AED7BB8EBC87A7394B1B4309775DC2 /* XCTestObservationCenter+Register.m */,
5F94F4B7D93544A44CDA76AFC8704CB7 /* Support Files */,
);
name = Nimble;
path = Nimble;
sourceTree = "<group>";
};
@@ -671,16 +717,13 @@
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 */,
076DFC5C2233CB1700A8D163 /* Event.swift */,
07756B6F218C2D590023935E /* AudioSessionController */,
075131B2218322E000D3BFB9 /* NowPlayingInfoController */,
075131B6218322E000D3BFB9 /* RemoteCommandController */,
581526FDBE4DC5C1C3F143696A8DE42E /* AVPlayerWrapper */,
A6AF9F48F4B25035E227BFDAE847B384 /* Observer */,
5D68DC904FAAE9CFC1059C6B3F4BB87D /* Pod */,
@@ -864,6 +907,11 @@
attributes = {
LastSwiftUpdateCheck = 0930;
LastUpgradeCheck = 0930;
TargetAttributes = {
1AB61EB02FDF0033DCB1F8416419F110 = {
LastSwiftMigration = 1010;
};
};
};
buildConfigurationList = 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */;
compatibilityVersion = "Xcode 3.2";
@@ -907,25 +955,31 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
075131BD218322E000D3BFB9 /* RemoteCommandController.swift in Sources */,
074B0D652228929B001A45A9 /* NowPlayingInfoKeyValue.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 */,
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 */,
076DFC5D2233CB1800A8D163 /* Event.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 */,
074B0D69222C23BB001A45A9 /* NowPlayingInfoCenter.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 */,
074B0D6322289268001A45A9 /* NowPlayingInfoControllerProtocol.swift in Sources */,
095BD8D416719E425189E8E4963A8D4E /* TimeEventFrequency.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1192,7 +1246,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
@@ -1412,7 +1466,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1010"
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>
+45 -15
View File
@@ -24,14 +24,19 @@
074A6483205C155E0083D868 /* AVPlayerTimeObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */; };
074A6485205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */; };
074A6487205E59B60083D868 /* AVPlayerWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */; };
074B0D67222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D66222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift */; };
074B0D6B222C247B001A45A9 /* NowPlayingInfoCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */; };
074B0D6D222C24DE001A45A9 /* NowPlayingInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */; };
076DFC5F22345EAF00A8D163 /* AudioPlayerEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 076DFC5E22345EAF00A8D163 /* AudioPlayerEventTests.swift */; };
07732651205EACA300C4D1CD /* WAV-MP3.wav in Resources */ = {isa = PBXBuildFile; fileRef = 07732650205EACA300C4D1CD /* WAV-MP3.wav */; };
07732653205EB1B500C4D1CD /* nasa_throttle_up.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */; };
07732654205ECA8B00C4D1CD /* WAV-MP3.wav in Resources */ = {isa = PBXBuildFile; fileRef = 07732650205EACA300C4D1CD /* WAV-MP3.wav */; };
07732655205ECE1C00C4D1CD /* nasa_throttle_up.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */; };
0775575920668B020002C6A1 /* QueueManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0775575820668B020002C6A1 /* QueueManagerTests.swift */; };
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 */; };
07EB8EE2222869B2000197DE /* NowPlayingInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07EB8EE022286980000197DE /* NowPlayingInfoTests.swift */; };
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; };
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
@@ -65,12 +70,17 @@
074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerTimeObserverTests.swift; sourceTree = "<group>"; };
074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerItemNotificationObserverTests.swift; sourceTree = "<group>"; };
074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerWrapperTests.swift; sourceTree = "<group>"; };
074B0D66222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoControllerTests.swift; sourceTree = "<group>"; };
074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoCenter.swift; sourceTree = "<group>"; };
074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoController.swift; sourceTree = "<group>"; };
076DFC5E22345EAF00A8D163 /* AudioPlayerEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerEventTests.swift; sourceTree = "<group>"; };
07732650205EACA300C4D1CD /* WAV-MP3.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = "WAV-MP3.wav"; sourceTree = "<group>"; };
07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = nasa_throttle_up.mp3; sourceTree = "<group>"; };
0775575820668B020002C6A1 /* QueueManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueManagerTests.swift; sourceTree = "<group>"; };
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>"; };
07EB8EE022286980000197DE /* NowPlayingInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoTests.swift; sourceTree = "<group>"; };
521F3AEC1228A2FA2637355F /* Pods-SwiftAudio_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftAudio_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftAudio_Tests/Pods-SwiftAudio_Tests.debug.xcconfig"; sourceTree = "<group>"; };
607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAudio_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -124,6 +134,16 @@
path = Source;
sourceTree = "<group>";
};
07756B67218A4E640023935E /* Mocks */ = {
isa = PBXGroup;
children = (
07756B68218A4E870023935E /* AudioSession.swift */,
074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */,
074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
607FACC71AFB9204008FA782 = {
isa = PBXGroup;
children = (
@@ -175,6 +195,7 @@
607FACE81AFB9204008FA782 /* Tests */ = {
isa = PBXGroup;
children = (
07756B67218A4E640023935E /* Mocks */,
0708ED732116EE0100EB29BD /* AudioPlayerTests.swift */,
607FACEB1AFB9204008FA782 /* AVPlayerObserverTests.swift */,
074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */,
@@ -184,7 +205,9 @@
078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */,
0708ED6B2116DA4B00EB29BD /* AudioSessionControllerTests.swift */,
07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */,
07CC171A213E912A005F880E /* SimpleAudioPlayerTests.swift */,
07EB8EE022286980000197DE /* NowPlayingInfoTests.swift */,
074B0D66222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift */,
076DFC5E22345EAF00A8D163 /* AudioPlayerEventTests.swift */,
0708ED712116E91300EB29BD /* Source */,
607FACE91AFB9204008FA782 /* Supporting Files */,
);
@@ -278,13 +301,13 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0830;
LastUpgradeCheck = 0830;
LastUpgradeCheck = 1010;
ORGANIZATIONNAME = CocoaPods;
TargetAttributes = {
607FACCF1AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = HPNZWPB9JK;
LastSwiftMigration = 0900;
LastSwiftMigration = 1010;
SystemCapabilities = {
com.apple.BackgroundModes = {
enabled = 1;
@@ -294,7 +317,7 @@
607FACE41AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = HPNZWPB9JK;
LastSwiftMigration = 0900;
LastSwiftMigration = 1010;
TestTargetID = 607FACCF1AFB9204008FA782;
};
};
@@ -443,17 +466,22 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
07756B69218A4E870023935E /* AudioSession.swift in Sources */,
074B0D67222C1EC7001A45A9 /* NowPlayingInfoControllerTests.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 */,
0708ED6C2116DA4C00EB29BD /* AudioSessionControllerTests.swift in Sources */,
074B0D6B222C247B001A45A9 /* NowPlayingInfoCenter.swift in Sources */,
07DBB1E1212C17E600BB4278 /* QueuedAudioPlayerTests.swift in Sources */,
076DFC5F22345EAF00A8D163 /* AudioPlayerEventTests.swift in Sources */,
074B0D6D222C24DE001A45A9 /* NowPlayingInfoController.swift in Sources */,
074A6485205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift in Sources */,
607FACEC1AFB9204008FA782 /* AVPlayerObserverTests.swift in Sources */,
074A6487205E59B60083D868 /* AVPlayerWrapperTests.swift in Sources */,
07EB8EE2222869B2000197DE /* NowPlayingInfoTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -499,12 +527,14 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
@@ -552,12 +582,14 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
@@ -598,8 +630,8 @@
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
@@ -615,8 +647,8 @@
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
@@ -637,8 +669,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 4.2;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudio_Example.app/SwiftAudio_Example";
};
name = Debug;
@@ -656,8 +687,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 4.2;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudio_Example.app/SwiftAudio_Example";
};
name = Release;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0900"
LastUpgradeVersion = "1010"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -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>
+1 -1
View File
@@ -14,7 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
application.beginReceivingRemoteControlEvents()
+1 -1
View File
@@ -46,7 +46,7 @@ extension QueueViewController: UITableViewDataSource, UITableViewDelegate {
case 0:
return 1
case 1:
return controller.player.nextItems.count ?? 0
return controller.player.nextItems.count
default:
return 0
}
+46 -47
View File
@@ -27,14 +27,17 @@ class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
controller.player.delegate = self
controller.player.event.stateChange.addListener(self, handleAudioPlayerStateChange)
controller.player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed)
controller.player.event.seek.addListener(self, handleAudioPlayerDidSeek)
controller.player.event.updateDuration.addListener(self, handleAudioPlayerUpdateDuration)
}
@IBAction func togglePlay(_ sender: Any) {
if (!controller.audioSessionController.audioSessionIsActive) {
try? controller.audioSessionController.activateSession()
}
try? controller.player.togglePlaying()
controller.player.togglePlaying()
}
@IBAction func previous(_ sender: Any) {
@@ -50,7 +53,7 @@ class ViewController: UIViewController {
}
@IBAction func scrubbing(_ sender: UISlider) {
try? controller.player.seek(to: Double(slider.value))
controller.player.seek(to: Double(slider.value))
}
@IBAction func scrubbingValueChanged(_ sender: UISlider) {
@@ -58,63 +61,59 @@ class ViewController: UIViewController {
elapsedTimeLabel.text = value.secondsToString()
remainingTimeLabel.text = (controller.player.duration - value).secondsToString()
}
}
extension ViewController: AudioPlayerDelegate {
func audioPlayer(playerDidChangeState state: AVPlayerWrapperState) {
func updateTimeValues() {
self.slider.maximumValue = Float(self.controller.player.duration)
self.slider.setValue(Float(self.controller.player.currentTime), animated: true)
self.elapsedTimeLabel.text = self.controller.player.currentTime.secondsToString()
self.remainingTimeLabel.text = (self.controller.player.duration - self.controller.player.currentTime).secondsToString()
}
func updateMetaData() {
if let item = controller.player.currentItem {
titleLabel.text = item.getTitle()
artistLabel.text = item.getArtist()
item.getArtwork({ (image) in
self.imageView.image = image
})
}
}
func setPlayButtonState(forAudioPlayerState state: AudioPlayerState) {
playButton.setTitle(state == .playing ? "Pause" : "Play", for: .normal)
switch state {
case .ready:
if let item = controller.player.currentItem {
titleLabel.text = item.getTitle()
artistLabel.text = item.getArtist()
item.getArtwork({ (image) in
self.imageView.image = image
})
}
// MARK: - AudioPlayer Event Handlers
func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
DispatchQueue.main.async {
self.setPlayButtonState(forAudioPlayerState: data)
switch data {
case .ready:
self.updateMetaData()
self.updateTimeValues()
case .loading, .playing, .paused, .idle:
self.updateTimeValues()
}
slider.maximumValue = Float(controller.player.duration)
slider.setValue(Float(controller.player.currentTime), animated: true)
elapsedTimeLabel.text = controller.player.currentTime.secondsToString()
remainingTimeLabel.text = (controller.player.duration - controller.player.currentTime).secondsToString()
case .loading, .playing, .paused, .idle:
slider.maximumValue = Float(controller.player.duration)
slider.setValue(Float(controller.player.currentTime), animated: true)
}
}
func audioPlayerItemDidComplete() {
}
func audioPlayer(secondsElapsed seconds: Double) {
func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
if !isScrubbing {
slider.setValue(Float(seconds), animated: false)
elapsedTimeLabel.text = controller.player.currentTime.secondsToString()
remainingTimeLabel.text = (controller.player.duration - controller.player.currentTime).secondsToString()
DispatchQueue.main.async {
self.updateTimeValues()
}
}
}
func audioPlayer(failedWithError error: Error?) {
}
func audioPlayer(seekTo seconds: Int, didFinish: Bool) {
func handleAudioPlayerDidSeek(data: AudioPlayer.SeekEventData) {
isScrubbing = false
}
func audioPlayer(didUpdateDuration duration: Double) {
slider.maximumValue = Float(controller.player.duration)
slider.setValue(Float(controller.player.currentTime), animated: true)
elapsedTimeLabel.text = controller.player.currentTime.secondsToString()
remainingTimeLabel.text = (controller.player.duration - controller.player.currentTime).secondsToString()
func handleAudioPlayerUpdateDuration(data: AudioPlayer.UpdateDurationEventData) {
DispatchQueue.main.async {
self.updateTimeValues()
}
}
}
+5 -4
View File
@@ -7,8 +7,8 @@ import AVFoundation
class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
var status: AVPlayerStatus?
var timeControlStatus: AVPlayerTimeControlStatus?
var status: AVPlayer.Status?
var timeControlStatus: AVPlayer.TimeControlStatus?
override func spec() {
@@ -19,6 +19,7 @@ class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
beforeEach {
player = AVPlayer()
player.volume = 0.0
observer = AVPlayerObserver(player: player)
observer.delegate = self
}
@@ -58,11 +59,11 @@ class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
}
}
func player(statusDidChange status: AVPlayerStatus) {
func player(statusDidChange status: AVPlayer.Status) {
self.status = status
}
func player(didChangeTimeControlStatus status: AVPlayerTimeControlStatus) {
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
self.timeControlStatus = status
}
+89 -71
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,12 +28,8 @@ 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: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.loading))
})
it("should eventually be ready", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.ready))
@@ -80,7 +38,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 +47,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 +64,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 +81,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 +107,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,12 +118,23 @@ 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))
})
})
context("when loading source with initial time", closure: {
let initialTime: TimeInterval = 4.0
beforeEach {
wrapper.load(from: LongSource.url, playWhenReady: true, initialTime: initialTime)
}
it("should eventually be playing", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.playing))
})
})
})
describe("its duration", {
@@ -175,7 +144,7 @@ class AVPlayerWrapperTests: QuickSpec {
context("when loading source", {
beforeEach {
try? wrapper.load(fromFilePath: Source.path, playWhenReady: false)
wrapper.load(from: URL(fileURLWithPath: LongSource.path), playWhenReady: false)
}
it("should eventually not be 0", closure: {
expect(wrapper.duration).toEventuallyNot(equal(0))
@@ -193,18 +162,70 @@ class AVPlayerWrapperTests: QuickSpec {
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)
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))
})
})
context("when playing from initial time", closure: {
let initialTime: TimeInterval = 4.0
beforeEach {
wrapper.load(from: LongSource.url, playWhenReady: false, initialTime: initialTime)
}
it("should eventuallt be equal to the initial time", closure: {
expect(wrapper.currentTime).toEventually(equal(initialTime))
})
})
})
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 +235,12 @@ class AVPlayerWrapperTests: QuickSpec {
}
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
func AVWrapperItemDidPlayToEndTime() {
}
var state: AVPlayerWrapperState? {
didSet {
print(state)
if let state = state {
self.stateUpdate?(state)
}
@@ -233,10 +254,6 @@ class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
self.state = state
}
func AVWrapperItemDidComplete() {
}
func AVWrapper(secondsElapsed seconds: Double) {
}
@@ -245,8 +262,9 @@ class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
}
var seekCompletion: (() -> Void)?
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
seekCompletion?()
}
func AVWrapper(didUpdateDuration duration: Double) {
+81
View File
@@ -0,0 +1,81 @@
import Quick
import Nimble
import MediaPlayer
@testable import SwiftAudio
class AudioPlayerEventTests: QuickSpec {
class EventListener {
var handleEvent: ((Void)) -> Void = { _ in
}
}
override func spec() {
describe("An event") {
var event: AudioPlayer.Event<(Void)>!
beforeEach {
event = AudioPlayer.Event()
}
describe("its invokers", {
context("when adding a listener", {
var listener: EventListener!
beforeEach {
listener = EventListener()
event.addListener(listener, listener!.handleEvent)
}
it("should have one element", closure: {
expect(event.invokers.count).toEventuallyNot(equal(0))
})
context("then that listener is deinitialized and an an event is emitted", {
beforeEach {
listener = nil
event.emit(data: ())
}
it("should remove the invoker", closure: {
expect(event.invokers.count).toEventually(equal(0))
})
})
})
context("when adding multiple listeners", {
var listeners: [EventListener]!
beforeEach {
listeners = [0..<15].map {_ in
let listener = EventListener()
event.addListener(listener, listener.handleEvent)
return listener
}
}
it("should have several listeners", closure: {
expect(event.invokers.count).toEventually(equal(listeners.count))
})
context("then removing one", {
beforeEach {
event.removeListener(listeners[listeners.count / 2])
}
it("should have one less invoker", closure: {
expect(event.invokers.count).toEventually(equal(listeners.count - 1))
})
})
})
})
}
}
}
+91 -57
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: {
@@ -43,17 +44,15 @@ class AudioPlayerTests: QuickSpec {
})
context("when playing an item", {
var holder: AudioPlayerDelegateHolder!
var listener: AudioPlayerEventListener!
beforeEach {
holder = AudioPlayerDelegateHolder()
audioPlayer.delegate = holder
holder.stateUpdate = { state in
print(state.rawValue)
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
listener.stateUpdate = { state in
if state == .ready {
try? audioPlayer.play()
audioPlayer.play()
}
}
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: false)
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
}
it("should eventually be playing", closure: {
@@ -62,16 +61,15 @@ class AudioPlayerTests: QuickSpec {
})
context("when pausing an item", {
var holder: AudioPlayerDelegateHolder!
var listener: AudioPlayerEventListener!
beforeEach {
holder = AudioPlayerDelegateHolder()
audioPlayer.delegate = holder
holder.stateUpdate = { (state) in
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
listener.stateUpdate = { (state) in
if state == .playing {
try? audioPlayer.pause()
audioPlayer.pause()
}
}
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: true)
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
}
it("should eventually be paused", closure: {
@@ -80,16 +78,15 @@ class AudioPlayerTests: QuickSpec {
})
context("when stopping an item", {
var holder: AudioPlayerDelegateHolder!
var listener: AudioPlayerEventListener!
beforeEach {
holder = AudioPlayerDelegateHolder()
audioPlayer.delegate = holder
holder.stateUpdate = { (state) in
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
listener.stateUpdate = { (state) in
if state == .playing {
audioPlayer.stop()
}
}
try? audioPlayer.loadItem(Source.getAudioItem(), playWhenReady: true)
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
}
it("should eventually be idle", closure: {
@@ -105,22 +102,28 @@ class AudioPlayerTests: QuickSpec {
})
context("when seeking to a time", {
let holder = AudioPlayerDelegateHolder()
let seekTime: TimeInterval = 0.5
let seekTime: TimeInterval = 1.0
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)
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))
})
})
context("when playing an item with an initial time", {
var item: DefaultAudioItemInitialTime!
beforeEach {
item = DefaultAudioItemInitialTime(audioUrl: LongSource.path, artist: nil, title: nil, albumTitle: nil, sourceType: .file, artwork: nil, initialTime: 4.0)
try? audioPlayer.load(item: item, playWhenReady: false)
}
it("should eventaully be equal to the initial time", closure: {
expect(audioPlayer.currentTime).toEventually(equal(item.getInitialTime()))
})
})
})
describe("its rate", {
@@ -130,7 +133,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,14 +141,57 @@ 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 {
class AudioPlayerEventListener {
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
var state: AudioPlayerState? {
didSet {
if let state = state {
@@ -154,32 +200,20 @@ class AudioPlayerDelegateHolder: AudioPlayerDelegate {
}
}
func audioPlayer(playerDidChangeState state: AudioPlayerState) {
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
var seekCompletion: (() -> Void)?
init(audioPlayer: AudioPlayer) {
audioPlayer.event.stateChange.addListener(self, handleDidUpdateState)
audioPlayer.event.seek.addListener(self, handleSeek)
}
func handleDidUpdateState(state: AudioPlayerState) {
self.state = state
}
func audioPlayerItemDidComplete() {
}
func audioPlayer(secondsElapsed seconds: Double) {
}
func audioPlayer(failedWithError error: Error?) {
}
func audioPlayer(seekTo seconds: Int, didFinish: Bool) {
}
func audioPlayer(didUpdateDuration duration: Double) {
if let state = self.state {
self.stateUpdate?(state)
}
func handleSeek(data: AudioPlayer.SeekEventData) {
seekCompletion?()
}
}
@@ -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())
@@ -55,7 +55,7 @@ class AudioSessionControllerTests: QuickSpec {
context("when a interruption arrives", {
var delegate: AudioSessionControllerDelegateImplementation!
beforeEach {
let notification = Notification(name: .AVAudioSessionInterruption, object: nil, userInfo: [
let notification = Notification(name: AVAudioSession.interruptionNotification, object: nil, userInfo: [
AVAudioSessionInterruptionTypeKey: UInt(0)
])
delegate = AudioSessionControllerDelegateImplementation()
@@ -69,18 +69,32 @@ 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 {
var interruptionType: AVAudioSessionInterruptionType? = nil
var interruptionType: AVAudioSession.InterruptionType? = nil
func handleInterruption(type: AVAudioSessionInterruptionType) {
func handleInterruption(type: AVAudioSession.InterruptionType) {
self.interruptionType = type
}
}
+66
View File
@@ -0,0 +1,66 @@
//
// 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 category: AVAudioSession.Category = AVAudioSession.Category.playback
var mode: AVAudioSession.Mode = AVAudioSession.Mode.default
var categoryOptions: AVAudioSession.CategoryOptions = []
var availableCategories: [AVAudioSession.Category] = []
var isOtherAudioPlaying: Bool = false
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws {}
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, policy: AVAudioSession.RouteSharingPolicy, options: AVAudioSession.CategoryOptions) throws {}
func setActive(_ active: Bool) throws {}
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws {}
}
class FailingAudioSession: AudioSession {
var category: AVAudioSession.Category = AVAudioSession.Category.playback
var mode: AVAudioSession.Mode = AVAudioSession.Mode.default
var categoryOptions: AVAudioSession.CategoryOptions = AVAudioSession.CategoryOptions.allowBluetooth
var availableCategories: [AVAudioSession.Category] = []
var isOtherAudioPlaying: Bool = false
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws {
throw AVError(AVError.unknown)
}
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, policy: AVAudioSession.RouteSharingPolicy, options: AVAudioSession.CategoryOptions) throws {
throw AVError(AVError.unknown)
}
func setActive(_ active: Bool) throws {
throw AVError(AVError.unknown)
}
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws {
throw AVError(AVError.unknown)
}
}
@@ -0,0 +1,18 @@
//
// NowPlayingInfoCenter.swift
// SwiftAudio_Tests
//
// Created by Jørgen Henrichsen on 03/03/2019.
// Copyright © 2019 CocoaPods. All rights reserved.
//
import Foundation
import AVFoundation
@testable import SwiftAudio
class NowPlayingInfoCenter_Mock: NowPlayingInfoCenter {
var nowPlayingInfo: [String : Any]? = nil
}
@@ -0,0 +1,66 @@
//
// NowPlayingInfoController.swift
// SwiftAudio_Tests
//
// Created by Jørgen Henrichsen on 03/03/2019.
// Copyright © 2019 CocoaPods. All rights reserved.
//
import Foundation
import MediaPlayer
@testable import SwiftAudio
class NowPlayingInfoController_Mock: NowPlayingInfoControllerProtocol {
var info: [String: Any] = [:]
required public init() {
}
required public init(infoCenter: NowPlayingInfoCenter) {
}
public func set(keyValues: [NowPlayingInfoKeyValue]) {
keyValues.forEach { (keyValue) in
info[keyValue.getKey()] = keyValue.getValue()
}
}
public func set(keyValue: NowPlayingInfoKeyValue) {
info[keyValue.getKey()] = keyValue.getValue()
}
func getTitle() -> String? {
return info[MediaItemProperty.title(nil).getKey()] as? String
}
func getArtist() -> String? {
return info[MediaItemProperty.artist(nil).getKey()] as? String
}
func getAlbumTitle() -> String? {
return info[MediaItemProperty.albumTitle(nil).getKey()] as? String
}
func getRate() -> Double? {
return info[NowPlayingInfoProperty.playbackRate(nil).getKey()] as? Double
}
func getDuration() -> Double? {
return info[MediaItemProperty.duration(nil).getKey()] as? Double
}
func getCurrentTime() -> Double? {
return info[NowPlayingInfoProperty.elapsedPlaybackTime(nil).getKey()] as? Double
}
func getArtwork() -> MPMediaItemArtwork? {
return info[MediaItemProperty.artwork(nil).getKey()] as? MPMediaItemArtwork
}
func clear() {
info = [:]
}
}
@@ -0,0 +1,72 @@
import Quick
import Nimble
import MediaPlayer
@testable import SwiftAudio
class NowPlayingInfoControllerTests: QuickSpec {
override func spec() {
describe("An NowPlayingInfoController") {
var nowPlayingController: NowPlayingInfoController!
beforeEach {
nowPlayingController = NowPlayingInfoController(infoCenter: NowPlayingInfoCenter_Mock())
}
describe("its info dictionary") {
context("when setting a value") {
beforeEach {
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
}
it("should not be empty") {
expect(nowPlayingController.info.count).toNot(equal(0))
}
context("then calling clear()") {
beforeEach {
nowPlayingController.clear()
}
it("should be empty", closure: {
expect(nowPlayingController.info.count).to(equal(0))
})
}
}
}
describe("its info center") {
context("when setting a value") {
beforeEach {
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
}
it("should not be nil") {
expect(nowPlayingController.infoCenter.nowPlayingInfo).toNot(beNil())
}
it("should not be empty") {
expect(nowPlayingController.infoCenter.nowPlayingInfo?.count).toNot(equal(0))
}
context("then calling clear()") {
beforeEach {
nowPlayingController.clear()
}
it("should be empty", closure: {
expect(nowPlayingController.infoCenter.nowPlayingInfo?.count).to(equal(0))
})
}
}
}
}
}
}
+73
View File
@@ -0,0 +1,73 @@
import Quick
import Nimble
import MediaPlayer
@testable import SwiftAudio
/// Tests that the AudioPlayer is automatically updating the values it should update in the NowPlayingInfoController.
class NowPlayingInfoTests: QuickSpec {
override func spec() {
describe("An AudioPlayer") {
var audioPlayer: AudioPlayer!
var nowPlayingController: NowPlayingInfoController_Mock!
beforeEach {
nowPlayingController = NowPlayingInfoController_Mock()
audioPlayer = AudioPlayer(nowPlayingInfoController: nowPlayingController)
audioPlayer.automaticallyUpdateNowPlayingInfo = true
audioPlayer.volume = 0
}
describe("its NowPlayingInfoController", {
context("when loading an AudioItem", {
var item: AudioItem!
beforeEach {
item = Source.getAudioItem()
try? audioPlayer.load(item: item, playWhenReady: false)
}
it("should eventually be updated with meta data", closure: {
expect(nowPlayingController.getTitle()).toEventuallyNot(beNil())
expect(nowPlayingController.getTitle()).toEventually(equal(item.getTitle()!))
expect(nowPlayingController.getArtist()).toEventuallyNot(beNil())
expect(nowPlayingController.getArtist()).toEventually(equal(item.getArtist()!))
expect(nowPlayingController.getAlbumTitle()).toEventuallyNot(beNil())
expect(nowPlayingController.getAlbumTitle()).toEventually(equal(item.getAlbumTitle()!))
expect(nowPlayingController.getArtwork()).toEventuallyNot(beNil())
})
})
context("when playing an AudioItem", {
var item: AudioItem!
beforeEach {
item = LongSource.getAudioItem()
try? audioPlayer.load(item: item, playWhenReady: true)
}
it("should eventually be updated with playback values", closure: {
expect(nowPlayingController.getRate()).toEventuallyNot(beNil())
expect(nowPlayingController.getDuration()).toEventuallyNot(beNil())
expect(nowPlayingController.getCurrentTime()).toEventuallyNot(beNil())
})
})
})
}
}
}
+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 {
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 {
let _ = 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))
})
})
})
}
}
}
+12 -1
View File
@@ -11,16 +11,27 @@ 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)
return DefaultAudioItem(audioUrl: Source.path, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .file, artwork: UIImage())
}
}
struct ShortSource {
static let path: String = Bundle.main.path(forResource: "ShortTestSound", ofType: "m4a")!
static let url: URL = URL(fileURLWithPath: ShortSource.path)
static func getAudioItem() -> AudioItem {
return DefaultAudioItem(audioUrl: ShortSource.path, sourceType: .file)
}
}
struct LongSource {
static let path: String = Bundle.main.path(forResource: "WAV-MP3", ofType: "wav")!
static let url: URL = URL(fileURLWithPath: LongSource.path)
static func getAudioItem() -> AudioItem {
return DefaultAudioItem(audioUrl: LongSource.path, sourceType: .file)
}
}
+89 -50
View File
@@ -1,6 +1,6 @@
# SwiftAudio
[![Build Status](https://travis-ci.org/jorgenhenrichsen/SwiftAudio.svg?branch=master)](https://travis-ci.org/jorgenhenrichsen/SwiftAudio)
[![Build Status](https://app.bitrise.io/app/3d3ac2ba8d817235/status.svg?token=PHIPu3oMde5GdQEOZ1Ilww&branch=master)](https://app.bitrise.io/app/3d3ac2ba8d817235)
[![Version](https://img.shields.io/cocoapods/v/SwiftAudio.svg?style=flat)](http://cocoapods.org/pods/SwiftAudio)
[![codecov](https://codecov.io/gh/jorgenhenrichsen/SwiftAudio/branch/master/graph/badge.svg)](https://codecov.io/gh/jorgenhenrichsen/SwiftAudio)
[![License](https://img.shields.io/cocoapods/l/SwiftAudio.svg?style=flat)](http://cocoapods.org/pods/SwiftAudio)
@@ -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,78 +18,127 @@ 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.7.1'
```
### Carthage
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
```ruby
github "jorgenhenrichsen/SwiftAudio" ~> 0.7.1
```
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.
```
To listen for events in the `AudioPlayer`, subscribe to events found in the `event` property of the `AudioPlayer`.
To subscribe to an event:
```swift
class MyCustomViewController: UIViewController {
let audioPlayer = AudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
audioPlayer.event.stateChange.addListener(self, handleAudioPlayerStateChange)
}
func handleAudioPlayerStateChange(state: AudioPlayerState) {
// Handle the event
}
}
```
#### QueuedAudioPlayer
The `QueuedAudioPlayer` is a subclass 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)
try? AudioSessionController.shared.set(category: .playback)
//...
// You should wait with activating the session until you actually start playback of audio.
// This is to avoid interrupting other audio without the need to do it.
try? AudioSessionController.activateSession()
try? AudioSessionController.shared.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` can automatically update `nowPlayingInfo` for you. This requires `automaticallyUpdateNowPlayingInfo` to be true (default), and that the `AudioItem` that is passed in return values for the getters. The `AudioPlayer` will update: artist, title, album, artwork, elapsed time, duration and rate.
Additional properties for items can be set by accessing the setter of the `nowPlayingInforController`:
```swift
let player = AudioPlayer()
player.load(item: someItem)
player.nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.isLiveStream(true))
```
The set(keyValue:) and set(keyValues:) accept both `MediaItemProperty` and `NowPlayingInfoProperty`.
The info can be forced to reload/update from the `AudioPlayer`.
```swift
audioPlayer.loadNowPlayingMetaValues()
audioPlayer.updateNowPlayingPlaybackValues()
```
The current info can be cleared with:
```swift
audioPlayer.nowPlayingInfoController.clear()
```
### 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,30 +147,20 @@ 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`. 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'
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`.
#### 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
player.remoteCommandController.handlePlayCommand = { (event) in
// Handle remote command here.
}
```
All available overrides can be found by looking at `RemoteCommandController`.
## 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`.
### Start playback from a certain point in time
Make your `AudioItem`-subclass conform to `InitialTiming` to be able to start playback from a certain time.
## Author
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudio'
s.version = '0.3.5'
s.version = '0.7.1'
s.summary = 'Easy audio streaming for iOS'
# This description is used to generate tags and improve search results.
+114 -207
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,12 @@ 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 _initialTime: TimeInterval?
fileprivate var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
didSet {
@@ -57,102 +46,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 +61,113 @@ 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?.asset.duration.seconds, !seconds.isNaN {
return seconds
}
guard avPlayer.timeControlStatus == .paused else {
return
else if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
return seconds
}
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, preferredTimescale: 1000)) { (finished) in
if let _ = self._initialTime {
self._initialTime = nil
if self._playWhenReady {
self.play()
}
}
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 +175,22 @@ class AVPlayerWrapper {
playerItemObserver.startObserving(item: currentItem)
}
/**
Reset to get ready for playing from a different source.
*/
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?) {
_initialTime = initialTime
self.pause()
self.load(from: url, playWhenReady: playWhenReady)
}
// MARK: - Util
private func reset(soft: Bool) {
playerItemObserver.stopObservingCurrentItem()
playerTimeObserver.unregisterForBoundaryTimeEvents()
playerItemNotificationObserver.stopObservingCurrentItem()
if !soft {
avPlayer.replaceCurrentItem(with: nil)
}
playerTimeObserver.unregisterForBoundaryTimeEvents()
playerItemNotificationObserver.stopObservingCurrentItem()
}
}
@@ -297,7 +199,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
// MARK: - AVPlayerObserverDelegate
func player(didChangeTimeControlStatus status: AVPlayerTimeControlStatus) {
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
switch status {
case .paused:
if currentItem == nil {
@@ -313,14 +215,19 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
}
}
func player(statusDidChange status: AVPlayerStatus) {
func player(statusDidChange status: AVPlayer.Status) {
switch status {
case .readyToPlay:
self._state = .ready
if _playWhenReady {
try? self.play()
if let initialTime = _initialTime {
self.seek(to: initialTime)
}
else if _playWhenReady {
self.play()
}
break
case .failed:
@@ -353,7 +260,7 @@ extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
// MARK: - AVPlayerItemNotificationObserverDelegate
func itemDidPlayToEndTime() {
delegate?.AVWrapperItemDidComplete()
delegate?.AVWrapperItemDidPlayToEndTime()
}
}
@@ -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(secondsElapsed seconds: Double)
func AVWrapper(failedWithError error: Error?)
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
func AVWrapper(didUpdateDuration duration: Double)
func AVWrapperItemDidPlayToEndTime()
}
@@ -0,0 +1,54 @@
//
// 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)
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?)
}
@@ -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
}
+54 -2
View File
@@ -6,6 +6,7 @@
//
import Foundation
import AVFoundation
public enum SourceType {
case stream
@@ -23,9 +24,20 @@ 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
}
/// Make your `AudioItem`-subclass conform to this protocol to control enable the ability to start an item at a specific time of playback.
public protocol InitialTiming {
func getInitialTime() -> TimeInterval
}
public class DefaultAudioItem: AudioItem {
public var audioUrl: String
public var artist: String?
@@ -66,10 +78,50 @@ 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
}
}
/// An AudioItem that also conforms to the `InitialTiming`-protocol
public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
public var initialTime: TimeInterval
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
self.initialTime = 0.0
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?, initialTime: TimeInterval) {
self.initialTime = initialTime
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public func getInitialTime() -> TimeInterval {
return initialTime
}
}
+148 -89
View File
@@ -10,11 +10,12 @@ import MediaPlayer
public typealias AudioPlayerState = AVPlayerWrapperState
@available(*, deprecated, message: "Delegates will be removed in future versions of SwiftAudio. Use event handlers instead.")
public protocol AudioPlayerDelegate: class {
func audioPlayer(playerDidChangeState state: AudioPlayerState)
func audioPlayerItemDidComplete()
func audioPlayer(itemPlaybackEndedWithReason reason: PlaybackEndedReason)
func audioPlayer(secondsElapsed seconds: Double)
@@ -23,22 +24,25 @@ 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
private var _wrapper: AVPlayerWrapperProtocol
/// The wrapper around the underlying AVPlayer
var wrapper: AVPlayerWrapperProtocol {
return _wrapper
}
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
public let remoteCommandController: RemoteCommandController
var _currentItem: AudioItem?
public let event = EventHolder()
public weak var delegate: AudioPlayerDelegate?
var _currentItem: AudioItem?
public var currentItem: AudioItem? {
return _currentItem
}
@@ -48,11 +52,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 +80,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 +89,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 +139,14 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
*/
init(infoCenter: MPNowPlayingInfoCenter = MPNowPlayingInfoCenter.default(), remoteCommandController: RemoteCommandController? = nil) {
self.wrapper = AVPlayerWrapper()
self.nowPlayingInfoController = NowPlayingInfoController(infoCenter: infoCenter)
self.remoteCommandController = remoteCommandController ?? RemoteCommandController()
public init(avPlayer: AVPlayer = AVPlayer(),
nowPlayingInfoController: NowPlayingInfoControllerProtocol = 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,40 +158,58 @@ 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 {
print("Loading: \(item)")
public func load(item: AudioItem, playWhenReady: Bool = true) throws {
let url: URL
switch item.getSourceType() {
case .stream:
try self.wrapper.load(fromUrlString: item.getSourceUrl(), playWhenReady: playWhenReady)
if let itemUrl = URL(string: item.getSourceUrl()) {
url = itemUrl
}
else {
throw APError.LoadError.invalidSourceUrl(item.getSourceUrl())
}
case .file:
try self.wrapper.load(fromFilePath: item.getSourceUrl(), playWhenReady: playWhenReady)
url = URL(fileURLWithPath: item.getSourceUrl())
}
wrapper.load(from: url,
playWhenReady: playWhenReady,
initialTime: (item as? InitialTiming)?.getInitialTime())
if let item = item as? TimePitching {
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
}
else {
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
}
self._currentItem = item
set(item: item)
setArtwork(forItem: item)
if (automaticallyUpdateNowPlayingInfo) {
self.loadNowPlayingMetaValues()
}
enableRemoteCommands(forItem: item)
}
/**
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()
}
/**
@@ -189,21 +218,22 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
public func stop() {
self.reset()
self.wrapper.stop()
self.event.playbackEnd.emit(data: .playerStopped)
self.delegate?.audioPlayer(itemPlaybackEndedWithReason: .playerStopped)
}
/**
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) {
if automaticallyUpdateNowPlayingInfo {
self.updateNowPlayingCurrentTime(seconds)
}
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)
}
@@ -220,53 +250,66 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
// MARK: - NowPlayingInfo
/**
Reloads the NowPlayingInfo from the current AudioItem.
Loads NowPlayingInfo-meta values with the values found in the current `AudioItem`. Use this if a change to the `AudioItem` is made and you want to update the `NowPlayingInfoController`s values.
Reloads:
- Artist
- Title
- Album title
- Album artwork
*/
public func reloadNowPlayingInfo() {
public func loadNowPlayingMetaValues() {
guard let item = currentItem else { return }
set(item: item)
setArtwork(forItem: item)
updatePlaybackValues()
}
public func add(property: NowPlayingInfoKeyValue) {
self.nowPlayingInfoController.set(keyValue: property)
}
func set(item: AudioItem) {
guard automaticallyUpdateNowPlayingInfo else { return }
nowPlayingInfoController.set(keyValues: [
MediaItemProperty.artist(item.getArtist()),
MediaItemProperty.title(item.getTitle()),
MediaItemProperty.albumTitle(item.getAlbumTitle()),
])
])
loadArtwork(forItem: item)
}
func setArtwork(forItem item: AudioItem) {
guard automaticallyUpdateNowPlayingInfo else { return }
/**
Resyncs the playbackvalues of the currently playing `AudioItem`.
Will resync:
- Current time
- Duration
- Playback rate
*/
public func updateNowPlayingPlaybackValues() {
updateNowPlayingDuration(duration)
updateNowPlayingCurrentTime(currentTime)
updateNowPlayingRate(rate)
}
private func updateNowPlayingDuration(_ duration: Double) {
nowPlayingInfoController.set(keyValue: MediaItemProperty.duration(duration))
}
private func updateNowPlayingRate(_ rate: Float) {
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.playbackRate(Double(rate)))
}
private func updateNowPlayingCurrentTime(_ currentTime: Double) {
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(currentTime))
}
private func loadArtwork(forItem item: AudioItem) {
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))
}
}
}
func updatePlaybackValues() {
guard automaticallyUpdateNowPlayingInfo else { return }
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(wrapper.currentTime))
nowPlayingInfoController.set(keyValue: MediaItemProperty.duration(wrapper.duration))
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.playbackRate(Double(wrapper.rate)))
}
// MARK: - Private
private func reset() {
func reset() {
self._currentItem = nil
}
@@ -274,31 +317,47 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
switch state {
case .playing, .paused: updatePlaybackValues()
case .ready:
if (automaticallyUpdateNowPlayingInfo) {
updateNowPlayingPlaybackValues()
}
case .playing, .paused:
if (automaticallyUpdateNowPlayingInfo) {
updateNowPlayingCurrentTime(currentTime)
updateNowPlayingRate(rate)
}
default: break
}
self.event.stateChange.emit(data: state)
self.delegate?.audioPlayer(playerDidChangeState: state)
}
func AVWrapperItemDidComplete() {
self.delegate?.audioPlayerItemDidComplete()
}
func AVWrapper(secondsElapsed seconds: Double) {
self.event.secondElapse.emit(data: seconds)
self.delegate?.audioPlayer(secondsElapsed: seconds)
}
func AVWrapper(failedWithError error: Error?) {
self.event.fail.emit(data: error)
self.delegate?.audioPlayer(failedWithError: error)
}
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
self.updatePlaybackValues()
if !didFinish && automaticallyUpdateNowPlayingInfo {
updateNowPlayingCurrentTime(currentTime)
}
self.event.seek.emit(data: (seconds, didFinish))
self.delegate?.audioPlayer(seekTo: seconds, didFinish: didFinish)
}
func AVWrapper(didUpdateDuration duration: Double) {
self.event.updateDuration.emit(data: duration)
self.delegate?.audioPlayer(didUpdateDuration: duration)
}
func AVWrapperItemDidPlayToEndTime() {
self.event.playbackEnd.emit(data: .playedUntilEnd)
self.delegate?.audioPlayer(itemPlaybackEndedWithReason: .playedUntilEnd)
}
}
@@ -0,0 +1,34 @@
//
// AudioSession.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 02/11/2018.
//
import Foundation
import AVFoundation
protocol AudioSession {
var isOtherAudioPlaying: Bool { get }
var category: AVAudioSession.Category { get }
var mode: AVAudioSession.Mode { get }
var categoryOptions: AVAudioSession.CategoryOptions { get }
var availableCategories: [AVAudioSession.Category] { get }
@available(iOS 10.0, *)
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws
@available(iOS 11.0, *)
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, policy: AVAudioSession.RouteSharingPolicy, options: AVAudioSession.CategoryOptions) throws
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws
}
extension AVAudioSession: AudioSession {}
@@ -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)
func handleInterruption(type: AVAudioSession.InterruptionType)
}
/**
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,13 +65,14 @@ public class AudioSessionController {
public weak var delegate: AudioSessionControllerDelegate?
private init() {
init(audioSession: AudioSession = AVAudioSession.sharedInstance()) {
self.audioSession = audioSession
registerForInterruptionNotification()
}
public func activateSession() throws {
do {
try audioSession.setActive(true)
try audioSession.setActive(true, options: [])
audioSessionIsActive = true
}
catch let error { throw error }
@@ -121,17 +80,14 @@ public class AudioSessionController {
public func deactivateSession() throws {
do {
try audioSession.setActive(false)
try audioSession.setActive(false, options: [])
audioSessionIsActive = false
}
catch let error { throw error }
}
/**
Set the audiosession.
*/
public func set(category: AudioSessionCategory) throws {
try audioSession.setCategory(category.getValue())
public func set(category: AVAudioSession.Category) throws {
try audioSession.setCategory(category, mode: audioSession.mode, options: audioSession.categoryOptions)
}
// MARK: - Interruptions
@@ -139,20 +95,20 @@ public class AudioSessionController {
private func registerForInterruptionNotification() {
notificationCenter.addObserver(self,
selector: #selector(handleInterruption),
name: .AVAudioSessionInterruption,
name: AVAudioSession.interruptionNotification,
object: nil)
_isObservingForInterruptions = true
}
private func unregisterForInterruptionNotification() {
notificationCenter.removeObserver(self, name: .AVAudioSessionInterruption, object: nil)
notificationCenter.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
_isObservingForInterruptions = false
}
@objc func handleInterruption(notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}
+121
View File
@@ -0,0 +1,121 @@
//
// Event.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 09/03/2019.
//
import Foundation
extension AudioPlayer {
public typealias StateChangeEventData = (AudioPlayerState)
public typealias PlaybackEndEventData = (PlaybackEndedReason)
public typealias SecondElapseEventData = (TimeInterval)
public typealias FailEventData = (Error?)
public typealias SeekEventData = (seconds: Int, didFinish: Bool)
public typealias UpdateDurationEventData = (Double)
public struct EventHolder {
/**
Emitted when the `AudioPlayer`s state is changed
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let stateChange: AudioPlayer.Event<StateChangeEventData> = AudioPlayer.Event()
/**
Emitted when the playback of the player, for some reason, has stopped.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let playbackEnd: AudioPlayer.Event<PlaybackEndEventData> = AudioPlayer.Event()
/**
Emitted when a second is elapsed in the `AudioPlayer`.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let secondElapse: AudioPlayer.Event<SecondElapseEventData> = AudioPlayer.Event()
/**
Emitted when the player encounters an error.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let fail: AudioPlayer.Event<FailEventData> = AudioPlayer.Event()
/**
Emitted when the player is done attempting to seek.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let seek: AudioPlayer.Event<SeekEventData> = AudioPlayer.Event()
/**
Emitted when the player updates its duration.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let updateDuration: AudioPlayer.Event<UpdateDurationEventData> = AudioPlayer.Event()
}
public typealias EventClosure<EventData> = (EventData) -> Void
class Invoker<EventData> {
// Signals false if the listener object is nil
let invoke: (EventData) -> Bool
weak var listener: AnyObject?
init<Listener: AnyObject>(listener: Listener, closure: @escaping EventClosure<EventData>) {
self.listener = listener
self.invoke = { [weak listener] (data: EventData) in
guard let _ = listener else {
return false
}
closure(data)
return true
}
}
}
public class Event<EventData> {
private let eventQueue: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.utility)
private let actionQueue: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated)
private let invokersSemaphore: DispatchSemaphore = DispatchSemaphore(value: 1)
var invokers: [Invoker<EventData>] = []
public func addListener<Listener: AnyObject>(_ listener: Listener, _ closure: @escaping EventClosure<EventData>) {
actionQueue.async {
self.invokersSemaphore.wait()
self.invokers.append(Invoker(listener: listener, closure: closure))
self.invokersSemaphore.signal()
}
}
public func removeListener(_ listener: AnyObject) {
actionQueue.async {
self.invokersSemaphore.wait()
self.invokers = self.invokers.filter({ (invoker) -> Bool in
if let listenerToCheck = invoker.listener {
return listenerToCheck !== listener
}
return true
})
self.invokersSemaphore.signal()
}
}
func emit(data: EventData) {
eventQueue.async {
self.invokersSemaphore.wait()
self.invokers = self.invokers.filter({ (invoker) -> Bool in
return invoker.invoke(data)
})
self.invokersSemaphore.signal()
}
}
}
}
@@ -1,51 +0,0 @@
//
// MediaInfoController.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 15/03/2018.
//
import Foundation
import MediaPlayer
public protocol NowPlayingInfoKeyValue {
func getKey() -> String
func getValue() -> Any?
}
/**
Wrapper class to control the NowPlayingInfoCenter
*/
public class NowPlayingInfoController {
let infoCenter: MPNowPlayingInfoCenter
var info: [String: Any]
public init(infoCenter: MPNowPlayingInfoCenter) {
self.infoCenter = infoCenter
self.info = [:]
}
/**
This updates a set of values in the now playing info.
- Warning: This will reset the now playing info completely! Use this function when starting playback of a new item.
*/
public func set(keyValues: [NowPlayingInfoKeyValue]) {
self.info = [:]
keyValues.forEach { (keyValue) in
info[keyValue.getKey()] = keyValue.getValue()
}
}
/**
This updates a single value in the now playing info.
*/
public func set(keyValue: NowPlayingInfoKeyValue) {
info[keyValue.getKey()] = keyValue.getValue()
self.infoCenter.nowPlayingInfo = info
}
}
@@ -0,0 +1,17 @@
//
// NowPlayingInfoCenter.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 03/03/2019.
//
import Foundation
import MediaPlayer
public protocol NowPlayingInfoCenter {
var nowPlayingInfo: [String: Any]? { get set }
}
extension MPNowPlayingInfoCenter: NowPlayingInfoCenter {}
@@ -0,0 +1,50 @@
//
// MediaInfoController.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 15/03/2018.
//
import Foundation
import MediaPlayer
public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
private var _infoCenter: NowPlayingInfoCenter
private var _info: [String: Any] = [:]
var infoCenter: NowPlayingInfoCenter {
return _infoCenter
}
var info: [String: Any] {
return _info
}
public required init() {
self._infoCenter = MPNowPlayingInfoCenter.default()
}
public required init(infoCenter: NowPlayingInfoCenter) {
self._infoCenter = infoCenter
}
public func set(keyValues: [NowPlayingInfoKeyValue]) {
keyValues.forEach { (keyValue) in
_info[keyValue.getKey()] = keyValue.getValue()
}
self._infoCenter.nowPlayingInfo = _info
}
public func set(keyValue: NowPlayingInfoKeyValue) {
_info[keyValue.getKey()] = keyValue.getValue()
self._infoCenter.nowPlayingInfo = _info
}
public func clear() {
self._info = [:]
self._infoCenter.nowPlayingInfo = _info
}
}
@@ -0,0 +1,24 @@
//
// NowPlayingInfoControllerProtocol.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 28/02/2019.
//
import Foundation
import MediaPlayer
public protocol NowPlayingInfoControllerProtocol {
init()
init(infoCenter: NowPlayingInfoCenter)
func set(keyValue: NowPlayingInfoKeyValue)
func set(keyValues: [NowPlayingInfoKeyValue])
func clear()
}
@@ -0,0 +1,14 @@
//
// NowPlayingInfoKeyValue.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 28/02/2019.
//
import Foundation
public protocol NowPlayingInfoKeyValue {
func getKey() -> String
func getValue() -> Any?
}
@@ -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
}
}
@@ -14,12 +14,12 @@ protocol AVPlayerObserverDelegate: class {
/**
Called when the AVPlayer.status changes.
*/
func player(statusDidChange status: AVPlayerStatus)
func player(statusDidChange status: AVPlayer.Status)
/**
Called when the AVPlayer.timeControlStatus changes.
*/
func player(didChangeTimeControlStatus status: AVPlayerTimeControlStatus)
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus)
}
@@ -92,9 +92,9 @@ class AVPlayerObserver: NSObject {
}
private func handleStatusChange(_ change: [NSKeyValueChangeKey: Any]?) {
let status: AVPlayerStatus
let status: AVPlayer.Status
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayerStatus(rawValue: statusNumber.intValue)!
status = AVPlayer.Status(rawValue: statusNumber.intValue)!
}
else {
status = .unknown
@@ -104,9 +104,9 @@ class AVPlayerObserver: NSObject {
private func handleTimeControlStatusChange(_ change: [NSKeyValueChangeKey: Any]?) {
let status: AVPlayerTimeControlStatus
let status: AVPlayer.TimeControlStatus
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayerTimeControlStatus(rawValue: statusNumber.intValue)!
status = AVPlayer.TimeControlStatus(rawValue: statusNumber.intValue)!
delegate?.player(didChangeTimeControlStatus: status)
}
}
+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()
}
}
+69 -13
View File
@@ -21,14 +21,35 @@ public class QueuedAudioPlayer: AudioPlayer {
*/
public var automaticallyPlayNextSong: Bool = true
public override init(infoCenter: MPNowPlayingInfoCenter = MPNowPlayingInfoCenter.default(), remoteCommandController: RemoteCommandController? = nil) {
super.init(infoCenter: infoCenter, remoteCommandController: remoteCommandController)
}
public override var currentItem: AudioItem? {
return queueManager.current
}
/**
The index of the current item.
*/
public var currentIndex: Int {
return queueManager.currentIndex
}
/**
Stops the player and clears the queue.
*/
public override func stop() {
super.stop()
}
override func reset() {
queueManager.clearQueue()
}
/**
All items currently in the queue.
*/
public var items: [AudioItem] {
return queueManager.items
}
/**
The previous items held by the queue.
*/
@@ -43,6 +64,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 +85,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 +102,37 @@ 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 {
event.playbackEnd.emit(data: .skippedToNext)
delegate?.audioPlayer(itemPlaybackEndedWithReason: .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 {
event.playbackEnd.emit(data: .skippedToPrevious)
delegate?.audioPlayer(itemPlaybackEndedWithReason: .skippedToPrevious)
let previousItem = try queueManager.previous()
try self.loadItem(previousItem, playWhenReady: true)
try self.load(item: previousItem, playWhenReady: true)
}
/**
@@ -101,8 +141,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 +153,10 @@ public class QueuedAudioPlayer: AudioPlayer {
- throws: `APError`
*/
public func jumpToItem(atIndex index: Int, playWhenReady: Bool = true) throws {
event.playbackEnd.emit(data: .jumpedToIndex)
delegate?.audioPlayer(itemPlaybackEndedWithReason: .jumpedToIndex)
let item = try queueManager.jump(to: index)
try self.loadItem(item, playWhenReady: playWhenReady)
try self.load(item: item, playWhenReady: playWhenReady)
}
/**
@@ -128,10 +170,24 @@ 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 AVWrapperItemDidPlayToEndTime() {
super.AVWrapperItemDidPlayToEndTime()
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] = [:]
public 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)
}
@@ -93,26 +95,16 @@ public class RemoteCommandController {
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
}
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
}
@@ -127,13 +119,8 @@ public class RemoteCommandController {
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
}
@@ -142,13 +129,8 @@ public class RemoteCommandController {
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
}
@@ -157,13 +139,8 @@ public class RemoteCommandController {
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
}
@@ -171,13 +148,8 @@ public class RemoteCommandController {
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
}
@@ -209,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(), remoteCommandController: RemoteCommandController? = nil) {
super.init(infoCenter: infoCenter, remoteCommandController: remoteCommandController)
}
/**
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)
}
}