Compare commits

..

29 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
24 changed files with 757 additions and 180 deletions
-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: xcode10.1
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 iphonesimulator12.1 -destination "OS=11.4,name=iPhone X" | xcpretty
- pod lib lint
after_success:
- bash <(curl -s https://codecov.io/bash) -J 'SwiftAudio'
+16
View File
@@ -12,6 +12,9 @@
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 */; };
@@ -19,6 +22,7 @@
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"; }; };
@@ -183,6 +187,9 @@
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>"; };
@@ -190,6 +197,7 @@
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>"; };
@@ -399,6 +407,9 @@
075131B3218322E000D3BFB9 /* MediaItemProperty.swift */,
075131B4218322E000D3BFB9 /* NowPlayingInfoController.swift */,
075131B5218322E000D3BFB9 /* NowPlayingInfoProperty.swift */,
074B0D6222289268001A45A9 /* NowPlayingInfoControllerProtocol.swift */,
074B0D642228929B001A45A9 /* NowPlayingInfoKeyValue.swift */,
074B0D68222C23BB001A45A9 /* NowPlayingInfoCenter.swift */,
);
name = NowPlayingInfoController;
path = SwiftAudio/Classes/NowPlayingInfoController;
@@ -709,6 +720,7 @@
0A362FBD9BA860D03408EC59B8DAD715 /* QueuedAudioPlayer.swift */,
7D4763A422F83644D194E97981775831 /* QueueManager.swift */,
EF4534C7E47CA5B502A19513117CBFC3 /* TimeEventFrequency.swift */,
076DFC5C2233CB1700A8D163 /* Event.swift */,
07756B6F218C2D590023935E /* AudioSessionController */,
075131B2218322E000D3BFB9 /* NowPlayingInfoController */,
075131B6218322E000D3BFB9 /* RemoteCommandController */,
@@ -944,6 +956,7 @@
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 */,
@@ -954,8 +967,10 @@
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 */,
074B0D69222C23BB001A45A9 /* NowPlayingInfoCenter.swift in Sources */,
075131AD2182FF1600D3BFB9 /* AVPlayerWrapperProtocol.swift in Sources */,
075131BC218322E000D3BFB9 /* RemoteCommand.swift in Sources */,
36DAEF0CCEB4AEFA64DFAF8197FD5C68 /* QueuedAudioPlayer.swift in Sources */,
@@ -964,6 +979,7 @@
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;
@@ -24,6 +24,10 @@
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 */; };
@@ -32,6 +36,7 @@
07756B69218A4E870023935E /* AudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07756B68218A4E870023935E /* AudioSession.swift */; };
078C908F210D263200555E80 /* AVPlayerItemObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078C908D210D25F700555E80 /* AVPlayerItemObserverTests.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>"; };
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>"; };
@@ -128,6 +138,8 @@
isa = PBXGroup;
children = (
07756B68218A4E870023935E /* AudioSession.swift */,
074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */,
074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */,
);
path = Mocks;
sourceTree = "<group>";
@@ -193,6 +205,9 @@
078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */,
0708ED6B2116DA4B00EB29BD /* AudioSessionControllerTests.swift */,
07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */,
07EB8EE022286980000197DE /* NowPlayingInfoTests.swift */,
074B0D66222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift */,
076DFC5E22345EAF00A8D163 /* AudioPlayerEventTests.swift */,
0708ED712116E91300EB29BD /* Source */,
607FACE91AFB9204008FA782 /* Supporting Files */,
);
@@ -452,16 +467,21 @@
buildActionMask = 2147483647;
files = (
07756B69218A4E870023935E /* AudioSession.swift in Sources */,
074B0D67222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift in Sources */,
0708ED702116E89900EB29BD /* Source.swift in Sources */,
0708ED742116EE0100EB29BD /* AudioPlayerTests.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;
};
+42 -43
View File
@@ -27,7 +27,10 @@ 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) {
@@ -58,63 +61,59 @@ class ViewController: UIViewController {
elapsedTimeLabel.text = value.secondsToString()
remainingTimeLabel.text = (controller.player.duration - value).secondsToString()
}
}
extension ViewController: AudioPlayerDelegate {
func audioPlayer(itemPlaybackEndedWithReason reason: PlaybackEndedReason) {
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 audioPlayer(playerDidChangeState state: AVPlayerWrapperState) {
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 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()
}
}
}
@@ -19,6 +19,7 @@ class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
beforeEach {
player = AVPlayer()
player.volume = 0.0
observer = AVPlayerObserver(player: player)
observer.delegate = self
}
+1 -1
View File
@@ -235,7 +235,7 @@ class AVPlayerWrapperTests: QuickSpec {
}
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
func AVWrapper(itemPlaybackDoneWithReason reason: PlaybackEndedReason) {
func AVWrapperItemDidPlayToEndTime() {
}
+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))
})
})
})
})
}
}
}
+20 -38
View File
@@ -44,12 +44,10 @@ 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 {
audioPlayer.play()
}
@@ -63,11 +61,10 @@ 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 {
audioPlayer.pause()
}
@@ -81,11 +78,10 @@ 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()
}
@@ -194,13 +190,8 @@ class AudioPlayerTests: QuickSpec {
}
class AudioPlayerDelegateHolder: AudioPlayerDelegate {
func audioPlayer(itemPlaybackEndedWithReason reason: PlaybackEndedReason) {
}
class AudioPlayerEventListener {
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
var state: AudioPlayerState? {
didSet {
if let state = state {
@@ -209,29 +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 audioPlayer(secondsElapsed seconds: Double) {
}
func audioPlayer(failedWithError error: Error?) {
}
var seekCompletion: (() -> Void)?
func audioPlayer(seekTo seconds: Int, didFinish: Bool) {
func handleSeek(data: AudioPlayer.SeekEventData) {
seekCompletion?()
}
func audioPlayer(didUpdateDuration duration: Double) {
if let state = self.state {
self.stateUpdate?(state)
}
}
}
@@ -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())
})
})
})
}
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ struct Source {
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())
}
}
+42 -9
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)
@@ -23,13 +23,13 @@ SwiftAudio is available through [CocoaPods](http://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod 'SwiftAudio', '~> 0.6.2'
pod 'SwiftAudio', '~> 0.7.1'
```
### Carthage
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
```ruby
github "jorgenhenrichsen/SwiftAudio" ~> 0.6.2
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).
@@ -43,10 +43,26 @@ let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
player.load(item: audioItem, playWhenReady: true) // Load the item and start playing when the player is ready.
```
Implement `AudioPlayerDelegate` to get notified about useful events and updates to the state of the `AudioPlayer`.
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 asubclass of `AudioPlayer` that maintains a queue of audio tracks.
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)
@@ -82,11 +98,11 @@ Current options for configuring the `AudioPlayer`:
### Audio Session
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()
```
**Important**: If you want audio to continue playing when the app is inactive, remember to activate background audio:
@@ -99,8 +115,25 @@ If you are storing progress for playback time on items when the app quits, it ca
To disable interruption notifcations set `isObservingForInterruptions` to `false`.
### Now Playing Info
The `AudioPlayer` will automatically update the `MPNowPlayingInfoCenter` with artist, title, album, artwork and time if the passed in `AudioItem` supports this. This functionality can be turned off by setting `automaticallyUpdateNowPlayingInfo` to `false`.
If you need to set additional properties for some items, access the player's `NowPlayingInfoController` and call `set(keyValue:)`. Available properties can be found in `NowPlayingInfoProperty`.
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'
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudio'
s.version = '0.6.2'
s.version = '0.7.1'
s.summary = 'Easy audio streaming for iOS'
# This description is used to generate tags and improve search results.
@@ -260,7 +260,7 @@ extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
// MARK: - AVPlayerItemNotificationObserverDelegate
func itemDidPlayToEndTime() {
delegate?.AVWrapper(itemPlaybackDoneWithReason: .playedUntilEnd)
delegate?.AVWrapperItemDidPlayToEndTime()
}
}
@@ -11,10 +11,10 @@ import Foundation
protocol AVPlayerWrapperDelegate: class {
func AVWrapper(didChangeState state: AVPlayerWrapperState)
func AVWrapper(itemPlaybackDoneWithReason: PlaybackEndedReason)
func AVWrapper(secondsElapsed seconds: Double)
func AVWrapper(failedWithError error: Error?)
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
func AVWrapper(didUpdateDuration duration: Double)
func AVWrapperItemDidPlayToEndTime()
}
+77 -31
View File
@@ -10,6 +10,7 @@ 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)
@@ -35,8 +36,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
return _wrapper
}
public let nowPlayingInfoController: NowPlayingInfoController
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
public let remoteCommandController: RemoteCommandController
public let event = EventHolder()
public weak var delegate: AudioPlayerDelegate?
var _currentItem: AudioItem?
@@ -137,7 +140,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
*/
public init(avPlayer: AVPlayer = AVPlayer(),
nowPlayingInfoController: NowPlayingInfoController = NowPlayingInfoController(),
nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
remoteCommandController: RemoteCommandController = RemoteCommandController()) {
self._wrapper = AVPlayerWrapper(avPlayer: avPlayer)
self.nowPlayingInfoController = nowPlayingInfoController
@@ -181,8 +184,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
}
self._currentItem = item
self.updateMetaValues(item: item)
setArtwork(forItem: item)
if (automaticallyUpdateNowPlayingInfo) {
self.loadNowPlayingMetaValues()
}
enableRemoteCommands(forItem: item)
}
@@ -211,15 +216,19 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
Stop playback, resetting the player.
*/
public func stop() {
AVWrapper(itemPlaybackDoneWithReason: .playerStopped)
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) {
if automaticallyUpdateNowPlayingInfo {
self.updateNowPlayingCurrentTime(seconds)
}
self.wrapper.seek(to: seconds)
}
@@ -240,26 +249,54 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
// MARK: - NowPlayingInfo
/// Reload all NowPlayingInfo for the playing item.
public func reloadNowPlayingInfo() {
/**
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 loadNowPlayingMetaValues() {
guard let item = currentItem else { return }
updateMetaValues(item: item)
setArtwork(forItem: item)
updatePlaybackValues()
}
func updateMetaValues(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
@@ -270,13 +307,6 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
}
}
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
func reset() {
@@ -287,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 AVWrapper(itemPlaybackDoneWithReason reason: PlaybackEndedReason) {
self.delegate?.audioPlayer(itemPlaybackEndedWithReason: reason)
}
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)
}
}
+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()
}
}
}
}
@@ -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 {}
@@ -9,45 +9,42 @@ import Foundation
import MediaPlayer
public protocol NowPlayingInfoKeyValue {
func getKey() -> String
func getValue() -> Any?
}
public class NowPlayingInfoController {
public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
let infoCenter: MPNowPlayingInfoCenter
private var _infoCenter: NowPlayingInfoCenter
private var _info: [String: Any] = [:]
var info: [String: Any]
/**
Create a new NowPlayingInfoController.
- parameter infoCenter: The MPNowPlayingInfoCenter to use. Default is `MPNowPlayingInfoCenter.default()`
*/
public init(infoCenter: MPNowPlayingInfoCenter = MPNowPlayingInfoCenter.default()) {
self.infoCenter = infoCenter
self.info = [:]
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
}
/**
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()
_info[keyValue.getKey()] = keyValue.getValue()
}
self._infoCenter.nowPlayingInfo = _info
}
/**
This updates a single value in the now playing info.
*/
public func set(keyValue: NowPlayingInfoKeyValue) {
info[keyValue.getKey()] = keyValue.getValue()
self.infoCenter.nowPlayingInfo = info
_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?
}
+22 -8
View File
@@ -25,6 +25,13 @@ public class QueuedAudioPlayer: AudioPlayer {
return queueManager.current
}
/**
The index of the current item.
*/
public var currentIndex: Int {
return queueManager.currentIndex
}
/**
Stops the player and clears the queue.
*/
@@ -36,6 +43,13 @@ public class QueuedAudioPlayer: AudioPlayer {
queueManager.clearQueue()
}
/**
All items currently in the queue.
*/
public var items: [AudioItem] {
return queueManager.items
}
/**
The previous items held by the queue.
*/
@@ -105,7 +119,8 @@ public class QueuedAudioPlayer: AudioPlayer {
- throws: `APError`
*/
public func next() throws {
AVWrapper(itemPlaybackDoneWithReason: .skippedToNext)
event.playbackEnd.emit(data: .skippedToNext)
delegate?.audioPlayer(itemPlaybackEndedWithReason: .skippedToNext)
let nextItem = try queueManager.next()
try self.load(item: nextItem, playWhenReady: true)
}
@@ -114,7 +129,8 @@ public class QueuedAudioPlayer: AudioPlayer {
Step to the previous item in the queue.
*/
public func previous() throws {
AVWrapper(itemPlaybackDoneWithReason: .skippedToPrevious)
event.playbackEnd.emit(data: .skippedToPrevious)
delegate?.audioPlayer(itemPlaybackEndedWithReason: .skippedToPrevious)
let previousItem = try queueManager.previous()
try self.load(item: previousItem, playWhenReady: true)
}
@@ -137,8 +153,8 @@ public class QueuedAudioPlayer: AudioPlayer {
- throws: `APError`
*/
public func jumpToItem(atIndex index: Int, playWhenReady: Bool = true) throws {
AVWrapper(itemPlaybackDoneWithReason: .jumpedToIndex)
event.playbackEnd.emit(data: .jumpedToIndex)
delegate?.audioPlayer(itemPlaybackEndedWithReason: .jumpedToIndex)
let item = try queueManager.jump(to: index)
try self.load(item: item, playWhenReady: playWhenReady)
}
@@ -170,10 +186,8 @@ public class QueuedAudioPlayer: AudioPlayer {
// MARK: - AVPlayerWrapperDelegate
override func AVWrapper(itemPlaybackDoneWithReason reason: PlaybackEndedReason) {
super.AVWrapper(itemPlaybackDoneWithReason: reason)
guard reason == .playedUntilEnd else { return }
override func AVWrapperItemDidPlayToEndTime() {
super.AVWrapperItemDidPlayToEndTime()
if automaticallyPlayNextSong {
try? self.next()
}