Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8026d0915 | |||
| 9d6534f2c9 | |||
| c55c25686c | |||
| 852578c914 | |||
| 0ae0427556 | |||
| 9170be5e36 | |||
| 3930096357 | |||
| 9e8ebdfca1 | |||
| 31f4745bae | |||
| 4a8af1bce3 | |||
| 8e6b72613d | |||
| ae760c4cac | |||
| b3856e0a7a | |||
| 757e5f476c | |||
| 1f5eae407d | |||
| 29660371c9 | |||
| c2530a0193 | |||
| 6804cf5163 | |||
| 6ea2e082b0 | |||
| b6b4599ef5 | |||
| 3ccf9ed20e | |||
| d6eb187788 | |||
| 1febb782d8 | |||
| c4c6f42ac0 | |||
| d8b4466629 | |||
| cb9dec49b2 | |||
| 7e46a91e73 | |||
| 8ca4a873a5 | |||
| 3f54de70bc | |||
| cae549a401 | |||
| 85fecef45e | |||
| e2375f7a82 | |||
| 3c8402503b | |||
| 4c46137498 | |||
| a4e023d75f | |||
| fe549a522a | |||
| 6cc0638a70 | |||
| 34c1f493ae | |||
| 30750a0c81 | |||
| e57cf9d2e5 | |||
| 0eeedc6467 | |||
| e96cbf6337 | |||
| debc1c519f | |||
| 09bec023a9 | |||
| 44e022389c | |||
| 05ca97b8eb | |||
| f6ff2b4cc0 | |||
| 9e3f5c0291 | |||
| 8ba40ca45f | |||
| 9919bde9fe | |||
| 3ba62d8657 | |||
| 0f283b171f | |||
| f6b5e30e85 | |||
| 04296fa681 | |||
| 252ed947d2 | |||
| b5fdb5c54e | |||
| 69d3a9c0c0 | |||
| 43821c68a9 | |||
| 610ff4c7f3 | |||
| 1fc533214f | |||
| 71f22c3e25 |
+1
-1
@@ -1 +1 @@
|
||||
4.0
|
||||
4.2
|
||||
|
||||
-17
@@ -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'
|
||||
+23
-6
@@ -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,7 +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 */; };
|
||||
07756B73218C2D590023935E /* AudioSessionCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07756B70218C2D590023935E /* AudioSessionCategory.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"; }; };
|
||||
@@ -184,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>"; };
|
||||
@@ -191,7 +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>"; };
|
||||
07756B70218C2D590023935E /* AudioSessionCategory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioSessionCategory.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>"; };
|
||||
@@ -401,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;
|
||||
@@ -420,7 +429,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
07756B72218C2D590023935E /* AudioSessionController.swift */,
|
||||
07756B70218C2D590023935E /* AudioSessionCategory.swift */,
|
||||
07756B71218C2D590023935E /* AudioSession.swift */,
|
||||
);
|
||||
name = AudioSessionController;
|
||||
@@ -712,6 +720,7 @@
|
||||
0A362FBD9BA860D03408EC59B8DAD715 /* QueuedAudioPlayer.swift */,
|
||||
7D4763A422F83644D194E97981775831 /* QueueManager.swift */,
|
||||
EF4534C7E47CA5B502A19513117CBFC3 /* TimeEventFrequency.swift */,
|
||||
076DFC5C2233CB1700A8D163 /* Event.swift */,
|
||||
07756B6F218C2D590023935E /* AudioSessionController */,
|
||||
075131B2218322E000D3BFB9 /* NowPlayingInfoController */,
|
||||
075131B6218322E000D3BFB9 /* RemoteCommandController */,
|
||||
@@ -898,6 +907,11 @@
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0930;
|
||||
LastUpgradeCheck = 0930;
|
||||
TargetAttributes = {
|
||||
1AB61EB02FDF0033DCB1F8416419F110 = {
|
||||
LastSwiftMigration = 1010;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
@@ -942,19 +956,21 @@
|
||||
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 */,
|
||||
075131B9218322E000D3BFB9 /* MediaItemProperty.swift in Sources */,
|
||||
07756B73218C2D590023935E /* AudioSessionCategory.swift in Sources */,
|
||||
121F6F4C3C3FBD49FBFF3BC91205C11B /* AVPlayerItemNotificationObserver.swift in Sources */,
|
||||
E10CAB33DDA27C101EF36E8923046511 /* AVPlayerItemObserver.swift in Sources */,
|
||||
13AFC3FFA6B2175A542C1279345CAAA3 /* AVPlayerObserver.swift in Sources */,
|
||||
C74928AC19FB27AA9D7AC4FC94A2D7F1 /* AVPlayerTimeObserver.swift in Sources */,
|
||||
07756B74218C2D590023935E /* AudioSession.swift in Sources */,
|
||||
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 */,
|
||||
@@ -963,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;
|
||||
@@ -1229,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 = "";
|
||||
@@ -1449,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";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0930"
|
||||
LastUpgradeVersion = "1010"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -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 */,
|
||||
);
|
||||
@@ -286,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;
|
||||
@@ -302,7 +317,7 @@
|
||||
607FACE41AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
DevelopmentTeam = HPNZWPB9JK;
|
||||
LastSwiftMigration = 0900;
|
||||
LastSwiftMigration = 1010;
|
||||
TestTargetID = 607FACCF1AFB9204008FA782;
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -507,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;
|
||||
@@ -560,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;
|
||||
@@ -606,8 +630,7 @@
|
||||
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;
|
||||
@@ -624,8 +647,7 @@
|
||||
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;
|
||||
@@ -647,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;
|
||||
@@ -666,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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -17,10 +17,10 @@ class AudioController {
|
||||
let audioSessionController = AudioSessionController.shared
|
||||
|
||||
let sources: [AudioItem] = [
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/67b51d90ffddd6bb3f095059997021b589845f81?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "33 \"GOD\"", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/6f9999d909b017eabef97234dd7a206355720d9d?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "715 - CRΣΣKS", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/bf9bdd403c67fdbe06a582e7b292487c8cfd1f7e?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "____45_____", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI"))
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/67b51d90ffddd6bb3f095059997021b589845f81?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "33 \"GOD\"", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/6f9999d909b017eabef97234dd7a206355720d9d?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "715 - CRΣΣKS", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/bf9bdd403c67fdbe06a582e7b292487c8cfd1f7e?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "____45_____", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI"))
|
||||
]
|
||||
|
||||
init() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -30,10 +30,6 @@ class AVPlayerWrapperTests: QuickSpec {
|
||||
beforeEach {
|
||||
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))
|
||||
@@ -128,6 +124,17 @@ class AVPlayerWrapperTests: QuickSpec {
|
||||
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", {
|
||||
@@ -137,7 +144,7 @@ class AVPlayerWrapperTests: QuickSpec {
|
||||
|
||||
context("when loading source", {
|
||||
beforeEach {
|
||||
wrapper.load(from: URL(fileURLWithPath: 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))
|
||||
@@ -151,18 +158,27 @@ class AVPlayerWrapperTests: QuickSpec {
|
||||
})
|
||||
|
||||
context("when seeking to a time", {
|
||||
var passed = false
|
||||
let holder = AVPlayerWrapperDelegateHolder()
|
||||
let seekTime: TimeInterval = 0.5
|
||||
beforeEach {
|
||||
wrapper.delegate = holder
|
||||
holder.seekCompletion = { passed = true }
|
||||
wrapper.load(from: Source.url, playWhenReady: false)
|
||||
wrapper.seek(to: seekTime)
|
||||
}
|
||||
|
||||
it("should eventually be equal to the seeked time", closure: {
|
||||
expect(passed).toEventually(beTrue())
|
||||
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))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -219,7 +235,7 @@ class AVPlayerWrapperTests: QuickSpec {
|
||||
}
|
||||
|
||||
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
|
||||
func AVWrapper(itemPlaybackDoneWithReason reason: PlaybackEndedReason) {
|
||||
func AVWrapperItemDidPlayToEndTime() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudio
|
||||
|
||||
@@ -43,14 +44,12 @@ 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.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
@@ -62,13 +61,12 @@ 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.load(item: Source.getAudioItem(), playWhenReady: true)
|
||||
@@ -80,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()
|
||||
}
|
||||
@@ -105,18 +102,26 @@ class AudioPlayerTests: QuickSpec {
|
||||
})
|
||||
|
||||
context("when seeking to a time", {
|
||||
var passed = false
|
||||
let holder = AudioPlayerDelegateHolder()
|
||||
let seekTime: TimeInterval = 0.5
|
||||
let seekTime: TimeInterval = 1.0
|
||||
beforeEach {
|
||||
audioPlayer.delegate = holder
|
||||
holder.seekCompletion = { passed = true }
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
audioPlayer.seek(to: seekTime)
|
||||
}
|
||||
|
||||
it("should eventually be equal to the seeked time", closure: {
|
||||
expect(passed).toEventually(beTrue())
|
||||
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()))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -151,19 +156,42 @@ class AudioPlayerTests: QuickSpec {
|
||||
expect(audioPlayer.currentItem).toNot(beNil())
|
||||
})
|
||||
})
|
||||
|
||||
context("when setting the timePitchAlgorithm", {
|
||||
|
||||
beforeEach {
|
||||
audioPlayer.audioTimePitchAlgorithm = .timeDomain
|
||||
}
|
||||
|
||||
context("then loading an item", {
|
||||
beforeEach {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
}
|
||||
|
||||
it("should have the applied timePitchAlgorithm", closure: {
|
||||
expect(audioPlayer.wrapper.currentItem?.audioTimePitchAlgorithm).to(equal(AVAudioTimePitchAlgorithm.timeDomain))
|
||||
})
|
||||
})
|
||||
|
||||
context("then loading a timepitching item", {
|
||||
beforeEach {
|
||||
let item = DefaultAudioItemTimePitching(audioUrl: Source.path, artist: nil, title: nil, albumTitle: nil, sourceType: .file, artwork: nil, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm.spectral)
|
||||
try? audioPlayer.load(item: item, playWhenReady: false)
|
||||
}
|
||||
|
||||
it("should have the applied timePitchAlgorithm", closure: {
|
||||
expect(audioPlayer.wrapper.currentItem?.audioTimePitchAlgorithm).to(equal(AVAudioTimePitchAlgorithm.spectral))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AudioPlayerDelegateHolder: AudioPlayerDelegate {
|
||||
func audioPlayer(itemPlaybackEndedWithReason reason: PlaybackEndedReason) {
|
||||
|
||||
}
|
||||
class AudioPlayerEventListener {
|
||||
|
||||
|
||||
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
|
||||
var state: AudioPlayerState? {
|
||||
didSet {
|
||||
if let state = state {
|
||||
@@ -172,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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -92,9 +92,9 @@ class AudioSessionControllerTests: QuickSpec {
|
||||
|
||||
class AudioSessionControllerDelegateImplementation: AudioSessionControllerDelegate {
|
||||
|
||||
var interruptionType: AVAudioSessionInterruptionType? = nil
|
||||
var interruptionType: AVAudioSession.InterruptionType? = nil
|
||||
|
||||
func handleInterruption(type: AVAudioSessionInterruptionType) {
|
||||
func handleInterruption(type: AVAudioSession.InterruptionType) {
|
||||
self.interruptionType = type
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,31 +14,43 @@ import AVFoundation
|
||||
|
||||
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
|
||||
|
||||
var availableCategories: [String] = []
|
||||
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws {}
|
||||
|
||||
func setCategory(_ category: String) throws {}
|
||||
|
||||
func setCategory(_ category: String, mode: String, options: AVAudioSessionCategoryOptions) 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, with options: AVAudioSessionSetActiveOptions) 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
|
||||
|
||||
var availableCategories: [String] = []
|
||||
|
||||
func setCategory(_ category: String) throws {
|
||||
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws {
|
||||
throw AVError(AVError.unknown)
|
||||
}
|
||||
|
||||
func setCategory(_ category: String, mode: String, options: AVAudioSessionCategoryOptions) throws {
|
||||
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, policy: AVAudioSession.RouteSharingPolicy, options: AVAudioSession.CategoryOptions) throws {
|
||||
throw AVError(AVError.unknown)
|
||||
}
|
||||
|
||||
@@ -46,7 +58,7 @@ class FailingAudioSession: AudioSession {
|
||||
throw AVError(AVError.unknown)
|
||||
}
|
||||
|
||||
func setActive(_ active: Bool, with options: AVAudioSessionSetActiveOptions) throws {
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -83,7 +83,7 @@ class QueueManagerTests: QuickSpec {
|
||||
|
||||
context("then replacing the item", closure: {
|
||||
beforeEach {
|
||||
try? manager.replaceCurrentItem(with: 1)
|
||||
manager.replaceCurrentItem(with: 1)
|
||||
}
|
||||
it("should have replaced the current item", closure: {
|
||||
expect(manager.current).to(equal(1))
|
||||
@@ -217,7 +217,7 @@ class QueueManagerTests: QuickSpec {
|
||||
var removed: Int?
|
||||
var initialCurrentIndex: Int!
|
||||
beforeEach {
|
||||
try? manager.jump(to: 3)
|
||||
let _ = try? manager.jump(to: 3)
|
||||
initialCurrentIndex = manager.currentIndex
|
||||
removed = try? manager.removeItem(at: initialCurrentIndex - 1)
|
||||
}
|
||||
|
||||
@@ -14,15 +14,24 @@ struct Source {
|
||||
static let url: URL = URL(fileURLWithPath: Source.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: Source.path, sourceType: .file, pitchAlgorithmType: .lowQualityZeroLatency)
|
||||
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: Source.path)
|
||||
static let url: URL = URL(fileURLWithPath: ShortSource.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: ShortSource.path, sourceType: .file, pitchAlgorithmType: .lowQualityZeroLatency)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# SwiftAudio
|
||||
|
||||
[](https://travis-ci.org/jorgenhenrichsen/SwiftAudio)
|
||||
[](https://app.bitrise.io/app/3d3ac2ba8d817235)
|
||||
[](http://cocoapods.org/pods/SwiftAudio)
|
||||
[](https://codecov.io/gh/jorgenhenrichsen/SwiftAudio)
|
||||
[](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.4.4'
|
||||
pod 'SwiftAudio', '~> 0.7.2'
|
||||
```
|
||||
|
||||
### Carthage
|
||||
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
|
||||
```ruby
|
||||
github "jorgenhenrichsen/SwiftAudio" ~> 0.4.4
|
||||
github "jorgenhenrichsen/SwiftAudio" ~> 0.7.2
|
||||
```
|
||||
Then follow the rest of Carthage instructions on [adding a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
|
||||
|
||||
@@ -39,17 +39,33 @@ Then follow the rest of Carthage instructions on [adding a framework](https://gi
|
||||
To get started playing some audio:
|
||||
```swift
|
||||
let player = AudioPlayer()
|
||||
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency)
|
||||
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, pitchAlgorithmType: .lowQualityZeroLatency)
|
||||
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
|
||||
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.
|
||||
```
|
||||
|
||||
@@ -77,15 +93,16 @@ Current options for configuring the `AudioPlayer`:
|
||||
- `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 `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:
|
||||
@@ -98,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'
|
||||
@@ -125,6 +159,9 @@ player.remoteCommandController.handlePlayCommand = { (event) in
|
||||
```
|
||||
All available overrides can be found by looking at `RemoteCommandController`.
|
||||
|
||||
### 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
|
||||
|
||||
Jørgen Henrichsen
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudio'
|
||||
s.version = '0.4.4'
|
||||
s.version = '0.7.2'
|
||||
s.summary = 'Easy audio streaming for iOS'
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
|
||||
@@ -36,6 +36,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
True if the last call to load(from:playWhenReady) had playWhenReady=true.
|
||||
*/
|
||||
fileprivate var _playWhenReady: Bool = true
|
||||
fileprivate var _initialTime: TimeInterval?
|
||||
|
||||
fileprivate var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
|
||||
didSet {
|
||||
@@ -85,7 +86,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
}
|
||||
|
||||
var duration: TimeInterval {
|
||||
if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
|
||||
if let seconds = currentItem?.asset.duration.seconds, !seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
else if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
else if let seconds = currentItem?.loadedTimeRanges.first?.timeRangeValue.duration.seconds,
|
||||
@@ -95,6 +99,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
var bufferedPosition: TimeInterval {
|
||||
return currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
|
||||
}
|
||||
|
||||
weak var delegate: AVPlayerWrapperDelegate? = nil
|
||||
|
||||
var bufferDuration: TimeInterval = 0
|
||||
@@ -143,7 +151,13 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
}
|
||||
|
||||
func seek(to seconds: TimeInterval) {
|
||||
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, 1)) { (finished) in
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -151,7 +165,6 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
func load(from url: URL, playWhenReady: Bool) {
|
||||
reset(soft: true)
|
||||
_playWhenReady = playWhenReady
|
||||
_state = .loading
|
||||
|
||||
// Set item
|
||||
let currentAsset = AVURLAsset(url: url)
|
||||
@@ -166,6 +179,12 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
playerItemObserver.startObserving(item: currentItem)
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -184,7 +203,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
|
||||
// MARK: - AVPlayerObserverDelegate
|
||||
|
||||
func player(didChangeTimeControlStatus status: AVPlayerTimeControlStatus) {
|
||||
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
|
||||
switch status {
|
||||
case .paused:
|
||||
if currentItem == nil {
|
||||
@@ -200,14 +219,19 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func player(statusDidChange status: AVPlayerStatus) {
|
||||
func player(statusDidChange status: AVPlayer.Status) {
|
||||
switch status {
|
||||
|
||||
|
||||
case .readyToPlay:
|
||||
self._state = .ready
|
||||
if _playWhenReady {
|
||||
|
||||
if let initialTime = _initialTime {
|
||||
self.seek(to: initialTime)
|
||||
}
|
||||
else if _playWhenReady {
|
||||
self.play()
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case .failed:
|
||||
@@ -240,7 +264,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()
|
||||
|
||||
}
|
||||
|
||||
Regular → Executable
+6
-2
@@ -19,10 +19,12 @@ protocol AVPlayerWrapperProtocol {
|
||||
|
||||
var duration: TimeInterval { get }
|
||||
|
||||
var bufferedPosition: TimeInterval { get }
|
||||
|
||||
var reasonForWaitingToPlay: AVPlayer.WaitingReason? { get }
|
||||
|
||||
var rate: Float { get set }
|
||||
|
||||
var rate: Float { get set }
|
||||
|
||||
var delegate: AVPlayerWrapperDelegate? { get set }
|
||||
|
||||
@@ -35,7 +37,7 @@ protocol AVPlayerWrapperProtocol {
|
||||
var isMuted: Bool { get set }
|
||||
|
||||
var automaticallyWaitsToMinimizeStalling: Bool { get set }
|
||||
|
||||
|
||||
|
||||
func play()
|
||||
|
||||
@@ -49,4 +51,6 @@ protocol AVPlayerWrapperProtocol {
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool)
|
||||
|
||||
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?)
|
||||
|
||||
}
|
||||
|
||||
@@ -20,14 +20,24 @@ public protocol AudioItem {
|
||||
func getTitle() -> String?
|
||||
func getAlbumTitle() -> String?
|
||||
func getSourceType() -> SourceType
|
||||
func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm
|
||||
func getArtwork(_ handler: @escaping (UIImage?) -> Void)
|
||||
|
||||
}
|
||||
|
||||
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?
|
||||
@@ -38,17 +48,14 @@ public struct DefaultAudioItem: AudioItem {
|
||||
|
||||
public var sourceType: SourceType
|
||||
|
||||
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
|
||||
|
||||
public var artwork: UIImage?
|
||||
|
||||
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, pitchAlgorithmType: AVAudioTimePitchAlgorithm, artwork: UIImage? = nil) {
|
||||
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, artwork: UIImage? = nil) {
|
||||
self.audioUrl = audioUrl
|
||||
self.artist = artist
|
||||
self.title = title
|
||||
self.albumTitle = albumTitle
|
||||
self.sourceType = sourceType
|
||||
self.pitchAlgorithmType = pitchAlgorithmType
|
||||
self.artwork = artwork
|
||||
}
|
||||
|
||||
@@ -71,13 +78,50 @@ public struct DefaultAudioItem: AudioItem {
|
||||
public func getSourceType() -> SourceType {
|
||||
return sourceType
|
||||
}
|
||||
|
||||
public func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm {
|
||||
return pitchAlgorithmType
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -28,10 +29,17 @@ public protocol AudioPlayerDelegate: class {
|
||||
|
||||
public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
|
||||
private var wrapper: AVPlayerWrapperProtocol
|
||||
private var _wrapper: AVPlayerWrapperProtocol
|
||||
|
||||
public let nowPlayingInfoController: NowPlayingInfoController
|
||||
/// The wrapper around the underlying AVPlayer
|
||||
var wrapper: AVPlayerWrapperProtocol {
|
||||
return _wrapper
|
||||
}
|
||||
|
||||
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
|
||||
public let remoteCommandController: RemoteCommandController
|
||||
|
||||
public let event = EventHolder()
|
||||
public weak var delegate: AudioPlayerDelegate?
|
||||
|
||||
var _currentItem: AudioItem?
|
||||
@@ -44,6 +52,12 @@ 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
|
||||
*/
|
||||
@@ -66,6 +80,13 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
return wrapper.duration
|
||||
}
|
||||
|
||||
/**
|
||||
The bufferedPosition of the current AudioItem.
|
||||
*/
|
||||
public var bufferedPosition: Double {
|
||||
return wrapper.bufferedPosition
|
||||
}
|
||||
|
||||
/**
|
||||
The current state of the underlying `AudioPlayer`.
|
||||
*/
|
||||
@@ -84,7 +105,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
*/
|
||||
public var bufferDuration: TimeInterval {
|
||||
get { return wrapper.bufferDuration }
|
||||
set { wrapper.bufferDuration = newValue }
|
||||
set { _wrapper.bufferDuration = newValue }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +113,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
*/
|
||||
public var timeEventFrequency: TimeEventFrequency {
|
||||
get { return wrapper.timeEventFrequency }
|
||||
set { wrapper.timeEventFrequency = newValue }
|
||||
set { _wrapper.timeEventFrequency = newValue }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,22 +121,22 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
*/
|
||||
public var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { return wrapper.automaticallyWaitsToMinimizeStalling }
|
||||
set { wrapper.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
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 }
|
||||
set { _wrapper.isMuted = newValue }
|
||||
}
|
||||
|
||||
public var rate: Float {
|
||||
get { return wrapper.rate }
|
||||
set { wrapper.rate = newValue }
|
||||
set { _wrapper.rate = newValue }
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
@@ -126,13 +147,13 @@ 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._wrapper = AVPlayerWrapper(avPlayer: avPlayer)
|
||||
self.nowPlayingInfoController = nowPlayingInfoController
|
||||
self.remoteCommandController = remoteCommandController
|
||||
|
||||
self.wrapper.delegate = self
|
||||
self._wrapper.delegate = self
|
||||
self.remoteCommandController.audioPlayer = self
|
||||
}
|
||||
|
||||
@@ -145,24 +166,35 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
- 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 {
|
||||
print("Loading: \(item)")
|
||||
let url: URL
|
||||
switch item.getSourceType() {
|
||||
case .stream:
|
||||
if let url = URL(string: item.getSourceUrl()) {
|
||||
wrapper.load(from: url, playWhenReady: playWhenReady)
|
||||
if let itemUrl = URL(string: item.getSourceUrl()) {
|
||||
url = itemUrl
|
||||
}
|
||||
else {
|
||||
throw APError.LoadError.invalidSourceUrl(item.getSourceUrl())
|
||||
}
|
||||
case .file:
|
||||
wrapper.load(from: URL(fileURLWithPath: item.getSourceUrl()), playWhenReady: playWhenReady)
|
||||
url = URL(fileURLWithPath: item.getSourceUrl())
|
||||
}
|
||||
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
|
||||
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
|
||||
self.updateMetaValues(item: item)
|
||||
setArtwork(forItem: item)
|
||||
|
||||
if (automaticallyUpdateNowPlayingInfo) {
|
||||
self.loadNowPlayingMetaValues()
|
||||
}
|
||||
enableRemoteCommands(forItem: item)
|
||||
}
|
||||
|
||||
@@ -191,15 +223,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)
|
||||
}
|
||||
|
||||
@@ -220,26 +256,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
|
||||
@@ -250,13 +314,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() {
|
||||
@@ -267,31 +324,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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,16 +13,21 @@ protocol AudioSession {
|
||||
|
||||
var isOtherAudioPlaying: Bool { get }
|
||||
|
||||
var availableCategories: [String] { get }
|
||||
var category: AVAudioSession.Category { get }
|
||||
|
||||
var mode: AVAudioSession.Mode { get }
|
||||
|
||||
func setCategory(_ category: String) throws
|
||||
var categoryOptions: AVAudioSession.CategoryOptions { get }
|
||||
|
||||
func setCategory(_ category: String, mode: String, options: AVAudioSessionCategoryOptions) throws
|
||||
var availableCategories: [AVAudioSession.Category] { get }
|
||||
|
||||
func setActive(_ active: Bool) throws
|
||||
@available(iOS 10.0, *)
|
||||
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws
|
||||
|
||||
func setActive(_ active: Bool, with options: AVAudioSessionSetActiveOptions) 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
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// AudioSessionCategory.swift
|
||||
// SwiftAudio
|
||||
//
|
||||
// Created by Jørgen Henrichsen on 02/11/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
|
||||
/**
|
||||
An enum wrapper around the AVAudioSessionCategories.
|
||||
For detailed info about the categories, see: [AudioSession Programming Guide](https://developer.apple.com/library/content/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioSessionCategoriesandModes/AudioSessionCategoriesandModes.html#//apple_ref/doc/uid/TP40007875-CH10)
|
||||
*/
|
||||
public enum AudioSessionCategory {
|
||||
|
||||
case ambient
|
||||
|
||||
case soloAmbient
|
||||
|
||||
case playback
|
||||
|
||||
case record
|
||||
|
||||
case playAndRecord
|
||||
|
||||
case multiRoute
|
||||
|
||||
func getValue() -> String {
|
||||
switch self {
|
||||
|
||||
case .ambient:
|
||||
return AVAudioSessionCategoryAmbient
|
||||
|
||||
case .soloAmbient:
|
||||
return AVAudioSessionCategorySoloAmbient
|
||||
|
||||
case .playback:
|
||||
return AVAudioSessionCategoryPlayback
|
||||
|
||||
case .record:
|
||||
return AVAudioSessionCategoryRecord
|
||||
|
||||
case .playAndRecord:
|
||||
return AVAudioSessionCategoryPlayAndRecord
|
||||
|
||||
case .multiRoute:
|
||||
return AVAudioSessionCategoryMultiRoute
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import AVFoundation
|
||||
|
||||
|
||||
public protocol AudioSessionControllerDelegate: class {
|
||||
func handleInterruption(type: AVAudioSessionInterruptionType)
|
||||
func handleInterruption(type: AVAudioSession.InterruptionType)
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ public class AudioSessionController {
|
||||
|
||||
public func activateSession() throws {
|
||||
do {
|
||||
try audioSession.setActive(true)
|
||||
try audioSession.setActive(true, options: [])
|
||||
audioSessionIsActive = true
|
||||
}
|
||||
catch let error { throw error }
|
||||
@@ -80,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
|
||||
@@ -98,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
|
||||
}
|
||||
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user