Compare commits

...

54 Commits

Author SHA1 Message Date
Tanha 6276e97c4c Release 1.3.0 2019-11-25 20:58:29 -08:00
Tanha 09142ce2d4 update to Swift 4.2 2019-11-25 20:57:54 -08:00
Tanha 90bc2262ec Release 1.2.0 2019-11-25 15:12:55 -08:00
Tanha 9594449215 nit 2019-11-25 15:05:49 -08:00
Tanha 6187c9f438 fix playing on seek 2019-11-25 14:30:30 -08:00
Tanha b28e815545 rename clean up 2019-11-25 13:49:06 -08:00
Tanha 17be73bbe8 fix fatal bug seeking 2019-11-25 13:44:54 -08:00
tanhakabir cd35f38db1 Merge pull request #13 from tanhakabir/fix_ui_seeking_bug
Fix ui when seeking to show "Loading" when not enough buffers ready
2019-11-25 10:44:28 -08:00
cendolinside123 3c752d581d Fix miniplayer on background (#12)
* 1. fix AVAudioSession configuration

* 1. fix seek bar didn't update on miniplayer on lockscreen when playing a song
2. setup miniplayer lockscreen seek bar speed (when slide  rate slider)

* setup project example to playing on the background
2019-11-25 10:44:00 -08:00
Tanha 1f20a48a20 fix example app bug 2019-11-25 10:37:11 -08:00
Tanha 3a585c1f43 fix playing status bug in disk engine 2019-11-25 10:30:06 -08:00
Tanha 5ac5b93ac4 separate enum to another file 2019-11-24 23:27:51 -08:00
Tanha b484f0bfb6 fix playing status when seeking 2019-11-24 23:24:25 -08:00
Tanha 0aeb8b0f88 change boolean playing status to enum 2019-11-21 01:46:42 -08:00
Tanha 8e7357860c shouldnot be forcing play 2019-11-21 01:24:45 -08:00
Tanha 936de8c996 minor fix 2019-11-20 23:10:03 -08:00
Tanha e986be9db5 clean up example app 2019-11-20 22:27:31 -08:00
Tanha 876d517f3d Release 1.1.1 2019-11-20 22:15:51 -08:00
Tanha 0a12c68274 Fix fatal error when seeking on streamed audio 2019-11-20 22:15:32 -08:00
cendolinside123 873e537301 fix seek bar on example app's player (#9) 2019-11-20 22:09:11 -08:00
Tanha 94c1a47641 Release 1.1.0 2019-11-20 16:36:58 -08:00
tanhakabir d0296ab012 Fix issue on streaming where it gets stuck in paused state and error of no more data to parse (#8)
* switch out audio clip for soundbite

* Fix being stuck in state of needing more data from the throttler
2019-11-20 16:35:31 -08:00
tanhakabir 2fd944d88e Fix play/pausing issue for saved audio (#7)
* update callback guards for updates for saved audio

* Fix play/pausing bug for saved audio
2019-11-20 13:38:52 -08:00
tanhakabir fc98c4c1c4 add separation of disk engine in SAPlayer, first iteration (#6) 2019-11-19 16:25:29 -08:00
Tanha 8bf6cbb56e Release 1.0.3 2019-11-18 13:46:50 -08:00
Tanha b97f97ca5e Fix fatal error on iOS 10.0
close #3
2019-11-18 13:39:06 -08:00
Tanha 0c7bcdcf90 Fix issue on example app that prevented downloaded audio being playable 2019-11-18 11:42:02 -08:00
Tanha 840122e603 remove build badge from README 2019-04-27 23:00:46 -07:00
Tanha 8518d10c6d v1.0.2 2019-04-27 22:56:57 -07:00
Tanha f214be28a9 nit 2019-04-27 22:37:17 -07:00
Tanha f219d9d1a0 nit 2019-04-27 22:36:37 -07:00
Tanha 8797c0d917 add API documentation for Downloader 2019-04-27 22:35:21 -07:00
Tanha 0121d05dff refractor deletion of downloaded files 2019-04-27 21:54:42 -07:00
Tanha 26faf62657 documentation for downloading 2019-04-27 21:42:12 -07:00
Tanha 61e79d067a ensure cancelling download also removed from queued downloads 2019-04-27 20:46:53 -07:00
Tanha 103838d1b8 add UI to see where file is saved on device 2019-04-27 19:43:43 -07:00
Tanha 47de2a5251 fix double download bug 2019-04-27 19:40:48 -07:00
Tanha d4d8f767e3 document downloading audio 2019-04-27 18:59:14 -07:00
tanhakabir c75da619cf Merge pull request #2 from tanhakabir/refractor_downloaded_audio
Refractor downloaded audio
2019-04-22 15:30:51 -07:00
Tanha aea6f5efaa add completion handler for individual entities to receive when download complete upon calling start 2019-04-22 15:30:11 -07:00
Tanha 2625b8f4db remove unused resume data in download worker 2019-04-10 14:45:56 -07:00
Tanha e6460513ea start piping for passing completion handlers for downloads 2019-02-28 14:45:50 -08:00
tanhakabir a2504f2726 Update README.md 2019-02-25 15:56:10 -08:00
Tanha 23f445ce4d seperate downloader from rest of SAPlayer implementation 2019-02-25 01:33:18 -08:00
Tanha 61fe0c6ebb nit 2019-02-24 23:24:06 -08:00
Tanha 72c4335386 nit 2019-02-24 23:23:57 -08:00
Tanha 640f0b92f0 make lockscreen artwork optional 2019-02-24 23:03:43 -08:00
Tanha c0f8db29c0 nit spelling 2019-02-24 21:36:46 -08:00
Tanha 285cd92514 nit 2019-02-24 21:35:10 -08:00
Tanha a5293a5b39 nit 2019-02-24 21:31:59 -08:00
Tanha 8430a7e8ce nit 2019-02-24 21:31:04 -08:00
Tanha 34e430713b nit 2019-02-24 21:25:10 -08:00
Tanha d23a5f8d62 Update README with Updates API 2019-02-24 21:24:04 -08:00
Tanha 9f89944bc5 nit 2019-02-24 20:40:32 -08:00
32 changed files with 613 additions and 156 deletions
+13 -2
View File
@@ -14,6 +14,7 @@
79D8DF73FA7CDD6E266BAE71D46E035F /* Pods-SwiftAudioPlayer_Tests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 50C71346CE708A211A5AFAC20BAE48CB /* Pods-SwiftAudioPlayer_Tests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
831B263D357A5FA2DDC7B1AE4B374092 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */; };
8F93DB166237195ED222EE55B6404625 /* Pods-SwiftAudioPlayer_Example-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B0B76CB1439F4D361322144E5A65C3A /* Pods-SwiftAudioPlayer_Example-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
A41AA0D2238BB9B600A467E1 /* SAPlayingStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */; };
A4681FC6220113880018AB51 /* SAPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F8D2200E00E0018AB51 /* SAPlayer.swift */; };
A4681FC72201138B0018AB51 /* SAPlayerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F912200E1950018AB51 /* SAPlayerDelegate.swift */; };
A4681FC82201138E0018AB51 /* SAPlayerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F8F2200E1450018AB51 /* SAPlayerPresenter.swift */; };
@@ -42,6 +43,7 @@
A4681FDF220113E20018AB51 /* DirectorThreadSafeClosures.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F872200DAD50018AB51 /* DirectorThreadSafeClosures.swift */; };
A4681FE0220113E40018AB51 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F802200D0500018AB51 /* Log.swift */; };
A4681FE1220113E70018AB51 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F8B2200DDD50018AB51 /* Constants.swift */; };
A4B4CC122223ED2A0045554B /* SAPlayerDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */; };
A4FBA6B2221B538E00D5A353 /* DownloadProgressDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A49B78C3221A78DE00BBA862 /* DownloadProgressDirector.swift */; };
A4FBA6B5221B74C900D5A353 /* SALockScreenInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */; };
A4FBA6B7221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */; };
@@ -94,6 +96,7 @@
99925F09FC9C6EA4B9C0508F4E2D1FE2 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A19C8F889C787C19BE4123C1896AF501 /* Pods-SwiftAudioPlayer_Example-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudioPlayer_Example-resources.sh"; sourceTree = "<group>"; };
A39F2A138CF40C1051CA9E227429A86D /* SwiftAudioPlayer.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SwiftAudioPlayer.modulemap; sourceTree = "<group>"; };
A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayingStatus.swift; sourceTree = "<group>"; };
A4523BC8220A0B3C0079C4BC /* Credited_LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = Credited_LICENSE; sourceTree = "<group>"; };
A4681F802200D0500018AB51 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
A4681F822200D9150018AB51 /* AudioEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioEngine.swift; sourceTree = "<group>"; };
@@ -124,6 +127,7 @@
A4681FBC220100AB0018AB51 /* AudioStreamEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioStreamEngine.swift; sourceTree = "<group>"; };
A4681FBE22010ECF0018AB51 /* LockScreenViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenViewProtocol.swift; sourceTree = "<group>"; };
A49B78C3221A78DE00BBA862 /* DownloadProgressDirector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadProgressDirector.swift; sourceTree = "<group>"; };
A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerDownloader.swift; sourceTree = "<group>"; };
A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SALockScreenInfo.swift; sourceTree = "<group>"; };
A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerUpdateSubscription.swift; sourceTree = "<group>"; };
A4FBA6B8221BAF8700D5A353 /* SAAudioAvailabilityRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAAudioAvailabilityRange.swift; sourceTree = "<group>"; };
@@ -276,6 +280,7 @@
A4681F932200E2020018AB51 /* Engine */ = {
isa = PBXGroup;
children = (
A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */,
A4FBA6B8221BAF8700D5A353 /* SAAudioAvailabilityRange.swift */,
A4681F822200D9150018AB51 /* AudioEngine.swift */,
A4681F942200E2220018AB51 /* AudioDiskEngine.swift */,
@@ -344,6 +349,7 @@
A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */,
A4681F8D2200E00E0018AB51 /* SAPlayer.swift */,
A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */,
A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */,
A4681F912200E1950018AB51 /* SAPlayerDelegate.swift */,
A4681F8F2200E1450018AB51 /* SAPlayerPresenter.swift */,
A4681FBE22010ECF0018AB51 /* LockScreenViewProtocol.swift */,
@@ -481,6 +487,9 @@
042ACE071BA515F4DE0E0C8007C3F0EE = {
LastSwiftMigration = 1010;
};
E50DAD13FFD3FC8036073A58BF8423D4 = {
LastSwiftMigration = 1010;
};
};
};
buildConfigurationList = 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */;
@@ -507,6 +516,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A41AA0D2238BB9B600A467E1 /* SAPlayingStatus.swift in Sources */,
A4681FDC220113D70018AB51 /* AudioDownloadWorker.swift in Sources */,
A4681FD8220113C60018AB51 /* AudioDataManager.swift in Sources */,
A4681FD1220113AF0018AB51 /* AudioParsable.swift in Sources */,
@@ -530,6 +540,7 @@
A4681FC82201138E0018AB51 /* SAPlayerPresenter.swift in Sources */,
A4681FD3220113B60018AB51 /* AudioParserPropertyListener.swift in Sources */,
A4681FCA220113940018AB51 /* AudioClockDirector.swift in Sources */,
A4B4CC122223ED2A0045554B /* SAPlayerDownloader.swift in Sources */,
A4681FD0220113A70018AB51 /* AudioConverterErrors.swift in Sources */,
A4FBA6B2221B538E00D5A353 /* DownloadProgressDirector.swift in Sources */,
A4681FD7220113C30018AB51 /* StreamProgressPTO.swift in Sources */,
@@ -665,7 +676,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 = "";
@@ -697,7 +708,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";
@@ -213,12 +213,12 @@
607FACCF1AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = R2392A68YQ;
LastSwiftMigration = 0900;
LastSwiftMigration = 1010;
};
607FACE41AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = R2392A68YQ;
LastSwiftMigration = 0900;
LastSwiftMigration = 1010;
TestTargetID = 607FACCF1AFB9204008FA782;
};
};
@@ -477,8 +477,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;
};
name = Debug;
};
@@ -493,8 +492,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;
};
name = Release;
};
@@ -515,8 +513,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)/SwiftAudioPlayer_Example.app/SwiftAudioPlayer_Example";
};
name = Debug;
@@ -534,8 +531,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)/SwiftAudioPlayer_Example.app/SwiftAudioPlayer_Example";
};
name = Release;
+1 -1
View File
@@ -15,7 +15,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.
return true
}
@@ -32,7 +32,9 @@
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="maximumTrackTintColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<connections>
<action selector="scrubberSeeked:" destination="vXZ-lx-hvc" eventType="valueChanged" id="jDA-wR-wxk"/>
<action selector="scrubberSeeked:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="hTi-fq-lrl"/>
<action selector="scrubberSeeked:" destination="vXZ-lx-hvc" eventType="touchUpOutside" id="mFP-SW-38w"/>
<action selector="scrubberStartedSeeking:" destination="vXZ-lx-hvc" eventType="touchDown" id="UXg-Wf-fKv"/>
</connections>
</slider>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="jUc-tP-CC5">
@@ -57,7 +59,7 @@
</connections>
</button>
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="1" minValue="0.10000000000000001" maxValue="32" translatesAutoresizingMaskIntoConstraints="NO" id="vfk-OJ-S3T">
<rect key="frame" x="14" y="564" width="347" height="31"/>
<rect key="frame" x="14" y="464" width="347" height="31"/>
<connections>
<action selector="rateChanged:" destination="vXZ-lx-hvc" eventType="valueChanged" id="FDJ-jA-bm8"/>
</connections>
@@ -65,7 +67,7 @@
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="joK-xi-MCo">
<rect key="frame" x="16" y="80" width="343" height="29"/>
<segments>
<segment title="20k Hertz"/>
<segment title="Soundbite"/>
<segment title="Acquired"/>
<segment title="Y Combinator"/>
</segments>
@@ -88,7 +90,7 @@
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="rate: 1.0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="yUQ-mI-ozK">
<rect key="frame" x="157" y="535" width="61" height="21"/>
<rect key="frame" x="157" y="435" width="61" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@@ -105,6 +107,12 @@
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="remote url: " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1IX-z5-wWx">
<rect key="frame" x="16" y="207" width="343" height="16"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
@@ -117,8 +125,9 @@
<constraint firstItem="joK-xi-MCo" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="AH1-Uu-eLB"/>
<constraint firstItem="joK-xi-MCo" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" constant="60" id="Ba7-nd-oCD"/>
<constraint firstItem="Urj-Dv-41y" firstAttribute="centerY" secondItem="j3w-gr-HzF" secondAttribute="centerY" id="Fvd-7V-Rr8"/>
<constraint firstItem="1IX-z5-wWx" firstAttribute="leading" secondItem="joK-xi-MCo" secondAttribute="leading" id="GeX-7f-jzu"/>
<constraint firstItem="0QE-3F-a4G" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="jUc-tP-CC5" secondAttribute="trailing" constant="8" symbolic="YES" id="JP5-yW-eVB"/>
<constraint firstItem="yUQ-mI-ozK" firstAttribute="top" secondItem="w2a-RA-zmI" secondAttribute="bottom" constant="200" id="K1K-8N-SpD"/>
<constraint firstItem="yUQ-mI-ozK" firstAttribute="top" secondItem="w2a-RA-zmI" secondAttribute="bottom" constant="100" id="K1K-8N-SpD"/>
<constraint firstItem="vfk-OJ-S3T" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="NOY-IO-NIJ"/>
<constraint firstItem="tFH-sY-Xu9" firstAttribute="centerY" secondItem="jUc-tP-CC5" secondAttribute="centerY" id="Rre-EY-kVY"/>
<constraint firstItem="KDu-ea-kF8" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="78" id="SRU-sX-z5b"/>
@@ -126,12 +135,15 @@
<constraint firstItem="lTK-Hd-Tl2" firstAttribute="top" secondItem="j3w-gr-HzF" secondAttribute="bottom" constant="8" id="Wwx-Uo-yIC"/>
<constraint firstItem="yUQ-mI-ozK" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="a66-h4-WVf"/>
<constraint firstItem="Urj-Dv-41y" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="aKt-EV-Bwd"/>
<constraint firstItem="tFH-sY-Xu9" firstAttribute="top" secondItem="1IX-z5-wWx" secondAttribute="bottom" constant="27" id="bIq-V0-Sac"/>
<constraint firstItem="tFH-sY-Xu9" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="62.5" id="cH6-q6-Lel"/>
<constraint firstItem="jUc-tP-CC5" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="cgM-Nj-yit"/>
<constraint firstItem="KDu-ea-kF8" firstAttribute="top" secondItem="joK-xi-MCo" secondAttribute="bottom" constant="32" id="dLw-rF-Pfb"/>
<constraint firstItem="w2a-RA-zmI" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="daz-b0-eCC"/>
<constraint firstItem="jUc-tP-CC5" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="tFH-sY-Xu9" secondAttribute="trailing" constant="8" symbolic="YES" id="fS9-Ce-4ph"/>
<constraint firstItem="Urj-Dv-41y" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="j3w-gr-HzF" secondAttribute="trailing" constant="8" symbolic="YES" id="fu0-ZZ-rj9"/>
<constraint firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" constant="16" id="gdg-7Y-7la"/>
<constraint firstAttribute="trailing" secondItem="1IX-z5-wWx" secondAttribute="trailing" constant="16" id="hHM-jO-RZd"/>
<constraint firstItem="6d9-Bc-hIz" firstAttribute="top" secondItem="joK-xi-MCo" secondAttribute="bottom" constant="32" id="m9s-An-IWV"/>
<constraint firstItem="vfk-OJ-S3T" firstAttribute="top" secondItem="yUQ-mI-ozK" secondAttribute="bottom" constant="8" id="oaW-rr-UVN"/>
<constraint firstAttribute="trailing" secondItem="0QE-3F-a4G" secondAttribute="trailing" constant="62.5" id="tg1-gr-hdd"/>
@@ -145,6 +157,7 @@
<outlet property="audioSelector" destination="joK-xi-MCo" id="GmY-Xg-be0"/>
<outlet property="bufferProgress" destination="lTK-Hd-Tl2" id="54k-by-qb2"/>
<outlet property="currentTimestampLabel" destination="j3w-gr-HzF" id="5Lh-aS-pat"/>
<outlet property="currentUrlLocationLabel" destination="1IX-z5-wWx" id="MuO-fF-ZxL"/>
<outlet property="downloadButton" destination="KDu-ea-kF8" id="5o4-1h-y06"/>
<outlet property="durationLabel" destination="Urj-Dv-41y" id="mIq-eh-int"/>
<outlet property="playPauseButton" destination="jUc-tP-CC5" id="e9C-zV-A1B"/>
+6
View File
@@ -22,6 +22,12 @@
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
<string>processing</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
+56 -17
View File
@@ -10,32 +10,32 @@ import UIKit
import SwiftAudioPlayer
class ViewController: UIViewController {
struct AudioInfo {
struct AudioInfo: Hashable {
let index: Int
var url: URL {
switch index {
case 0:
return URL(string: "https://traffic.megaphone.fm/TTH7630150098.mp3")!
return URL(string: "https://cdn.fastlearner.media/bensound-rumble.mp3")!
case 1:
return URL(string: "https://chtbl.com/track/18338/traffic.libsyn.com/secure/acquired/acquired_-_armrev_2.mp3?dest-id=376122")!
case 2:
return URL(string: "https://backtracks.fm/ycombinator/pr/0f685f72-29b1-11e9-9bcf-0ece7a7d2472/111---jake-klamka-and-kevin-hale---y-combinator.mp3?s=1&amp;sd=1&amp;u=1549423185")!
default:
return URL(string: "https://traffic.megaphone.fm/TTH7630150098.mp3")!
return URL(string: "https://cdn.fastlearner.media/bensound-rumble.mp3")!
}
}
var title: String {
switch index {
case 0:
return "Twenty Thousand Hertz"
return "Soundbite"
case 1:
return "Acquired"
case 2:
return "Y Combinator"
default:
return "Twenty Thousand Hertz"
return "Soundbite"
}
}
@@ -43,15 +43,23 @@ class ViewController: UIViewController {
let releaseDate: Int = 1550790640
}
var savedUrls: [AudioInfo: URL] = [:]
var selectedAudio: AudioInfo = AudioInfo(index: 0) {
didSet {
if SAPlayer.Downloader.isDownloaded(withRemoteUrl: selectedAudio.url) {
downloadButton.setTitle("Delete downloaded", for: .normal)
streamButton.isEnabled = false
} else {
downloadButton.setTitle("Download", for: .normal)
streamButton.isEnabled = true
}
self.currentUrlLocationLabel.text = "remote url: \(selectedAudio.url.absoluteString)"
}
}
@IBOutlet weak var currentUrlLocationLabel: UILabel!
@IBOutlet weak var bufferProgress: UIProgressView!
@IBOutlet weak var scrubberSlider: UISlider!
@@ -71,6 +79,7 @@ class ViewController: UIViewController {
var isDownloading: Bool = false
var isStreaming: Bool = false
var beingSeeked: Bool = false
var duration: Double = 0.0
@@ -94,17 +103,20 @@ class ViewController: UIViewController {
adjustSpeed()
isPlayable = false
selectedAudio = AudioInfo(index: 0)
_ = SAPlayer.Updates.Duration.subscribe { [weak self] (url, duration) in
guard let self = self else { return }
guard url == self.selectedAudio.url else { return }
guard url == self.selectedAudio.url || url == self.savedUrls[self.selectedAudio] else { return }
self.durationLabel.text = SAPlayer.prettifyTimestamp(duration)
self.duration = duration
}
_ = SAPlayer.Updates.ElapsedTime.subscribe { [weak self] (url, position) in
guard let self = self else { return }
guard url == self.selectedAudio.url else { return }
guard self.beingSeeked == false else { return }
guard url == self.selectedAudio.url || url == self.savedUrls[self.selectedAudio] else { return }
self.currentTimestampLabel.text = SAPlayer.prettifyTimestamp(position)
guard self.duration != 0 else { return }
@@ -113,18 +125,21 @@ class ViewController: UIViewController {
}
_ = SAPlayer.Updates.AudioDownloading.subscribe { [weak self] (url, progress) in
print(progress)
guard let self = self else { return }
guard url == self.selectedAudio.url else { return }
if self.isDownloading {
self.downloadButton.setTitle("Cancel \(String(format: "%02d", (progress * 100)))%", for: .normal)
DispatchQueue.main.async {
UIView.performWithoutAnimation {
self.downloadButton.setTitle("Cancel \(String(format: "%.2f", (progress * 100)))%", for: .normal)
}
}
}
}
_ = SAPlayer.Updates.StreamingBuffer.subscribe{ [weak self] (url, buffer) in
guard let self = self else { return }
guard url == self.selectedAudio.url else { return }
guard url == self.selectedAudio.url || url == self.savedUrls[self.selectedAudio] else { return }
if self.duration == 0.0 { return }
@@ -141,12 +156,21 @@ class ViewController: UIViewController {
_ = SAPlayer.Updates.PlayingStatus.subscribe { [weak self] (url, playing) in
guard let self = self else { return }
guard url == self.selectedAudio.url else { return }
guard url == self.selectedAudio.url || url == self.savedUrls[self.selectedAudio] else { return }
if playing {
switch playing {
case .playing:
self.isPlayable = true
self.playPauseButton.setTitle("Pause", for: .normal)
} else {
return
case .paused:
self.isPlayable = true
self.playPauseButton.setTitle("Play", for: .normal)
return
case .buffering:
self.isPlayable = false
self.playPauseButton.setTitle("Loading", for: .normal)
return
}
}
}
@@ -162,10 +186,17 @@ class ViewController: UIViewController {
selectedAudio = AudioInfo(index: selected)
SAPlayer.shared.mediaInfo = SALockScreenInfo(title: selectedAudio.title, artist: selectedAudio.artist, artwork: UIImage(), releaseDate: selectedAudio.releaseDate)
// if let savedUrl = savedUrls[selectedAudio] {}
}
@IBAction func scrubberStartedSeeking(_ sender: UISlider) {
beingSeeked = true
}
@IBAction func scrubberSeeked(_ sender: Any) {
SAPlayer.shared.seekTo(seconds: Double(scrubberSlider.value))
let value = Double(scrubberSlider.value) * duration
SAPlayer.shared.seekTo(seconds: value)
beingSeeked = false
}
@@ -175,15 +206,23 @@ class ViewController: UIViewController {
@IBAction func downloadTouched(_ sender: Any) {
if !isDownloading {
if SAPlayer.Downloader.isDownloaded(withRemoteUrl: selectedAudio.url) {
SAPlayer.Downloader.deleteDownload(withRemoteUrl: selectedAudio.url)
if let savedUrl = SAPlayer.Downloader.getSavedUrl(forRemoteUrl: selectedAudio.url) {
SAPlayer.Downloader.deleteDownloaded(withSavedUrl: savedUrl)
downloadButton.setTitle("Download", for: .normal)
streamButton.isEnabled = true
isDownloading = false
} else {
downloadButton.setTitle("Cancel 0%", for: .normal)
isDownloading = true
SAPlayer.Downloader.downloadAudio(withRemoteUrl: selectedAudio.url)
SAPlayer.Downloader.downloadAudio(withRemoteUrl: selectedAudio.url, completion: { [weak self] url in
guard let self = self else { return }
DispatchQueue.main.async {
self.currentUrlLocationLabel.text = "saved to: \(url.lastPathComponent)"
self.savedUrls[self.selectedAudio] = url
SAPlayer.shared.initializeSavedAudio(withSavedUrl: url)
}
})
streamButton.isEnabled = false
}
} else {
+116 -5
View File
@@ -1,6 +1,5 @@
# SwiftAudioPlayer
[![CI Status](https://img.shields.io/travis/tanhakabir/SwiftAudioPlayer.svg?style=flat)](https://travis-ci.org/tanhakabir/SwiftAudioPlayer)
[![Version](https://img.shields.io/cocoapods/v/SwiftAudioPlayer.svg?style=flat)](https://cocoapods.org/pods/SwiftAudioPlayer)
[![License](https://img.shields.io/cocoapods/l/SwiftAudioPlayer.svg?style=flat)](https://cocoapods.org/pods/SwiftAudioPlayer)
[![Platform](https://img.shields.io/cocoapods/p/SwiftAudioPlayer.svg?style=flat)](https://cocoapods.org/pods/SwiftAudioPlayer)
@@ -33,20 +32,20 @@ pod 'SwiftAudioPlayer'
### Usage
To play remote audio:
```
```swift
let url = URL(string: "https://randomwebsite.com/audio.mp3")!
SAPlayer.shared.initializeAudio(withRemoteUrl: url)
SAPlayer.shared.play()
```
To set the display information for the lockscreen:
```
```swift
let info = SALockScreenInfo(title: "Random audio", artist: "Foo", artwork: UIImage(), releaseDate: 123456789)
SAPlayer.shared.mediaInfo = info
```
To receive streaming progress:
```
```swift
@IBOutlet weak var bufferProgress: UIProgressView!
override func viewDidLoad() {
@@ -64,7 +63,11 @@ override func viewDidLoad() {
}
}
```
Look at the [Updates](#SAPlayer.Updates) section to see usage details and other updates to follow.
**Important:** For app in background downloading please refer to [note](#important-step-for-background-downloads).
For more details and specifics look at the [API documentation](#api-in-detail) below.
## Contact
@@ -79,6 +82,114 @@ Feel free to reach out to either of us:
[tanhakabir](https://github.com/tanhakabir), tanhakabir.ca@gmail.com
[JonMercer](https://github.com/JonMercer), mercer.jon@gmail.com
## License
### License
SwiftAudioPlayer is available under the MIT license. See the LICENSE file for more info.
---
# API in detail
## SAPlayer.Downloader
Use functionaity from Downloader to save audio files from remote locations for future offline playback.
Audio files are saved under custom naming scheme on device and are recoverable with original remote URL for file.
#### Important step for background downloads
To ensure that your app will keep downloading audio in the background be sure to add the following to `AppDelegate.swift`:
```swift
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
SAPlayer.Downloader.setBackgroundCompletionHandler(completionHandler)
}
```
### Downloading
Downloads will be held on pause when active stream is started, and will resume downloads when streaming is done.
Use the following to start downloading audio in the background:
```swift
func downloadAudio(withRemoteUrl url: URL, completion: @escaping (_ savedUrl: URL) -> ())
```
It will call the completion handler you pass after successful download with the location of the downloaded file on the device.
And use the following to stop any active or prevent future downloads of the corresponding remote URL:
```swift
func cancelDownload(withRemoteUrl url: URL)
```
### Manage Downloaded
Use the following to manage downloaded audio files.
Checks if downloaded already:
```swift
func isDownloaded(withRemoteUrl url: URL) -> Bool
```
Get URL of audio file saved on device corresponding to remote location:
```swift
func getSavedUrl(forRemoteUrl url: URL) -> URL?
```
Delete downloaded audio if it exists:
```swift
func deleteDownloaded(withSavedUrl url: URL)
```
## SAPlayer.Updates
Receive updates for changing values from the player, such as the duration, elapsed time of playing audio, download progress, and etc.
All subscription functions for updates take the form of:
```swift
func subscribe(_ closure: @escaping (_ url: URL, _ payload: <Payload>) -> ()) -> UInt
```
- `closure`: The closure that will receive the updates. It's recommended to have a weak reference to a class that uses these functions.
- `url`: The corresponding remote URL for the update. In the case there might be multiple files observed, such as downloading many files at once or switching over from playing one audio to another and the updates corresponding to the previous aren't silenced on switch-over.
- `payload`: The updated value.
- Returns: the id for the subscription in the case you would like to unsubscribe to updates for the closure.
Similarily unsubscribe takes the form of:
```swift
func unsubscribe(_ id: UInt)
```
- `id`: The closure with this id will stop receiving updates.
### ElapsedTime
Payload = `Double`
Changes in the timestamp/elapsed time of the current initialized audio. Aka, where the scrubber's pointer of the audio should be at.
Subscribe to this to update views on changes in position of which part of audio is being played.
### Duration
Payload = `Double`
Changes in the duration of the current initialized audio. Especially helpful for audio that is being streamed and can change with more data.
### PlayingStatus
Payload = `SAPlayingStatus`
Changes in the playing status of the player. Can be one of the following 3: `playing`, `paused`, `buffering`.
### StreamingBuffer
Payload = `SAAudioAvailabilityRange`
Changes in the progress of downloading audio for streaming. Information about range of audio available and if the audio is playable. Look at SAAudioAvailabilityRange for more information.
For progress of downloading audio that saves to the phone for playback later, look at AudioDownloading instead.
### AudioDownloading
Payload = `Double`
Changes in the progress of downloading audio in the background. This does not correspond to progress in streaming downloads, look at StreamingBuffer for streaming progress.
+4 -8
View File
@@ -31,7 +31,7 @@ class AudioClockDirector {
private var needleClosures: DirectorThreadSafeClosures<Needle> = DirectorThreadSafeClosures()
private var durationClosures: DirectorThreadSafeClosures<Duration> = DirectorThreadSafeClosures()
private var playingStatusClosures: DirectorThreadSafeClosures<IsPlaying> = DirectorThreadSafeClosures()
private var playingStatusClosures: DirectorThreadSafeClosures<SAPlayingStatus> = DirectorThreadSafeClosures()
private var bufferClosures: DirectorThreadSafeClosures<SAAudioAvailabilityRange> = DirectorThreadSafeClosures()
private init() {}
@@ -60,7 +60,7 @@ class AudioClockDirector {
// Playing status
func attachToChangesInPlayingStatus(closure: @escaping (Key, IsPlaying) throws -> Void) -> UInt{
func attachToChangesInPlayingStatus(closure: @escaping (Key, SAPlayingStatus) throws -> Void) -> UInt{
return playingStatusClosures.attach(closure: closure)
}
@@ -103,12 +103,8 @@ extension AudioClockDirector {
}
extension AudioClockDirector {
func audioPaused(_ key: Key) {
playingStatusClosures.broadcast(key: key, payload: false)
}
func audioPlaying(_ key: Key) {
playingStatusClosures.broadcast(key: key, payload: true)
func audioPlayingStatusWasChanged(_ key: Key, status: SAPlayingStatus) {
playingStatusClosures.broadcast(key: key, payload: status)
}
}
+10 -8
View File
@@ -79,17 +79,13 @@ class AudioEngine: AudioEngineProtocol {
}
}
var isPlaying = false {
var playingStatus: SAPlayingStatus? = nil {
didSet {
guard isPlaying != oldValue else {
guard playingStatus != oldValue, let status = playingStatus else {
return
}
if isPlaying {
AudioClockDirector.shared.audioPlaying(key)
} else {
AudioClockDirector.shared.audioPaused(key)
}
AudioClockDirector.shared.audioPlayingStatusWasChanged(key, status: status)
}
}
@@ -149,7 +145,13 @@ class AudioEngine: AudioEngineProtocol {
}
func updateIsPlaying() {
isPlaying = engine.isRunning && playerNode.isPlaying
if !bufferedSeconds.isPlayable {
playingStatus = .buffering
return
}
let isPlaying = engine.isRunning && playerNode.isPlaying
playingStatus = isPlaying ? .playing : .paused
}
func play() {
+8 -4
View File
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -58,6 +58,7 @@ import AVFoundation
class AudioStreamEngine: AudioEngine {
//Constants
private let MAX_POLL_BUFFER_COUNT = 300 //Having one buffer in engine at a time is choppy.
private let MIN_BUFFERS_TO_BE_PLAYABLE = 1
private let PCM_BUFFER_SIZE: AVAudioFrameCount = 8192
private let queue = DispatchQueue(label: "SwiftAudioPlayer.engine", qos: .userInitiated)
@@ -86,7 +87,9 @@ class AudioStreamEngine: AudioEngine {
didSet {
if numberOfBuffersScheduledFromPoll > MAX_POLL_BUFFER_COUNT {
shouldPollForNextBuffer = false
}
if numberOfBuffersScheduledFromPoll > MIN_BUFFERS_TO_BE_PLAYABLE {
if wasPlaying {
play()
wasPlaying = false
@@ -213,7 +216,7 @@ class AudioStreamEngine: AudioEngine {
private func updateNetworkBufferRange() { //for ui
let range = converter.pollNetworkAudioAvailabilityRange()
isPlayable = (numberOfBuffersScheduledInTotal > 0 && range.1 > 0) && predictedStreamDuration > 0
isPlayable = (numberOfBuffersScheduledInTotal >= MIN_BUFFERS_TO_BE_PLAYABLE && range.1 > 0) && predictedStreamDuration > 0
Log.debug("loaded \(range), numberOfBuffersScheduledInTotal: \(numberOfBuffersScheduledInTotal), isPlayable: \(isPlayable)")
bufferedSeconds = SAAudioAvailabilityRange(startingNeedle: range.0, durationLoadedByNetwork: range.1, isPlayable: isPlayable)
}
@@ -262,7 +265,6 @@ class AudioStreamEngine: AudioEngine {
self.needle = needle //to tick while paused
queue.sync { [weak self] in
self?.seekHelperDispatchQueue(needle: needle)
}
@@ -290,6 +292,8 @@ class AudioStreamEngine: AudioEngine {
playerNode.stop()
shouldPollForNextBuffer = true
updateNetworkBufferRange()
}
override func invalidate() {
+31 -5
View File
@@ -36,6 +36,7 @@ protocol AudioThrottleable {
func tellAudioFormatFound()
func tellByteOffset(offset: UInt64)
func tellSeek(offset: UInt64)
func tellBytesPerAudioPacket(count: UInt64)
func pollRangeOfBytesAvailable() -> (UInt64, UInt64)
func invalidate()
}
@@ -47,6 +48,10 @@ class AudioThrottler: AudioThrottleable {
var alreadySent: Bool
var next: NetworkDataWrapper?
var byteCount: UInt {
return UInt(data.count)
}
var endOffset: UInt {
return startOffset + UInt(data.count) - 1
}
@@ -94,6 +99,8 @@ class AudioThrottler: AudioThrottleable {
var byteOffsetBecauseOfSeek: UInt = 0
var totalBytesExpected: Int64? //this got sent up twice. Once at beginning of stream and second from network seek. We honor the first send
var largestPollingOffsetDifference: UInt64 = 1
required init(withRemoteUrl url: AudioURL, withDelegate delegate: AudioThrottleDelegate) {
self.url = url
self.delegate = delegate
@@ -131,17 +138,36 @@ class AudioThrottler: AudioThrottleable {
shouldThrottle = true //the above layer has enough info that we can throttle
}
func tellBytesPerAudioPacket(count: UInt64) {
if count > largestPollingOffsetDifference {
largestPollingOffsetDifference = count
}
}
func tellByteOffset(offset: UInt64) {
Log.debug("offset \(offset)")
for wrappedNetworkData in networkData {
if wrappedNetworkData.containsOffset(UInt(offset)) {
Log.debug("offset within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
Log.debug("offset: \(offset) within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset) is next sent: \(wrappedNetworkData.isNextSent())")
if wrappedNetworkData.alreadySent {
if !wrappedNetworkData.isNextSent() {
if let next = wrappedNetworkData.next {
Log.debug("Sending next network packet with range: \(next.startOffset) to \(next.endOffset)")
next.alreadySent = true
delegate?.shouldProcess(networkData: next.data)
var bytesSent: UInt = 0
var current = wrappedNetworkData
// Sometimes the next data packet is smaller than a full audio chunk size, so we need to ensure we send up enough packets for the audio chunk. This prevented Issue #4 where tsreaming would randomly get stuck in a state needing more data up the chain.
// https://github.com/tanhakabir/SwiftAudioPlayer/issues/4
while bytesSent < largestPollingOffsetDifference {
if let next = current.next {
Log.debug("Sending next network packet with range: \(next.startOffset) to \(next.endOffset)")
next.alreadySent = true
delegate?.shouldProcess(networkData: next.data)
bytesSent += next.byteCount
current = next
} else {
return
}
}
}
return
+5 -1
View File
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -174,6 +174,10 @@ class AudioConverter: AudioConvertable {
}
private func getPacketIndex(forNeedle needle: Needle) -> AVAudioPacketCount? {
guard needle >= 0 else {
Log.error("needle should never be a negative number! needle received: \(needle)")
return nil
}
guard let frame = frameOffset(forTime: TimeInterval(needle)) else { return nil }
guard let framesPerPacket = parser.fileAudioFormat?.streamDescription.pointee.mFramesPerPacket else { return nil }
return AVAudioPacketCount(frame) / AVAudioPacketCount(framesPerPacket)
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
+1 -1
View File
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
+10 -3
View File
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -98,7 +98,14 @@ class AudioParser: AudioParsable {
return predictedCount
}
var sumOfParsedAudioBytes:UInt32 = 0
var sumOfParsedAudioBytes:UInt32 = 0 {
didSet {
if let byteCount = averageBytesPerPacket {
throttler.tellBytesPerAudioPacket(count: UInt64(byteCount))
}
}
}
var numberOfPacketsParsed:UInt32 = 0
var audioPackets: [(AudioStreamPacketDescription?,Data)] = [] {
didSet {
@@ -163,7 +170,7 @@ class AudioParser: AudioParsable {
if isParsingComplete {
throw ParserError.readerAskingBeyondEndOfFile
} else {
Log.debug("Tried to pull packet at index: \(packetIndex) when only have: \(audioPackets.count)")
Log.debug("Tried to pull packet at index: \(packetIndex) when only have: \(audioPackets.count), we predict \(totalPredictedPacketCount) in total")
throw ParserError.notEnoughDataForReader
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -8,7 +8,7 @@
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
// documentation of modified files from source and a copy of the Apache License 2.0
// in the project which he have under the name Credited_LICENSE.
// in the project which is under the name Credited_LICENSE.
//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
+33
View File
@@ -0,0 +1,33 @@
//
// SAPlayingStatus.swift
// SwiftAudioPlayer
//
// Created by Tanha Kabir on 2019-11-24.
// Copyright © 2019 Tanha Kabir, Jon Mercer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
public enum SAPlayingStatus {
case playing
case paused
case buffering
}
+28 -3
View File
@@ -56,12 +56,22 @@ extension LockScreenViewProtocol {
nowPlayingInfo[MPMediaItemPropertyPodcastTitle] = title
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0.0 //because default is 1.0. If we pause audio then it keeps ticking
nowPlayingInfo[MPMediaItemPropertyReleaseDate] = Date(timeIntervalSince1970: TimeInterval(releaseDate))
nowPlayingInfo[MPMediaItemPropertyArtwork] =
MPMediaItemArtwork(boundsSize: info.artwork.size) { size in
return info.artwork
if let artwork = info.artwork {
nowPlayingInfo[MPMediaItemPropertyArtwork] =
MPMediaItemArtwork(boundsSize: artwork.size) { size in
return artwork
}
} else {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: UIImage().size) { size in
return UIImage()
}
}
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
@@ -137,4 +147,19 @@ extension LockScreenViewProtocol {
func updateLockscreenPlaybackDuration(duration: Duration) {
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyPlaybackDuration] = NSNumber(value: duration)
}
func updateLockscreenPaused(){
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
}
func updateLockscreenPlaying(){
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
}
func updateLockscreenChangePlaybackRate(speed: Double){
if speed > 0.0{
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = speed
}
}
}
+12 -6
View File
@@ -45,8 +45,9 @@ protocol AudioDataManagable {
func deleteStream(withRemoteURL url: AudioURL)
func getPersistedUrl(withRemoteURL url: AudioURL) -> URL?
func startDownload(withRemoteURL url: AudioURL)
func deleteDownload(withRemoteURL url: AudioURL)
func startDownload(withRemoteURL url: AudioURL, completion: @escaping (URL) -> ())
func cancelDownload(withRemoteURL url: AudioURL)
func deleteDownload(withLocalURL url: URL)
}
class AudioDataManager: AudioDataManagable {
@@ -152,11 +153,12 @@ extension AudioDataManager {
return FileStorage.Audio.locate(url.key)
}
func startDownload(withRemoteURL url: AudioURL) {
func startDownload(withRemoteURL url: AudioURL, completion: @escaping (URL) -> ()) {
let key = url.key
if FileStorage.Audio.isStored(key) {
if let savedUrl = FileStorage.Audio.locate(key), FileStorage.Audio.isStored(key) {
globalDownloadProgressCallback(key, 1.0)
completion(savedUrl)
return
}
@@ -171,13 +173,17 @@ extension AudioDataManager {
return
}
downloadWorker.start(withID: key, withRemoteUrl: url, withResumeData: nil)
downloadWorker.start(withID: key, withRemoteUrl: url, completion: completion)
}
func deleteDownload(withRemoteURL url: AudioURL) {
func cancelDownload(withRemoteURL url: AudioURL) {
downloadWorker.stop(withID: url.key, callback: nil)
FileStorage.Audio.delete(url.key)
}
func deleteDownload(withLocalURL url: URL) {
FileStorage.delete(url)
}
}
// MARK:- Listeners
@@ -33,7 +33,7 @@ protocol AudioDataDownloadable: AnyObject {
func getProgressOfDownload(withID id: ID) -> Double?
func start(withID id: ID, withRemoteUrl remoteUrl: URL, withResumeData data: Data?)
func start(withID id: ID, withRemoteUrl remoteUrl: URL, completion: @escaping (URL) -> ())
func stop(withID id: ID, callback: ((_ dataSoFar: Data?, _ totalBytesExpected: Int64?) -> ())?)
func pauseAllActive() //Because of streaming
func resumeAllActive() //Because of streaming
@@ -85,31 +85,36 @@ class AudioDownloadWorker: NSObject, AudioDataDownloadable {
return activeDownloads.filter { $0.info.id == id }.first?.progress
}
func start(withID id: ID, withRemoteUrl remoteUrl: URL, withResumeData data: Data? = nil) {
Log.info("paramID: \(id) activeDownloadIDs: \((activeDownloads.map { $0.info.id } ).toLog)")
func start(withID id: ID, withRemoteUrl remoteUrl: URL, completion: @escaping (URL) -> ()) {
Log.info("startExternal paramID: \(id) activeDownloadIDs: \((activeDownloads.map { $0.info.id } ).toLog)")
let temp = activeDownloads.filter { $0.info.id == id }.count
guard temp == 0 else {
return
}
let rank = Date.getUTC()
let info = queuedDownloads.updatePreservingOldCompletionHandlers(withID: id, withRemoteUrl: remoteUrl, completion: completion)
guard numberOfActive < MAX_CONCURRENT_DOWNLOADS else {
queuedDownloads.update(with: DownloadInfo(id: id, remoteUrl: remoteUrl, rank: rank))
start(withInfo: info)
}
fileprivate func start(withInfo info: DownloadInfo) {
Log.info("paramID: \(info.id) activeDownloadIDs: \((activeDownloads.map { $0.info.id } ).toLog)")
let temp = activeDownloads.filter { $0.info.id == info.id }.count
guard temp == 0 else {
return
}
var task: URLSessionDownloadTask
if let resumeData = data {
task = session.downloadTask(withResumeData: resumeData)
} else {
task = session.downloadTask(with: remoteUrl)
guard numberOfActive < MAX_CONCURRENT_DOWNLOADS else {
_ = queuedDownloads.updatePreservingOldCompletionHandlers(withID: info.id, withRemoteUrl: info.remoteUrl)
return
}
task.taskDescription = id
queuedDownloads.remove(info)
let activeTask = ActiveDownload(info: DownloadInfo(id: id, remoteUrl: remoteUrl, rank: rank), task: task)
let task: URLSessionDownloadTask = session.downloadTask(with: info.remoteUrl)
task.taskDescription = info.id
let activeTask = ActiveDownload(info: info, task: task)
activeDownloads.append(activeTask)
activeTask.task.resume()
@@ -145,6 +150,7 @@ class AudioDownloadWorker: NSObject, AudioDataDownloadable {
}
}
queuedDownloads.remove(withMatchingId: id)
callback?(nil, nil)
}
}
@@ -189,10 +195,15 @@ extension AudioDownloadWorker: URLSessionDownloadDelegate {
}
completionHandler(task.info.id, nil)
for handler in task.info.completionHandlers {
handler(destinationUrl)
}
activeDownloads = activeDownloads.filter { $0 != task }
if let queued = queuedDownloads.popHighestRanked() {
start(withID: queued.id, withRemoteUrl: queued.remoteUrl)
start(withInfo: queued)
}
}
@@ -256,9 +267,18 @@ extension AudioDownloadWorker {
// MARK:- Helper Classes
extension AudioDownloadWorker {
fileprivate struct DownloadInfo: Hashable {
static func == (lhs: AudioDownloadWorker.DownloadInfo, rhs: AudioDownloadWorker.DownloadInfo) -> Bool {
return lhs.id == rhs.id && lhs.remoteUrl == rhs.remoteUrl
}
var hashValue: Int {
return id.hashValue ^ remoteUrl.hashValue
}
let id: ID
let remoteUrl: URL
let rank: Int
var completionHandlers: [(URL) -> ()]
}
private class ActiveDownload: Hashable {
@@ -298,6 +318,47 @@ extension Set where Element == AudioDownloadWorker.DownloadInfo {
return ret
}
mutating func updatePreservingOldCompletionHandlers(withID id: ID, withRemoteUrl remoteUrl: URL, completion: ((URL) -> ())? = nil) -> AudioDownloadWorker.DownloadInfo {
let rank = Date.getUTC()
let tempHandlers: [(URL) -> ()] = completion != nil ? [completion!] : []
var newInfo = AudioDownloadWorker.DownloadInfo.init(id: id, remoteUrl: remoteUrl, rank: rank, completionHandlers: tempHandlers)
if let previous = self.update(with: newInfo) {
let prevHandlers = previous.completionHandlers
let newHandlers = prevHandlers + tempHandlers
newInfo = AudioDownloadWorker.DownloadInfo.init(id: id, remoteUrl: remoteUrl, rank: rank, completionHandlers: newHandlers)
self.update(with: newInfo)
}
return newInfo
}
mutating func remove(withMatchingId id: ID) {
var toRemove: AudioDownloadWorker.DownloadInfo? = nil
var matchCount = 0
for item in self.enumerated() {
if item.element.id == id {
toRemove = item.element
matchCount += 1
}
}
guard matchCount <= 1 else {
Log.error("Found \(matchCount) matches of queued info with the same id of: \(id), this should have never happened.")
return
}
if let removeInfo = toRemove {
self.remove(removeInfo)
}
}
}
extension String {
+3 -3
View File
@@ -36,19 +36,19 @@ struct FileStorage {
Note: It is not guaranteed that the file actually exists.
*/
private static func getUrl(givenAName name: NameFile, inDirectory dir: FileManager.SearchPathDirectory) -> URL {
static func getUrl(givenAName name: NameFile, inDirectory dir: FileManager.SearchPathDirectory) -> URL {
let directoryPath = NSSearchPathForDirectoriesInDomains(dir, .userDomainMask, true)[0] as String
let url = URL(fileURLWithPath: directoryPath)
return url.appendingPathComponent(name)
}
private static func isStored(_ url: URL) -> Bool{
static func isStored(_ url: URL) -> Bool{
// https://stackoverflow.com/questions/42897844/swift-3-0-filemanager-fileexistsatpath-always-return-false
// When determining if a file exists, we must use .path not .absolute string!
return FileManager.default.fileExists(atPath: url.path)
}
private static func delete(_ url: URL) {
static func delete(_ url: URL) {
if !isStored(url) {
return
}
+8 -2
View File
@@ -26,15 +26,21 @@
import Foundation
import UIKit
/**
UTC corresponds to epoch time (number of seconds that have elapsed since January 1, 1970, midnight UTC/GMT). https://www.epochconverter.com/ is a useful site to convert to human readable format.
*/
public typealias UTC = Int
/**
Use to set what will be displayed in the lockscreen.
*/
public struct SALockScreenInfo {
var title: String
var artist: String
var artwork: UIImage
var artwork: UIImage?
var releaseDate: UTC
public init(title: String, artist: String, artwork: UIImage, releaseDate: UTC) {
public init(title: String, artist: String, artwork: UIImage?, releaseDate: UTC) {
self.title = title
self.artist = artist
self.artwork = artwork
+19 -28
View File
@@ -94,7 +94,7 @@ public class SAPlayer {
}
}
//MARK: - Player Controls
//MARK: - External Player Controls
extension SAPlayer {
public func togglePlayAndPause() {
presenter.handleTogglePlayingAndPausing()
@@ -120,37 +120,19 @@ extension SAPlayer {
presenter.handleSeek(toNeedle: seconds)
}
public func initializeSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
self.mediaInfo = mediaInfo
presenter.handlePlaySavedAudio(withSavedUrl: url)
}
public func initializeAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
self.mediaInfo = mediaInfo
presenter.handlePlayAudio(withRemoteUrl: url)
presenter.handlePlayStreamedAudio(withRemoteUrl: url)
}
}
extension SAPlayer {
public struct Downloader {
public static func downloadAudio(withRemoteUrl url: URL) {
SAPlayer.shared.addUrlToMapping(url: url)
AudioDataManager.shared.startDownload(withRemoteURL: url)
}
public static func cancelDownload(withRemoteUrl url: URL) {
AudioDataManager.shared.deleteDownload(withRemoteURL: url)
}
public static func deleteDownload(withRemoteUrl url: URL) {
AudioDataManager.shared.deleteDownload(withRemoteURL: url)
}
public static func isDownloaded(withRemoteUrl url: URL) -> Bool {
return AudioDataManager.shared.getPersistedUrl(withRemoteURL: url) != nil
}
public static func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ()) {
AudioDataManager.shared.setBackgroundCompletionHandler(completionHandler)
}
}
}
//MARK: - Internal implementation of delegate
extension SAPlayer: SAPlayerDelegate {
func startAudioDownloaded(withSavedUrl url: AudioURL) {
player?.pause()
@@ -177,7 +159,9 @@ extension SAPlayer: SAPlayerDelegate {
} else {
// Fallback on earlier versions
}
try AVAudioSession.sharedInstance().setActive(true, with: .notifyOthersOnDeactivation)
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: AVAudioSession.Mode(rawValue: convertFromAVAudioSessionMode(AVAudioSession.Mode.default)), options: .allowAirPlay)
try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
} catch {
Log.monitor("Problem setting up AVAudioSession to play in:: \(error.localizedDescription)")
}
@@ -188,7 +172,9 @@ extension SAPlayer: SAPlayerDelegate {
}
func seekEngine(toNeedle needle: Needle) {
player?.seek(toNeedle: needle)
var seekToNeedle = needle < 0 ? 0 : needle
seekToNeedle = needle > Needle(duration) ? Needle(duration) : needle
player?.seek(toNeedle: seekToNeedle)
}
func setSpeedEngine(withMultiple multiple: Double) {
@@ -196,3 +182,8 @@ extension SAPlayer: SAPlayerDelegate {
}
}
// Helper function inserted by Swift 4.2 migrator.
fileprivate func convertFromAVAudioSessionMode(_ input: AVAudioSession.Mode) -> String {
return input.rawValue
}
+102
View File
@@ -0,0 +1,102 @@
//
// SAPlayerDownloader.swift
// SwiftAudioPlayer
//
// Created by Tanha Kabir on 2019-02-25.
// Copyright © 2019 Tanha Kabir, Jon Mercer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
extension SAPlayer {
/**
Actions relating to downloading remote audio to the device for offline playback.
- Note: All saved urls generated from downloaded audio corresponds to a specific remote url. Thus, can be queryed if original remote url is known.
- Important: Please ensure that you have passed in the background download completion handler in the AppDelegate with `setBackgroundCompletionHandler` to allow for downloading audio while app is in the background.
*/
public struct Downloader {
/**
Download audio from a remote url. Will save the audio on the device for playback later.
Save the saved url of the downloaded audio for future playback or query for the saved url with the same remote url in the future.
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter url: The remote url to download audio from.
- Parameter completion: Completion handler that will return once the download is successful and complete.
- Parameter savedUrl: The url of where the audio was saved locally on the device. Will receive once download has completed.
*/
public static func downloadAudio(withRemoteUrl url: URL, completion: @escaping (_ savedUrl: URL) -> ()) {
SAPlayer.shared.addUrlToMapping(url: url)
AudioDataManager.shared.startDownload(withRemoteURL: url, completion: completion)
}
/**
Cancel downloading audio from a specific remote url if actively downloading. If download has not started yet, it will remove from the list of future downloads queued.
- Parameter url: The remote url corresponding to the active download you want to cancel.
*/
public static func cancelDownload(withRemoteUrl url: URL) {
AudioDataManager.shared.cancelDownload(withRemoteURL: url)
}
/**
Delete downloaded audio file from device at url.
- Note: This will delete any file saved on device at the local url. This, however, is intended to use for audio files.
- Parameter url: The url of the audio to delete from the device.
*/
public static func deleteDownloaded(withSavedUrl url: URL) {
AudioDataManager.shared.deleteDownload(withLocalURL: url)
}
/**
Check if audio at remote url is downloaded on device.
- Parameter url: The remote url corresponding to the audio file you want to see if downloaded.
- Returns: Whether of not file at remote url is downloaded on device.
*/
public static func isDownloaded(withRemoteUrl url: URL) -> Bool {
return AudioDataManager.shared.getPersistedUrl(withRemoteURL: url) != nil
}
/**
Get url of audio file downloaded from remote url onto on device if it exists.
- Parameter url: The remote url corresponding to the audio file you want the device url of.
- Returns: Url of audio file on device if it exists.
*/
public static func getSavedUrl(forRemoteUrl url: URL) -> URL? {
return AudioDataManager.shared.getPersistedUrl(withRemoteURL: url)
}
/**
Pass along the completion handler from `AppDelegate` to ensure downloading continues while app is in background.
- Parameter completionHandler: The completion hander from `AppDelegate` to use for app in the background downloads.
*/
public static func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ()) {
AudioDataManager.shared.setBackgroundCompletionHandler(completionHandler)
}
}
}
+20 -14
View File
@@ -35,7 +35,7 @@ class SAPlayerPresenter {
var duration: Duration?
private var key: String?
private var isPlaying = false
private var isPlaying: SAPlayingStatus = .buffering
private var mediaInfo: SALockScreenInfo?
private var urlKeyMap: [Key: URL] = [:]
@@ -60,21 +60,23 @@ class SAPlayerPresenter {
urlKeyMap[url.key] = url
}
func handlePlayAudio(withRemoteUrl url: URL) {
func handlePlaySavedAudio(withSavedUrl url: URL) {
attachForUpdates(url: url)
delegate?.startAudioDownloaded(withSavedUrl: url)
}
func handlePlayStreamedAudio(withRemoteUrl url: URL) {
attachForUpdates(url: url)
delegate?.startAudioStreamed(withRemoteUrl: url)
}
private func attachForUpdates(url: URL) {
AudioClockDirector.shared.detachFromChangesInDuration(withID: durationRef)
AudioClockDirector.shared.detachFromChangesInNeedle(withID: needleRef)
AudioClockDirector.shared.detachFromChangesInPlayingStatus(withID: playingStatusRef)
self.key = url.key
if let savedUrl = AudioDataManager.shared.getPersistedUrl(withRemoteURL: url) {
self.key = savedUrl.key
urlKeyMap[savedUrl.key] = url
delegate?.startAudioDownloaded(withSavedUrl: savedUrl)
} else {
urlKeyMap[url.key] = url
delegate?.startAudioStreamed(withRemoteUrl: url)
}
urlKeyMap[url.key] = url
durationRef = AudioClockDirector.shared.attachToChangesInDuration(closure: { [weak self] (key, duration) in
guard let self = self else { throw DirectorError.closureIsDead }
@@ -124,16 +126,18 @@ class SAPlayerPresenter {
extension SAPlayerPresenter {
func handlePause() {
delegate?.pauseEngine()
self.delegate?.updateLockscreenPaused()
}
func handlePlay() {
delegate?.playEngine()
self.delegate?.updateLockscreenPlaying()
}
func handleTogglePlayingAndPausing() {
if isPlaying {
if isPlaying == .playing {
handlePause()
} else {
} else if isPlaying == .paused {
handlePlay()
}
}
@@ -154,13 +158,15 @@ extension SAPlayerPresenter {
func handleSetSpeed(withMultiple: Double) {
delegate?.setSpeedEngine(withMultiple: withMultiple)
self.delegate?.updateLockscreenChangePlaybackRate(speed: withMultiple)
}
}
//MARK:- For lock screen
extension SAPlayerPresenter {
func getIsPlaying() -> Bool {
return isPlaying
return isPlaying == .playing
}
}
+7 -7
View File
@@ -40,7 +40,7 @@ extension SAPlayer {
/**
Subscribe to updates in elapsed time of the playing audio. Aka, the current timestamp of the audio.
Note: It's recommended to have a weak reference to a class that uses this fuction
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in time.
- Parameter url: The corresponding remote URL for the updated playing time.
@@ -72,7 +72,7 @@ extension SAPlayer {
/**
Subscribe to updates to changes in duration of the current audio initialized.
Note: It's recommended to have a weak reference to a class that uses this fuction
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
- Parameter url: The corresponding remote URL for the updated duration.
@@ -104,14 +104,14 @@ extension SAPlayer {
/**
Subscribe to updates to changes in the playing/paused status of audio.
Note: It's recommended to have a weak reference to a class that uses this fuction
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
- Parameter url: The corresponding remote URL for the updated duration.
- Parameter playingStatus: Whether the player is playing audio or paused.
- Returns: the id for the subscription in the case you would like to unsubscribe to updates for the closure.
*/
public static func subscribe(_ closure: @escaping (_ url: URL, _ playingStatus: Bool) -> ()) -> UInt {
public static func subscribe(_ closure: @escaping (_ url: URL, _ playingStatus: SAPlayingStatus) -> ()) -> UInt {
return AudioClockDirector.shared.attachToChangesInPlayingStatus(closure: { (key, isPlaying) in
guard let url = SAPlayer.shared.getUrl(forKey: key) else { return }
closure(url, isPlaying)
@@ -129,14 +129,14 @@ extension SAPlayer {
}
/**
Updates to changes in the progress of downloading audio for streaming. Information about range of audio available and if the audio is playable. Look at SAAudioAvailabilityRange for more information.
Updates to changes in the progress of downloading audio for streaming. Information about range of audio available and if the audio is playable. Look at `SAAudioAvailabilityRange` for more information.
*/
public struct StreamingBuffer {
/**
Subscribe to updates to changes in the progress of downloading audio for streaming. Information about range of audio available and if the audio is playable. Look at SAAudioAvailabilityRange for more information. For progress of downloading audio that saves to the phone for playback later, look at AudioDownloading instead.
Note: It's recommended to have a weak reference to a class that uses this fuction
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
- Parameter url: The corresponding remote URL for the updated streaming progress.
@@ -168,7 +168,7 @@ extension SAPlayer {
/**
Subscribe to updates to changes in the progress of downloading audio. This does not correspond to progress in streaming downloads, look at StreamingBuffer for streaming progress.
Note: It's recommended to have a weak reference to a class that uses this fuction
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
- Parameter url: The corresponding remote URL for the updated download progress.
+7 -1
View File
@@ -33,7 +33,13 @@ extension Date {
*/
static func getUTC64() -> UInt {
//"On 32-bit platforms, UInt is the same size as UInt32, and on 64-bit platforms, UInt is the same size as UInt64."
return UInt(Date().timeIntervalSince1970.bitPattern)
if #available(iOS 11.0, *) {
return UInt(Date().timeIntervalSince1970.bitPattern)
} else {
let time = Date().timeIntervalSince1970.bitPattern & 0xFFFFFFFF;
return UInt(time)
}
}
/**
@@ -38,6 +38,12 @@ class DirectorThreadSafeClosures<P> {
private var closures: [UInt: TypeClosure] = [:]
private var cache: [Key: P] = [:]
var count: Int {
get {
return closures.count
}
}
func broadcast(key: Key, payload: P) {
queue.sync {
self.cache[key] = payload
+2 -2
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioPlayer'
s.version = '0.1.0'
s.version = '1.3.0'
s.summary = 'SwiftAudioPlayer is a Swift based audio player that can handle streaming from a remote location and audio manipulation.'
# This description is used to generate tags and improve search results.
@@ -31,7 +31,7 @@ SwiftAudioPlayer is a Swift based audio player that can handle streaming from a
s.ios.deployment_target = '10.0'
s.source_files = 'Source/**/*'
s.swift_version = '4.0'
s.swift_version = '4.2'
# s.resource_bundles = {
# 'SwiftAudioPlayer' => ['SwiftAudioPlayer/Assets/*.png']