Compare commits

...

38 Commits

Author SHA1 Message Date
tanhakabir 4684a92380 Release 6.2.0 2021-08-14 21:08:44 -07:00
tanhakabir 2cff597e45 Add documentation 2021-08-14 21:06:38 -07:00
tanhakabir 98dc7cfa3c Release 6.1.0 2021-08-12 23:53:47 -07:00
tanhakabir 4f1242f56d Merge pull request #136 from tanhakabir/issue-loop
Issue loop
2021-08-12 19:44:47 -07:00
tanhakabir f3c91ccc34 Fix buffering causing full pause 2021-08-12 19:44:12 -07:00
tanhakabir 2d88b69aa7 Fix update loops killing on end of audio 2021-08-12 16:58:38 -07:00
tanhakabir f67b939ac4 Merge branch 'master' into issue-loop 2021-08-12 16:27:34 -07:00
tanhakabir a0e9b973e0 checkpoint, working but bug in seek 2021-08-12 16:27:20 -07:00
tanhakabir ef54080a68 Merge pull request #133 from cntrump/pr_fix_some_required_condition_is_false_cases
Fix some "required condition is false" cases.
2021-08-12 16:26:39 -07:00
tanhakabir 2d35bbad59 Merge pull request #135 from cntrump/pr_improve_tvos_support
Improve tvOS support
2021-08-12 15:55:04 -07:00
tanhakabir 13b68920d1 Merge pull request #134 from cntrump/pr_improve_lockscreen_info
Improve LockScreen Info
2021-08-12 14:04:15 -07:00
Lvv.me 2e8f44c553 Improve tvOS support 2021-08-12 21:00:21 +08:00
Lvv.me 58ac9b5ae5 Improve LockScreen Info
- Add albumTitle for SALockScreenInfo
- Support disable skip forward/backward command
2021-08-10 21:22:31 +08:00
Lvv.me 706ab5961c Fix some "required condition is false" cases.
case 1:
```
"required condition is false: nil == owningEngine || GetEngine() == owningEngine"
```

case 2:
```
"required condition is false: _engine->IsRunning()"
```
2021-08-10 17:38:08 +08:00
tanhakabir 50139ca8c5 Merge pull request #130 from cntrump/pr_improve_dataTask_config_order
set taskDescription before resume() called.
2021-08-09 00:30:29 -07:00
tanhakabir 6c3e52b66e Merge pull request #131 from cntrump/pr_add_missing_return
Add missing `return` in doneCallback() for AudioStreamWorker.
2021-08-09 00:30:08 -07:00
Lvv.me 6d955687a3 Add missing return in doneCallback() for AudioStreamWorker. 2021-08-09 14:11:13 +08:00
Lvv.me 38d5740f4d set taskDescription before resume() called. 2021-08-09 11:12:55 +08:00
tanhakabir 4cbfb4b16b Improve end of audio detection 2021-08-08 11:22:22 -07:00
tanhakabir 01668790f3 Release 6.0.0 2021-08-08 11:04:23 -07:00
tanhakabir 1cf8fb99ba Remove unnecessary project files 2021-08-08 11:02:37 -07:00
tanhakabir 2d3fe83a56 Merge pull request #125 from cntrump/pr_spm_and_tvos_support
Add SPM and tvOS support
2021-08-08 10:46:57 -07:00
tanhakabir 5f63b52592 support custom HTTPHeaderFields
Fixes #85

