Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6276e97c4c | |||
| 09142ce2d4 | |||
| 90bc2262ec | |||
| 9594449215 | |||
| 6187c9f438 | |||
| b28e815545 | |||
| 17be73bbe8 | |||
| cd35f38db1 | |||
| 3c752d581d | |||
| 1f20a48a20 | |||
| 3a585c1f43 | |||
| 5ac5b93ac4 | |||
| b484f0bfb6 | |||
| 0aeb8b0f88 | |||
| 8e7357860c | |||
| 936de8c996 | |||
| e986be9db5 | |||
| 876d517f3d | |||
| 0a12c68274 | |||
| 873e537301 | |||
| 94c1a47641 | |||
| d0296ab012 | |||
| 2fd944d88e | |||
| fc98c4c1c4 | |||
| 8bf6cbb56e | |||
| b97f97ca5e | |||
| 0c7bcdcf90 | |||
| 840122e603 | |||
| 8518d10c6d | |||
| f214be28a9 | |||
| f219d9d1a0 | |||
| 8797c0d917 | |||
| 0121d05dff | |||
| 26faf62657 | |||
| 61e79d067a | |||
| 103838d1b8 | |||
| 47de2a5251 | |||
| d4d8f767e3 | |||
| c75da619cf | |||
| aea6f5efaa | |||
| 2625b8f4db | |||
| e6460513ea | |||
| a2504f2726 | |||
| 23f445ce4d | |||
| 61fe0c6ebb | |||
| 72c4335386 | |||
| 640f0b92f0 | |||
| c0f8db29c0 | |||
| 285cd92514 | |||
| a5293a5b39 | |||
| 8430a7e8ce | |||
| 34e430713b | |||
| d23a5f8d62 | |||
| 9f89944bc5 |
+13
-2
@@ -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;
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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&sd=1&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 {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# SwiftAudioPlayer
|
||||
|
||||
[](https://travis-ci.org/tanhakabir/SwiftAudioPlayer)
|
||||
[](https://cocoapods.org/pods/SwiftAudioPlayer)
|
||||
[](https://cocoapods.org/pods/SwiftAudioPlayer)
|
||||
[](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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
Reference in New Issue
Block a user