Co-Authored-By: cntrump <me@lvv.me>
2021-08-08 10:31:56 -07:00
Lvv.me 9111ac6257 Add SPM and tvOS support 2021-08-08 17:08:54 +08:00
tanhakabir bfbb979897 Add stopping engine and playerNode on invalidate() and deinit
Co-Authored-By: cntrump <me@lvv.me>
2021-08-07 19:06:59 -07:00
tanhakabir 0b40a6f0b4 Release 5.2.0 2021-07-20 10:50:43 -07:00
tanhakabir f9465f54a0 Merge pull request #118 from dylancom/issue-117
fix: prevent mediainfo not being available yet in callbacks
2021-07-20 10:32:34 -07:00
tanhakabir 8ce28db471 Add documentation for important order 2021-07-20 10:32:16 -07:00
Dylan Companjen a84f834f45 fix: prevent mediainfo not being available yet in callbacks 2021-07-19 18:40:01 +02:00
tanhakabir e3e3af2b7a Release 5.1.0 2021-07-18 23:09:08 -07:00
tanhakabir 6987458f0a Merge pull request #114 from dylancom/issue-112
feat: pause player on interruption, resume when necessary
2021-07-18 22:58:50 -07:00
tanhakabir c912d5f381 update formatting and control functions 2021-07-18 22:57:58 -07:00
Dylan c444ae4c9f feat: pause player on interruption, resume when necessary 2021-07-16 15:08:44 +02:00
tanhakabir 6d3f3c6d6f Update README.md 2021-07-12 14:18:08 -04:00
tanhakabir bb7f1d1d0a Release 5.0.5 2021-05-09 17:16:22 -07:00
tanhakabir 6c446f27e0 fix maintaining rate changes while skip silences is enabled (#110) 2021-05-09 17:15:44 -07:00
tanhakabir c513c723ed Release 5.0.4 2021-05-08 18:57:11 -07:00
tanhakabir b34a264aec Fix bug introduced by queuing for lockscreen info (#108)
* add extra data for queuing and change order of clearing mediaInfo

* make SAPlayer the only source for mediaInfo

Co-authored-by: @dylancom
Co-authored-by: @dezinezync
2021-05-08 18:55:08 -07:00
22 changed files with 333 additions and 188 deletions
-24
View File
@@ -7,12 +7,10 @@
objects = {
/* Begin PBXBuildFile section */
27E3EC64A90305ACA68AE35A7DC597E0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */; };
2A421C2A94DF56A00FF73322C6B470C8 /* SwiftAudioPlayer-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E268C8D5FBBF7E0E790D3AA6A70FEC2 /* SwiftAudioPlayer-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
3A31FEF49CC8C3B757EEB4EBCC9BCCF4 /* Pods-SwiftAudioPlayer_Tests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 351771425C270B04BF2A07F0262DA192 /* Pods-SwiftAudioPlayer_Tests-dummy.m */; };
418D41690EF20077112E2BE86E32FB6A /* Pods-SwiftAudioPlayer_Example-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = AB41D88A2C694FBDF26EA56381EED25F /* Pods-SwiftAudioPlayer_Example-dummy.m */; };
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, ); }; };
A40DBE292391D9CA00F86146 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40DBE282391D9C900F86146 /* Data.swift */; };
A411CE4625F9609D0039E1CD /* SAPlayerFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = A411CE4525F9609D0039E1CD /* SAPlayerFeatures.swift */; };
@@ -53,7 +51,6 @@
A4FBA6B5221B74C900D5A353 /* SALockScreenInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */; };
A4FBA6B7221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */; };
A4FBA6B9221BAF8700D5A353 /* SAAudioAvailabilityRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B8221BAF8700D5A353 /* SAAudioAvailabilityRange.swift */; };
B73D01578ABBDB6FF402D868A6C547FF /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */; };
E08AD6157EF688FE832F866CBCDA3532 /* SwiftAudioPlayer-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = FB83B3B4253D41C37C5563D34D450BF8 /* SwiftAudioPlayer-dummy.m */; };
/* End PBXBuildFile section */
@@ -90,7 +87,6 @@
509D93CD81F074F6E7C4B9DE13210ACF /* Pods_SwiftAudioPlayer_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftAudioPlayer_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
50C71346CE708A211A5AFAC20BAE48CB /* Pods-SwiftAudioPlayer_Tests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-SwiftAudioPlayer_Tests-umbrella.h"; sourceTree = "<group>"; };
55AB0CDF00C23619C7F54FE21D0C9534 /* Pods-SwiftAudioPlayer_Example-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudioPlayer_Example-frameworks.sh"; sourceTree = "<group>"; };
5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.3.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
69AF5444212FEC2674325627F26305AD /* Pods-SwiftAudioPlayer_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-SwiftAudioPlayer_Example.release.xcconfig"; sourceTree = "<group>"; };
6EC04ECC8F7CB2AF2E4E042A6A8ECFA1 /* SwiftAudioPlayer.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; path = SwiftAudioPlayer.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
70839C5AD428953FAF3091E814FF6E31 /* Pods-SwiftAudioPlayer_Example.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-SwiftAudioPlayer_Example.modulemap"; sourceTree = "<group>"; };
@@ -156,7 +152,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B73D01578ABBDB6FF402D868A6C547FF /* Foundation.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -164,7 +159,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
27E3EC64A90305ACA68AE35A7DC597E0 /* Foundation.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -172,7 +166,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
831B263D357A5FA2DDC7B1AE4B374092 /* Foundation.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -218,14 +211,6 @@
path = "Target Support Files/Pods-SwiftAudioPlayer_Tests";
sourceTree = "<group>";
};
5E0D919E635D23B70123790B8308F8EF /* iOS */ = {
isa = PBXGroup;
children = (
5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */,
);
name = iOS;
sourceTree = "<group>";
};
5F444B7A1C462A30A1CA4CCD3A7CF7B0 /* Targets Support Files */ = {
isa = PBXGroup;
children = (
@@ -258,7 +243,6 @@
children = (
93A4A3777CF96A4AAC1D13BA6DCCEA73 /* Podfile */,
D2A5FF8756A6E3EEEA69006E1A3C81F7 /* Development Pods */,
BC3CA7F9E30CC8F7E2DD044DD34432FC /* Frameworks */,
21D946895A4F57F51246F3EBCF330719 /* Products */,
5F444B7A1C462A30A1CA4CCD3A7CF7B0 /* Targets Support Files */,
);
@@ -385,14 +369,6 @@
path = Directors;
sourceTree = "<group>";
};
BC3CA7F9E30CC8F7E2DD044DD34432FC /* Frameworks */ = {
isa = PBXGroup;
children = (
5E0D919E635D23B70123790B8308F8EF /* iOS */,
);
name = Frameworks;
sourceTree = "<group>";
};
D2A5FF8756A6E3EEEA69006E1A3C81F7 /* Development Pods */ = {
isa = PBXGroup;
children = (
@@ -7,7 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
41B4A1BE666DAEDD342DBACF /* Pods_SwiftAudioPlayer_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E9F82E3AA46F1DA40F32F7F /* Pods_SwiftAudioPlayer_Tests.framework */; };
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; };
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
@@ -15,7 +14,6 @@
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; };
607FACEC1AFB9204008FA782 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* Tests.swift */; };
A470FEE2260303DA00F135FF /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FEE1260303DA00F135FF /* Model.swift */; };
E5808EC0557FB2395AA56468 /* Pods_SwiftAudioPlayer_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E5C0E3F3235B6FFE85EF425 /* Pods_SwiftAudioPlayer_Example.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -30,9 +28,7 @@
/* Begin PBXFileReference section */
0B7D1E6C00E83B4AF8AA1781 /* Pods-SwiftAudioPlayer_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftAudioPlayer_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftAudioPlayer_Tests/Pods-SwiftAudioPlayer_Tests.release.xcconfig"; sourceTree = "<group>"; };
1E5C0E3F3235B6FFE85EF425 /* Pods_SwiftAudioPlayer_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftAudioPlayer_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4B5DD2AE0B23A759D18926DC /* Pods-SwiftAudioPlayer_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftAudioPlayer_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftAudioPlayer_Example/Pods-SwiftAudioPlayer_Example.release.xcconfig"; sourceTree = "<group>"; };
4E9F82E3AA46F1DA40F32F7F /* Pods_SwiftAudioPlayer_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftAudioPlayer_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
607FACD01AFB9204008FA782 /* SwiftAudioPlayer_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAudioPlayer_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -56,7 +52,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E5808EC0557FB2395AA56468 /* Pods_SwiftAudioPlayer_Example.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -64,22 +59,12 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
41B4A1BE666DAEDD342DBACF /* Pods_SwiftAudioPlayer_Tests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
408E805A4561B2F63083E539 /* Frameworks */ = {
isa = PBXGroup;
children = (
1E5C0E3F3235B6FFE85EF425 /* Pods_SwiftAudioPlayer_Example.framework */,
4E9F82E3AA46F1DA40F32F7F /* Pods_SwiftAudioPlayer_Tests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
4246ED1215E81CA7B8F0AB36 /* Pods */ = {
isa = PBXGroup;
children = (
@@ -99,7 +84,6 @@
607FACE81AFB9204008FA782 /* Tests */,
607FACD11AFB9204008FA782 /* Products */,
4246ED1215E81CA7B8F0AB36 /* Pods */,
408E805A4561B2F63083E539 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -116,30 +116,32 @@
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Skip Silences" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="M2y-FP-H1D">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Skip Silences" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="M2y-FP-H1D">
<rect key="frame" x="89" y="504" width="101" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="2cn-E5-TeQ">
<rect key="frame" x="226" y="499" width="49" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="2cn-E5-TeQ">
<rect key="frame" x="226" y="499" width="51" height="31"/>
<connections>
<action selector="skipSilencesSwitched:" destination="vXZ-lx-hvc" eventType="valueChanged" id="p7X-Y8-7hO"/>
</connections>
</switch>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="IGe-aU-Y6D">
<rect key="frame" x="226" y="540" width="49" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="IGe-aU-Y6D">
<rect key="frame" x="226" y="540" width="51" height="31"/>
<connections>
<action selector="sleepSwitched:" destination="vXZ-lx-hvc" eventType="valueChanged" id="noa-m8-VHy"/>
</connections>
</switch>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Sleep After 5 s" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vf6-kr-yWa">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sleep After 5 s" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vf6-kr-yWa">
<rect key="frame" x="83" y="545" width="112" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Loop" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JOr-pf-CKN">
<rect key="frame" x="152" y="588" width="38" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@@ -158,14 +160,22 @@
<action selector="streamTouched:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="AXY-N7-87Y"/>
</connections>
</button>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="cfU-Rp-Kqf">
<rect key="frame" x="226" y="583" width="51" height="31"/>
<connections>
<action selector="loopSwitched:" destination="vXZ-lx-hvc" eventType="valueChanged" id="psj-Vs-9BI"/>
</connections>
</switch>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="nsl-df-P21" firstAttribute="top" secondItem="y5i-MZ-Qat" secondAttribute="bottom" constant="8" id="0aM-Sz-J9k"/>
<constraint firstItem="lTK-Hd-Tl2" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="16" id="1wb-IW-jYz"/>
<constraint firstItem="j3w-gr-HzF" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="26c-ZJ-768"/>
<constraint firstItem="JOr-pf-CKN" firstAttribute="top" secondItem="vf6-kr-yWa" secondAttribute="bottom" constant="22" id="4UI-XL-M9D"/>
<constraint firstItem="jUc-tP-CC5" firstAttribute="top" secondItem="KDu-ea-kF8" secondAttribute="bottom" constant="80" id="5sT-An-9vw"/>
<constraint firstItem="6d9-Bc-hIz" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="KDu-ea-kF8" secondAttribute="trailing" constant="8" symbolic="YES" id="60t-zV-EiY"/>
<constraint firstItem="2cn-E5-TeQ" firstAttribute="centerY" secondItem="M2y-FP-H1D" secondAttribute="centerY" id="6QX-Ru-ZbO"/>
<constraint firstItem="joK-xi-MCo" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="7KA-Mg-HFD"/>
<constraint firstItem="vfk-OJ-S3T" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="8PP-Pp-1Hc"/>
<constraint firstItem="joK-xi-MCo" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="AH1-Uu-eLB"/>
@@ -174,19 +184,27 @@
<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="cfU-Rp-Kqf" firstAttribute="leading" secondItem="JOr-pf-CKN" secondAttribute="trailing" constant="36" id="JxU-kl-pkL"/>
<constraint firstItem="yUQ-mI-ozK" firstAttribute="top" secondItem="w2a-RA-zmI" secondAttribute="bottom" constant="100" id="K1K-8N-SpD"/>
<constraint firstItem="IGe-aU-Y6D" firstAttribute="centerY" secondItem="vf6-kr-yWa" secondAttribute="centerY" id="K1s-td-R7b"/>
<constraint firstItem="vf6-kr-yWa" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="83" id="M0b-b2-UnQ"/>
<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="43" id="SRU-sX-z5b"/>
<constraint firstItem="cfU-Rp-Kqf" firstAttribute="centerY" secondItem="JOr-pf-CKN" secondAttribute="centerY" id="Tox-y4-XVg"/>
<constraint firstItem="w2a-RA-zmI" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="Vki-IZ-AdN"/>
<constraint firstItem="lTK-Hd-Tl2" firstAttribute="top" secondItem="j3w-gr-HzF" secondAttribute="bottom" constant="8" id="Wwx-Uo-yIC"/>
<constraint firstItem="IGe-aU-Y6D" firstAttribute="leading" secondItem="vf6-kr-yWa" secondAttribute="trailing" constant="31" id="XpW-wP-Iyh"/>
<constraint firstItem="vf6-kr-yWa" firstAttribute="top" secondItem="M2y-FP-H1D" secondAttribute="bottom" constant="20" id="Y8L-El-ycq"/>
<constraint firstItem="nsl-df-P21" firstAttribute="leading" secondItem="vfk-OJ-S3T" secondAttribute="leading" id="a5C-nZ-8Jc"/>
<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="M2y-FP-H1D" firstAttribute="top" secondItem="vfk-OJ-S3T" secondAttribute="bottom" constant="26" id="bsl-hj-xUt"/>
<constraint firstItem="tFH-sY-Xu9" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="62.5" id="cH6-q6-Lel"/>
<constraint firstItem="yUQ-mI-ozK" firstAttribute="top" secondItem="nsl-df-P21" secondAttribute="bottom" constant="8" id="cKV-wk-6P9"/>
<constraint firstItem="jUc-tP-CC5" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="cgM-Nj-yit"/>
<constraint firstItem="JOr-pf-CKN" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="152" id="cgd-E2-XpJ"/>
<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"/>
@@ -194,11 +212,13 @@
<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="pVf-cJ-9ca" firstAttribute="centerX" secondItem="joK-xi-MCo" secondAttribute="centerX" id="lOM-Fa-KdR"/>
<constraint firstItem="2cn-E5-TeQ" firstAttribute="leading" secondItem="M2y-FP-H1D" secondAttribute="trailing" constant="36" id="laG-3h-LI7"/>
<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 firstItem="nsl-df-P21" firstAttribute="trailing" secondItem="vfk-OJ-S3T" secondAttribute="trailing" id="r5e-Wq-dqV"/>
<constraint firstItem="y5i-MZ-Qat" firstAttribute="centerX" secondItem="nsl-df-P21" secondAttribute="centerX" id="reC-GA-ZgT"/>
<constraint firstAttribute="trailing" secondItem="0QE-3F-a4G" secondAttribute="trailing" constant="62.5" id="tg1-gr-hdd"/>
<constraint firstItem="M2y-FP-H1D" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="89" id="vcF-gP-oe0"/>
<constraint firstAttribute="trailing" secondItem="6d9-Bc-hIz" secondAttribute="trailing" constant="44" id="vtN-y4-iqp"/>
<constraint firstItem="0QE-3F-a4G" firstAttribute="centerY" secondItem="jUc-tP-CC5" secondAttribute="centerY" id="xDi-tj-bBF"/>
<constraint firstItem="lTK-Hd-Tl2" firstAttribute="top" secondItem="jUc-tP-CC5" secondAttribute="bottom" constant="40" id="ytQ-s4-kJm"/>
@@ -212,6 +232,7 @@
<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="loopSwitch" destination="cfU-Rp-Kqf" id="wTZ-Sr-mV4"/>
<outlet property="playPauseButton" destination="jUc-tP-CC5" id="e9C-zV-A1B"/>
<outlet property="rateLabel" destination="yUQ-mI-ozK" id="Dx4-lO-A1B"/>
<outlet property="rateSlider" destination="vfk-OJ-S3T" id="mNc-ET-aNM"/>
+1 -1
View File
@@ -47,7 +47,7 @@ struct AudioInfo: Hashable {
var lockscreenInfo: SALockScreenInfo {
get {
return SALockScreenInfo(title: self.title, artist: self.artist, artwork: nil, releaseDate: self.releaseDate)
return SALockScreenInfo(title: self.title, artist: self.artist, albumTitle: nil, artwork: nil, releaseDate: self.releaseDate)
}
}
+26 -14
View File
@@ -37,6 +37,7 @@ class ViewController: UIViewController {
var isDownloading: Bool = false
var isStreaming: Bool = false
var beingSeeked: Bool = false
var loopEnabled = false
var downloadId: UInt?
@@ -69,6 +70,7 @@ class ViewController: UIViewController {
super.viewDidLoad()
SAPlayer.Downloader.allowUsingCellularData = true
SAPlayer.shared.HTTPHeaderFields = ["User-Agent": "foobar"]
// SAPlayer.shared.DEBUG_MODE = true
@@ -132,7 +134,7 @@ class ViewController: UIViewController {
// unsubscribeFromChanges()
// subscribeToChanges()
SAPlayer.shared.mediaInfo = SALockScreenInfo(title: selectedAudio.title, artist: selectedAudio.artist, artwork: UIImage(), releaseDate: selectedAudio.releaseDate)
SAPlayer.shared.mediaInfo = SALockScreenInfo(title: selectedAudio.title, artist: selectedAudio.artist, albumTitle: nil, artwork: UIImage(), releaseDate: selectedAudio.releaseDate)
}
func checkIfAudioDownloaded() {
@@ -212,8 +214,10 @@ class ViewController: UIViewController {
self.playPauseButton.setTitle("Loading", for: .normal)
return
case .ended:
self.isPlayable = false
self.playPauseButton.setTitle("Done", for: .normal)
if !self.loopEnabled {
self.isPlayable = false
self.playPauseButton.setTitle("Done", for: .normal)
}
return
}
}
@@ -259,7 +263,12 @@ class ViewController: UIViewController {
@IBAction func rateChanged(_ sender: Any) {
let speed = rateSlider.value
rateLabel.text = "rate: \(speed)x"
SAPlayer.shared.rate = speed
if skipSilencesSwitch.isOn {
SAPlayer.Features.SkipSilences.setRateSafely(speed) // if using Skip Silences, we need use this version of setting rate to safely change the rate with the feature enabled.
} else {
SAPlayer.shared.rate = speed
}
}
@IBAction func reverbChanged(_ sender: Any) {
let reverb = reverbSlider.value
@@ -330,16 +339,6 @@ class ViewController: UIViewController {
}
@IBAction func playPauseTouched(_ sender: Any) {
// if lastPlayedAudioIndex != selectedAudio.index {
// if let savedUrl = selectedAudio.savedUrl {
// SAPlayer.shared.startSavedAudio(withSavedUrl: savedUrl)
// } else {
// SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url)
// }
//
// return
// }
SAPlayer.shared.togglePlayAndPause()
}
@@ -382,5 +381,18 @@ class ViewController: UIViewController {
_ = SAPlayer.Features.SleepTimer.disable()
}
}
@IBOutlet weak var loopSwitch: UISwitch!
@IBAction func loopSwitched(_ sender: Any) {
loopEnabled = loopSwitch.isOn
if loopSwitch.isOn {
SAPlayer.Features.Loop.enable()
} else {
SAPlayer.Features.Loop.disable()
}
}
}
+30
View File
@@ -0,0 +1,30 @@
// swift-tools-version:5.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "SwiftAudioPlayer",
platforms: [
.iOS(.v10), .tvOS(.v10)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "SwiftAudioPlayer",
targets: ["SwiftAudioPlayer"])
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "SwiftAudioPlayer",
path: "Source"
)
],
swiftLanguageVersions: [.v5]
)
+4 -9
View File
@@ -14,6 +14,7 @@ Thus, using [AudioToolbox](https://developer.apple.com/documentation/audiotoolbo
1. Realtime audio manipulation that includes going up to 10x speed, using [equalizers and other manipulations](https://developer.apple.com/documentation/avfaudio/avaudiouniteq)
1. Stream online audio using AVAudioEngine
1. Stream radio
1. Play locally saved audio with the same API
1. Download audio
1. Queue up downloaded and streamed audio for autoplay
@@ -24,6 +25,7 @@ Thus, using [AudioToolbox](https://developer.apple.com/documentation/audiotoolbo
These are community supported audio manipulation features using this audio engine. You can implement your own version of these features and you can look at [SAPlayerFeatures](https://github.com/tanhakabir/SwiftAudioPlayer/blob/master/Source/SAPlayerFeatures.swift) to learn how they were implemented using the library.
1. Skip silences in audio
1. Sleep timer to stop playing audio after a delay
1. Loop audio playback for both streamed and saved audio
### Requirements
@@ -116,16 +118,9 @@ For more details and specifics look at the [API documentation](#api-in-detail) b
## Contact
### Issues
### Issues or questions
Submit any issues or requests [on the Github repo](https://github.com/tanhakabir/SwiftAudioPlayer/issues).
### Any questions?
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
Submit any issues, requests, and questions [on the Github repo](https://github.com/tanhakabir/SwiftAudioPlayer/issues).
### License
-1
View File
@@ -71,7 +71,6 @@ class AudioDiskEngine: AudioEngine {
doRepeatedly(timeInterval: 0.2) { [weak self] in
guard let self = self else { return }
guard self.playingStatus != .ended else { return }
self.updateIsPlaying()
self.updateNeedle()
+38 -30
View File
@@ -45,6 +45,7 @@ class AudioEngine: AudioEngineProtocol {
var engine: AVAudioEngine!
var playerNode: AVAudioPlayerNode!
private var engineInvalidated: Bool = false
static let defaultEngineAudioFormat: AVAudioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 2, interleaved: false)!
@@ -102,6 +103,8 @@ class AudioEngine: AudioEngineProtocol {
AudioClockDirector.shared.changeInAudioBuffered(key, buffered: bufferedSeconds)
}
}
private var audioModifiers: [AVAudioUnit]?
init(url: AudioURL, delegate:AudioEngineDelegate?, engineAudioFormat: AVAudioFormat) {
self.key = url.key
@@ -115,39 +118,40 @@ class AudioEngine: AudioEngineProtocol {
func initHelper(_ engineAudioFormat: AVAudioFormat) {
engine.attach(playerNode)
for node in SAPlayer.shared.audioModifiers {
engine.attach(node)
}
if SAPlayer.shared.audioModifiers.count > 0 {
var i = 0
let node = SAPlayer.shared.audioModifiers[i]
engine.connect(playerNode, to: node, format: engineAudioFormat)
i += 1
while i < SAPlayer.shared.audioModifiers.count {
let lastNode = SAPlayer.shared.audioModifiers[i - 1]
let currNode = SAPlayer.shared.audioModifiers[i]
engine.connect(lastNode, to: currNode, format: engineAudioFormat)
i += 1
}
let finalNode = SAPlayer.shared.audioModifiers[SAPlayer.shared.audioModifiers.count - 1]
engine.connect(finalNode, to: engine.mainMixerNode, format: engineAudioFormat)
} else {
audioModifiers = SAPlayer.shared.audioModifiers
defer { engine.prepare() }
guard let audioModifiers = audioModifiers, audioModifiers.count > 0 else {
engine.connect(playerNode, to: engine.mainMixerNode, format: engineAudioFormat)
return
}
engine.prepare()
audioModifiers.forEach { engine.attach($0) }
var i = 0
let node = audioModifiers[i]
engine.connect(playerNode, to: node, format: engineAudioFormat)
i += 1
while i < audioModifiers.count {
let lastNode = audioModifiers[i - 1]
let currNode = audioModifiers[i]
engine.connect(lastNode, to: currNode, format: engineAudioFormat)
i += 1
}
let finalNode = audioModifiers[audioModifiers.count - 1]
engine.connect(finalNode, to: engine.mainMixerNode, format: engineAudioFormat)
}
deinit {
if state == .resumed {
playerNode.stop()
engine.stop()
}
@@ -172,7 +176,7 @@ class AudioEngine: AudioEngineProtocol {
Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { [weak self] (timer: Timer) in
guard let self = self else { return }
guard self.playingStatus != .ended else {
guard !self.engineInvalidated else {
self.delegate = nil
return
}
@@ -193,8 +197,6 @@ class AudioEngine: AudioEngineProtocol {
let isPlaying = engine.isRunning && playerNode.isPlaying
playingStatus = isPlaying ? .playing : .paused
// playingStatus = .paused
}
func play() {
@@ -230,6 +232,12 @@ class AudioEngine: AudioEngineProtocol {
}
func invalidate() {
engineInvalidated = true
playerNode.stop()
engine.stop()
if let audioModifiers = audioModifiers, audioModifiers.count > 0 {
audioModifiers.forEach { engine.detach($0) }
}
}
}
+14 -3
View File
@@ -74,12 +74,18 @@ class AudioStreamEngine: AudioEngine {
didSet {
Log.debug("number of buffers scheduled in total: \(numberOfBuffersScheduledInTotal)")
if numberOfBuffersScheduledInTotal == 0 {
if playingStatus == .playing { wasPlaying = true }
pause()
// delegate?.didError()
// TODO: we should not have an error here. We should instead have the throttler
// propegate when it doesn't enough buffers while they were playing
// TODO: "Make this a legitimate warning to user about needing more data from stream"
}
if numberOfBuffersScheduledInTotal > MIN_BUFFERS_TO_BE_PLAYABLE && wasPlaying {
wasPlaying = false
play()
}
}
}
@@ -164,7 +170,6 @@ class AudioStreamEngine: AudioEngine {
doRepeatedly(timeInterval: timeInterval) { [weak self] in
guard let self = self else { return }
guard self.playingStatus != .ended else { return }
self.repeatedUpdates()
}
@@ -203,7 +208,7 @@ class AudioStreamEngine: AudioEngine {
Log.debug("processed buffer for engine of frame length \(nextScheduledBuffer.frameLength)")
queue.async { [weak self] in
if #available(iOS 11.0, *) {
if #available(iOS 11.0, tvOS 11.0, *) {
// to make sure the pcm buffers are properly free'd from memory we need to nil them after the player has used them
self?.playerNode.scheduleBuffer(nextScheduledBuffer, completionCallbackType: .dataConsumed, completionHandler: { (_) in
nextScheduledBuffer = nil
@@ -328,7 +333,13 @@ class AudioStreamEngine: AudioEngine {
}
override func invalidate() {
queue.sync { [weak self] in
self?.invalidateHelperDispatchQueue()
self?.converter.invalidate()
}
}
private func invalidateHelperDispatchQueue() {
super.invalidate()
converter.invalidate()
}
}
+1
View File
@@ -154,6 +154,7 @@ class AudioThrottler: AudioThrottleable {
extension Array where Element == Data {
var sum: Int {
get {
guard count > 0 else { return 0 }
return self.reduce(0) { $0 + $1.count }
}
}
+1 -1
View File
@@ -70,7 +70,7 @@ public struct SAAudioAvailabilityRange {
var needleAtEnd = false
if(totalDurationBuffered > 0 && needle > 0) {
needleAtEnd = needle >= totalDurationBuffered - 1
needleAtEnd = needle >= totalDurationBuffered - 5
}
// if most of the audio is buffered for long audio or in short audio there isn't many seconds left to buffer it means wwe've reached the end of the audio
+8 -4
View File
@@ -39,7 +39,7 @@ extension LockScreenViewProtocol {
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
}
@available(iOS 10.0, *)
@available(iOS 10.0, tvOS 10.0, *)
func setLockScreenInfo(withMediaInfo info: SALockScreenInfo?, duration: Duration) {
var nowPlayingInfo:[String : Any] = [:]
@@ -50,6 +50,7 @@ extension LockScreenViewProtocol {
let title = info.title
let artist = info.artist
let albumTitle = info.albumTitle ?? artist
let releaseDate = info.releaseDate
// For some reason we need to set a duration here for the needle?
@@ -57,7 +58,7 @@ extension LockScreenViewProtocol {
nowPlayingInfo[MPMediaItemPropertyTitle] = title
nowPlayingInfo[MPMediaItemPropertyArtist] = artist
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = artist
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = albumTitle
//nowPlayingInfo[MPMediaItemPropertyGenre] = //maybe later when we have it
//nowPlayingInfo[MPMediaItemPropertyIsExplicit] = //maybe later when we have it
nowPlayingInfo[MPMediaItemPropertyAlbumArtist] = artist
@@ -168,7 +169,10 @@ extension LockScreenViewProtocol {
}
func updateLockscreenSkipIntervals() {
MPRemoteCommandCenter.shared().skipBackwardCommand.preferredIntervals = [skipBackwardSeconds] as [NSNumber]
MPRemoteCommandCenter.shared().skipForwardCommand.preferredIntervals = [skipForwardSeconds] as [NSNumber]
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.skipBackwardCommand.isEnabled = skipBackwardSeconds > 0
commandCenter.skipBackwardCommand.preferredIntervals = [skipBackwardSeconds] as [NSNumber]
commandCenter.skipForwardCommand.isEnabled = skipForwardSeconds > 0
commandCenter.skipForwardCommand.preferredIntervals = [skipForwardSeconds] as [NSNumber]
}
}
+6
View File
@@ -31,6 +31,7 @@ protocol AudioDataManagable {
var allowCellular: Bool { get set }
func setHTTPHeaderFields(_ fields: [String: String]?)
func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ())
func setAllowCellularDownloadPreference(_ preference: Bool)
@@ -96,6 +97,11 @@ class AudioDataManager: AudioDataManagable {
streamingCallbacks = []
}
func setHTTPHeaderFields(_ fields: [String: String]?) {
streamWorker.HTTPHeaderFields = fields
downloadWorker.HTTPHeaderFields = fields
}
func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ()) {
backgroundCompletion = completionHandler
}
@@ -31,6 +31,8 @@ protocol AudioDataDownloadable: AnyObject {
var numberOfActive: Int { get }
var numberOfQueued: Int { get }
var HTTPHeaderFields: [String: String]? { get set }
func getProgressOfDownload(withID id: ID) -> Double?
func start(withID id: ID, withRemoteUrl remoteUrl: URL, completion: @escaping (URL) -> ())
@@ -57,6 +59,8 @@ class AudioDownloadWorker: NSObject, AudioDataDownloadable {
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
var HTTPHeaderFields: [String: String]?
private var activeDownloads: [ActiveDownload] = []
private var queuedDownloads = Set<DownloadInfo>()
@@ -111,7 +115,10 @@ class AudioDownloadWorker: NSObject, AudioDataDownloadable {
queuedDownloads.remove(info)
let task: URLSessionDownloadTask = session.downloadTask(with: info.remoteUrl)
var request = URLRequest(url: info.remoteUrl)
HTTPHeaderFields?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
let task: URLSessionDownloadTask = session.downloadTask(with: request)
task.taskDescription = info.id
let activeTask = ActiveDownload(info: info, task: task)
+13 -4
View File
@@ -44,6 +44,9 @@ import Foundation
protocol AudioDataStreamable {
//if user taps download then starts to stream
init(progressCallback: @escaping (_ id: ID, _ dto: StreamProgressDTO) -> (), doneCallback: @escaping (_ id: ID, _ error: Error?)->Bool) //Bool is should save or not
var HTTPHeaderFields: [String: String]? { get set }
func start(withID id: ID, withRemoteURL url: URL, withInitialData data: Data?, andTotalBytesExpectedPreviously previousTotalBytesExpected: Int64?)
func pause(withId id: ID)
func resume(withId id: ID)
@@ -66,6 +69,8 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
fileprivate let doneCallback: (_ id: ID, _ error: Error?) -> Bool
private var session: URLSession!
var HTTPHeaderFields: [String: String]?
private var id: ID?
private var url: URL?
private var task: URLSessionDataTask?
@@ -89,7 +94,7 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
let config = URLSessionConfiguration.background(withIdentifier: "SwiftAudioPlayer.stream")
// Specifies that the phone should keep trying till it receives connection instead of dropping immediately
if #available(iOS 11.0, *) {
if #available(iOS 11.0, tvOS 11.0, *) {
config.waitsForConnectivity = true
}
self.session = URLSession(configuration: config, delegate: self, delegateQueue: nil) //TODO: should we use ephemeral
@@ -105,6 +110,7 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
if let data = data {
var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: TIMEOUT)
HTTPHeaderFields?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
request.addValue("bytes=\(data.count)-", forHTTPHeaderField: "Range")
task = session.dataTask(with: request)
task?.taskDescription = id
@@ -121,10 +127,11 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
task?.resume()
} else {
task = session.dataTask(with: url)
task?.resume()
var request = URLRequest(url: url)
HTTPHeaderFields?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
task = session.dataTask(with: request)
task?.taskDescription = id
task?.resume()
}
}
@@ -217,6 +224,7 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
self.progressCallback(id, StreamProgressDTO(progress: 0, data: Data(), totalBytesExpected: totalBytesExpectedForWholeFile))
var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: TIMEOUT)
HTTPHeaderFields?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
request.addValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
task = session.dataTask(with: request)
task?.resume()
@@ -314,6 +322,7 @@ extension AudioStreamWorker: URLSessionDataDelegate {
Log.monitor("\(task.currentRequest?.url?.absoluteString ?? "nil url") error: \(err.localizedDescription)")
let _ = doneCallback(id, err)
return
}
let shouldSave = doneCallback(id, nil)
+3 -1
View File
@@ -37,12 +37,14 @@ public typealias UTC = Int
public struct SALockScreenInfo {
var title: String
var artist: String
var albumTitle: String?
var artwork: UIImage?
var releaseDate: UTC
public init(title: String, artist: String, artwork: UIImage?, releaseDate: UTC) {
public init(title: String, artist: String, albumTitle: String?, artwork: UIImage?, releaseDate: UTC) {
self.title = title
self.artist = artist
self.albumTitle = albumTitle
self.artwork = artwork
self.releaseDate = releaseDate
}
+70 -36
View File
@@ -45,6 +45,15 @@ public class SAPlayer {
private var presenter: SAPlayerPresenter!
private var player: AudioEngine?
/**
Any necessary header fields for streaming and downloading requests can be set here.
*/
public var HTTPHeaderFields: [String: String]? {
didSet {
AudioDataManager.shared.setHTTPHeaderFields(HTTPHeaderFields)
}
}
/**
Access the engine of the player. Engine is nil if player has not been initialized with audio.
@@ -123,13 +132,6 @@ public class SAPlayer {
node.rate = value
playbackRateOfAudioChanged(rate: value)
// if skip silences was on, reset it to have the new rate
// TODO fix this to rate being broadcasted and handled in only Features.SkipSilences https://github.com/tanhakabir/SwiftAudioPlayer/issues/77
// if Features.SkipSilences.enabled && !(value == rate ?? 1.0 - 0.5 || value == rate ?? 1.0 + 0.5) {
// _ = Features.SkipSilences.disable()
// _ = Features.SkipSilences.enable()
// }
}
}
@@ -183,7 +185,7 @@ public class SAPlayer {
public var audioQueued: [URL] {
get {
return presenter.audioQueue.map { (queued) -> URL in
return queued.1
return queued.url
}
}
}
@@ -233,11 +235,7 @@ public class SAPlayer {
- Note: Setting this to nil clears the information displayed on the lockscreen media player.
*/
public var mediaInfo: SALockScreenInfo? = nil {
didSet {
presenter.handleLockscreenInfo(info: mediaInfo)
}
}
public var mediaInfo: SALockScreenInfo? = nil
private init() {
presenter = SAPlayerPresenter(delegate: self)
@@ -255,6 +253,7 @@ public class SAPlayer {
}
audioModifiers.append(AVAudioUnitTimePitch(audioComponentDescription: componentDescription))
NotificationCenter.default.addObserver(self, selector: #selector(handleInterruption), name: AVAudioSession.interruptionNotification, object: nil)
}
/**
@@ -294,6 +293,36 @@ public class SAPlayer {
func addUrlToMapping(url: URL) {
presenter.addUrlToKeyMap(url)
}
@objc func handleInterruption(notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}
// Switch over the interruption type.
switch type {
case .began:
// An interruption began. Update the UI as necessary.
pause()
case .ended:
// An interruption ended. Resume playback, if appropriate.
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
// An interruption ended. Resume playback.
play()
} else {
// An interruption ended. Don't resume playback.
}
default: ()
}
}
}
public enum SAPlayerBitrate {
@@ -414,12 +443,13 @@ extension SAPlayer {
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
*/
public func startSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
self.mediaInfo = mediaInfo
presenter.handlePlaySavedAudio(withSavedUrl: url)
}
@available(*, deprecated, renamed: "startSavedAudio")
public func initializeSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
// Because we support queueing, we want to clear off any existing players.
// Therefore, instantiate new player every time, destroy any existing ones.
// This prevents a crash where an owning engine already exists.
presenter.handleClear()
// order here matters, need to set media info before trying to play audio
self.mediaInfo = mediaInfo
presenter.handlePlaySavedAudio(withSavedUrl: url)
}
@@ -453,16 +483,17 @@ extension SAPlayer {
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
*/
public func startRemoteAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate = .high, mediaInfo: SALockScreenInfo? = nil) {
// Because we support queueing, we want to clear off any existing players.
// Therefore, instantiate new player every time, destroy any existing ones.
// This prevents a crash where an owning engine already exists.
presenter.handleClear()
// order here matters, need to set media info before trying to play audio
self.mediaInfo = mediaInfo
presenter.handlePlayStreamedAudio(withRemoteUrl: url, bitrate: bitrate)
}
@available(*, deprecated, renamed: "startRemoteAudio")
public func initializeRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
self.mediaInfo = mediaInfo
presenter.handlePlayStreamedAudio(withRemoteUrl: url, bitrate: .high)
}
/**
Stops any streaming in progress.
*/
@@ -474,18 +505,21 @@ extension SAPlayer {
Queues remote audio to be played next. The URLs in the queue can be both remote or on disk but once the queued audio starts playing it will start buffering and loading then. This means no guarantee for a 'gapless' playback where there might be several moments in between one audio ending and another starting due to buffering remote audio.
- Parameter withRemoteUrl: The URL of the remote audio.
- Parameter bitrate: The bitrate of the streamed audio. By default the bitrate is set to high for streaming saved audio files. If you want to stream radios then you should use the `low` bitrate option.
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
*/
public func queueRemoteAudio(withRemoteUrl url: URL) {
presenter.handleQueueStreamedAudio(withRemoteUrl: url)
public func queueRemoteAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate = .high, mediaInfo: SALockScreenInfo? = nil) {
presenter.handleQueueStreamedAudio(withRemoteUrl: url, mediaInfo: mediaInfo, bitrate: bitrate)
}
/**
Queues saved audio to be played next. The URLs in the queuecan be both remote or on disk but once the queued audio starts playing it will start buffering and loading then. This means no guarantee for a 'gapless' playback where there might be several moments in between one audio ending and another starting due to buffering remote audio.
- Parameter withSavedUrl: The URL of the audio saved on the device.
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
*/
public func queueSavedAudio(withSavedUrl url: URL) {
presenter.handleQueueSavedAudio(withSavedUrl: url)
public func queueSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
presenter.handleQueueSavedAudio(withSavedUrl: url, mediaInfo: mediaInfo)
}
/**
@@ -499,22 +533,22 @@ extension SAPlayer {
//MARK: - Internal implementation of delegate
extension SAPlayer: SAPlayerDelegate {
func startAudioDownloaded(withSavedUrl url: AudioURL) {
internal func startAudioDownloaded(withSavedUrl url: AudioURL) {
player = AudioDiskEngine(withSavedUrl: url, delegate: presenter)
}
func startAudioStreamed(withRemoteUrl url: AudioURL, bitrate: SAPlayerBitrate) {
internal func startAudioStreamed(withRemoteUrl url: AudioURL, bitrate: SAPlayerBitrate) {
player = AudioStreamEngine(withRemoteUrl: url, delegate: presenter, bitrate: bitrate)
}
func clearEngine() {
internal func clearEngine() {
player?.pause()
player?.invalidate()
player = nil
Log.info("cleared engine")
}
func playEngine() {
internal func playEngine() {
becomeDeviceAudioPlayer()
player?.play()
}
@@ -522,7 +556,7 @@ extension SAPlayer: SAPlayerDelegate {
//Start taking control as the device's player
private func becomeDeviceAudioPlayer() {
do {
if #available(iOS 11.0, *) {
if #available(iOS 11.0, tvOS 11.0, *) {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, policy: .longFormAudio, options: [])
} else {
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: AVAudioSession.Mode(rawValue: convertFromAVAudioSessionMode(AVAudioSession.Mode.default)), options: .allowAirPlay)
@@ -533,11 +567,11 @@ extension SAPlayer: SAPlayerDelegate {
}
}
func pauseEngine() {
internal func pauseEngine() {
player?.pause()
}
func seekEngine(toNeedle needle: Needle) {
internal func seekEngine(toNeedle needle: Needle) {
var seekToNeedle = needle < 0 ? 0 : needle
seekToNeedle = needle > Needle(duration ?? 0) ? Needle(duration ?? 0) : needle
player?.seek(toNeedle: seekToNeedle)
+1
View File
@@ -27,6 +27,7 @@ import Foundation
import CoreMedia
protocol SAPlayerDelegate: AnyObject, LockScreenViewProtocol {
var mediaInfo: SALockScreenInfo? { get set }
var skipForwardSeconds: Double { get set }
var skipBackwardSeconds: Double { get set }
+45 -2
View File
@@ -29,7 +29,8 @@ extension SAPlayer {
/**
Enable feature to skip silences in spoken word audio. The player will speed up the rate of audio playback when silence is detected. This can be called at any point of audio playback.
- Important: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
- Precondition: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
- Important: If you want to change the rate of the overall player while having skip silences on, please use `SAPlayer.Features.SkipSilences.setRateSafely()` to properly set the rate of the player. Any rate changes to the player will be ignored while using Skip Silences otherwise.
*/
public static func enable() -> Bool {
guard let engine = SAPlayer.shared.engine else { return false }
@@ -72,7 +73,7 @@ extension SAPlayer {
/**
Disable feature to skip silences in spoken word audio. The player will speed up the rate of audio playback when silence is detected. This can be called at any point of audio playback.
- Important: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
- Precondition: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
*/
public static func disable() -> Bool {
guard let engine = SAPlayer.shared.engine else { return false }
@@ -83,6 +84,16 @@ extension SAPlayer {
return true
}
/**
Use this function to set the overall rate of the player for when skip silences is on. This ensures that the overall rate will be what is set through this function even as skip silences is on; if this function is not used then any changes asked of from the overall player while skip silences is on won't be recorded!
- Important: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
*/
public static func setRateSafely(_ rate: Float) {
originalRate = rate
SAPlayer.shared.rate = rate
}
private static func scaledPower(power: Float) -> Float {
guard power.isFinite else { return 0.0 }
let minDb: Float = -80.0
@@ -120,5 +131,37 @@ extension SAPlayer {
timer?.invalidate()
}
}
/**
Feature to play the current playing audio on repeat until feature is disabled.
*/
public struct Loop {
static var enabled: Bool = false
static var playingStatusId: UInt?
/**
Enable feature to play the current playing audio on loop. This will continue until the feature is disabled. And this feature works for both remote and saved audio.
*/
public static func enable() {
enabled = true
guard playingStatusId == nil else { return }
playingStatusId = SAPlayer.Updates.PlayingStatus.subscribe({ (url, status) in
if status == .ended && enabled {
SAPlayer.shared.seekTo(seconds: 0.0)
SAPlayer.shared.play()
}
})
}
/**
Disable feature playing audio on loop.
*/
public static func disable() {
enabled = false
}
}
}
}
+28 -26
View File
@@ -28,6 +28,20 @@ import AVFoundation
import MediaPlayer
class SAPlayerPresenter {
struct QueueItem {
var loc: Location
var url: URL
var mediaInfo: SALockScreenInfo?
var bitrate: SAPlayerBitrate
init(loc: Location, url: URL, mediaInfo: SALockScreenInfo?, bitrate: SAPlayerBitrate = .high) {
self.loc = loc
self.url = url
self.mediaInfo = mediaInfo
self.bitrate = bitrate
}
}
enum Location {
case remote
case disk
@@ -41,14 +55,13 @@ class SAPlayerPresenter {
private var key: String?
private var isPlaying: SAPlayingStatus = .buffering
private var mediaInfo: SALockScreenInfo?
private var urlKeyMap: [Key: URL] = [:]
var durationRef:UInt = 0
var needleRef:UInt = 0
var playingStatusRef:UInt = 0
var audioQueue: [(Location, URL)] = []
var audioQueue: [QueueItem] = []
init(delegate: SAPlayerDelegate?) {
self.delegate = delegate
@@ -70,7 +83,7 @@ class SAPlayerPresenter {
needle = nil
duration = nil
key = nil
mediaInfo = nil
delegate?.mediaInfo = nil
delegate?.clearLockScreenInfo()
AudioClockDirector.shared.detachFromChangesInDuration(withID: durationRef)
@@ -79,29 +92,21 @@ class SAPlayerPresenter {
}
func handlePlaySavedAudio(withSavedUrl url: URL) {
// Because we support queueing, we want to clear off any existing players.
// Therefore, instantiate new player every time, destroy any existing ones.
// This prevents a crash where an owning engine already exists.
handleClear()
attachForUpdates(url: url)
delegate?.startAudioDownloaded(withSavedUrl: url)
}
func handlePlayStreamedAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate) {
// Because we support queueing, we want to clear off any existing players.
// Therefore, instantiate new player every time, destroy any existing ones.
// This prevents a crash where an owning engine already exists.
handleClear()
attachForUpdates(url: url)
delegate?.startAudioStreamed(withRemoteUrl: url, bitrate: bitrate)
}
func handleQueueStreamedAudio(withRemoteUrl url: URL) {
audioQueue.append((.remote, url))
func handleQueueStreamedAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo?, bitrate: SAPlayerBitrate) {
audioQueue.append(QueueItem(loc: .remote, url: url, mediaInfo: mediaInfo, bitrate: bitrate))
}
func handleQueueSavedAudio(withSavedUrl url: URL) {
audioQueue.append((.disk, url))
func handleQueueSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo?) {
audioQueue.append(QueueItem(loc: .disk, url: url, mediaInfo: mediaInfo))
}
private func attachForUpdates(url: URL) {
@@ -120,7 +125,7 @@ class SAPlayerPresenter {
self.delegate?.updateLockscreenPlaybackDuration(duration: duration)
self.duration = duration
self.delegate?.setLockScreenInfo(withMediaInfo: self.mediaInfo, duration: duration)
self.delegate?.setLockScreenInfo(withMediaInfo: self.delegate?.mediaInfo, duration: duration)
})
needleRef = AudioClockDirector.shared.attachToChangesInNeedle(closure: { [weak self] (key, needle) in
@@ -164,11 +169,6 @@ class SAPlayerPresenter {
delegate?.clearEngine()
detachFromUpdates()
}
@available(iOS 10.0, *)
func handleLockscreenInfo(info: SALockScreenInfo?) {
self.mediaInfo = info
}
}
//MARK:- Used by outside world including:
@@ -238,24 +238,26 @@ extension SAPlayerPresenter {
return
}
let nextAudioURL = audioQueue.removeFirst()
let key = nextAudioURL.1.key
let key = nextAudioURL.url.key
Log.info("getting ready to play \(nextAudioURL)")
AudioQueueDirector.shared.changeInQueue(key, url: nextAudioURL.1)
AudioQueueDirector.shared.changeInQueue(key, url: nextAudioURL.url)
handleClear()
delegate?.mediaInfo = nextAudioURL.mediaInfo
// We need to give a second to clean up the previous engine properly. Deinit takes some time.
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { [weak self] (_) in
guard let self = self else { return }
switch nextAudioURL.0 {
switch nextAudioURL.loc {
case .remote:
self.handlePlayStreamedAudio(withRemoteUrl: nextAudioURL.1, bitrate: .high) // TODO fix to add option for low birate
self.handlePlayStreamedAudio(withRemoteUrl: nextAudioURL.url, bitrate: nextAudioURL.bitrate)
break
case .disk:
self.handlePlaySavedAudio(withSavedUrl: nextAudioURL.1)
self.handlePlaySavedAudio(withSavedUrl: nextAudioURL.url)
}
self.shouldPlayImmediately = true
+3 -3
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioPlayer'
s.version = '5.0.3'
s.version = '6.2.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.
@@ -26,9 +26,9 @@ SwiftAudioPlayer is a Swift based audio player that can handle streaming from a
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'tanhakabir' => 'tanhakabir.ca@gmail.com', 'JonMercer' => 'mercer.jon@gmail.com' }
s.source = { :git => 'https://github.com/tanhakabir/SwiftAudioPlayer.git', :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/_tanhakabir'
s.social_media_url = 'https://twitter.com/_tanhakabir'
s.ios.deployment_target = '10.0'
s.platforms = { :ios => '10.0', :tvos => '10.0' }
s.source_files = 'Source/**/*'
s.swift_version = '5.0'